Compare commits

..

77 Commits

Author SHA1 Message Date
Jakob Borg
5f01afb7ea build: No need for outdated go2xunit 2016-09-18 21:02:42 +02:00
fti7
6fe2fa5ff0 gui: Slightly lighten the dark theme
Skip-check: authors

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3605
2016-09-18 13:47:36 +00:00
Jakob Borg
b371b1fe34 lib/versioner: Test both spaces and parens in ext versioner paths
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3610
2016-09-18 12:24:55 +00:00
Jakob Borg
90c0a39df8 lib/versioner: Test for external versioner
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3609
2016-09-17 20:34:50 +00:00
Lars K.W. Gohlke
70c5a5dff1 lib/versioner: Rename versioner_test to simple_test
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3603
2016-09-16 11:01:43 +00:00
Jakob Borg
da0b7cc7f2 lib/model: Correct lock taking order in ConnectionStats (fixes #3596)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3597
2016-09-14 19:38:55 +00:00
Jakob Borg
139e9b144e lib/config: Fix tests for changes in previous commit 2016-09-13 22:20:22 +02:00
Jakob Borg
77c0a19451 vendor: Update github.com/d4l3k/messagediff 2016-09-13 22:20:22 +02:00
Jakob Borg
58cbd19742 vendor: Update golang.org/cznic/... 2016-09-13 22:20:22 +02:00
Jakob Borg
9bf6917ae8 vendor: Update golang.org/x/crypto/... 2016-09-13 22:20:22 +02:00
Jakob Borg
897cca0a82 vendor: Update golang.org/x/net/... 2016-09-13 22:20:22 +02:00
Jakob Borg
6af09c61be vendor: Update github.com/thejerf/suture 2016-09-13 22:20:22 +02:00
Jakob Borg
c3c7798446 vendor: Update github.com/gobwas/glob 2016-09-13 22:20:22 +02:00
Jakob Borg
06dc91fadf vendor: Update github.com/syndtr/goleveldb 2016-09-13 22:20:22 +02:00
Jakob Borg
526cab538a jenkins: Don't fetch --prune unnecessarily, print build version on Windows 2016-09-13 22:18:55 +02:00
Jakob Borg
81d19a00aa vendor: Add github.com/cznic/lldb and friends (new recursive dependency) 2016-09-13 21:57:19 +02:00
Jakob Borg
ca755ec9e0 vendor: Add golang.org/x/net/bpf 2016-09-13 21:56:33 +02:00
Jakob Borg
4f6206cb2d build: Simpler creation of Debian packages
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3591
2016-09-12 12:21:07 +00:00
Jakob Borg
7fb53ec954 lib/config: Correct name of discovery-v6-4 server 2016-09-12 11:30:06 +02:00
Jakob Borg
d8b5070ca8 lib/config: Update default set of discovery servers 2016-09-12 09:55:45 +02:00
Jakob Borg
5e99d38412 all: Use github.com/minio/sha256-simd
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3581
2016-09-09 09:57:51 +00:00
Laurent Etiemble
3990014073 cmd/syncthing: Conditionally enable CORS
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3541
LGTM: AudriusButkevicius
2016-09-06 22:16:50 +00:00
Jakob Borg
3e51206a6b build, jenkins: Jenkins version tag should be same as when building manually 2016-09-06 13:02:17 +02:00
Aranjedeath
7569b75d61 cmd/strelaysrv: Correct go get command in README
Skip-check: authors

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3564
2016-09-04 21:06:30 +00:00
Jakob Borg
8fcabac518 jenkins: Add batch file for Windows 2016-09-04 16:43:56 +02:00
Jakob Borg
abb0cfde72 jenkins: Add scripts for automated builds (Linux & Mac) 2016-09-04 15:30:16 +02:00
Jakob Borg
7990ffcc60 cmd/syncthing: Copy config on upgrade, instead of renaming (fixes #3525)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3560
2016-09-03 21:29:32 +00:00
Jakob Borg
49910a1d85 lib/config, cmd/syncthing: Enforce localhost only connections
When the GUI/API is bound to localhost, we enforce that the Host header
looks like localhost. This can be disabled by setting
insecureSkipHostCheck in the GUI config.

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3558
2016-09-03 08:33:34 +00:00
Jakob Borg
46a143e80e lib/model: Handle deleted-then-ignored files (fixes #3502)
When files that were previously marked as deleted became ignored, we
used to do nothing at all. This changes that behavior to set the Invalid
bit (that we should rename to Ignored). This then becomes an update to
other devices that they should not trust our knowledge about the file in
question.

Read this diff without whitespace...

Tested by
- creating a bunch of files on s1
- letting them sync to s2
- shutting down s2
- deleting the files on s1 and rescanning
- adding the files to .stignore on s1 and rescanning
- starting up s2 and letting it sync
- observing the files are not deleted on s2, and it considers itself up
  to date.

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3557
2016-09-02 13:23:24 +00:00
Jakob Borg
69b7f26e4c lib/model, cmd/syncthing: Also account for deleted files in folder summary events (ref #3496)
This should probably be reflected in the GUI somewhere as well...
2016-09-02 10:45:39 +02:00
Jakob Borg
5b37d0356c lib/model, gui: Correct completion percentages when there are lots of deletes (fixes #3496)
We used to consider deleted files & directories 128 bytes large. After
the delta indexes change a bug slipped in where deleted files would be
weighted according to their old non-deleted size. Both ways are
incorrect (but the latest change made it worse), as if there are more
files deleted than remaining data in the repo the needSize can be
greater than the globalSize, resulting in a negative completion
percentage.

This change makes it so that deleted items are zero bytes large, which
makes more sense. Instead we expose the number of files that we need to
delete as a separate field in the Completion() result, and hack the
percentage down to 95% complete if it was 100% complete but we need to
delete files. This latter part is sort of ugly, but necessary to give
the user some sort of feedback.

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3556
2016-09-02 06:45:46 +00:00
Jakob Borg
1188ebbb7b gui, man: Update docs & translations 2016-08-23 10:42:09 +02:00
Audrius Butkevicius
76b903b2e0 lib/upgrade: Cleanup failed upgrades (fixes #3500, fixes #3530)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3535
2016-08-23 06:53:39 +00:00
Audrius Butkevicius
be38c2111f cmd/strelaysrv: Add uPNP support, ability to set listen protocol (fixes #3503, fixes #3505, fixes #3506) 2016-08-23 08:43:27 +02:00
Audrius Butkevicius
1de787fab8 cmd/strelaypoolsrv: Ability to select listen protocol 2016-08-23 08:42:57 +02:00
Audrius Butkevicius
81f683a61c cmd/stdiscosrv: Generate keys if missing on startup (fixes #3511) 2016-08-23 08:41:49 +02:00
Audrius Butkevicius
db6f68d031 cmd/stdiscosrv: Use UTC in database timestamps (fixes #3509) 2016-08-23 08:41:15 +02:00
Jakob Borg
d0a1c805e9 cmd/syncthing: uintptr may not be stored in a variable
Must do the whole uintptr(unsafe.Pointer(&whatever)) directly in the
call to sycall.Call, as per https://golang.org/pkg/unsafe/.
2016-08-22 18:24:26 +02:00
Jakob Borg
00a654845f cmd/syncthing: Remove old temp index dbs on startup (fixes #3529)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3532
2016-08-22 12:19:19 +00:00
Jakob Borg
04dad8485a authors: Add calmh email 2016-08-18 19:38:15 +02:00
Jakob Borg
0b1475169f lib/model: Correct virtual mtime handling (fixes #3516)
We previously set the mtime on the temp file, and then renamed it to the
real path. Unfortunately that means we'd save the real timestamp under
the under the temp name ".syncthing.foo.tmp" when the actual file that
we will look up on the next scan is "foo". This moves the Chtimes later,
ensuring that it gets recorded correctly under the right name.

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3519
2016-08-16 18:22:19 +00:00
Audrius Butkevicius
6ec4fbc82b lib/model: Add minumum interval for progress emitter (fixes #3517)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3518
2016-08-16 18:22:01 +00:00
Jakob Borg
18cc7a663b lib: Remove osutil.Remove & osutil.RemoveAll (fixes #3513)
These are no longer required with Go 1.7. Change made by removing the
functions, doing a global s/osutil.Remove/os.Remove/.

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3514
2016-08-16 10:01:58 +00:00
Jakob Borg
cf5febad47 build, cmd, lib: Minimum supported compiler version is Go 1.5 2016-08-15 08:37:32 +02:00
Jakob Borg
42849af5a8 cmd/syncthing: Default folder should also have lower case ID 2016-08-13 23:23:18 +02:00
Jakob Borg
e6364407a9 cmd/stdiscosrv: Fix index creation checks on startup 2016-08-12 11:39:10 +02:00
Jakob Borg
480b78f2c8 cmd/stdiscosrv: Longer address in schema 2016-08-12 11:38:37 +02:00
Jakob Borg
fa8f339478 gui: Fix division by zero in completion calc (ref #3493) 2016-08-12 08:49:16 +02:00
Jakob Borg
7776839c82 cmd/syncthing, gui: Improve completion calculation (fixes #3492)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3493
2016-08-12 06:41:43 +00:00
Jakob Borg
7114cacb85 gui, man: Update docs & translations 2016-08-10 11:41:46 +02:00
Jakob Borg
e52be3d83e lib/connections, lib/model: Refactor connection close handling (fixes #3466)
So there were some issues here. The main problem was that
model.Close(deviceID) was overloaded to mean "the connection was closed
by the protocol layer" and "i want to close this connection". That meant
it could get called twice - once *to* close the connection and then once
more when the connection *was* closed.

After this refactor there is instead a Closed(conn) method that is the
callback. I didn't need to change the parameter in the end, but I think
it's clearer what it means when it takes the connection that was closed
instead of a device ID. To close a connection, the new close(deviceID)
method is used instead, which only closes the underlying connection and
leaves the cleanup to the Closed() callback.

I also changed how we do connection switching. Instead of the connection
service calling close and then adding the connection, it just adds the
new connection. The model knows that it already has a connection and
makes sure to close and clean out that one before adding the new
connection.

To make sure to sequence this properly I added a new map of channels
that get created on connection add and closed by Closed(), so that
AddConnection() can do the close and wait for the cleanup to happen
before proceeding.

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3490
2016-08-10 09:37:32 +00:00
Antoine Lamielle
c9cf01e0b6 gui: weighting % of devices according to folder size (fixes #1300)
The completion of remote devices was based only on the average of the percentages of all folders, which is irrelevant in case of two folders with very different sizes.

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3481
LGTM: calmh, AudriusButkevicius
2016-08-09 19:58:44 +00:00
Jakob Borg
dcbf68e104 lib/versioner: Hack to make test coverage stable from run to run
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3485
2016-08-08 18:27:55 +00:00
Jakob Borg
c2d8c07137 lib/events: Hack to make test coverage stable from run to run
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3484
2016-08-08 18:09:40 +00:00
Jakob Borg
a4ed50ca85 build, lib: Correct total test coverage calculation
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3483
2016-08-08 16:29:32 +00:00
Jakob Borg
b3788c8ea0 authors: Fixup 0x010C 2016-08-08 08:34:36 +02:00
Jakob Borg
946c074a41 authors: Add 0x010C 2016-08-08 08:19:02 +02:00
Jakob Borg
19f79afb0f build: Setup should install golint 2016-08-07 21:58:27 +02:00
Audrius Butkevicius
af3b6f9c83 lib/model, lib/config: Support "live" device removal, folder unsharing and folder configuration changes
Furthermore:
1. Cleans configs received, migrates them as we receive them.
2. Clears indexes of devices we no longer share the folder with

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3478
2016-08-07 16:21:59 +00:00
Jakob Borg
fbe42c156d gui: Move "ignore patterns" away from "remove" in folder edit dialog 2016-08-07 14:26:32 +02:00
Jakob Borg
a1f6cbd354 lib/protocol: Clean away outdated files 2016-08-07 14:24:25 +02:00
Audrius Butkevicius
a4f052ad31 lib/connections: Fix connection switching
It seems that it would be impossible to drop down to relay after establishing a direct connection
Also, we should not drop the existing connection until after we've passed the validation steps,
and it seems it's being dropped in two places unnecesserily at the moment.

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3480
2016-08-07 12:20:37 +00:00
Jakob Borg
ea87bcefd6 lib/protocol, lib/model: Implement high precision time stamps (fixes #3305)
This adds a new nanoseconds field to the FileInfo, populates it during
scans and sets the non-truncated time in Chtimes calls.

The actual file modification time is defined as modified_s seconds +
modified_ns nanoseconds. It's expected that the modified_ns field is <=
1e9 (that is, all whole seconds should go in the modified_s field) but
not really enforced. Given that it's an int32 the timestamp can be
adjusted += ~2.9 seconds by the modified_ns field...

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3431
2016-08-06 13:05:59 +00:00
Jakob Borg
0655991a19 lib/db, lib/fs, lib/model: Introduce fs.MtimeFS, remove VirtualMtimeRepo
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3479
2016-08-05 17:45:45 +00:00
Jakob Borg
f368d2278f lib/config, lib/connections: Refactor handling of ignored devices (fixes #3470)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3471
2016-08-05 09:29:49 +00:00
Jakob Borg
1eb6db6ca8 cmd/syncthing, lib/...: Correctly handle ignores & invalid file names (fixes #3012, fixes #3457, fixes #3458)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3464
2016-08-05 07:13:52 +00:00
Jakob Borg
a25b63e2df cmd/syncthing: Delete old format indexes after a while (fixes #3468)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3469
2016-08-02 15:44:09 +00:00
Jakob Borg
ffe7a2fcd7 cmd/syncthing, lib/config: Enable HTTP CPU/heap profile collection for users
This adds a config to enable debug functions on the API server, which is
by default disabled. When enabled, the /rest/debug things become
available and become available without requiring a CSRF token (although
authentication is required if configured).

We also add a new endpoint /rest/debug/cpuprof?duration=15s (with the
duration being configurable, defaulting to 30s). This runs a CPU profile
for the duration and returns it as a file. It sets headers so that a
browser will save the file with an informative name.

The same is done for heap profiles, /rest/debug/heapprof, which does not
take any parameters.

The purpose of this is that any user can enable debugging under
advanced, then point their browser to the endpoint above and get a file
that contains a CPU or heap profile we can use, with the filename
telling us what version and architecture the profile is from.

On the command line, this becomes

    curl -O -J http://localhost:8082/rest/debug/cpuprof?duration=5s
    curl: Saved to filename
    'syncthing-cpu-darwin-amd64-v0.14.3+4-g935bcc0-110307.pprof'

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3467
2016-08-02 11:06:45 +00:00
Audrius Butkevicius
08b5a7908f gui: Add one-off notifications that need to be acked
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3254
2016-08-02 08:07:30 +00:00
derekriemer
a8cd9d0154 gui: Improve accessibility (fixes #3297)
skip-check: authors

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3463
2016-07-31 22:59:44 +00:00
Jakob Borg
297240facf all: Rename LocalVersion to Sequence (fixes #3461)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3462
2016-07-29 19:54:24 +00:00
Jakob Borg
a022b0cfff gui, man: Update docs & translations 2016-07-28 13:15:14 +02:00
Jakob Borg
72026db599 lib/db, lib/model: Create temp sorting database in config dir (fixes #3449)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3454
2016-07-27 21:38:43 +00:00
Jakob Borg
aafc96f58f lib/model, lib/protocol: Sequence ClusterConfig properly (fixes #3448)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3452
2016-07-27 21:36:25 +00:00
Jakob Borg
7c7e8648ff lib/model: Trigger a puller iteration on connection (fixes #3451)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3453
2016-07-27 21:35:41 +00:00
Jakob Borg
24e2ce0764 build: Allow easy influencing build user and build host
To facilitate reproducible builds.
2016-07-27 23:27:47 +02:00
aviau
d7cb4d407b man: Include stdiscosrv and strelaysrv manpages
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3450
2016-07-27 15:00:10 +00:00
330 changed files with 14780 additions and 5307 deletions

1
.gitattributes vendored
View File

@@ -6,3 +6,4 @@ vendor/** -text=auto
# Diffs on these files are meaningless
*.svg -diff
*.pb.go -diff

1
.gitignore vendored
View File

@@ -5,6 +5,7 @@ stdiscosrv.exe
*.tar.gz
*.zip
*.asc
*.deb
.jshintrc
coverage.out
files/pidx

View File

@@ -13,6 +13,7 @@ Alexandre Viau (aviau) <alexandre@alexandreviau.net> <aviau@debian.org>
Anderson Mesquita (andersonvom) <andersonvom@gmail.com>
Andrew Dunham (andrew-d) <andrew@du.nham.ca>
Andrey D (scienmind) <scintertech@cryptolab.net>
Antoine Lamielle (0x010C) <antoine.lamielle@0x010c.fr> <gh@0x010c.fr>
Antony Male (canton7) <antony.male@gmail.com>
Arthur Axel fREW Schmidt (frioux) <frew@afoolishmanifesto.com> <frioux@gmail.com>
Audrius Butkevicius (AudriusButkevicius) <audrius.butkevicius@gmail.com>
@@ -50,7 +51,7 @@ Gilli Sigurdsson (gillisig) <gilli@vx.is>
Jaakko Hannikainen (jgke) <jgke@jgke.fi>
Jacek Szafarkiewicz (hadogenes) <szafar@linux.pl>
Jake Peterson (acogdev) <jake@acogdev.com>
Jakob Borg (calmh) <jakob@nym.se>
Jakob Borg (calmh) <jakob@nym.se> <jakob@kastelo.net>
James Patterson (jpjp) <jamespatterson@operamail.com> <jpjp@users.noreply.github.com>
Jaroslav Malec (dzarda) <dzardacz@gmail.com>
Jens Diemer (jedie) <github.com@jensdiemer.de> <git@jensdiemer.de>

3
NICKS
View File

@@ -1,6 +1,8 @@
# This file maps email addresses used in commits to nicks used the changelog.
# It is auto generated from the AUTHORS file by script/authors.go.
0x010C <antoine.lamielle@0x010c.fr>
0x010C <gh@0x010c.fr>
acogdev <jake@acogdev.com>
alessandro.g89 <alessandro.g89@gmail.com>
alex2108 <register-github@alex-graf.de>
@@ -21,6 +23,7 @@ buinsky <vix_booja@tut.by>
burkemw3 <mburke@amplify.com>
burkemw3 <burkemw3@gmail.com>
calmh <jakob@nym.se>
calmh <jakob@kastelo.net>
canton7 <antony.male@gmail.com>
Cathryne <cathryne.linenweaver@gmail.com>
Cathryne <Cathryne@users.noreply.github.com>

View File

@@ -27,7 +27,6 @@ import (
"strconv"
"strings"
"syscall"
"text/template"
"time"
)
@@ -43,12 +42,12 @@ var (
)
type target struct {
name string
buildPkg string
binaryName string
archiveFiles []archiveFile
debianFiles []archiveFile
tags []string
name string
buildPkg string
binaryName string
archiveFiles []archiveFile
installationFiles []archiveFile
tags []string
}
type archiveFile struct {
@@ -76,7 +75,7 @@ var targets = map[string]target{
{src: "AUTHORS", dst: "AUTHORS.txt", perm: 0644},
// All files from etc/ and extra/ added automatically in init().
},
debianFiles: []archiveFile{
installationFiles: []archiveFile{
{src: "{{binary}}", dst: "deb/usr/bin/{{binary}}", perm: 0755},
{src: "README.md", dst: "deb/usr/share/doc/syncthing/README.txt", perm: 0644},
{src: "LICENSE", dst: "deb/usr/share/doc/syncthing/LICENSE.txt", perm: 0644},
@@ -106,11 +105,12 @@ var targets = map[string]target{
{src: "cmd/stdiscosrv/LICENSE", dst: "LICENSE.txt", perm: 0644},
{src: "AUTHORS", dst: "AUTHORS.txt", perm: 0644},
},
debianFiles: []archiveFile{
installationFiles: []archiveFile{
{src: "{{binary}}", dst: "deb/usr/bin/{{binary}}", perm: 0755},
{src: "cmd/stdiscosrv/README.md", dst: "deb/usr/share/doc/stdiscosrv/README.txt", perm: 0644},
{src: "cmd/stdiscosrv/LICENSE", dst: "deb/usr/share/doc/stdiscosrv/LICENSE.txt", perm: 0644},
{src: "AUTHORS", dst: "deb/usr/share/doc/stdiscosrv/AUTHORS.txt", perm: 0644},
{src: "man/stdiscosrv.1", dst: "deb/usr/share/man/man1/stdiscosrv.1", perm: 0644},
},
tags: []string{"purego"},
},
@@ -124,11 +124,12 @@ var targets = map[string]target{
{src: "cmd/strelaysrv/LICENSE", dst: "LICENSE.txt", perm: 0644},
{src: "AUTHORS", dst: "AUTHORS.txt", perm: 0644},
},
debianFiles: []archiveFile{
installationFiles: []archiveFile{
{src: "{{binary}}", dst: "deb/usr/bin/{{binary}}", perm: 0755},
{src: "cmd/strelaysrv/README.md", dst: "deb/usr/share/doc/strelaysrv/README.txt", perm: 0644},
{src: "cmd/strelaysrv/LICENSE", dst: "deb/usr/share/doc/strelaysrv/LICENSE.txt", perm: 0644},
{src: "AUTHORS", dst: "deb/usr/share/doc/strelaysrv/AUTHORS.txt", perm: 0644},
{src: "man/strelaysrv.1", dst: "deb/usr/share/man/man1/strelaysrv.1", perm: 0644},
},
},
"strelaypoolsrv": {
@@ -141,7 +142,7 @@ var targets = map[string]target{
{src: "cmd/strelaypoolsrv/LICENSE", dst: "LICENSE.txt", perm: 0644},
{src: "AUTHORS", dst: "AUTHORS.txt", perm: 0644},
},
debianFiles: []archiveFile{
installationFiles: []archiveFile{
{src: "{{binary}}", dst: "deb/usr/bin/{{binary}}", perm: 0755},
{src: "cmd/strelaypoolsrv/README.md", dst: "deb/usr/share/doc/relaysrv/README.txt", perm: 0644},
{src: "cmd/strelaypoolsrv/LICENSE", dst: "deb/usr/share/doc/relaysrv/LICENSE.txt", perm: 0644},
@@ -161,12 +162,12 @@ func init() {
syncthingPkg.archiveFiles = append(syncthingPkg.archiveFiles, archiveFile{src: file, dst: file, perm: 0644})
}
for _, file := range listFiles("extra") {
syncthingPkg.debianFiles = append(syncthingPkg.debianFiles, archiveFile{src: file, dst: "deb/usr/share/doc/syncthing/" + filepath.Base(file), perm: 0644})
syncthingPkg.installationFiles = append(syncthingPkg.installationFiles, archiveFile{src: file, dst: "deb/usr/share/doc/syncthing/" + filepath.Base(file), perm: 0644})
}
targets["syncthing"] = syncthingPkg
}
const minGoVersion = 1.3
const minGoVersion = 1.5
func main() {
log.SetOutput(os.Stdout)
@@ -296,6 +297,9 @@ func runCommand(cmd string, target target) {
}
}
case "version":
fmt.Println(getVersion())
default:
log.Fatalf("Unknown command %q", cmd)
}
@@ -346,12 +350,12 @@ func checkRequiredGoVersion() (float64, bool) {
}
func setup() {
runPrint("go", "get", "-v", "github.com/golang/lint/golint")
runPrint("go", "get", "-v", "golang.org/x/tools/cmd/cover")
runPrint("go", "get", "-v", "golang.org/x/net/html")
runPrint("go", "get", "-v", "github.com/FiloSottile/gvt")
runPrint("go", "get", "-v", "github.com/axw/gocov/gocov")
runPrint("go", "get", "-v", "github.com/AlekSi/gocov-xml")
runPrint("go", "get", "-v", "bitbucket.org/tebeka/go2xunit")
runPrint("go", "get", "-v", "github.com/alecthomas/gometalinter")
runPrint("go", "get", "-v", "github.com/mitchellh/go-wordwrap")
}
@@ -488,46 +492,31 @@ func buildDeb(target target) {
build(target, []string{"noupgrade"})
for i := range target.debianFiles {
target.debianFiles[i].src = strings.Replace(target.debianFiles[i].src, "{{binary}}", target.binaryName, 1)
target.debianFiles[i].dst = strings.Replace(target.debianFiles[i].dst, "{{binary}}", target.binaryName, 1)
for i := range target.installationFiles {
target.installationFiles[i].src = strings.Replace(target.installationFiles[i].src, "{{binary}}", target.binaryName, 1)
target.installationFiles[i].dst = strings.Replace(target.installationFiles[i].dst, "{{binary}}", target.binaryName, 1)
}
for _, af := range target.debianFiles {
for _, af := range target.installationFiles {
if err := copyFile(af.src, af.dst, af.perm); err != nil {
log.Fatal(err)
}
}
os.MkdirAll("deb/DEBIAN", 0755)
data := map[string]string{
"name": target.name,
"arch": debarch,
"version": version[1:],
"date": time.Now().Format(time.RFC1123),
}
debTemplateFiles := append(listFiles("debtpl/common"), listFiles("debtpl/"+target.name)...)
for _, file := range debTemplateFiles {
tpl, err := template.New(filepath.Base(file)).ParseFiles(file)
if err != nil {
log.Fatal(err)
}
outFile := filepath.Join("deb/DEBIAN", filepath.Base(file))
out, err := os.Create(outFile)
if err != nil {
log.Fatal(err)
}
if err := tpl.Execute(out, data); err != nil {
log.Fatal(err)
}
if err := out.Close(); err != nil {
log.Fatal(err)
}
info, _ := os.Lstat(file)
os.Chmod(outFile, info.Mode())
maintainer := "Syncthing Release Management <release@syncthing.net>"
debver := version
if strings.HasPrefix(debver, "v") {
debver = debver[1:]
}
runPrint("fpm", "-t", "deb", "-s", "dir", "-C", "deb",
"-n", "syncthing", "-v", debver, "-a", debarch,
"--vendor", maintainer, "-m", maintainer,
"-d", "libc6",
"-d", "procps", // because postinst script
"--url", "https://syncthing.net/",
"--description", "Open Source Continuous File Synchronization",
"--after-upgrade", "script/post-upgrade",
"--license", "MPL-2")
}
func copyFile(src, dst string, perm os.FileMode) error {
@@ -639,7 +628,9 @@ func ldflags() string {
func rmr(paths ...string) {
for _, path := range paths {
log.Println("rm -r", path)
if debug {
log.Println("rm -r", path)
}
os.RemoveAll(path)
}
}
@@ -755,6 +746,10 @@ func buildStamp() int64 {
}
func buildUser() string {
if v := os.Getenv("BUILD_USER"); v != "" {
return v
}
u, err := user.Current()
if err != nil {
return "unknown-user"
@@ -763,6 +758,10 @@ func buildUser() string {
}
func buildHost() string {
if v := os.Getenv("BUILD_HOST"); v != "" {
return v
}
h, err := os.Hostname()
if err != nil {
return "unknown-host"

View File

@@ -104,7 +104,7 @@ case "${1:-default}" in
# For every package in the repo
for dir in $(go list ./lib/... ./cmd/...) ; do
# run the tests
GOPATH="$(pwd)/Godeps/_workspace:$GOPATH" go test -race -coverprofile=profile.out $dir
GOPATH="$(pwd)/Godeps/_workspace:$GOPATH" go test -coverprofile=profile.out $dir
if [ -f profile.out ] ; then
# and if there was test output, append it to coverage.out
grep -v "mode: " profile.out >> coverage.out
@@ -112,6 +112,11 @@ case "${1:-default}" in
fi
done
notCovered=$(egrep -c '\s0$' coverage.out)
total=$(wc -l coverage.out | awk '{print $1}')
coverPct=$(awk "BEGIN{print (1 - $notCovered / $total) * 100}")
echo "Total coverage is $coverPct%"
gocov convert coverage.out | gocov-xml > coverage.xml
# This is usually run from within Jenkins. If it is, we need to

View File

@@ -14,6 +14,7 @@ import (
"time"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/tlsutil"
"github.com/thejerf/suture"
)
@@ -86,7 +87,11 @@ func main() {
if !useHTTP {
cert, err = tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
log.Fatalln("Failed to load X509 key pair:", err)
log.Println("Failed to load keypair. Generating one, this might take a while...")
cert, err = tlsutil.NewCertificate(certFile, keyFile, "stdiscosrv", 3072)
if err != nil {
log.Fatalln("Failed to generate X509 key pair:", err)
}
}
devID := protocol.NewDeviceID(cert.Certificate[0])

View File

@@ -27,8 +27,9 @@ func postgresSetup(db *sql.DB) error {
return err
}
var tmp string
row := db.QueryRow(`SELECT 'DevicesDeviceIDIndex'::regclass`)
if err = row.Scan(nil); err != nil {
if err = row.Scan(&tmp); err != nil {
_, err = db.Exec(`CREATE INDEX DevicesDeviceIDIndex ON Devices (DeviceID)`)
}
if err != nil {
@@ -36,7 +37,7 @@ func postgresSetup(db *sql.DB) error {
}
row = db.QueryRow(`SELECT 'DevicesSeenIndex'::regclass`)
if err = row.Scan(nil); err != nil {
if err = row.Scan(&tmp); err != nil {
_, err = db.Exec(`CREATE INDEX DevicesSeenIndex ON Devices (Seen)`)
}
if err != nil {
@@ -46,14 +47,14 @@ func postgresSetup(db *sql.DB) error {
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS Addresses (
DeviceID CHAR(63) NOT NULL,
Seen TIMESTAMP NOT NULL,
Address VARCHAR(256) NOT NULL
Address VARCHAR(2048) NOT NULL
)`)
if err != nil {
return err
}
row = db.QueryRow(`SELECT 'AddressesDeviceIDSeenIndex'::regclass`)
if err = row.Scan(nil); err != nil {
if err = row.Scan(&tmp); err != nil {
_, err = db.Exec(`CREATE INDEX AddressesDeviceIDSeenIndex ON Addresses (DeviceID, Seen)`)
}
if err != nil {
@@ -61,7 +62,7 @@ func postgresSetup(db *sql.DB) error {
}
row = db.QueryRow(`SELECT 'AddressesDeviceIDAddressIndex'::regclass`)
if err = row.Scan(nil); err != nil {
if err = row.Scan(&tmp); err != nil {
_, err = db.Exec(`CREATE INDEX AddressesDeviceIDAddressIndex ON Addresses (DeviceID, Address)`)
}
if err != nil {

View File

@@ -451,7 +451,7 @@ func (s *querysrv) getDeviceSeen(device protocol.DeviceID) (time.Time, error) {
if err := row.Scan(&seen); err != nil {
return time.Time{}, err
}
return seen, nil
return seen.In(time.UTC), nil
}
func handlePing(w http.ResponseWriter, r *http.Request) {

View File

@@ -40,7 +40,8 @@ func main() {
log.Println("Lstat:")
log.Printf(" Size: %d bytes", fi.Size())
log.Printf(" Mode: 0%o", fi.Mode())
log.Printf(" Time: %v (%d)", fi.ModTime(), fi.ModTime().Unix())
log.Printf(" Time: %v", fi.ModTime())
log.Printf(" %d.%09d", fi.ModTime().Unix(), fi.ModTime().Nanosecond())
log.Println()
if !fi.Mode().IsDir() && !fi.Mode().IsRegular() {
@@ -52,7 +53,8 @@ func main() {
log.Println("Stat:")
log.Printf(" Size: %d bytes", fi.Size())
log.Printf(" Mode: 0%o", fi.Mode())
log.Printf(" Time: %v (%d)", fi.ModTime(), fi.ModTime().Unix())
log.Printf(" Time: %v", fi.ModTime())
log.Printf(" %d.%09d", fi.ModTime().Unix(), fi.ModTime().Nanosecond())
log.Println()
}

View File

@@ -66,7 +66,7 @@ func checkServers(deviceID protocol.DeviceID, servers ...string) {
}()
}
for _ = range servers {
for range servers {
res := <-resc
u, _ := url.Parse(res.server)

View File

@@ -10,6 +10,7 @@ import (
"encoding/binary"
"fmt"
"log"
"time"
"github.com/syncthing/syncthing/lib/db"
"github.com/syncthing/syncthing/lib/protocol"
@@ -53,7 +54,13 @@ func dump(ldb *db.Instance) {
fmt.Printf("[fstat] K:%x V:%x\n", it.Key(), it.Value())
case db.KeyTypeVirtualMtime:
fmt.Printf("[mtime] K:%x V:%x\n", it.Key(), it.Value())
folder := binary.BigEndian.Uint32(key[1:])
name := nulString(key[1+4:])
val := it.Value()
var real, virt time.Time
real.UnmarshalBinary(val[:len(val)/2])
virt.UnmarshalBinary(val[len(val)/2:])
fmt.Printf("[mtime] F:%d N:%q R:%v V:%v\n", folder, name, real, virt)
case db.KeyTypeFolderIdx:
key := binary.BigEndian.Uint32(it.Key()[1:])

View File

@@ -76,6 +76,7 @@ var (
permRelaysFile string
ipHeader string
geoipPath string
proto string
getMut = sync.NewRWMutex()
getLRUCache *lru.Cache
@@ -105,6 +106,7 @@ func main() {
flag.StringVar(&permRelaysFile, "perm-relays", "", "Path to list of permanent relays")
flag.StringVar(&ipHeader, "ip-header", "", "Name of header which holds clients ip:port. Only meaningful when running behind a reverse proxy.")
flag.StringVar(&geoipPath, "geoip", "GeoLite2-City.mmdb", "Path to GeoLite2-City database")
flag.StringVar(&proto, "protocol", "tcp", "Protocol used for listening. 'tcp' for IPv4 and IPv6, 'tcp4' for IPv4, 'tcp6' for IPv6")
flag.Parse()
@@ -154,12 +156,12 @@ func main() {
},
}
listener, err = tls.Listen("tcp", listen, tlsCfg)
listener, err = tls.Listen(proto, listen, tlsCfg)
} else {
if debug {
log.Println("Starting plain listener on", listen)
}
listener, err = net.Listen("tcp", listen)
listener, err = net.Listen(proto, listen)
}
if err != nil {

View File

@@ -5,7 +5,7 @@ strelaysrv
This is the relay server for the `syncthing` project.
To get it, run `go get github.com/syncthing/strelaysrv` or download the
To get it, run `go get github.com/syncthing/syncthing/cmd/strelaysrv` or download the
[latest build](http://build.syncthing.net/job/strelaysrv/lastSuccessfulBuild/artifact/)
from the build server.

View File

@@ -23,7 +23,7 @@ var (
numConnections int64
)
func listener(addr string, config *tls.Config) {
func listener(proto, addr string, config *tls.Config) {
tcpListener, err := net.Listen("tcp", addr)
if err != nil {
log.Fatalln(err)
@@ -167,10 +167,16 @@ func protocolConnectionHandler(tcpConn net.Conn, config *tls.Config) {
continue
}
peerOutbox <- serverInvitation
select {
case peerOutbox <- serverInvitation:
if debug {
log.Println("Sent invitation from", id, "to", requestedPeer)
}
default:
if debug {
log.Println("Could not send invitation from", id, "to", requestedPeer, "as peer disconnected")
}
if debug {
log.Println("Sent invitation from", id, "to", requestedPeer)
}
conn.Close()

View File

@@ -24,6 +24,11 @@ import (
"github.com/syncthing/syncthing/lib/relay/protocol"
"github.com/syncthing/syncthing/lib/tlsutil"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/nat"
_ "github.com/syncthing/syncthing/lib/pmp"
_ "github.com/syncthing/syncthing/lib/upnp"
syncthingprotocol "github.com/syncthing/syncthing/lib/protocol"
)
@@ -48,6 +53,7 @@ func init() {
var (
listen string
debug bool
proto string
sessionAddress []byte
sessionPort uint16
@@ -70,12 +76,17 @@ var (
pools []string
providedBy string
defaultPoolAddrs = "https://relays.syncthing.net/endpoint"
natEnabled bool
natLease int
natRenewal int
natTimeout int
)
func main() {
log.SetFlags(log.Lshortfile | log.LstdFlags)
var dir, extAddress string
var dir, extAddress, proto string
flag.StringVar(&listen, "listen", ":22067", "Protocol listen address")
flag.StringVar(&dir, "keys", ".", "Directory where cert.pem and key.pem is stored")
@@ -89,14 +100,22 @@ func main() {
flag.StringVar(&poolAddrs, "pools", defaultPoolAddrs, "Comma separated list of relay pool addresses to join")
flag.StringVar(&providedBy, "provided-by", "", "An optional description about who provides the relay")
flag.StringVar(&extAddress, "ext-address", "", "An optional address to advertise as being available on.\n\tAllows listening on an unprivileged port with port forwarding from e.g. 443, and be connected to on port 443.")
flag.StringVar(&proto, "protocol", "tcp", "Protocol used for listening. 'tcp' for IPv4 and IPv6, 'tcp4' for IPv4, 'tcp6' for IPv6")
flag.BoolVar(&natEnabled, "nat", false, "Use UPnP/NAT-PMP to acquire external port mapping")
flag.IntVar(&natLease, "nat-lease", 60, "NAT lease length in minutes")
flag.IntVar(&natRenewal, "nat-renewal", 30, "NAT renewal frequency in minutes")
flag.IntVar(&natTimeout, "nat-timeout", 10, "NAT discovery timeout in seconds")
flag.Parse()
if extAddress == "" {
extAddress = listen
}
addr, err := net.ResolveTCPAddr("tcp", extAddress)
if len(providedBy) > 30 {
log.Fatal("Provided-by cannot be longer than 30 characters")
}
addr, err := net.ResolveTCPAddr(proto, extAddress)
if err != nil {
log.Fatal(err)
}
@@ -149,6 +168,37 @@ func main() {
log.Println("ID:", id)
}
wrapper := config.Wrap("config", config.New(id))
wrapper.SetOptions(config.OptionsConfiguration{
NATLeaseM: natLease,
NATRenewalM: natRenewal,
NATTimeoutS: natTimeout,
})
natSvc := nat.NewService(id, wrapper)
mapping := mapping{natSvc.NewMapping(nat.TCP, addr.IP, addr.Port)}
if natEnabled {
go natSvc.Serve()
found := make(chan struct{})
mapping.OnChanged(func(_ *nat.Mapping, _, _ []nat.Address) {
select {
case found <- struct{}{}:
default:
}
})
// Need to wait a few extra seconds, since NAT library waits exactly natTimeout seconds on all interfaces.
timeout := time.Duration(natTimeout+2) * time.Second
log.Printf("Waiting %s to acquire NAT mapping", timeout)
select {
case <-found:
log.Printf("Found NAT mapping: %s", mapping.ExternalAddresses())
case <-time.After(timeout):
log.Println("Timeout out waiting for NAT mapping.")
}
}
if sessionLimitBps > 0 {
sessionLimiter = ratelimit.NewBucketWithRate(float64(sessionLimitBps), int64(2*sessionLimitBps))
}
@@ -160,7 +210,7 @@ func main() {
go statusService(statusAddr)
}
uri, err := url.Parse(fmt.Sprintf("relay://%s/?id=%s&pingInterval=%s&networkTimeout=%s&sessionLimitBps=%d&globalLimitBps=%d&statusAddr=%s&providedBy=%s", extAddress, id, pingInterval, networkTimeout, sessionLimitBps, globalLimitBps, statusAddr, providedBy))
uri, err := url.Parse(fmt.Sprintf("relay://%s/?id=%s&pingInterval=%s&networkTimeout=%s&sessionLimitBps=%d&globalLimitBps=%d&statusAddr=%s&providedBy=%s", mapping.Address(), id, pingInterval, networkTimeout, sessionLimitBps, globalLimitBps, statusAddr, providedBy))
if err != nil {
log.Fatalln("Failed to construct URI", err)
}
@@ -178,11 +228,11 @@ func main() {
for _, pool := range pools {
pool = strings.TrimSpace(pool)
if len(pool) > 0 {
go poolHandler(pool, uri)
go poolHandler(pool, uri, mapping)
}
}
go listener(listen, tlsCfg)
go listener(proto, listen, tlsCfg)
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
@@ -212,7 +262,7 @@ func main() {
func monitorLimits() {
limitCheckTimer = time.NewTimer(time.Minute)
for _ = range limitCheckTimer.C {
for range limitCheckTimer.C {
if atomic.LoadInt64(&numConnections)+atomic.LoadInt64(&numProxies) > descriptorLimit {
atomic.StoreInt32(&overLimit, 1)
log.Println("Gone past our connection limits. Starting to refuse new/drop idle connections.")
@@ -222,3 +272,15 @@ func monitorLimits() {
limitCheckTimer.Reset(time.Minute)
}
}
type mapping struct {
*nat.Mapping
}
func (m *mapping) Address() nat.Address {
ext := m.ExternalAddresses()
if len(ext) > 0 {
return ext[0]
}
return m.Mapping.Address()
}

View File

@@ -12,16 +12,19 @@ import (
"time"
)
func poolHandler(pool string, uri *url.URL) {
func poolHandler(pool string, uri *url.URL, mapping mapping) {
if debug {
log.Println("Joining", pool)
}
for {
uriCopy := *uri
uriCopy.Host = mapping.Address().String()
var b bytes.Buffer
json.NewEncoder(&b).Encode(struct {
URL string `json:"url"`
}{
uri.String(),
uriCopy.String(),
})
resp, err := http.Post(pool, "application/json", &b)
@@ -39,7 +42,7 @@ func poolHandler(pool string, uri *url.URL) {
log.Println(pool, "under load, will retry in a minute")
time.Sleep(time.Minute)
continue
} else if resp.StatusCode == 403 {
} else if resp.StatusCode == 401 {
log.Println(pool, "failed to join due to IP address not matching external address. Aborting")
return
} else if resp.StatusCode == 200 {

View File

@@ -130,7 +130,7 @@ func printProgress(prefix string, count *int64) {
expectedIterations := float64(int(1) << uint(wantBits))
fmt.Printf("Want %d bits for prefix %q, about %.2g certs to test (statistically speaking)\n", wantBits, prefix, expectedIterations)
for _ = range time.NewTicker(15 * time.Second).C {
for range time.NewTicker(15 * time.Second).C {
tried := atomic.LoadInt64(count)
elapsed := time.Since(started)
rate := float64(tried) / elapsed.Seconds()

View File

@@ -17,6 +17,7 @@ import (
"path/filepath"
"reflect"
"runtime"
"runtime/pprof"
"sort"
"strconv"
"strings"
@@ -67,10 +68,10 @@ type apiService struct {
type modelIntf interface {
GlobalDirectoryTree(folder, prefix string, levels int, dirsonly bool) map[string]interface{}
Completion(device protocol.DeviceID, folder string) float64
Completion(device protocol.DeviceID, folder string) model.FolderCompletion
Override(folder string)
NeedFolderFiles(folder string, page, perpage int) ([]db.FileInfoTruncated, []db.FileInfoTruncated, []db.FileInfoTruncated, int)
NeedSize(folder string) (nfiles int, bytes int64)
NeedSize(folder string) (nfiles, ndeletes int, bytes int64)
ConnectionStats() map[string]interface{}
DeviceStatistics() map[string]stats.DeviceStatistics
FolderStatistics() map[string]stats.FolderStatistics
@@ -90,8 +91,8 @@ type modelIntf interface {
ConnectedTo(deviceID protocol.DeviceID) bool
GlobalSize(folder string) (nfiles, deleted int, bytes int64)
LocalSize(folder string) (nfiles, deleted int, bytes int64)
CurrentLocalVersion(folder string) (int64, bool)
RemoteLocalVersion(folder string) (int64, bool)
CurrentSequence(folder string) (int64, bool)
RemoteSequence(folder string) (int64, bool)
State(folder string) (string, time.Time, error)
}
@@ -268,8 +269,12 @@ func (s *apiService) Serve() {
postRestMux.HandleFunc("/rest/system/debug", s.postSystemDebug) // [enable] [disable]
// Debug endpoints, not for general use
getRestMux.HandleFunc("/rest/debug/peerCompletion", s.getPeerCompletion)
getRestMux.HandleFunc("/rest/debug/httpmetrics", s.getSystemHTTPMetrics)
debugMux := http.NewServeMux()
debugMux.HandleFunc("/rest/debug/peerCompletion", s.getPeerCompletion)
debugMux.HandleFunc("/rest/debug/httpmetrics", s.getSystemHTTPMetrics)
debugMux.HandleFunc("/rest/debug/cpuprof", s.getCPUProf) // duration
debugMux.HandleFunc("/rest/debug/heapprof", s.getHeapProf)
getRestMux.Handle("/rest/debug/", s.whenDebugging(debugMux))
// A handler that splits requests between the two above and disables
// caching
@@ -308,6 +313,11 @@ func (s *apiService) Serve() {
// Add the CORS handling
handler = corsMiddleware(handler)
if addressIsLocalhost(guiCfg.Address()) && !guiCfg.InsecureSkipHostCheck {
// Verify source host
handler = localhostMiddleware(handler)
}
handler = debugMiddleware(handler)
srv := http.Server{
@@ -364,6 +374,9 @@ func (s *apiService) VerifyConfiguration(from, to config.Configuration) error {
}
func (s *apiService) CommitConfiguration(from, to config.Configuration) bool {
// No action required when this changes, so mask the fact that it changed at all.
from.GUI.Debugging = to.GUI.Debugging
if to.GUI == from.GUI {
return true
}
@@ -429,10 +442,12 @@ func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Process OPTIONS requests
if r.Method == "OPTIONS" {
// Add a generous access-control-allow-origin header for CORS requests
w.Header().Add("Access-Control-Allow-Origin", "*")
// Only GET/POST Methods are supported
w.Header().Set("Access-Control-Allow-Methods", "GET, POST")
// Only this custom header can be set
w.Header().Set("Access-Control-Allow-Headers", "X-API-Key")
// Only these headers can be set
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, X-API-Key")
// The request is meant to be cached 10 minutes
w.Header().Set("Access-Control-Max-Age", "600")
@@ -487,6 +502,28 @@ func withDetailsMiddleware(id protocol.DeviceID, h http.Handler) http.Handler {
})
}
func localhostMiddleware(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if addressIsLocalhost(r.Host) {
h.ServeHTTP(w, r)
return
}
http.Error(w, "Host check error", http.StatusForbidden)
})
}
func (s *apiService) whenDebugging(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if s.cfg.GUI().Debugging {
h.ServeHTTP(w, r)
return
}
http.Error(w, "Debugging disabled", http.StatusBadRequest)
})
}
func (s *apiService) restPing(w http.ResponseWriter, r *http.Request) {
sendJSON(w, map[string]string{"ping": "pong"})
}
@@ -563,8 +600,12 @@ func (s *apiService) getDBCompletion(w http.ResponseWriter, r *http.Request) {
return
}
sendJSON(w, map[string]float64{
"completion": s.model.Completion(device, folder),
comp := s.model.Completion(device, folder)
sendJSON(w, map[string]interface{}{
"completion": comp.CompletionPct,
"needBytes": comp.NeedBytes,
"globalBytes": comp.GlobalBytes,
"needDeletes": comp.NeedDeletes,
})
}
@@ -585,8 +626,8 @@ func folderSummary(cfg configIntf, m modelIntf, folder string) map[string]interf
localFiles, localDeleted, localBytes := m.LocalSize(folder)
res["localFiles"], res["localDeleted"], res["localBytes"] = localFiles, localDeleted, localBytes
needFiles, needBytes := m.NeedSize(folder)
res["needFiles"], res["needBytes"] = needFiles, needBytes
needFiles, needDeletes, needBytes := m.NeedSize(folder)
res["needFiles"], res["needDeletes"], res["needBytes"] = needFiles, needDeletes, needBytes
res["inSyncFiles"], res["inSyncBytes"] = globalFiles-needFiles, globalBytes-needBytes
@@ -596,10 +637,11 @@ func folderSummary(cfg configIntf, m modelIntf, folder string) map[string]interf
res["error"] = err.Error()
}
lv, _ := m.CurrentLocalVersion(folder)
rv, _ := m.RemoteLocalVersion(folder)
ourSeq, _ := m.CurrentSequence(folder)
remoteSeq, _ := m.RemoteSequence(folder)
res["version"] = lv + rv
res["version"] = ourSeq + remoteSeq // legacy
res["sequence"] = ourSeq + remoteSeq // new name
ignorePatterns, _, _ := m.GetIgnores(folder)
res["ignorePatterns"] = false
@@ -1121,7 +1163,7 @@ func (s *apiService) getPeerCompletion(w http.ResponseWriter, r *http.Request) {
for _, device := range folder.DeviceIDs() {
deviceStr := device.String()
if s.model.ConnectedTo(device) {
tot[deviceStr] += s.model.Completion(device, folder.ID)
tot[deviceStr] += s.model.Completion(device, folder.ID).CompletionPct
} else {
tot[deviceStr] = 0
}
@@ -1165,6 +1207,32 @@ func (s *apiService) getSystemBrowse(w http.ResponseWriter, r *http.Request) {
sendJSON(w, ret)
}
func (s *apiService) getCPUProf(w http.ResponseWriter, r *http.Request) {
duration, err := time.ParseDuration(r.FormValue("duration"))
if err != nil {
duration = 30 * time.Second
}
filename := fmt.Sprintf("syncthing-cpu-%s-%s-%s-%s.pprof", runtime.GOOS, runtime.GOARCH, Version, time.Now().Format("150405")) // hhmmss
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", "attachment; filename="+filename)
pprof.StartCPUProfile(w)
time.Sleep(duration)
pprof.StopCPUProfile()
}
func (s *apiService) getHeapProf(w http.ResponseWriter, r *http.Request) {
filename := fmt.Sprintf("syncthing-heap-%s-%s-%s-%s.pprof", runtime.GOOS, runtime.GOARCH, Version, time.Now().Format("150405")) // hhmmss
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", "attachment; filename="+filename)
runtime.GC()
pprof.WriteHeapProfile(w)
}
func (s *apiService) toNeedSlice(fs []db.FileInfoTruncated) []jsonDBFileInfo {
res := make([]jsonDBFileInfo, len(fs))
for i, f := range fs {
@@ -1186,8 +1254,8 @@ func (f jsonFileInfo) MarshalJSON() ([]byte, error) {
"deleted": f.Deleted,
"invalid": f.Invalid,
"noPermissions": f.NoPermissions,
"modified": time.Unix(f.Modified, 0),
"localVersion": f.LocalVersion,
"modified": protocol.FileInfo(f).ModTime(),
"sequence": f.Sequence,
"numBlocks": len(f.Blocks),
"version": jsonVersionVector(f.Version),
})
@@ -1204,8 +1272,8 @@ func (f jsonDBFileInfo) MarshalJSON() ([]byte, error) {
"deleted": f.Deleted,
"invalid": f.Invalid,
"noPermissions": f.NoPermissions,
"modified": time.Unix(f.Modified, 0),
"localVersion": f.LocalVersion,
"modified": db.FileInfoTruncated(f).ModTime(),
"sequence": f.Sequence,
})
}
@@ -1241,3 +1309,17 @@ func dirNames(dir string) []string {
sort.Strings(dirs)
return dirs
}
func addressIsLocalhost(addr string) bool {
host, _, err := net.SplitHostPort(addr)
if err != nil {
// There was no port, so we assume the address was just a hostname
host = addr
}
switch host {
case "127.0.0.1", "::1", "localhost":
return true
default:
return false
}
}

View File

@@ -37,6 +37,16 @@ func csrfMiddleware(unique string, prefix string, cfg config.GUIConfiguration, n
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Allow requests carrying a valid API key
if cfg.IsValidAPIKey(r.Header.Get("X-API-Key")) {
// Set the access-control-allow-origin header for CORS requests
// since a valid API key has been provided
w.Header().Add("Access-Control-Allow-Origin", "*")
next.ServeHTTP(w, r)
return
}
if strings.HasPrefix(r.URL.Path, "/rest/debug") {
// Debugging functions are only available when explicitly
// enabled, and can be accessed without a CSRF token
next.ServeHTTP(w, r)
return
}

View File

@@ -78,7 +78,7 @@ func trackCPUUsage() {
var prevTime = time.Now().UnixNano()
var rusage prusage_t
var pid = os.Getpid()
for _ = range time.NewTicker(time.Second).C {
for range time.NewTicker(time.Second).C {
err := solarisPrusage(pid, &rusage)
if err != nil {
l.Warnln("getting prusage:", err)

View File

@@ -16,6 +16,7 @@ import (
"net"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
"time"
@@ -460,7 +461,6 @@ func TestHTTPLogin(t *testing.T) {
if resp.StatusCode != http.StatusOK {
t.Errorf("Unexpected non-200 return code %d for authed request (ISO-8859-1)", resp.StatusCode)
}
}
func startHTTP(cfg *mockedConfig) (string, error) {
@@ -491,7 +491,12 @@ func startHTTP(cfg *mockedConfig) (string, error) {
if err != nil {
return "", fmt.Errorf("Weird address from API service: %v", err)
}
baseURL := fmt.Sprintf("http://127.0.0.1:%d", tcpAddr.Port)
host, _, _ := net.SplitHostPort(cfg.gui.RawAddress)
if host == "" || host == "0.0.0.0" {
host = "127.0.0.1"
}
baseURL := fmt.Sprintf("http://%s", net.JoinHostPort(host, strconv.Itoa(tcpAddr.Port)))
return baseURL, nil
}
@@ -666,3 +671,250 @@ func testConfigPost(data io.Reader) (*http.Response, error) {
req.Header.Set("X-API-Key", testAPIKey)
return cli.Do(req)
}
func TestHostCheck(t *testing.T) {
// An API service bound to localhost should reject non-localhost host Headers
cfg := new(mockedConfig)
cfg.gui.RawAddress = "127.0.0.1:0"
baseURL, err := startHTTP(cfg)
if err != nil {
t.Fatal(err)
}
// A normal HTTP get to the localhost-bound service should succeed
resp, err := http.Get(baseURL)
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Error("Regular HTTP get: expected 200 OK, not", resp.Status)
}
// A request with a suspicious Host header should fail
req, _ := http.NewRequest("GET", baseURL, nil)
req.Host = "example.com"
resp, err = http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusForbidden {
t.Error("Suspicious Host header: expected 403 Forbidden, not", resp.Status)
}
// A request with an explicit "localhost:8384" Host header should pass
req, _ = http.NewRequest("GET", baseURL, nil)
req.Host = "localhost:8384"
resp, err = http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Error("Explicit localhost:8384: expected 200 OK, not", resp.Status)
}
// A request with an explicit "localhost" Host header (no port) should pass
req, _ = http.NewRequest("GET", baseURL, nil)
req.Host = "localhost"
resp, err = http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Error("Explicit localhost: expected 200 OK, not", resp.Status)
}
// A server with InsecureSkipHostCheck set behaves differently
cfg = new(mockedConfig)
cfg.gui.RawAddress = "127.0.0.1:0"
cfg.gui.InsecureSkipHostCheck = true
baseURL, err = startHTTP(cfg)
if err != nil {
t.Fatal(err)
}
// A request with a suspicious Host header should be allowed
req, _ = http.NewRequest("GET", baseURL, nil)
req.Host = "example.com"
resp, err = http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Error("Incorrect host header, check disabled: expected 200 OK, not", resp.Status)
}
// A server bound to a wildcard address also doesn't do the check
cfg = new(mockedConfig)
cfg.gui.RawAddress = "0.0.0.0:0"
cfg.gui.InsecureSkipHostCheck = true
baseURL, err = startHTTP(cfg)
if err != nil {
t.Fatal(err)
}
// A request with a suspicious Host header should be allowed
req, _ = http.NewRequest("GET", baseURL, nil)
req.Host = "example.com"
resp, err = http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Error("Incorrect host header, wildcard bound: expected 200 OK, not", resp.Status)
}
// This should all work over IPv6 as well
cfg = new(mockedConfig)
cfg.gui.RawAddress = "[::1]:0"
baseURL, err = startHTTP(cfg)
if err != nil {
t.Fatal(err)
}
// A normal HTTP get to the localhost-bound service should succeed
resp, err = http.Get(baseURL)
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Error("Regular HTTP get (IPv6): expected 200 OK, not", resp.Status)
}
// A request with a suspicious Host header should fail
req, _ = http.NewRequest("GET", baseURL, nil)
req.Host = "example.com"
resp, err = http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusForbidden {
t.Error("Suspicious Host header (IPv6): expected 403 Forbidden, not", resp.Status)
}
// A request with an explicit "localhost:8384" Host header should pass
req, _ = http.NewRequest("GET", baseURL, nil)
req.Host = "localhost:8384"
resp, err = http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Error("Explicit localhost:8384 (IPv6): expected 200 OK, not", resp.Status)
}
}
func TestAddressIsLocalhost(t *testing.T) {
testcases := []struct {
address string
result bool
}{
// These are all valid localhost addresses
{"localhost", true},
{"::1", true},
{"127.0.0.1", true},
{"localhost:8080", true},
{"[::1]:8080", true},
{"127.0.0.1:8080", true},
// These are all non-localhost addresses
{"example.com", false},
{"example.com:8080", false},
{"192.0.2.10", false},
{"192.0.2.10:8080", false},
{"0.0.0.0", false},
{"0.0.0.0:8080", false},
{"::", false},
{"[::]:8080", false},
{":8080", false},
}
for _, tc := range testcases {
result := addressIsLocalhost(tc.address)
if result != tc.result {
t.Errorf("addressIsLocalhost(%q)=%v, expected %v", tc.address, result, tc.result)
}
}
}
func TestAccessControlAllowOriginHeader(t *testing.T) {
const testAPIKey = "foobarbaz"
cfg := new(mockedConfig)
cfg.gui.APIKey = testAPIKey
baseURL, err := startHTTP(cfg)
if err != nil {
t.Fatal(err)
}
cli := &http.Client{
Timeout: time.Second,
}
req, _ := http.NewRequest("GET", baseURL+"/rest/system/status", nil)
req.Header.Set("X-API-Key", testAPIKey)
resp, err := cli.Do(req)
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatal("GET on /rest/system/status should succeed, not", resp.Status)
}
if resp.Header.Get("Access-Control-Allow-Origin") != "*" {
t.Fatal("GET on /rest/system/status should return a 'Access-Control-Allow-Origin: *' header")
}
}
func TestOptionsRequest(t *testing.T) {
const testAPIKey = "foobarbaz"
cfg := new(mockedConfig)
cfg.gui.APIKey = testAPIKey
baseURL, err := startHTTP(cfg)
if err != nil {
t.Fatal(err)
}
cli := &http.Client{
Timeout: time.Second,
}
req, _ := http.NewRequest("OPTIONS", baseURL+"/rest/system/status", nil)
resp, err := cli.Do(req)
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusNoContent {
t.Fatal("OPTIONS on /rest/system/status should succeed, not", resp.Status)
}
if resp.Header.Get("Access-Control-Allow-Origin") != "*" {
t.Fatal("OPTIONS on /rest/system/status should return a 'Access-Control-Allow-Origin: *' header")
}
if resp.Header.Get("Access-Control-Allow-Methods") != "GET, POST" {
t.Fatal("OPTIONS on /rest/system/status should return a 'Access-Control-Allow-Methods: GET, POST' header")
}
if resp.Header.Get("Access-Control-Allow-Headers") != "Content-Type, X-API-Key" {
t.Fatal("OPTIONS on /rest/system/status should return a 'Access-Control-Allow-Headers: Content-Type, X-API-KEY' header")
}
}

View File

@@ -21,7 +21,7 @@ func trackCPUUsage() {
var prevUsage int64
var prevTime = time.Now().UnixNano()
var rusage syscall.Rusage
for _ = range time.NewTicker(time.Second).C {
for range time.NewTicker(time.Second).C {
syscall.Getrusage(syscall.RUSAGE_SELF, &rusage)
curTime := time.Now().UnixNano()
timeDiff := curTime - prevTime

View File

@@ -34,7 +34,7 @@ func trackCPUUsage() {
prevTime := ctime.Nanoseconds()
prevUsage := ktime.Nanoseconds() + utime.Nanoseconds() // Always overflows
for _ = range time.NewTicker(time.Second).C {
for range time.NewTicker(time.Second).C {
err := syscall.GetProcessTimes(handle, &ctime, &etime, &ktime, &utime)
if err != nil {
continue

View File

@@ -16,7 +16,6 @@ import (
"log"
"net"
"net/http"
_ "net/http/pprof"
"net/url"
"os"
"os/signal"
@@ -664,8 +663,14 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
}
}
if cfg.Raw().OriginalVersion == 15 {
// The config version 15->16 migration is about handling ignores and
// delta indexes and requires that we drop existing indexes that
// have been incorrectly ignore filtered.
ldb.DropDeltaIndexIDs()
}
m := model.NewModel(cfg, myID, myDeviceName(cfg), "syncthing", Version, ldb, protectedFiles)
cfg.Subscribe(m)
if t := os.Getenv("STDEADLOCKTIMEOUT"); len(t) > 0 {
it, err := strconv.Atoi(t)
@@ -884,19 +889,32 @@ func loadOrCreateConfig() *config.Wrapper {
}
func archiveAndSaveConfig(cfg *config.Wrapper) error {
// To prevent previous config from being cleaned up, quickly touch it too
now := time.Now()
_ = os.Chtimes(cfg.ConfigPath(), now, now) // May return error on Android etc; no worries
// Copy the existing config to an archive copy
archivePath := cfg.ConfigPath() + fmt.Sprintf(".v%d", cfg.Raw().OriginalVersion)
l.Infoln("Archiving a copy of old config file format at:", archivePath)
if err := osutil.Rename(cfg.ConfigPath(), archivePath); err != nil {
if err := copyFile(cfg.ConfigPath(), archivePath); err != nil {
return err
}
// Do a regular atomic config sve
return cfg.Save()
}
func copyFile(src, dst string) error {
bs, err := ioutil.ReadFile(src)
if err != nil {
return err
}
if err := ioutil.WriteFile(dst, bs, 0600); err != nil {
// Attempt to clean up
os.Remove(dst)
return err
}
return nil
}
func startAuditing(mainService *suture.Supervisor) {
auditFile := timestampedLoc(locAuditLog)
fd, err := os.OpenFile(auditFile, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
@@ -941,7 +959,7 @@ func defaultConfig(myName string) config.Configuration {
if !noDefaultFolder {
l.Infoln("Default folder created and/or linked to new config")
folderID := rand.String(5) + "-" + rand.String(5)
folderID := strings.ToLower(rand.String(5) + "-" + rand.String(5))
defaultFolder = config.NewFolderConfiguration(folderID, locations[locDefFolder])
defaultFolder.Label = "Default Folder (" + folderID + ")"
defaultFolder.RescanIntervalS = 60
@@ -1119,13 +1137,16 @@ func autoUpgrade(cfg *config.Wrapper) {
// suitable time after they have gone out of fashion.
func cleanConfigDirectory() {
patterns := map[string]time.Duration{
"panic-*.log": 7 * 24 * time.Hour, // keep panic logs for a week
"audit-*.log": 7 * 24 * time.Hour, // keep audit logs for a week
"index": 14 * 24 * time.Hour, // keep old index format for two weeks
"index*.converted": 14 * 24 * time.Hour, // keep old converted indexes for two weeks
"config.xml.v*": 30 * 24 * time.Hour, // old config versions for a month
"*.idx.gz": 30 * 24 * time.Hour, // these should for sure no longer exist
"backup-of-v0.8": 30 * 24 * time.Hour, // these neither
"panic-*.log": 7 * 24 * time.Hour, // keep panic logs for a week
"audit-*.log": 7 * 24 * time.Hour, // keep audit logs for a week
"index": 14 * 24 * time.Hour, // keep old index format for two weeks
"index-v0.11.0.db": 14 * 24 * time.Hour, // keep old index format for two weeks
"index-v0.13.0.db": 14 * 24 * time.Hour, // keep old index format for two weeks
"index*.converted": 14 * 24 * time.Hour, // keep old converted indexes for two weeks
"config.xml.v*": 30 * 24 * time.Hour, // old config versions for a month
"*.idx.gz": 30 * 24 * time.Hour, // these should for sure no longer exist
"backup-of-v0.8": 30 * 24 * time.Hour, // these neither
"tmp-index-sorter.*": time.Minute, // these should never exist on startup
}
for pat, dur := range patterns {

View File

@@ -20,9 +20,8 @@ var (
func memorySize() (int64, error) {
var memoryStatusEx [64]byte
binary.LittleEndian.PutUint32(memoryStatusEx[:], 64)
p := uintptr(unsafe.Pointer(&memoryStatusEx[0]))
ret, _, callErr := syscall.Syscall(uintptr(globalMemoryStatusEx), 1, p, 0, 0)
ret, _, callErr := syscall.Syscall(uintptr(globalMemoryStatusEx), 1, uintptr(unsafe.Pointer(&memoryStatusEx[0])), 0, 0)
if ret == 0 {
return 0, callErr
}

View File

@@ -21,8 +21,8 @@ func (m *mockedModel) GlobalDirectoryTree(folder, prefix string, levels int, dir
return nil
}
func (m *mockedModel) Completion(device protocol.DeviceID, folder string) float64 {
return 0
func (m *mockedModel) Completion(device protocol.DeviceID, folder string) model.FolderCompletion {
return model.FolderCompletion{}
}
func (m *mockedModel) Override(folder string) {}
@@ -31,8 +31,8 @@ func (m *mockedModel) NeedFolderFiles(folder string, page, perpage int) ([]db.Fi
return nil, nil, nil, 0
}
func (m *mockedModel) NeedSize(folder string) (nfiles int, bytes int64) {
return 0, 0
func (m *mockedModel) NeedSize(folder string) (nfiles, ndeletes int, bytes int64) {
return 0, 0, 0
}
func (m *mockedModel) ConnectionStats() map[string]interface{} {
@@ -103,11 +103,11 @@ func (m *mockedModel) LocalSize(folder string) (nfiles, deleted int, bytes int64
return 0, 0, 0
}
func (m *mockedModel) CurrentLocalVersion(folder string) (int64, bool) {
func (m *mockedModel) CurrentSequence(folder string) (int64, bool) {
return 0, false
}
func (m *mockedModel) RemoteLocalVersion(folder string) (int64, bool) {
func (m *mockedModel) RemoteSequence(folder string) (int64, bool) {
return 0, false
}

View File

@@ -207,9 +207,11 @@ func (c *folderSummaryService) sendSummary(folder string) {
// remote device.
comp := c.model.Completion(devCfg.DeviceID, folder)
events.Default.Log(events.FolderCompletion, map[string]interface{}{
"folder": folder,
"device": devCfg.DeviceID.String(),
"completion": comp,
"folder": folder,
"device": devCfg.DeviceID.String(),
"completion": comp.CompletionPct,
"needBytes": comp.NeedBytes,
"globalBytes": comp.GlobalBytes,
})
}
}

View File

@@ -9,7 +9,6 @@ package main
import (
"bytes"
"crypto/rand"
"crypto/sha256"
"crypto/tls"
"encoding/json"
"fmt"
@@ -19,6 +18,8 @@ import (
"strings"
"time"
"github.com/minio/sha256-simd"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/dialer"
"github.com/syncthing/syncthing/lib/model"

View File

@@ -1,5 +0,0 @@
{{.name}} ({{.version}}); urgency=medium
* Packaging of {{.version}}.
-- Syncthing Release Management <release@syncthing.net> {{.date}}

View File

@@ -1 +0,0 @@
9

View File

@@ -1,16 +0,0 @@
Package: syncthing
Version: {{.version}}
Priority: optional
Section: net
Architecture: {{.arch}}
Depends: libc6, procps
Homepage: https://syncthing.net/
Maintainer: Syncthing Release Management <release@syncthing.net>
Description: Open Source Continuous File Synchronization
Syncthing is an application that lets you synchronize your files across
multiple devices. This means the creation, modification or deletion of files
on one machine will automatically be replicated to your other devices. We
believe your data is your data alone and you deserve to choose where it is
stored. Therefore Syncthing does not upload your data to the cloud but
exchanges your data across your machines as soon as they are online at the
same time.

View File

@@ -1,6 +0,0 @@
#!/bin/bash
set -euo pipefail
if [[ ${1:-} == configure ]]; then
pkill -HUP -x syncthing || true
fi

View File

@@ -5,11 +5,13 @@ Dark theme
Author: alessandro.g89
Source: https://userstyles.org/styles/122502/syncthing-dark
Modified by: fti7
**/
body {
color: #aaa !important;
background-color: black !important;
background-color: #272727 !important;
}
a:hover,a:focus,a.focus{
@@ -20,8 +22,10 @@ a:hover,a:focus,a.focus{
/* navbar */
.navbar {
background-color: #333 !important;
border-color: #333 !important;
border-width: 2px !important;
border-color: #424242 !important;
border-top-width: 1px !important;
border-bottom-width: 1px !important;
}
.navbar-text, .dropdown>a, .dropdown-menu>li>a, .hidden-xs>a, .navbar-link {
@@ -29,7 +33,7 @@ a:hover,a:focus,a.focus{
}
.dropdown-menu {
border-color: #333 !important;
border-color: #424242 !important;
border-width: 2px !important;
background-color: #222 !important;
}
@@ -40,7 +44,7 @@ a:hover,a:focus,a.focus{
}
.open>.dropdown-toggle, .dropdown-toggle:hover {
border-color: #333 !important;
border-color: #424242 !important;
background-color: #222 !important;
}
@@ -51,7 +55,7 @@ a:hover,a:focus,a.focus{
li.hidden-xs:hover, .navbar-link:hover, .navbar-link:focus {
outline: none !important;
border-color: #333 !important;
border-color: #424242 !important;
background-color: #222 !important;
}
@@ -63,18 +67,18 @@ li.hidden-xs:hover, .navbar-link:hover, .navbar-link:focus {
/* main panel */
.panel {
background-color: #111 !important;
background-color: #323232 !important;
border-width: 2px !important;
}
.panel-default {
border-color: #222 !important;
border-color: #424242 !important;
}
.panel-default > .panel-heading {
color: #aaa !important;
border-color: #222 !important;
background-color: #222 !important;
background-color: #3B3B3B !important;
}
.panel-warning > .panel-heading {
color: #222 !important;
@@ -85,16 +89,16 @@ li.hidden-xs:hover, .navbar-link:hover, .navbar-link:focus {
}
.panel-footer {
background-color: #111 !important;
background-color: #2D2D2D !important;
border-width: 0 !important;
}
.table-striped>tbody>tr:nth-of-type(odd) {
background-color: #181818 !important;
background-color: #2E2E2E !important;
}
.panel-group .panel-heading+.panel-collapse>.panel-body, .panel-group .panel-heading+.panel-collapse>.list-group {
border-top: 1px solid #222 !important;
border-top: 1px solid #424242 !important;
}
.identicon rect {
@@ -156,11 +160,11 @@ li.hidden-xs:hover, .navbar-link:hover, .navbar-link:focus {
/* modal dialogs */
.modal-header {
border-bottom-color: #222 !important;
border-bottom-color: #424242 !important;
}
.modal-header:not(.alert) {
background-color: #222;
background-color: #3B3B3B;
}
.alert-info {
@@ -179,12 +183,12 @@ li.hidden-xs:hover, .navbar-link:hover, .navbar-link:focus {
.modal-content {
border-color: #666 !important;
border-width: 2px !important;
background-color: #111 !important;
background-color: #272727 !important;
}
.modal-footer {
border-color: #111 !important;
background-color: #111 !important;
border-color: #303030 !important;
background-color: #2D2D2D !important;
}
.help-block {
@@ -193,8 +197,8 @@ li.hidden-xs:hover, .navbar-link:hover, .navbar-link:focus {
.form-control {
color: #aaa !important;
border-color: #444 !important;
background-color: black !important;
border-color: #424242 !important;
background-color: #3B3B3B !important;
}
code.ng-binding{
@@ -204,8 +208,8 @@ code.ng-binding{
.well, .form-control[readonly="readonly"], .popover { /* read-only fields*/
color: #666 !important;
border-color: #444 !important;
background-color: #111 !important;
border-color: #424242 !important;
background-color: #3B3B3B !important;
}
/* buttons for pagination */

View File

@@ -32,7 +32,7 @@
"Comment, when used at the start of a line": "Comentario, cuando es utilizado al inicio de una línea.",
"Compression": "Compresión",
"Connection Error": "Error de conexión",
"Connection Type": "Connection Type",
"Connection Type": "Tipo de conexión",
"Copied from elsewhere": "Copiado desde otra parte.",
"Copied from original": "Copiado del original",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 los siguientes contribuidores:",
@@ -40,7 +40,7 @@
"Danger!": "Peligro!",
"Delete": "Suprimir",
"Deleted": "Suprimido",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Device \"{{name}}\" ({{device}} at {{address}}) wants to connect. Add new device?",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Dispositivo \"{{name}}\" ({{device}} en {{address}}) quiere conectar. ¿Añadir nuevo dispositivo?",
"Device ID": "ID del dispositivo",
"Device Identification": "Identificación del dispositivo",
"Device Name": "Nombre del dispositivo",
@@ -56,10 +56,10 @@
"Edit Device": "Editar dispositivo",
"Edit Folder": "Editar repositorio",
"Editing": "Editando",
"Enable NAT traversal": "Enable NAT traversal",
"Enable Relaying": "Enable Relaying",
"Enable NAT traversal": "Habilitar NAT trasversal",
"Enable Relaying": "Habilitar Retransmisión",
"Enable UPnP": "Permitir UPnP",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Introduce las direcciones (\"tcp://ip:port\", \"tcp://host:port\") separadas por comas o \"dynamic\" para ejecutar un descubrimiento automático de la dirección. ",
"Enter ignore patterns, one per line.": "Añadir patrones de exclusión, uno por línea.",
"Error": "Error",
"External File Versioning": "Control de versiones externo",
@@ -72,10 +72,10 @@
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Los archivos están protegidos frente a los cambios realizados en otros dispositivos, peros los cambios realizados en este dispositivo serán envíados al resto del grupo",
"Folder": "Carpeta",
"Folder ID": "ID del repositorio",
"Folder Label": "Folder Label",
"Folder Label": "Etiqueta de Carpeta",
"Folder Master": "Repositorio maestro",
"Folder Path": "Ruta del repositorio",
"Folder Type": "Folder Type",
"Folder Type": "Tipo de Carpeta",
"Folders": "Repositorios",
"GUI": "GUI",
"GUI Authentication Password": "Contraseña de autenticación de la GUI",
@@ -84,7 +84,7 @@
"Generate": "Generar",
"Global Discovery": "Búsqueda en internet",
"Global Discovery Server": "Servidor global de identificación",
"Global Discovery Servers": "Global Discovery Servers",
"Global Discovery Servers": "Servidores globales de identificación",
"Global State": "Estado global",
"Help": "Ayuda",
"Home page": "Pagina de inicio",
@@ -98,15 +98,15 @@
"Keep Versions": "Conservar versiones",
"Largest First": "Más grande primero",
"Last File Received": "Último archivo recibido",
"Last Scan": "Last Scan",
"Last Scan": "Último escaneo",
"Last seen": "Visto por ultima vez",
"Later": "Más tarde",
"Listeners": "Listeners",
"Listeners": "Receptor",
"Local Discovery": "Búsqueda en red local",
"Local State": "Estado local",
"Local State (Total)": "Estado local (total)",
"Major Upgrade": "Actualización mayor",
"Master": "Master",
"Master": "Maestro",
"Maximum Age": "Edad máxima",
"Metadata Only": "Sólo metadatos",
"Minimum Free Disk Space": "Espacio mínimo libre en disco",
@@ -123,7 +123,7 @@
"OK": "OK",
"Off": "Apagado",
"Oldest First": "Antiguo primero",
"Optional descriptive label for the folder. Can be different on each device.": "Optional descriptive label for the folder. Can be different on each device.",
"Optional descriptive label for the folder. Can be different on each device.": "Etiqueta descriptiva opcional para la carpeta. Puede ser diferente en cada dispositivo.",
"Options": "Opciones",
"Out of Sync": "Fuera de sincronización",
"Out of Sync Items": "Ítems no sincronizados",
@@ -134,20 +134,20 @@
"Pause": "Pausa",
"Paused": "En pausa",
"Please consult the release notes before performing a major upgrade.": "Por favor consulta las notas de lanzamiento antes de realizar una actualizacón mayor.",
"Please set a GUI Authentication User and Password in the Settings dialog.": "Please set a GUI Authentication User and Password in the Settings dialog.",
"Please set a GUI Authentication User and Password in the Settings dialog.": "Por favor, establece un Usuario y Contraseña de Autenticación en la GUI en el diálogo de Configuración",
"Please wait": "Aguarde por favor",
"Preview": "Vista previa",
"Preview Usage Report": "Ver reporte de uso",
"Quick guide to supported patterns": "Guía rápida sobre los patrones soportados",
"RAM Utilization": "Utilización de RAM",
"Random": "Aleatorio",
"Relay Servers": "Relay Servers",
"Relay Servers": "Servidores de Retransmisión",
"Relayed via": "retransmitida vía",
"Relays": "Retransmisores",
"Release Notes": "Notas de lanzamiento",
"Remote Devices": "Dispositivos Remotos",
"Remove": "Eliminar",
"Required identifier for the folder. Must be the same on all cluster devices.": "Required identifier for the folder. Must be the same on all cluster devices.",
"Required identifier for the folder. Must be the same on all cluster devices.": "Identificación requerida para la carpeta. Debe ser la misma en todos los dispositivos del grupo.",
"Rescan": "Reescanear",
"Rescan All": "Reescanear todo",
"Rescan Interval": "Intervalo de reescaneo",
@@ -193,11 +193,11 @@
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing parece estar apagado, o hay un problema con su 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 parece estar experimentando un problema al procesar su solicitud. Por favor, recargue el navegador o reinicie Syncthing si el problema persiste.",
"The Syncthing admin interface is configured to allow remote access without a password.": "La interfaz administrativa del Syncthing está configurada para permitir acceso remoto sin una contraseña.",
"The aggregated statistics are publicly available at the URL below.": "The aggregated statistics are publicly available at the URL below.",
"The aggregated statistics are publicly available at the URL below.": "Las estadísticas agregadas están disponibles públicamente en la dirección de abajo.",
"The aggregated statistics are publicly available at {%url%}.": "Las estadísticas acumuladas están disponibles públicamente en {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configuración ha sido guardada pero no activada.\nSyncthing debe reiniciarse para activar la nueva configuración.",
"The device ID cannot be blank.": "La ID del dispositivo no puede estar en blanco.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "La ID de dispositivo a introducir ser puede encontrar en el menú \"Acciones > Mostrar ID\" en el otro dispositivo. Espacios y guiones son opcionales (ignorados). ",
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "La ID del dispositivo a introducir se puede encontrar en la opción de menú \"Edición > Mostrar ID\" en el otro dispositivo. Espacios y guiones son opcionales (ignorados).",
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "El informe de uso se envía encriptado diariamente. Se utiliza para hacer un seguimiento de plataformas comunes, tamaño de repositorios y versiones de la aplicación. Si el conjunto de datos cambia será notificado mediante este dialogo nuevamente.",
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "La ID del dispositivo introducida no es válida. Debe ser una cadena de 52 o 56 caracteres consistente en letras y números, con espacios y guiones opcionales.",
@@ -210,7 +210,7 @@
"The following items could not be synchronized.": "Los siguientes artículos no pueden ser sincronizados.",
"The maximum age must be a number and cannot be blank.": "La edad máxima debe ser un número y no puede estar en blanco.",
"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, establece en 0 para mantener versiones para siempre).",
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).",
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "El porcentaje de espacio libre en disco mínimo debe ser un número no negativo entre 0 y 100 (incluidos).",
"The number of days must be a number and cannot be blank.": "El número de días debe ser un número y no puede estar vacío.",
"The number of days to keep files in the trash can. Zero means forever.": "El tiempo máximo para mantener un archivo en el cubo de basura (en días, establece en 0 para mantener versiones para siempre).",
"The number of old versions to keep, per file.": "El numero de versiones anteriores a conservar, por archivo.",
@@ -247,5 +247,5 @@
"items": "ítems",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} quiere compartir repositorio \"{{folder}}\".",
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} qiuere compartir el repositorio \"{{folderLabel}}\" ({{folder}}).",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} quiere compartir la carpeta \"{{folderlabel}}\" ({{folder}})."
}

View File

@@ -1,14 +1,14 @@
{
"A device with that ID is already added.": "La machine portant cette ID est déjà présente.",
"A negative number of days doesn't make sense.": "Un nombre négatif de jours n'a pas de sens.",
"A device with that ID is already added.": "L'appareil portant cette ID est déjà présent.",
"A negative number of days doesn't make sense.": "Ce champ n'accepte qu'un entier positif ou nul.",
"A new major version may not be compatible with previous versions.": "Une nouvelle version majeure peut présenter des incompatibilités avec les versions antérieures.",
"API Key": "Clé API",
"About": "À propos",
"Actions": "Actions",
"Add": "Ajouter",
"Add Device": "Ajouter une machine",
"Add Device": "Ajouter un appareil",
"Add Folder": "Ajouter un partage",
"Add Remote Device": "Ajouter une machine",
"Add Remote Device": "Ajouter un appareil distant",
"Add new folder?": "Ajouter un nouveau partage ?",
"Address": "Adresse",
"Addresses": "Adresses",
@@ -16,17 +16,17 @@
"Advanced Configuration": "Configuration avancée",
"Advanced settings": "Réglages experts",
"All Data": "Toutes les données",
"Allow Anonymous Usage Reporting?": "Autoriser le rapport anonyme de statistiques d'utilisation ?",
"Allow Anonymous Usage Reporting?": "Autoriser l'envoi de statistiques d'utilisation anonymisées ?",
"Alphabetic": "Alphabétique",
"An external command handles the versioning. It has to remove the file from the synced folder.": "Une commande externe gère les versions de fichiers. Elle supprime les fichiers dans le répertoire synchronisé.",
"Anonymous Usage Reporting": "Rapport anonyme de statistiques d'utilisation",
"Any devices configured on an introducer device will be added to this device as well.": "Toute machine ajoutée depuis une machine introductrice sera aussi ajoutée sur cette machine.",
"Any devices configured on an introducer device will be added to this device as well.": "Tout appareil ajouté depuis un appareil introducteur sera aussi ajouté sur votre appareil.",
"Automatic upgrades": "Mises à jour automatiques",
"Be careful!": "Faites attention !",
"Bugs": "Bugs",
"CPU Utilization": "Utilisation du CPU",
"Changelog": "Historique des changements",
"Clean out after": "Nettoyer après",
"Clean out after": "Purger après :",
"Close": "Fermer",
"Command": "Commande",
"Comment, when used at the start of a line": "Commentaire lorsque utilisé en début de ligne",
@@ -40,10 +40,10 @@
"Danger!": "Attention !",
"Delete": "Supprimer",
"Deleted": "Supprimé",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "La machine \"{{name}}\" ({{device}} à l'IP {{address}}) souhaite se connecter. L'acceptez-vous ?",
"Device ID": "ID de la machine",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "L'appareil \"{{name}}\" ({{device}} à l'IP {{address}}) souhaite se connecter. L'acceptez-vous ?",
"Device ID": "ID de l'appareil",
"Device Identification": "Identification de l'appareil",
"Device Name": "Nom de la machine",
"Device Name": "Nom de l'appareil",
"Device {%device%} ({%address%}) wants to connect. Add new device?": "L'appareil {{device}} ({{address}}) veut se connecter. Voulez-vous ajouter cette appareil ?",
"Devices": "Appareil",
"Disconnected": "Déconnecté",
@@ -53,7 +53,7 @@
"Downloaded": "Téléchargé",
"Downloading": "En cours de téléchargement",
"Edit": "Modifier",
"Edit Device": "Modifier la machine",
"Edit Device": "Modifier l'appareil",
"Edit Folder": "Modifier le partage",
"Editing": "Édition",
"Enable NAT traversal": "Activer transfert d'adresses NAT",
@@ -89,11 +89,11 @@
"Help": "Aide",
"Home page": "Page d'accueil",
"Ignore": "Ignorer",
"Ignore Patterns": "Masques d'exclusion",
"Ignore Patterns": "Règles d'exclusion",
"Ignore Permissions": "Ignorer les permissions",
"Incoming Rate Limit (KiB/s)": "Limite du débit entrant (KiB/s)",
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Une configuration incorrecte peut créer des dommages dans vos répertoires et mettre Syncthing hors-service.",
"Introducer": "Initiateur",
"Introducer": "Introducteur",
"Inversion of the given condition (i.e. do not exclude)": "Inverser la condition donnée (i.e. ne pas exclure)",
"Keep Versions": "Conserver les versions",
"Largest First": "Les plus volumineux en premier",
@@ -117,13 +117,13 @@
"New Folder": "Nouveau partage",
"Newest First": "Les plus récents en premier",
"No": "Non",
"No File Versioning": "Pas de version de fichier",
"No File Versioning": "Pas de préservation",
"Normal": "Normal",
"Notice": "Notification",
"OK": "OK",
"Off": "Éteint",
"Oldest First": "Les plus anciens en premier",
"Optional descriptive label for the folder. Can be different on each device.": "Étiquette conviviale pour le partage. Elle peut être différente sur chaque machine.",
"Optional descriptive label for the folder. Can be different on each device.": "Étiquette conviviale pour le partage. Elle peut être différente sur chaque appareil.",
"Options": "Options",
"Out of Sync": "Désynchronisé",
"Out of Sync Items": "Objets non synchronisés",
@@ -145,7 +145,7 @@
"Relayed via": "Relayée par",
"Relays": "Relais",
"Release Notes": "Notes de version",
"Remote Devices": "Machines distantes",
"Remote Devices": "Appareils distants",
"Remove": "Enlever",
"Required identifier for the folder. Must be the same on all cluster devices.": "Identifiant obligatoire du partage. Il doit être identique chez chaque participant.",
"Rescan": "Réanalyser",
@@ -154,32 +154,32 @@
"Restart": "Redémarrer",
"Restart Needed": "Redémarrage nécessaire",
"Restarting": "Redémarrage",
"Resume": "Résumer",
"Resume": "Reprise",
"Reused": "Réutilisé",
"Save": "Sauver",
"Scan Time Remaining": "Temps d'analyse restant",
"Scanning": "Analyse en cours",
"Select the devices to share this folder with.": "Sélectionner les machines invitées à ce partage.",
"Select the folders to share with this device.": "Sélectionner les partages auxquels cette machine participe.",
"Select the devices to share this folder with.": "Synchroniser avec :",
"Select the folders to share with this device.": "Sélectionner les partages auxquels participe cet appareil.",
"Settings": "Configuration",
"Share": "Partager",
"Share Folder": "Partager",
"Share Folders With Device": "Partages avec cette machine",
"Share With Devices": "Partage avec des appareils",
"Share Folders With Device": "Partages avec cet appareil",
"Share With Devices": "Synchroniser avec des appareils",
"Share this folder?": "Acceptez-vous ce partage ?",
"Shared With": "Partagé avec",
"Short identifier for the folder. Must be the same on all cluster devices.": "Identifiant court du partage. Il sera le même sur l'ensemble des machines du groupe.",
"Shared With": "Synchronisé avec",
"Short identifier for the folder. Must be the same on all cluster devices.": "Identifiant court du partage. Il sera le même sur l'ensemble des appareils du groupe.",
"Show ID": "Afficher mon ID",
"Show QR": "Afficher l'image QR",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Affiché à la place de l'ID de l'appareil dans le groupe. Sera proposé aux autres appareils comme nom optionnel par défaut.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Affiché à la place de l'ID de l'appareil dans le groupe. Si laissé vide, il sera mis à jour par le nom proposé par l'appareil distant.",
"Shutdown": "Arrêter",
"Shutdown Complete": "Arrêté !",
"Simple File Versioning": "Suivi simple des versions de fichier",
"Simple File Versioning": "Suivi simple des versions de fichiers",
"Single level wildcard (matches within a directory only)": "Astérisque à un seul niveau (correspond uniquement à lintérieur du répertoire)",
"Smallest First": "Les plus petits en premier",
"Source Code": "Code source",
"Staggered File Versioning": "Versions échelonnées de fichier",
"Staggered File Versioning": "Versions échelonnées de fichiers",
"Start Browser": "Démarrer le navigateur web",
"Statistics": "Statistiques",
"Stopped": "Arrêté",
@@ -197,8 +197,8 @@
"The aggregated statistics are publicly available at {%url%}.": "Les statistiques agrégées sont disponibles publiquement à l'adresse {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configuration a été enregistrée mais pas activée. Syncthing doit redémarrer afin d'activer la nouvelle configuration.",
"The device ID cannot be blank.": "L'ID de l'appareil ne peut être vide.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "L'ID de machine à saisir ici se trouve dans le menu \"Actions > Afficher mon ID\" de la machine distante. Les tirets et espaces sont optionnels (et ignorés).",
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "L'ID de machine à entrer se trouve dans le menu \"Éditer > Montrer l'ID\" de la machine distante. Les espaces et les tirets sont optionnels (ils seront ignorés).",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "L'ID d'appareil à saisir ici se trouve dans le menu \"Actions > Afficher mon ID\" sur l'appareil distant. Les tirets et espaces sont optionnels (et ignorés).",
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "L'ID de machine à saisir ici se trouve dans le menu \"Actions > Afficher mon ID\" de l'appareil distant. Les tirets et espaces sont optionnels (et ignorés).",
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Le rapport d'utilisation chiffré est envoyé quotidiennement. Il sert à répertorier les plateformes utilisées, la taille des partages et les versions de l'application. Si les données rapportées sont modifiées cette boite de dialogue vous redemandera votre confirmation.",
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "L'ID de l'appareil inséré ne semble pas être valide. Il devrait ressembler à une chaîne de 52 ou 56 caractères comprenant des lettres, des chiffres et potentiellement des espaces et des traits d'union.",
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "Le premier paramètre de ligne de commande est le chemin du répertoire partagé, et le second est le chemin relatif dans le répertoire.",
@@ -219,10 +219,10 @@
"The rate limit must be a non-negative number (0: no limit)": "La limite de débit ne doit pas être négative (0: Aucune limite)",
"The rescan interval must be a non-negative number of seconds.": "L'intervalle d'analyse ne doit pas être un nombre négatif de secondes.",
"They are retried automatically and will be synced when the error is resolved.": "Ils seront réessayés automatiquement et synchronisés quand l'erreur sera résolue.",
"This Device": "Cette machine",
"This Device": "Cet appareil",
"This can easily give hackers access to read and change any files on your computer.": "Ceci peut aisément permettre à un intrus de lire et modifier n'importe quel fichier de votre ordinateur. ",
"This is a major version upgrade.": "Ceci est une mise à jour majeure.",
"Trash Can File Versioning": "Gestion des versions de fichier style poubelle.",
"Trash Can File Versioning": "Style poubelle.",
"Unknown": "Inconnu",
"Unshared": "Non partagé",
"Unused": "Non utilisé",
@@ -239,7 +239,7 @@
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Les versions seront supprimées automatiquement, si elles dépassent la durée maximum de conservation, ou si leur nombre est supérieur à la valeur autorisée dans l'intervalle.",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Attention, ce chemin est un sous-répertoire du partage existant \"{{otherFolder}}\". Ceci peut causer des problèmes tels que duplications de fichiers ou suppressions intempestives sur les autres machines.",
"When adding a new device, keep in mind that this device must be added on the other side too.": "Lorsqu'un appareil est ajouté, gardez à l'esprit que cet appareil doit aussi être ajouté de l'autre coté.",
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Lorsqu'un nouveau partage est ajouté, gardez à l'esprit que son ID est utilisée pour lier les répertoires à travers les machines. L'ID est sensible à la casse et sera forcément la même sur toutes les machines participant à ce partage.",
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Lorsqu'un nouveau partage est ajouté, gardez à l'esprit que son ID est utilisée pour lier les répertoires à travers les appareils. L'ID est sensible à la casse et sera forcément la même sur toutes les appareils participant à ce partage.",
"Yes": "Oui",
"You must keep at least one version.": "Vous devez garder au minimum une version.",
"days": "Jours",

View File

@@ -1,32 +1,32 @@
{
"A device with that ID is already added.": "Une machine avec cet ID a déjà été ajoutée.",
"A negative number of days doesn't make sense.": "Un nombre négatif de jours n'a pas de sens.",
"A device with that ID is already added.": "L'appareil portant cette ID est déjà présent.",
"A negative number of days doesn't make sense.": "Ce champ n'accepte qu'un entier positif ou nul.",
"A new major version may not be compatible with previous versions.": "Une nouvelle version majeure peut présenter des incompatibilités avec les versions antérieures.",
"API Key": "Clé API",
"About": "À propos",
"Actions": "Actions",
"Add": "Ajouter",
"Add Device": "Ajouter une machine",
"Add Folder": "Ajouter un dossier",
"Add Remote Device": "Ajouter une machine distante",
"Add new folder?": "Ajouter un nouveau dossier ?",
"Add Device": "Ajouter un appareil",
"Add Folder": "Ajouter un partage",
"Add Remote Device": "Ajouter un appareil distant",
"Add new folder?": "Ajouter un nouveau partage ?",
"Address": "Adresse",
"Addresses": "Adresses",
"Advanced": "Avancé",
"Advanced Configuration": "Configuration avancée",
"Advanced settings": "Configuration avancée",
"All Data": "Toutes les données",
"Allow Anonymous Usage Reporting?": "Autoriser le rapport anonyme de statistiques d'utilisation ?",
"Allow Anonymous Usage Reporting?": "Autoriser l'envoi de statistiques d'utilisation anonymisées ?",
"Alphabetic": "Alphabétique",
"An external command handles the versioning. It has to remove the file from the synced folder.": "Une commande externe gère les versions de fichiers. Elle supprime les fichiers dans le dossier synchronisé.",
"An external command handles the versioning. It has to remove the file from the synced folder.": "Une commande externe gère les versions de fichiers. Elle supprime les fichiers dans le répertoire synchronisé.",
"Anonymous Usage Reporting": "Rapport anonyme de statistiques d'utilisation",
"Any devices configured on an introducer device will be added to this device as well.": "Toute machine ajoutée depuis une machine initiatrice sera aussi ajoutée sur cette machine.",
"Any devices configured on an introducer device will be added to this device as well.": "Tout appareil ajouté depuis un appareil introducteur sera aussi ajouté sur cet appareil.",
"Automatic upgrades": "Mises à jour automatiques",
"Be careful!": "Faites attention !",
"Bugs": "Bugs",
"CPU Utilization": "Utilisation du CPU",
"Changelog": "Historique des changements",
"Clean out after": "Nettoyer après",
"Clean out after": "Purger après :",
"Close": "Fermer",
"Command": "Commande",
"Comment, when used at the start of a line": "Commentaire lorsque utilisé en début de ligne",
@@ -40,12 +40,12 @@
"Danger!": "Attention !",
"Delete": "Supprimer",
"Deleted": "Supprimé",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "La machine \"{{name}}\" ({{device}} à {{address}}) veut se connecter. L'acceptez-vous ?",
"Device ID": "ID de la machine",
"Device Identification": "Identifiant de la machine",
"Device Name": "Nom de la machine",
"Device {%device%} ({%address%}) wants to connect. Add new device?": "La machine {{device}} ({{address}}) veut se connecter. Voulez-vous ajouter cette machine ?",
"Devices": "Machines",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "L'appareil \"{{name}}\" ({{device}} à {{address}}) veut se connecter. L'acceptez-vous ?",
"Device ID": "ID de l'appareil",
"Device Identification": "Identifiant de l'appareil",
"Device Name": "Nom de l'appareil",
"Device {%device%} ({%address%}) wants to connect. Add new device?": "L'appareil {{device}} ({{address}}) veut se connecter. Voulez-vous l'ajouter ?",
"Devices": "Appareils",
"Disconnected": "Déconnecté",
"Discovery": "Découverte",
"Documentation": "Documentation",
@@ -53,47 +53,47 @@
"Downloaded": "Téléchargé",
"Downloading": "En cours de téléchargement",
"Edit": "Modifier",
"Edit Device": "Modifier la machine",
"Edit Folder": "Modifier le dossier",
"Edit Device": "Modifier l'appareil",
"Edit Folder": "Modifier le partage",
"Editing": "Modifications",
"Enable NAT traversal": "Activer la translation d'adresses (NAT)",
"Enable Relaying": "Relayage possible",
"Enable UPnP": "Activer UPnP",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Entrer les adresses (\"tcp://ip:port\" ou \"tcp://host:port\") séparées par une virgule ou \"dynamic\" afin d'activer la recherche automatique de l'adresse.",
"Enter ignore patterns, one per line.": "Entrer les masques de filtrage, un par ligne.",
"Enter ignore patterns, one per line.": "Entrez les masques d'exclusion, un par ligne.",
"Error": "Erreur",
"External File Versioning": "Gestion externe des versions de fichiers",
"Failed Items": "Fichiers en échec",
"File Pull Order": "Ordre de récupération des fichiers",
"File Versioning": "Versions de fichier",
"File Versioning": "Méthode de préservation des fichiers",
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Les bits de permission de fichier sont ignorés lors de la recherche de changements. Utilisé sur les systèmes de fichiers FAT.",
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Les fichiers sont déplacés vers le dossier .stversions quand ils sont remplacés ou effacés par Syncthing.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Les fichiers sont déplacés, avec horodatage, dans le dossier .stversions quand ils sont remplacés ou supprimés par Syncthing.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Les fichiers sont protégés des changements réalisés sur les autres machines, mais les changements réalisés sur celle-ci seront transférés aux autres machines.",
"Folder": "Dossier",
"Folder ID": "ID du dossier",
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Les fichiers sont déplacés dans le sous-répertoire .stversions quand ils sont remplacés ou supprimés par Syncthing.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Les fichiers sont déplacés et horodatés dans le sous-répertoire .stversions quand ils sont remplacés ou supprimés par Syncthing.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Les fichiers sont protégés des changements réalisés sur les autres appareils, mais les changements réalisés sur celui-ci seront transférés aux autres.",
"Folder": "Partage",
"Folder ID": "ID du partage",
"Folder Label": "Étiquette du partage",
"Folder Master": "Dossier maître",
"Folder Path": "Chemin du dossier",
"Folder Master": "Partage maître",
"Folder Path": "Chemin racine du partage",
"Folder Type": "Type de partage",
"Folders": "Dossiers",
"Folders": "Partages",
"GUI": "GUI",
"GUI Authentication Password": "Mot de passe d'authentification GUI",
"GUI Authentication User": "Utilisateur autorisé GUI",
"GUI Listen Addresses": "Adresse du GUI",
"Generate": "Générer",
"Global Discovery": "Recherche globale",
"Global Discovery Server": "Serveur global de recherche",
"Global Discovery Server": "Serveur de découverte globale",
"Global Discovery Servers": "Serveurs de découverte globale",
"Global State": "État global",
"Help": "Aide",
"Home page": "Page d'accueil",
"Ignore": "Ignorer",
"Ignore Patterns": "Masques d'exclusion",
"Ignore Patterns": "Exclusions ...",
"Ignore Permissions": "Ignorer les permissions",
"Incoming Rate Limit (KiB/s)": "Limite du débit de réception (Ko/s)",
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Une configuration incorrecte peut créer des dommages dans vos dossiers et mettre hors-service Syncthing",
"Introducer": "Initiateur",
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Une configuration incorrecte peut créer des dommages dans vos répertoires et mettre Syncthing hors-service.",
"Introducer": "introducteur",
"Inversion of the given condition (i.e. do not exclude)": "Inverser la condition donnée (i.e. ne pas exclure)",
"Keep Versions": "Conserver les versions",
"Largest First": "Les plus volumineux en premier",
@@ -102,7 +102,7 @@
"Last seen": "Dernière apparition",
"Later": "Plus tard",
"Listeners": "Systèmes en écoute",
"Local Discovery": "Recherche locale",
"Local Discovery": "Découverte locale",
"Local State": "État local",
"Local State (Total)": "État local (Total)",
"Major Upgrade": "Mise à jour majeure",
@@ -113,24 +113,24 @@
"Move to top of queue": "Déplacer en haut de la file",
"Multi level wildcard (matches multiple directory levels)": "Astérisque à plusieurs niveaux (correspond aux répertoires et sous-répertoires)",
"Never": "Jamais",
"New Device": "Nouvelle machine",
"New Folder": "Nouveau dossier",
"New Device": "Nouvel appareil",
"New Folder": "Nouveau partage",
"Newest First": "Les plus récents en premier",
"No": "Non",
"No File Versioning": "Pas de version de fichier",
"No File Versioning": "Pas de préservation",
"Normal": "Normal",
"Notice": "Notification",
"OK": "OK",
"Off": "Éteint",
"Off": "Désactivé(e)",
"Oldest First": "Les plus anciens en premier",
"Optional descriptive label for the folder. Can be different on each device.": "Étiquette optionnelle pour le partage. Peut être différente sur chaque machine.",
"Optional descriptive label for the folder. Can be different on each device.": "Étiquette optionnelle pour le partage. Peut être différente sur chaque appareil.",
"Options": "Options",
"Out of Sync": "Désynchronisé",
"Out of Sync Items": "Fichiers non synchronisés",
"Outgoing Rate Limit (KiB/s)": "Limite du débit d'émission (Ko/s)",
"Override Changes": "Écraser les changements",
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Le chemin du dossier sur l'ordinateur local sera créé si il n'existe pas. Le caractère tilde (~) peut être utilisé comme raccourci vers",
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Chemin où les versions doivent être conservées (laisser vide pour le chemin par défaut de .stversions dans le répertoire)",
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Chemin vers le répertoire dans l'appareil local. Il sera créé s'il n'existe pas. Le caractère tilde (~, ou ~+Espace sous Windows+Azerty) peut être utilisé comme raccourci vers",
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Chemin où les copies doivent être conservées (laisser vide pour le chemin par défaut de .stversions dans le répertoire)",
"Pause": "Pause",
"Paused": "En pause",
"Please consult the release notes before performing a major upgrade.": "Veuillez consulter les notes de version avant de réaliser une mise à jour majeure.",
@@ -145,67 +145,67 @@
"Relayed via": "Relayée par",
"Relays": "Relais",
"Release Notes": "Notes de version",
"Remote Devices": "Machines distantes",
"Remote Devices": "Appareils distants",
"Remove": "Enlever",
"Required identifier for the folder. Must be the same on all cluster devices.": "Identifiant du partage. Doit être le même sur l'ensemble des machines concernées.",
"Rescan": "Réanalyse",
"Required identifier for the folder. Must be the same on all cluster devices.": "Identifiant du partage. Doit être le même sur l'ensemble des appareils concernés.",
"Rescan": "Réanalyser",
"Rescan All": "Réanalyser tout",
"Rescan Interval": "Intervalle d'analyse",
"Restart": "Redémarrer",
"Restart Needed": "Redémarrage nécessaire",
"Restarting": "Redémarrage",
"Resume": "Résumer",
"Resume": "Reprise",
"Reused": "Réutilisé",
"Save": "Sauver",
"Save": "Enregistrer",
"Scan Time Remaining": "Temps d'analyse restant",
"Scanning": "Analyse en cours",
"Select the devices to share this folder with.": "Sélectionner les machines avec qui partager ce dossier.",
"Select the folders to share with this device.": "Sélectionner les dossiers à partager avec cette machine.",
"Select the devices to share this folder with.": "Synchroniser avec :",
"Select the folders to share with this device.": "Sélectionner les partages auxquels participe cet appareil.",
"Settings": "Configuration",
"Share": "Partager",
"Share Folder": "Partager le dossier",
"Share Folders With Device": "Partager des dossiers avec des machines",
"Share With Devices": "Partage avec des appareils",
"Share this folder?": "Voulez-vous partager ce dossier ?",
"Shared With": "Partagé avec",
"Short identifier for the folder. Must be the same on all cluster devices.": "Identifiant court du dossier. Il doit être le même sur l'ensemble des machines du groupe.",
"Share Folder": "Partager",
"Share Folders With Device": "Partages avec cet appareil",
"Share With Devices": "Synchroniser avec des appareils",
"Share this folder?": "Acceptez-vous ce partage ?",
"Shared With": "Synchronisé avec",
"Short identifier for the folder. Must be the same on all cluster devices.": "Identifiant court du partage. Il sera le même sur l'ensemble des appareils du groupe.",
"Show ID": "Afficher mon ID",
"Show QR": "Afficher le QR",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Affiché à la place de l'ID de la machine dans le groupe. Sera proposé aux autres machines comme nom optionnel par défaut.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Affiché à la place de l'ID de la machine dans le groupe. Si laissé vide, il sera mis à jour par le nom proposé par la machine distante.",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Affiché à la place de l'ID de l'appareil dans le groupe. Sera proposé aux autres appareils comme nom convivial par défaut.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Affiché à la place de l'ID de l'appareil dans le groupe. Si laissé vide, il sera mis à jour par le nom proposé par l'appareil distant.",
"Shutdown": "Arrêter",
"Shutdown Complete": "Arrêté !",
"Simple File Versioning": "Suivi simple des versions de fichier",
"Single level wildcard (matches within a directory only)": "Astérisque à un seul niveau (correspond uniquement à lintérieur du dossier)",
"Simple File Versioning": "Suivi simple des versions de fichiers",
"Single level wildcard (matches within a directory only)": "Astérisque à un seul niveau (correspond uniquement à lintérieur du répertoire)",
"Smallest First": "Les plus petits en premier",
"Source Code": "Code source",
"Staggered File Versioning": "Versions échelonnées de fichier",
"Staggered File Versioning": "Versions échelonnées de fichiers",
"Start Browser": "Démarrer le navigateur web",
"Statistics": "Statistiques",
"Stopped": "Arrêté",
"Support": "Aide",
"Sync Protocol Listen Addresses": "Adresse d'écoute du protocole de synchronisation",
"Syncing": "Synchronisation en cours",
"Syncthing has been shut down.": "Syncthing a été éteint.",
"Syncthing has been shut down.": "Syncthing a été arrêté.",
"Syncthing includes the following software or portions thereof:": "Syncthing intègre les logiciels suivants (ou des éléments provenant de ces logiciels) :",
"Syncthing is restarting.": "Syncthing est cours de redémarrage.",
"Syncthing is upgrading.": "Syncthing est cours de mise à jour.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing semble être éteint, ou il y a un problème avec votre connexion Internet. Nouvelle tentative ...",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing semble avoir un problème pour traiter votre demande. S'il vous plait, rafraichissez la page ou redémarrer Syncthing si le problème persiste.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing semble être arrêté, ou il y a un problème avec votre connexion Internet. Nouvelle tentative ...",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing semble avoir un problème pour traiter votre demande. S'il vous plaît, rafraichissez la page ou redémarrez Syncthing si le problème persiste.",
"The Syncthing admin interface is configured to allow remote access without a password.": "L'interface d'administration de Syncthing est paramétrée pour autoriser les accès à distance sans mot de passe !!!",
"The aggregated statistics are publicly available at the URL below.": "Les statistiques agrégées sont disponibles publiquement à l'adresse ci-dessous.",
"The aggregated statistics are publicly available at {%url%}.": "Les statistiques agrégées sont disponibles publiquement à l'adresse {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configuration a été enregistrée mais pas activée. Syncthing doit redémarrer afin d'activer la nouvelle configuration.",
"The device ID cannot be blank.": "L'ID de la machine ne peut être vide.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "L'ID de machine à saisir se trouve dans le menu \"Actions > Afficher mon ID\" de la machine distante. Espaces et tirets sont optionnels (ignorés).",
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "L'ID de machine à saisir se trouve dans le menu \"Modifications > Afficher mon ID\" de la machine distante. Les espaces et les tirets sont optionnels (ils seront ignorés).",
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Le rapport d'utilisation chiffré est envoyé quotidiennement. Il sert à répertorier les plateformes utilisées, la taille des dossiers et les versions de l'application. Si les données rapportées sont modifiées cette boite de dialogue vous redemandera votre confirmation.",
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "L'ID de la machine inséré ne semble pas être valide. Il devrait ressembler à une chaîne de 52 ou 56 caractères comprenant des lettres, des chiffres et potentiellement des espaces et des traits d'union.",
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "Le premier paramètre de ligne de commande est le chemin du dossier, et le second est le chemin relatif dans le dossier.",
"The folder ID cannot be blank.": "L'identifiant (ID) du dossier ne peut être vide.",
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "L'ID du dossier doit être un identifiant court (64 caractères ou moins) comprenant uniquement des lettres, chiffre, points (.), traits d'union (-) et tirets bas (_).",
"The folder ID must be unique.": "L'ID du dossier doit être unique.",
"The folder path cannot be blank.": "Le chemin du dossier ne peut pas être vide.",
"The device ID cannot be blank.": "L'ID de l'appareil ne peut être vide.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "L'ID d'appareil à saisir ici se trouve dans le menu \"Actions > Afficher mon ID\" de l'appareil distant. Espaces et tirets sont optionnels (ignorés).",
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "L'ID d'appareil à saisir ici se trouve dans le menu \"Modifications > Afficher mon ID\" de l'appareil distant. Les espaces et les tirets sont optionnels (ils seront ignorés).",
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Le rapport d'utilisation chiffré est envoyé quotidiennement. Il sert à répertorier les plateformes utilisées, la taille des partages et les versions de l'application. Si le jeu de données rapportées devait être changé, il vous sera demandé de le valider de nouveau via ce message.",
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "L'ID de l'appareil inséré ne semble pas être valide. Il devrait ressembler à une chaîne de 52 ou 56 caractères comprenant des lettres, des chiffres et potentiellement des espaces et des traits d'union.",
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "Le premier paramètre de ligne de commande est le chemin du répertoire partagé, et le second est le chemin relatif dans le répertoire.",
"The folder ID cannot be blank.": "L'ID du partage ne peut être vide.",
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "L'ID du partage doit être un identifiant court (64 caractères ou moins), constitué de lettres, chiffres, points (.), tirets milieu (-) et tirets bas (_) seulement.",
"The folder ID must be unique.": "L'ID du partage doit être unique.",
"The folder path cannot be blank.": "Le chemin vers le répertoire ne peut pas être vide.",
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "Les intervalles suivant sont utilisés: la première heure une version est conservée chaque 30 secondes, le premier jour une version est conservée chaque heure, les premiers 30 jours une version est conservée chaque jour, jusqu'à la limite d'âge maximum une version est conservée chaque semaine.",
"The following items could not be synchronized.": "Les fichiers suivants ne peuvent pas être synchronisés.",
"The maximum age must be a number and cannot be blank.": "L'âge maximum doit être un nombre et ne peut être vide.",
@@ -219,10 +219,10 @@
"The rate limit must be a non-negative number (0: no limit)": "La limite de débit ne doit pas être négative (0: Aucune limite)",
"The rescan interval must be a non-negative number of seconds.": "L'intervalle d'analyse ne doit pas être un nombre négatif de secondes.",
"They are retried automatically and will be synced when the error is resolved.": "Ils seront réessayés automatiquement et synchronisés quand l'erreur sera résolue.",
"This Device": "Cette machine",
"This Device": "Cet appareil",
"This can easily give hackers access to read and change any files on your computer.": "Ceci peut aisément permettre à un intrus de lire et modifier n'importe quel fichier de votre ordinateur.",
"This is a major version upgrade.": "Ceci est une mise à jour majeure.",
"Trash Can File Versioning": "Gestion des versions de fichier style poubelle.",
"Trash Can File Versioning": "Style poubelle",
"Unknown": "Inconnu",
"Unshared": "Non partagé",
"Unused": "Non utilisé",
@@ -237,15 +237,15 @@
"Version": "Version",
"Versions Path": "Emplacement des versions",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Les versions seront supprimées automatiquement, si elles dépassent la durée maximum de conservation, ou si leur nombre est supérieur à la valeur autorisée dans l'intervalle.",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "[b]Attention, ce chemin est un sous-répertoire du partage existant \"{{otherFolder}}\". Ceci peut causer des problèmes tels que duplications de fichiers ou suppressions intempestives sur les autres machines.[/b]",
"When adding a new device, keep in mind that this device must be added on the other side too.": "Lorsqu'une machine est ajoutée, gardez à l'esprit que cette machine doit aussi être ajoutée de l'autre coté.",
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Lorsqu'un nouveau dossier est ajouté, gardez à l'esprit que son ID est utilisé pour lier les dossiers à travers les machines. Les ID sont sensibles à la casse et doivent être identiques à travers tous les nœuds.",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Attention, ce chemin est un sous-répertoire du partage existant \"{{otherFolder}}\". Ceci peut causer des problèmes tels que duplications de fichiers ou suppressions intempestives sur les autres appareils.",
"When adding a new device, keep in mind that this device must be added on the other side too.": "Lorsqu'un appareil est ajouté, gardez à l'esprit que votre appareil doit aussi être ajoutée de l'autre coté.",
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Lorsqu'un nouveau partage est ajouté, gardez à l'esprit que son ID est utilisée pour lier les répertoires à travers les appareils. L'ID est sensible à la casse et sera forcément la même sur toutes les appareils participant à ce partage.",
"Yes": "Oui",
"You must keep at least one version.": "Vous devez garder au minimum une version.",
"days": "Jours",
"full documentation": "documentation complète",
"items": "fichiers",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} veut partager le dossier \"{{folder}}\".",
"items": "éléments",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vous invite au partage \"{{folderLabel}}\".",
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} vous invite au partage \"{{folderLabel}}\" ({{folder}}).",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} vous invite au partage \"{{folderLabel}}\" ({{folder}})."
}

View File

@@ -35,7 +35,7 @@
"Connection Type": "Tilkoplingstype",
"Copied from elsewhere": "Kopiert frå ein annan stad",
"Copied from original": "Kopiert frå originalen",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
"Copyright © 2014-2016 the following Contributors:": "Opphavsrett © 2014-2016 for følgjande bidragsyterar:",
"Copyright © 2015 the following Contributors:": "Opphavsrett © 2015 og desse bidragsytarane:",
"Danger!": "Fare!",
"Delete": "Slett",
@@ -123,7 +123,7 @@
"OK": "OK",
"Off": "Av",
"Oldest First": "Elste fyrst",
"Optional descriptive label for the folder. Can be different on each device.": "Optional descriptive label for the folder. Can be different on each device.",
"Optional descriptive label for the folder. Can be different on each device.": "Valfri merkelapp på katalogen. Denne kan være ulik på andre enheter.",
"Options": "Val",
"Out of Sync": "Ikkje synkronisert",
"Out of Sync Items": "Ikkje-synkroniserte element",
@@ -147,7 +147,7 @@
"Release Notes": "Utgivingsnotat",
"Remote Devices": "Eksterne Einingar",
"Remove": "Fjern",
"Required identifier for the folder. Must be the same on all cluster devices.": "Required identifier for the folder. Must be the same on all cluster devices.",
"Required identifier for the folder. Must be the same on all cluster devices.": "Påkrevd identifikator for katalogen. Denne må være lik på alle einingar i samme klynge.",
"Rescan": "Skann På Ny",
"Rescan All": "Skann alle på nytt",
"Rescan Interval": "Skanneintervall",
@@ -197,7 +197,7 @@
"The aggregated statistics are publicly available at {%url%}.": "Samla statistikk er opent tilgjengeleg på {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Instillingane har blitt lagra men ikkje aktivert. Syncthing må starta på ny for å aktivera dei nye instillingane.",
"The device ID cannot be blank.": "Eining ID kan ikkje vera tom.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Einings-IDen som skal oppgis her kan hentast fram via \"Rediger > Vis ID\"-dialogboksen på den andre eininga. Mellomrom og bindestrek er valfritt (blir ignorert).",
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Einings-ID-en du skal nytta her finn du i meldingsvindauget \"Rediger > Vis ID\" på den andre eininga. Mellomrom og bindestrek er valfrie (vert ignorert).",
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Den krypterte bruksrapporten vert send dagleg. Han vert nytta til å spora vanlege plattformer, mappestorleikar og programutgåvene. Om datasettet endrar seg, vil dette meldingsvindauget dukka opp att og du vil verta beden om å godkjenna det.",
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Einings-ID-en er ikkje gyldig. Han må vera på 52 eller 56 teikn og vera samansett av bokstavar og tal med valfrie mellomrom og bindestrekar.",
@@ -237,7 +237,7 @@
"Version": "Versjon",
"Versions Path": "Utgåvebane",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Utgåver vert automatisk sletta når maksimal levetid er nådd eller når det høgaste tillate talet på filer innan eit intervall vert overskride.",
"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 \"{%otherFolder%}\".": "Åtvaring, denne banen er ei undermappe av den eksisterande mappa \"{{otherFolder}}\".",
"When adding a new device, keep in mind that this device must be added on the other side too.": "Hugs at når ei ny eining vert lagt til må ho òg leggjast til på andre sida.",
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Hugs at når ei ny mappe vert lagt til, vert mappe-ID-en brukt til å binda saman mappene mellom einingane. Det er skilnad på store og små bokstavar, så ID-ane må vera identiske på alle einingane.",
"Yes": "Ja",

View File

@@ -40,7 +40,7 @@
"Danger!": "Perigo!",
"Delete": "Apagar",
"Deleted": "Apagado",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Dispositivo \"{{name}}\" ({{device}} em {{address}}) deseja se conectar. Adicionar novo dispositivo?",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Dispositivo \"{{name}}\" ({{device}} em {{address}}) quer se conectar. Adicionar novo dispositivo?",
"Device ID": "ID do dispositivo",
"Device Identification": "Identificação do dispositivo",
"Device Name": "Nome do dispositivo",
@@ -246,6 +246,6 @@
"full documentation": "documentação completa",
"items": "itens",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} quer compartilhar a pasta \"{{folder}}\".",
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} deseja compartilhar a pasta \"{{folderLabel}}\" ({{folder}}).",
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} quer compartilhar a pasta \"{{folderLabel}}\" ({{folder}}).",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} quer compartilhar a pasta \"{{folderlabel}}\" ({{folder}})."
}

View File

@@ -42,12 +42,12 @@
"Deleted": "Borttaget",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Enhet \"{{name}}\" ({{device}} på {{address}}) vill ansluta. Lägg till ny enhet?",
"Device ID": "Enhets ID",
"Device Identification": "Enhetsidentifikation",
"Device Name": "Enhetens namn",
"Device Identification": "Enhets identifikation",
"Device Name": "Enhets namn",
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Enheten {{device}} ({{address}}) vill ansluta. Lägg till ny enhet?",
"Devices": "Enheter",
"Disconnected": "Frånkopplad",
"Discovery": "Upptäckt",
"Discovery": "Annonsering",
"Documentation": "Dokumentation",
"Download Rate": "Nedladdningshastighet",
"Downloaded": "Hämtat",
@@ -80,18 +80,18 @@
"GUI": "Grafiskt gränssnitt",
"GUI Authentication Password": "Lösenord för GUI",
"GUI Authentication User": "Användare för GUI",
"GUI Listen Addresses": "Lyssningsadresser för GUI",
"GUI Listen Addresses": "Lyssnadresser för GUI",
"Generate": "Generera",
"Global Discovery": "Global upptäckt",
"Global Discovery Server": "Global upptäcktsserver",
"Global Discovery Servers": "Globala upptäcktsservrar",
"Global Discovery": "Global annonsering",
"Global Discovery Server": "Global annonseringsserver",
"Global Discovery Servers": "Globala annonseringsservrar",
"Global State": "Global status",
"Help": "Hjälp",
"Home page": "Hemsida",
"Ignore": "Ignorera",
"Ignore Patterns": "Ignorera mönster",
"Ignore Permissions": "Ignorera rättigheter",
"Incoming Rate Limit (KiB/s)": "Nedladdningshastighetsgräns (KiB/s)",
"Incoming Rate Limit (KiB/s)": "Inkommande hastighetsbegränsning (KiB/s)",
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Inkorrekt konfiguration kan skada innehållet i katalogen and få Syncthing att sluta fungera.",
"Introducer": "introduktör",
"Inversion of the given condition (i.e. do not exclude)": "Vänder på villkoret, d.v.s. exkluderar inte.",
@@ -102,7 +102,7 @@
"Last seen": "Senast sedd",
"Later": "Senare",
"Listeners": "Lyssnare",
"Local Discovery": "Lokal upptäckt",
"Local Discovery": "Lokal annonsering",
"Local State": "Lokal status",
"Local State (Total)": "Lokal status (totalt)",
"Major Upgrade": "Stor uppgradering",
@@ -127,7 +127,7 @@
"Options": "Alternativ",
"Out of Sync": "Osynkroniserad",
"Out of Sync Items": "Osynkroniserade objekt",
"Outgoing Rate Limit (KiB/s)": "Uppladdningshastighetsgräns (KiB/s)",
"Outgoing Rate Limit (KiB/s)": "Utgående hastighetsbegränsning (KiB/s)",
"Override Changes": "Åsidosätt förändringar",
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Sökväg till katalogen på din dator. Kommer att skapas om det inte finns. Tecknet tilde (~) kan användas som en genväg för",
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Sökväg där versioner sparas (lämna tomt för att använda .stversions i den ordinarie katalogen).",
@@ -171,8 +171,8 @@
"Short identifier for the folder. Must be the same on all cluster devices.": "Kort identifieringssträng för katalogen. Måste vara samma på alla enheter i klustret.",
"Show ID": "Visa ID",
"Show QR": "Visa QR",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Visas i stället för enhetens ID. Skickas till andra enheter som namn på denna enhet.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Visas i stället för enhetens ID. Sätts till namnet på den andra enheten vid första anslutning om det lämnas tomt.",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Visas i stället för enhets ID i samlingsstatusen. Skickas till andra enheter som namn på denna enhet.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Visas i stället för enhets ID i samlingsstatusen. Sätts till namnet på den andra enheten vid första anslutning om det lämnas tomt.",
"Shutdown": "Stäng av",
"Shutdown Complete": "Avstängning klar",
"Simple File Versioning": "Enkel filversionshantering",
@@ -184,7 +184,7 @@
"Statistics": "Statistik",
"Stopped": "Stoppad",
"Support": "Support",
"Sync Protocol Listen Addresses": "Synkroniseringsprotokollets lyssningsadresser",
"Sync Protocol Listen Addresses": "Synkroniseringsprotokollets lyssnaradresser",
"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:",
@@ -196,11 +196,11 @@
"The aggregated statistics are publicly available at the URL below.": "Den aggregerade statistiken är offentligt tillgängliga på webbadressen nedan.",
"The aggregated statistics are publicly available at {%url%}.": "Sammanställd statistik finns publikt tillgänglig på {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Konfigurationen har sparats men inte aktiverats. Syncthing måste startas om för att aktivera den nya konfigurationen.",
"The device ID cannot be blank.": "Enhetens ID kan inte vara tomt.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Enhetens ID som behövs här kan du hitta i \"Redigera > Visa ID\"-dialogen på den andra enheten. Mellanrum och bindestreck är valfria (ignoreras).",
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Enhetens ID som behövs här kan du hitta i \"Redigera > Visa ID\"-dialogen på den andra enheten. Mellanrum och bindestreck är valfria (ignoreras).",
"The device ID cannot be blank.": "Enhets ID:t kan inte vara tomt.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Enhets ID:t som behövs här kan du hitta i \"Funktioner > Visa ID\"-dialogrutan på den andra enheten. Mellanrum och bindestreck är valfria (ignoreras).",
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Enhets ID:t som behövs här kan du hitta i \"Funktioner > Visa ID\"-dialogrutan på den andra enheten. Mellanrum och bindestreck är valfria (ignoreras).",
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Den krypterade användarstatistiken skickas dagligen. Den används för att spåra vanliga plattformar, katalogstorlekar och versioner. Om datan som rapporteras ändras så kommer du att bli tillfrågad igen.",
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Det inmatade enhetens ID verkar inte korrekt. Det ska vara en 52 eller 56 teckens sträng bestående av siffror och bokstäver, eventuellt med mellanrum och bindestreck.",
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Det inmatade enhets ID:t verkar inte korrekt. Det ska vara en 52 eller 56 teckens sträng bestående av siffror och bokstäver, eventuellt med mellanrum och bindestreck.",
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "Den första kommandoparametern är sökvägen till mappen och den andra parametern är den relativa sökvägen i mappen.",
"The folder ID cannot be blank.": "Katalogens ID får inte vara tomt.",
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "Katalogens ID måste vara en kort sträng (64 tecken eller mindre), bestående av endast bokstäver, siffror, punkt (.), bindestreck (-) och understreck (_).",

View File

@@ -41,7 +41,7 @@
"Delete": "删除",
"Deleted": "已删除",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "设备 \"{{name}}\"(位于 {{address}} 的 {{device}})请求连接。是否添加新设备?",
"Device ID": "设备标识",
"Device ID": "设备 ID",
"Device Identification": "设备标识",
"Device Name": "设备名",
"Device {%device%} ({%address%}) wants to connect. Add new device?": "设备:{{device}} 地址:({{address}}) 请求连接。是否添加新设备?",
@@ -71,7 +71,7 @@
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "当某个文件在其他设备被替换或删除时,本设备将会在 .stversions 文件夹中保留该文件的备份,并在文件名中加入时间戳信息。",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "在其它设备中对该文件夹内文件的修改并不会被同步到本机,但是在本机上对其的修改,则会被同步到其它设备中。",
"Folder": "文件夹",
"Folder ID": "文件夹标识",
"Folder ID": "文件夹 ID",
"Folder Label": "文件夹标签",
"Folder Master": "主文件夹",
"Folder Path": "文件夹路径",
@@ -127,7 +127,7 @@
"Options": "选项",
"Out of Sync": "未同步",
"Out of Sync Items": "未同步的项目",
"Outgoing Rate Limit (KiB/s)": "上传速度限制(千字节/秒)",
"Outgoing Rate Limit (KiB/s)": "上传速度限制 (KiB/s)",
"Override Changes": "撤销改变",
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "文件夹在本地的路径。如果不存在,则会被创建。波浪线符号(~)是如下路径的缩略符:",
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "用来存储历史版本的文件夹(留空则将默认会存储在.stversions文件夹中",
@@ -169,7 +169,7 @@
"Share this folder?": "是否共享该文件夹?",
"Shared With": "共享给",
"Short identifier for the folder. Must be the same on all cluster devices.": "文件夹的别名。必须在所有设备上保持一致。",
"Show ID": "显示设备标识",
"Show ID": "显示 ID",
"Show QR": "显示 QR 码",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "在设备丛中,显示该名称,而不是设备 ID。亦会作为一个可选的默认名称被发送到其他设备。",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "在设备丛中,将会显示本名称,而不是设备 ID。如果设置为空则会使用目标设备提供的默认名称。",
@@ -190,21 +190,21 @@
"Syncthing includes the following software or portions thereof:": "Syncthing 使用了下列软件或其中的一部分",
"Syncthing is restarting.": "Syncthing 正在重启",
"Syncthing is upgrading.": "Syncthing 正在升级",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing 似乎关闭了,或者您的网络连接存在故障。重试中...",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing 似乎关闭了,或者您的网络连接存在故障。重试中",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing 在处理您的请求时似乎遇到了问题。如果问题持续,请刷新页面,或重启 Syncthing。",
"The Syncthing admin interface is configured to allow remote access without a password.": "当前配置允许在不使用密码的情况下远程访问 Syncthing 管理界面",
"The aggregated statistics are publicly available at the URL below.": "全局统计数据公布于以下 URL。",
"The aggregated statistics are publicly available at {%url%}.": "全局统计公布于 {{url}}",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "设置已经保存但是还未生效。Syncthing 需要重启以启用新的设置。",
"The device ID cannot be blank.": "设备标识不能为空",
"The device ID cannot be blank.": "设备 ID 不能为空",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "在这里所需要输入的设备 ID可以在目标设备的“操作->显示 ID”中看到。空格和横线可选将会被忽略。",
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "在这里所需要输入的设备标识,可以在目标设备的“选项->显示设备标识”中看到。空格和横线可选(将会被忽略)。",
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "在这里所需要输入的设备 ID,可以在目标设备的“选项->显示 ID”中看到。空格和横线可选(将会被忽略)。",
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "经过加密的使用报告会每天发送。它用来跟踪统计使用本软件的平台,文件夹大小,以及本软件的版本。如果报告的内容有任何变化,本对话框会再次弹出提示您。",
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "输入的设备标识似乎无效。设备标识长度必须为52或56的字母和数字,空格和横线不在内。",
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "输入的设备 ID 似乎无效。设备 ID 包含字母和数字,长度为 52 或 56,空格和横线不在内。",
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "命令行的第一个参数是文件夹的路径,第二个参数是文件夹内的相对路径。",
"The folder ID cannot be blank.": "文件夹标识不能为空。",
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "文件夹标识不得长于 64 字符,且仅能包含字母、数字、半角句号(.)、横线(-和下划线_。",
"The folder ID must be unique.": "文件夹标识不得重复",
"The folder ID cannot be blank.": "文件夹 ID 不能为空。",
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "文件夹 ID 不得长于 64 字符,且仅能包含字母、数字、半角句号(.)、横线(-和下划线_。",
"The folder ID must be unique.": "文件夹 ID 不得重复",
"The folder path cannot be blank.": "文件夹路径不能为空",
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "保留的历史版本会遵循以下条件:最近一小时内的历史版本,更新间隔小于三十秒的仅保留一份。最近一天内的历史版本,更新间隔小于一小时的仅保留一份。最近一个月内的历史版本,更新间隔小于一天的仅保留一份。距离现在超过一个月且小于最长保留时间的,更新间隔小于一周的仅保留一份。",
"The following items could not be synchronized.": "下列项目无法被同步。",

View File

@@ -32,7 +32,7 @@
<nav class="navbar navbar-top navbar-default" role="navigation">
<div class="container">
<span class="navbar-brand">
<span class="navbar-brand" aria-hidden="true">
<img class="logo hidden-xs" src="assets/img/logo-horizontal.svg" height="32" width="117"/>
<img class="logo hidden visible-xs" src="assets/img/favicon-default.png" height="32"/>
</span>
@@ -58,7 +58,7 @@
</a>
</li>
<li class="dropdown action-menu">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
<span class="fa fa-cog"></span>
<span class="hidden-xs" translate>Actions</span>
<span class="caret"></span>
@@ -66,17 +66,17 @@
<ul class="dropdown-menu">
<li><a href="" ng-click="editSettings()"><span class="fa fa-fw fa-cog"></span>&nbsp;<span translate>Settings</span></a></li>
<li><a href="" data-toggle="modal" data-target="#idqr" ng-click="currentDevice=thisDevice()"><span class="fa fa-fw fa-qrcode"></span>&nbsp;<span translate>Show ID</span></a></li>
<li class="divider"></li>
<li class="divider" aria-hidden="true"></li>
<li><a href="" ng-click="shutdown()"><span class="fa fa-fw fa-power-off"></span>&nbsp;<span translate>Shutdown</span></a></li>
<li><a href="" ng-click="restart()"><span class="fa fa-fw fa-refresh"></span>&nbsp;<span translate>Restart</span></a></li>
<li class="divider"></li>
<li class="divider" aria-hidden="true"></li>
<li class="visible-xs">
<a href="https://docs.syncthing.net/intro/gui.html" target="_blank">
<span class="fa fa-fw fa-book"></span>&nbsp;<span translate>Help</span>
</a>
</li>
<li><a href="" data-toggle="modal" data-target="#about"><span class="fa fa-fw fa-heart-o"></span>&nbsp;<span translate>About</span></a></li>
<li class="divider"></li>
<li class="divider" aria-hidden="true"></li>
<li><a href="" ng-click="advanced()"><span class="fa fa-fw fa-cogs"></span>&nbsp;<span translate>Advanced</span></a></li>
</ul>
</li>
@@ -248,17 +248,19 @@
</div>
</div>
<div ng-if="config && config.options && config.options.unackedNotificationIDs" ng-include="'syncthing/core/notifications.html'"></div>
<!-- First regular row -->
<div class="row">
<!-- Folder list (top left) -->
<div class="col-md-6">
<h3 translate>Folders</h3>
<div class="col-md-6" aria-labelledby="folder_list" role="region" >
<h3 id="folder_list" translate>Folders</h3>
<div class="panel-group" id="folders">
<div class="panel panel-default" ng-repeat="folder in folderList()">
<button class="btn panel-heading" data-toggle="collapse" data-parent="#folders" data-target="#folder-{{$index}}">
<button class="btn panel-heading" data-toggle="collapse" data-parent="#folders" data-target="#folder-{{$index}}" aria-expanded="false">
<div class="panel-progress" ng-show="folderStatus(folder) == 'syncing'" ng-attr-style="width: {{syncPercentage(folder.id)}}%"></div>
<div class="panel-progress" ng-show="folderStatus(folder) == 'scanning' && scanProgress[folder.id] != undefined" ng-attr-style="width: {{scanPercentage(folder.id)}}%"></div>
<h4 class="panel-title">
@@ -437,10 +439,10 @@
<!-- This device -->
<div class="col-md-6">
<div class="col-md-6" aria-label="{{'Devices' | translate}}" role="region">
<h3 translate>This Device</h3>
<div class="panel panel-default" ng-repeat="deviceCfg in [thisDevice()]">
<button class="btn panel-heading" data-toggle="collapse" data-target="#device-this">
<button class="btn panel-heading" data-toggle="collapse" data-target="#device-this" aria-expanded="true">
<h4 class="panel-title">
<identicon class="panel-icon" data-value="deviceCfg.deviceID"></identicon>
<div class="panel-title-text">{{deviceName(deviceCfg)}}</div>
@@ -516,7 +518,7 @@
<h3 translate>Remote Devices</h3>
<div class="panel-group" id="devices">
<div class="panel panel-default" ng-repeat="deviceCfg in otherDevices()">
<button class="btn panel-heading" data-toggle="collapse" data-parent="#devices" data-target="#device-{{$index}}">
<button class="btn panel-heading" data-toggle="collapse" data-parent="#devices" data-target="#device-{{$index}}" aria-expanded="false">
<div class="panel-progress" ng-show="deviceStatus(deviceCfg) == 'syncing'" ng-attr-style="width: {{completion[deviceCfg.deviceID]._total | number:0}}%"></div>
<h4 class="panel-title">
<identicon class="panel-icon" data-value="deviceCfg.deviceID"></identicon>
@@ -672,6 +674,7 @@
<script src="syncthing/core/localeService.js"></script>
<script src="syncthing/core/modalDirective.js"></script>
<script src="syncthing/core/naturalFilter.js"></script>
<script src="syncthing/core/notificationDirective.js"></script>
<script src="syncthing/core/pathIsSubDirDirective.js"></script>
<script src="syncthing/core/popoverDirective.js"></script>
<script src="syncthing/core/selectOnClickDirective.js"></script>

View File

@@ -12,7 +12,7 @@
<p translate>Copyright &copy; 2014-2016 the following Contributors:</p>
<div class="row">
<div class="col-md-12" id="contributor-list">
Jakob Borg, Audrius Butkevicius, Alexander Graf, Anderson Mesquita, Antony Male, Ben Schulz, Caleb Callaway, Daniel Harte, Lars K.W. Gohlke, Lode Hoste, Michael Ploujnikov, Philippe Schommers, Ryan Sullivan, Sergey Mishin, Stefan Tatschner, Aaron Bieber, Adam Piggott, Alessandro G., Alexandre Viau, Andrew Dunham, Andrey D, Arthur Axel fREW Schmidt, Bart De Vries, Ben Curthoys, Ben Sidhom, Benny Ng, Brandon Philips, Brendan Long, Brian R. Becker, Carsten Hagemann, Cathryne Linenweaver, Cedric Staniewski, Chris Howie, Chris Joel, Colin Kennedy, Daniel Bergmann, Daniel Martí, David Rimmer, Denis A., Dennis Wilson, Dominik Heidler, Elias Jarlebring, Emil Hessman, Erik Meitner, Federico Castagnini, Felix Ableitner, Felix Unterpaintner, Francois-Xavier Gsell, Frank Isemann, Gilli Sigurdsson, Jaakko Hannikainen, Jacek Szafarkiewicz, Jake Peterson, James Patterson, Jaroslav Malec, Jens Diemer, Jochen Voss, Johan Vromans, Karol Różycki, Kelong Cong, Ken'ichi Kamada, Kevin Allen, Laurent Etiemble, Lord Landon Agahnim, Majed Abdulaziz, Marc Laporte, Marc Pujol, Marcin Dziadus, Mateusz Naściszewski, Matt Burke, Max Schulze, Michael Jephcote, Michael Tilli, Nate Morrison, Pascal Jungblut, Peter Hoeg, Phill Luby, Piotr Bejda, Scott Klupfel, Stefan Kuntz, Tim Abell, Tobias Nygren, Tomas Cerveny, Tully Robinson, Tyler Brazier, Veeti Paananen, Victor Buinsky, Vil Brekin, William A. Kennington III, Wulf Weich, Yannic A.
Jakob Borg, Audrius Butkevicius, Alexander Graf, Anderson Mesquita, Antony Male, Ben Schulz, Caleb Callaway, Daniel Harte, Lars K.W. Gohlke, Lode Hoste, Michael Ploujnikov, Philippe Schommers, Ryan Sullivan, Sergey Mishin, Stefan Tatschner, Aaron Bieber, Adam Piggott, Alessandro G., Alexandre Viau, Andrew Dunham, Andrey D, Antoine Lamielle, Arthur Axel fREW Schmidt, Bart De Vries, Ben Curthoys, Ben Sidhom, Benny Ng, Brandon Philips, Brendan Long, Brian R. Becker, Carsten Hagemann, Cathryne Linenweaver, Cedric Staniewski, Chris Howie, Chris Joel, Colin Kennedy, Daniel Bergmann, Daniel Martí, David Rimmer, Denis A., Dennis Wilson, Dominik Heidler, Elias Jarlebring, Emil Hessman, Erik Meitner, Federico Castagnini, Felix Ableitner, Felix Unterpaintner, Francois-Xavier Gsell, Frank Isemann, Gilli Sigurdsson, Jaakko Hannikainen, Jacek Szafarkiewicz, Jake Peterson, James Patterson, Jaroslav Malec, Jens Diemer, Jochen Voss, Johan Vromans, Karol Różycki, Kelong Cong, Ken'ichi Kamada, Kevin Allen, Laurent Etiemble, Lord Landon Agahnim, Majed Abdulaziz, Marc Laporte, Marc Pujol, Marcin Dziadus, Mateusz Naściszewski, Matt Burke, Max Schulze, Michael Jephcote, Michael Tilli, Nate Morrison, Pascal Jungblut, Peter Hoeg, Phill Luby, Piotr Bejda, Scott Klupfel, Stefan Kuntz, Tim Abell, Tobias Nygren, Tomas Cerveny, Tully Robinson, Tyler Brazier, Veeti Paananen, Victor Buinsky, Vil Brekin, William A. Kennington III, Wulf Weich, Yannic A.
</div>
</div>
<hr/>

View File

@@ -4,7 +4,7 @@ angular.module('syncthing.core')
return {
restrict: 'EA',
template:
'<a ng-if="visible" href="#" class="dropdown-toggle" data-toggle="dropdown" aria-expanded="true"><span class="fa fa-globe"></span><span class="hidden-xs">&nbsp;{{localesNames[currentLocale] || "English"}}</span> <span class="caret"></span></a>'+
'<a ng-if="visible" href="#" class="dropdown-toggle" data-toggle="dropdown" aria-expanded="false"><span class="fa fa-globe"></span><span class="hidden-xs">&nbsp;{{localesNames[currentLocale] || "English"}}</span> <span class="caret"></span></a>'+
'<ul ng-if="visible" class="dropdown-menu">'+
'<li ng-repeat="(i,name) in localesNames" ng-class="{active: i==currentLocale}">'+
'<a href="#" data-ng-click="changeLanguage(i)">{{name}}</a>'+

View File

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

View File

@@ -0,0 +1,16 @@
<!--
<notification id='exampleNotification'>
<div class="panel panel-warning">
<div class="panel-heading"><h3 class="panel-title"><span class="fa fa-exclamation-circle"></span><span translate>Notice</span></h3></div>
<div class="panel-body">
<p translate>This is an example notification. ID of the notification should be appended to Options.UnackedNotificationIDs of the config.</p>
</div>
<div class="panel-footer">
<button type="button" class="btn btn-sm btn-default pull-right" ng-click="dismiss()">
<span class="fa fa-check"></span>&nbsp;<span translate>OK</span>
</button>
<div class="clearfix"></div>
</div>
</div>
</notification>
-->

View File

@@ -309,18 +309,8 @@ angular.module('syncthing.core')
if (!$scope.completion[data.device]) {
$scope.completion[data.device] = {};
}
$scope.completion[data.device][data.folder] = data.completion;
var tot = 0,
cnt = 0;
for (var cmp in $scope.completion[data.device]) {
if (cmp === "_total") {
continue;
}
tot += $scope.completion[data.device][cmp];
cnt += 1;
}
$scope.completion[data.device]._total = tot / cnt;
$scope.completion[data.device][data.folder] = data;
recalcCompletion(data.device);
});
$scope.$on(Events.FOLDER_ERRORS, function (event, arg) {
@@ -448,6 +438,32 @@ angular.module('syncthing.core')
}
}
function recalcCompletion(device) {
var total = 0, needed = 0, deletes = 0;
for (var folder in $scope.completion[device]) {
if (folder === "_total") {
continue;
}
total += $scope.completion[device][folder].globalBytes;
needed += $scope.completion[device][folder].needBytes;
deletes += $scope.completion[device][folder].needDeletes;
}
if (total == 0) {
$scope.completion[device]._total = 100;
} else {
$scope.completion[device]._total = 100 * (1 - needed / total);
}
if (needed == 0 && deletes > 0) {
// We don't need any data, but we have deletes that we need
// to do. Drop down the completion percentage to indicate
// that we have stuff to do.
$scope.completion[device]._total = 95;
}
console.log("recalcCompletion", device, $scope.completion[device]);
}
function refreshCompletion(device, folder) {
if (device === $scope.myID) {
return;
@@ -457,20 +473,8 @@ angular.module('syncthing.core')
if (!$scope.completion[device]) {
$scope.completion[device] = {};
}
$scope.completion[device][folder] = data.completion;
var tot = 0,
cnt = 0;
for (var cmp in $scope.completion[device]) {
if (cmp === "_total") {
continue;
}
tot += $scope.completion[device][cmp];
cnt += 1;
}
$scope.completion[device]._total = tot / cnt;
console.log("refreshCompletion", device, folder, $scope.completion[device]);
$scope.completion[device][folder] = data;
recalcCompletion(device);
}).error($scope.emitHTTPError);
}

View File

@@ -181,14 +181,14 @@
<button type="button" class="btn btn-primary btn-sm" ng-click="saveFolder()" ng-disabled="folderEditor.$invalid">
<span class="fa fa-check"></span>&nbsp;<span translate>Save</span>
</button>
<button type="button" class="btn btn-default btn-sm" id="editIgnoresButton" ng-click="editIgnores()" ng-if="editingExisting">
<span class="fa fa-eye-slash"></span>&nbsp;<span translate>Ignore Patterns</span>
</button>
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
<span class="fa fa-times"></span>&nbsp;<span translate>Close</span>
</button>
<button type="button" class="btn btn-warning pull-left btn-sm" ng-click="deleteFolder(currentFolder.id)" ng-if="editingExisting">
<span class="fa fa-minus-circle"></span>&nbsp;<span translate>Remove</span>
</button>
<button type="button" class="btn btn-default pull-left btn-sm" id="editIgnoresButton" ng-click="editIgnores()" ng-if="editingExisting">
<span class="fa fa-eye-slash"></span>&nbsp;<span translate>Ignore Patterns</span>
</button>
</div>
</modal>

View File

@@ -9,8 +9,8 @@
<div class="panel-group" id="advancedAccordion" role="tablist" aria-multiselectable="true">
<div class="panel panel-default">
<div class="panel-heading" role="tab" id="guiHeading" data-toggle="collapse" data-parent="#advancedAccordion" href="#guiConfig" aria-expanded="true" aria-controls="guiConfig" style="cursor: pointer">
<h4 class="panel-title" translate>GUI</h4>
<div class="panel-heading" role="tab" id="guiHeading" data-toggle="collapse" data-parent="#advancedAccordion" href="#guiConfig" aria-expanded="true" aria-controls="guiConfig" style="cursor: pointer;">
<h4 class="panel-title" translate tabindex="0">GUI</h4>
</div>
<div id="guiConfig" class="panel-collapse collapse in" role="tabpanel" aria-labelledby="guiHeading">
<div class="panel-body">
@@ -27,8 +27,8 @@
</div>
<div class="panel panel-default">
<div class="panel-heading" role="tab" id="optionsHeading" data-toggle="collapse" data-parent="#advancedAccordion" href="#optionsConfig" aria-expanded="true" aria-controls="optionsConfig" style="cursor: pointer">
<h4 class="panel-title" translate>Options</h4>
<div class="panel-heading" role="tab" id="optionsHeading" data-toggle="collapse" data-parent="#advancedAccordion" href="#optionsConfig" aria-expanded="false" aria-controls="optionsConfig" style="cursor: pointer;">
<h4 class="panel-title" tabindex="0" translate>Options</h4>
</div>
<div id="optionsConfig" class="panel-collapse collapse" role="tabpanel" aria-labelledby="optionsHeading">
<div class="panel-body">
@@ -45,11 +45,11 @@
</div>
<div class="panel panel-default" ng-repeat="folder in advancedConfig.folders">
<div class="panel-heading" role="tab" id="folder{{$index}}Heading" data-toggle="collapse" data-parent="#advancedAccordion" href="#folder{{$index}}Config" aria-expanded="true" aria-controls="folder{{$index}}Config" style="cursor: pointer">
<h4 ng-if="folder.label.length == 0" class="panel-title">
<div class="panel-heading" role="tab" id="folder{{$index}}Heading" data-toggle="collapse" data-parent="#advancedAccordion" href="#folder{{$index}}Config" aria-expanded="false" aria-controls="folder{{$index}}Config" style="cursor: pointer;">
<h4 ng-if="folder.label.length == 0" class="panel-title" tabindex="0">
<span translate>Folder</span> "{{folder.id}}"
</h4>
<h4 ng-if="folder.label.length != 0" class="panel-title">
<h4 ng-if="folder.label.length != 0" class="panel-title" tabindex="0">
<span translate>Folder</span> "{{folder.label}}" ({{folder.id}})
</h4>
</div>

51
jenkins/build-linux.bash Executable file
View File

@@ -0,0 +1,51 @@
#!/bin/bash
set -euo pipefail
# 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 http://mozilla.org/MPL/2.0/.
# This script should be run by Jenkins as './src/github.com/syncthing/syncthing/jenkins/build-linux.bash',
# that is, it should be run from $GOPATH.
. src/github.com/syncthing/syncthing/jenkins/common.bash
init
# after init we are in the source directory
clean
fetchExtra
buildSource
build
test
testWithCoverage
platforms=(
dragonfly-amd64
freebsd-amd64 freebsd-386
linux-amd64 linux-386 linux-arm linux-arm64 linux-ppc64 linux-ppc64le
netbsd-amd64 netbsd-386
openbsd-amd64 openbsd-386
solaris-amd64
)
echo Building
for plat in "${platforms[@]}"; do
echo Building "$plat"
goos="${plat%-*}"
goarch="${plat#*-}"
go run build.go -goos "$goos" -goarch "$goarch" tar
mv *.tar.gz "$WORKSPACE"
echo
done
go run build.go -goarch amd64 deb
go run build.go -goarch i386 deb
go run build.go -goarch armel deb
go run build.go -goarch armhf deb
mv *.deb "$WORKSPACE"

37
jenkins/build-macos.bash Executable file
View File

@@ -0,0 +1,37 @@
#!/bin/bash
set -euo pipefail
# 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 http://mozilla.org/MPL/2.0/.
# This script should be run by Jenkins as './src/github.com/syncthing/syncthing/jenkins/build-macos.bash',
# that is, it should be run from $GOPATH.
. src/github.com/syncthing/syncthing/jenkins/common.bash
init
# after init we are in the source directory
clean
fetchExtra
build
test
platforms=(
darwin-amd64 darwin-386
)
echo Building
for plat in "${platforms[@]}"; do
echo Building "$plat"
goos="${plat%-*}"
goarch="${plat#*-}"
go run build.go -goos "$goos" -goarch "$goarch" tar
mv *.tar.gz "$WORKSPACE"
echo
done

55
jenkins/build-windows.bat Normal file
View File

@@ -0,0 +1,55 @@
@echo off
rem Copyright (C) 2016 The Syncthing Authors.
rem
rem This Source Code Form is subject to the terms of the Mozilla Public
rem License, v. 2.0. If a copy of the MPL was not distributed with this file,
rem You can obtain one at http://mozilla.org/MPL/2.0/.
rem This batch file should be run from the GOPATH.
rem It expects to run on amd64, for windows-amd64 Go to be installed in C:\go
rem and for windows-386 Go to be installed in C:\go-386.
rem cURL should be installed in C:\Program Files\cURL.
set ORIGPATH="C:\Program Files\cURL\bin";%PATH%
set PATH=c:\go\bin;%ORIGPATH%
set GOROOT=c:\go
cd >gopath
set /p GOPATH= <gopath
cd src\github.com\syncthing\syncthing
echo Initializing ^& cleaning
go version
git clean -fxd || goto error
go run build.go version
echo.
echo Fetching extras
mkdir extra
curl -s -L -o extra/Getting-Started.pdf https://docs.syncthing.net/pdf/Getting-Started.pdf || goto :error
curl -s -L -o extra/FAQ.pdf https://docs.syncthing.net/pdf/FAQ.pdf || goto :error
echo.
echo Testing
go run build.go test || goto :error
echo.
echo Building (amd64)
go run build.go zip || goto :error
echo.
set PATH=c:\go-386\bin;%ORIGPATH%
set GOROOT=c:\go-386
echo building (386)
go run build.go zip || goto :error
echo.
goto :EOF
:error
echo code #%errorlevel%.
exit /b %errorlevel%

84
jenkins/common.bash Normal file
View File

@@ -0,0 +1,84 @@
#!/bin/bash
set -euo pipefail
# 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 http://mozilla.org/MPL/2.0/.
ulimit -t 600 || true
ulimit -d 1024000 || true
ulimit -m 1024000 || true
export CGO_ENABLED=0
export GO386=387
export GOARM=5
function init {
echo Initializing
export GOPATH=$(pwd)
export WORKSPACE="${WORKSPACE:-$GOPATH}"
go version
rm -f *.tar.gz *.zip *.deb
cd src/github.com/syncthing/syncthing
version=$(go run build.go version)
echo "Building $version"
echo
}
function clean {
echo Cleaning
rm -rf "$GOPATH/pkg"
git clean -fxd
echo
}
function fetchExtra {
echo Fetching extra resources
mkdir extra
curl -s -o extra/Getting-Started.pdf http://docs.syncthing.net/pdf/Getting-Started.pdf
curl -s -o extra/FAQ.pdf http://docs.syncthing.net/pdf/FAQ.pdf
echo
}
function checkAuthorsCopyright {
echo Basic metadata checks
go run script/check-authors.go
go run script/check-copyright.go lib/ cmd/ script/
echo
}
function build {
echo Build
go run build.go
echo
}
function test {
echo Test with race
CGO_ENABLED=1 go run build.go -race test
echo
}
function testWithCoverage {
echo Test with coverage
CGO_ENABLED=1 ./build.sh test-cov
notCovered=$(egrep -c '\s0$' coverage.out)
total=$(wc -l coverage.out | awk '{print $1}')
coverPct=$(awk "BEGIN{print (1 - $notCovered / $total) * 100}")
echo "$coverPct" > "coverage.txt"
echo "Test coverage is $coverPct%%"
echo
}
function buildSource {
echo Archiving source
echo "$version" > RELEASE
pushd .. >/dev/null
tar c -z -f "$WORKSPACE/syncthing-source-$version.tar.gz" --exclude .git syncthing
popd >/dev/null
echo
}

View File

@@ -57,7 +57,7 @@ func TestReplaceCommit(t *testing.T) {
if w.RequiresRestart() {
t.Fatal("Should not require restart")
}
if w.Raw().Version != 1 {
if w.Raw().Version != CurrentVersion {
t.Fatal("Config should have changed")
}
@@ -76,7 +76,7 @@ func TestReplaceCommit(t *testing.T) {
if !w.RequiresRestart() {
t.Fatal("Should require restart")
}
if w.Raw().Version != 2 {
if w.Raw().Version != CurrentVersion {
t.Fatal("Config should have changed")
}
@@ -92,7 +92,7 @@ func TestReplaceCommit(t *testing.T) {
if !w.RequiresRestart() {
t.Fatal("Should still require restart")
}
if w.Raw().Version != 2 {
if w.Raw().Version != CurrentVersion {
t.Fatal("Config should not have changed")
}
}

View File

@@ -26,7 +26,7 @@ import (
const (
OldestHandledVersion = 10
CurrentVersion = 15
CurrentVersion = 16
MaxRescanIntervalS = 365 * 24 * 60 * 60
)
@@ -42,16 +42,16 @@ var (
// DefaultDiscoveryServersV4 should be substituted when the configuration
// contains <globalAnnounceServer>default-v4</globalAnnounceServer>.
DefaultDiscoveryServersV4 = []string{
"https://discovery-v4-1.syncthing.net/v2/?id=SR7AARM-TCBUZ5O-VFAXY4D-CECGSDE-3Q6IZ4G-XG7AH75-OBIXJQV-QJ6NLQA", // 194.126.249.5, Sweden
"https://discovery-v4-2.syncthing.net/v2/?id=DVU36WY-H3LVZHW-E6LLFRE-YAFN5EL-HILWRYP-OC2M47J-Z4PE62Y-ADIBDQC", // 45.55.230.38, USA
"https://discovery-v4-3.syncthing.net/v2/?id=VK6HNJ3-VVMM66S-HRVWSCR-IXEHL2H-U4AQ4MW-UCPQBWX-J2L2UBK-NVZRDQZ", // 128.199.95.124, Singapore
"https://discovery-v4-4.syncthing.net/v2/?id=LYXKCHX-VI3NYZR-ALCJBHF-WMZYSPK-QG6QJA3-MPFYMSO-U56GTUK-NA2MIAW", // 95.85.19.244, NL
}
// DefaultDiscoveryServersV6 should be substituted when the configuration
// contains <globalAnnounceServer>default-v6</globalAnnounceServer>.
DefaultDiscoveryServersV6 = []string{
"https://discovery-v6-1.syncthing.net/v2/?id=SR7AARM-TCBUZ5O-VFAXY4D-CECGSDE-3Q6IZ4G-XG7AH75-OBIXJQV-QJ6NLQA", // 2001:470:28:4d6::5, Sweden
"https://discovery-v6-2.syncthing.net/v2/?id=DVU36WY-H3LVZHW-E6LLFRE-YAFN5EL-HILWRYP-OC2M47J-Z4PE62Y-ADIBDQC", // 2604:a880:800:10::182:a001, USA
"https://discovery-v6-3.syncthing.net/v2/?id=VK6HNJ3-VVMM66S-HRVWSCR-IXEHL2H-U4AQ4MW-UCPQBWX-J2L2UBK-NVZRDQZ", // 2400:6180:0:d0::d9:d001, Singapore
"https://discovery-v6-4.syncthing.net/v2/?id=LYXKCHX-VI3NYZR-ALCJBHF-WMZYSPK-QG6QJA3-MPFYMSO-U56GTUK-NA2MIAW", // 2a03:b0c0:0:1010::4ed:3001, NL
}
// DefaultDiscoveryServers should be substituted when the configuration
// contains <globalAnnounceServer>default</globalAnnounceServer>.
@@ -70,7 +70,10 @@ func New(myID protocol.DeviceID) Configuration {
util.SetDefaults(&cfg.Options)
util.SetDefaults(&cfg.GUI)
cfg.prepare(myID)
// Can't happen.
if err := cfg.prepare(myID); err != nil {
panic("bug: error in preparing new folder: " + err.Error())
}
return cfg
}
@@ -164,6 +167,36 @@ func (cfg *Configuration) WriteXML(w io.Writer) error {
}
func (cfg *Configuration) prepare(myID protocol.DeviceID) error {
var myName string
// Ensure this device is present in the config
for _, device := range cfg.Devices {
if device.DeviceID == myID {
goto found
}
}
myName, _ = os.Hostname()
cfg.Devices = append(cfg.Devices, DeviceConfiguration{
DeviceID: myID,
Name: myName,
})
found:
if err := cfg.clean(); err != nil {
return err
}
// Ensure that we are part of the devices
for i := range cfg.Folders {
cfg.Folders[i].Devices = ensureDevicePresent(cfg.Folders[i].Devices, myID)
}
return nil
}
func (cfg *Configuration) clean() error {
util.FillNilSlices(&cfg.Options)
// Initialize any empty slices
@@ -176,6 +209,9 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) error {
if cfg.Options.AlwaysLocalNets == nil {
cfg.Options.AlwaysLocalNets = []string{}
}
if cfg.Options.UnackedNotificationIDs == nil {
cfg.Options.UnackedNotificationIDs = []string{}
}
// Prepare folders and check for duplicates. Duplicates are bad and
// dangerous, can't currently be resolved in the GUI, and shouldn't
@@ -215,6 +251,9 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) error {
if cfg.Version == 14 {
convertV14V15(cfg)
}
if cfg.Version == 15 {
convertV15V16(cfg)
}
// Build a list of available devices
existingDevices := make(map[protocol.DeviceID]bool)
@@ -222,26 +261,14 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) error {
existingDevices[device.DeviceID] = true
}
// Ensure this device is present in the config
if !existingDevices[myID] {
myName, _ := os.Hostname()
cfg.Devices = append(cfg.Devices, DeviceConfiguration{
DeviceID: myID,
Name: myName,
})
existingDevices[myID] = true
}
// Ensure that the device list is free from duplicates
cfg.Devices = ensureNoDuplicateDevices(cfg.Devices)
sort.Sort(DeviceConfigurationList(cfg.Devices))
// Ensure that any loose devices are not present in the wrong places
// Ensure that there are no duplicate devices
// Ensure that puller settings are sane
// Ensure that the versioning configuration parameter map is not nil
for i := range cfg.Folders {
cfg.Folders[i].Devices = ensureDevicePresent(cfg.Folders[i].Devices, myID)
cfg.Folders[i].Devices = ensureExistingDevices(cfg.Folders[i].Devices, existingDevices)
cfg.Folders[i].Devices = ensureNoDuplicateFolderDevices(cfg.Folders[i].Devices)
if cfg.Folders[i].Versioning.Params == nil {
@@ -267,6 +294,16 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) error {
cfg.GUI.APIKey = rand.String(32)
}
// The list of ignored devices should not contain any devices that have
// been manually added to the config.
newIgnoredDevices := []protocol.DeviceID{}
for _, dev := range cfg.IgnoredDevices {
if !existingDevices[dev] {
newIgnoredDevices = append(newIgnoredDevices, dev)
}
}
cfg.IgnoredDevices = newIgnoredDevices
return nil
}
@@ -285,6 +322,11 @@ func convertV14V15(cfg *Configuration) {
cfg.Version = 15
}
func convertV15V16(cfg *Configuration) {
// Triggers a database tweak
cfg.Version = 16
}
func convertV13V14(cfg *Configuration) {
// Not using the ignore cache is the new default. Disable it on existing
// configurations.

View File

@@ -64,6 +64,7 @@ func TestDefaultValues(t *testing.T) {
AlwaysLocalNets: []string{},
OverwriteRemoteDevNames: false,
TempIndexMinBlocks: 10,
UnackedNotificationIDs: []string{},
}
cfg := New(device1)
@@ -103,6 +104,9 @@ func TestDeviceConfig(t *testing.T) {
AutoNormalize: true,
MinDiskFreePct: 1,
MaxConflicts: -1,
Versioning: VersioningConfiguration{
Params: map[string]string{},
},
},
}
@@ -194,6 +198,7 @@ func TestOverriddenValues(t *testing.T) {
AlwaysLocalNets: []string{},
OverwriteRemoteDevNames: true,
TempIndexMinBlocks: 100,
UnackedNotificationIDs: []string{},
}
cfg, err := Load("testdata/overridenvalues.xml", device1)
@@ -697,3 +702,73 @@ func TestV14ListenAddressesMigration(t *testing.T) {
}
}
}
func TestIgnoredDevices(t *testing.T) {
// Verify that ignored devices that are also present in the
// configuration are not in fact ignored.
wrapper, err := Load("testdata/ignoreddevices.xml", device1)
if err != nil {
t.Fatal(err)
}
if wrapper.IgnoredDevice(device1) {
t.Errorf("Device %v should not be ignored", device1)
}
if !wrapper.IgnoredDevice(device3) {
t.Errorf("Device %v should be ignored", device3)
}
}
func TestGetDevice(t *testing.T) {
// Verify that the Device() call does the right thing
wrapper, err := Load("testdata/ignoreddevices.xml", device1)
if err != nil {
t.Fatal(err)
}
// device1 is mentioned in the config
device, ok := wrapper.Device(device1)
if !ok {
t.Error(device1, "should exist")
}
if device.DeviceID != device1 {
t.Error("Should have returned", device1, "not", device.DeviceID)
}
// device3 is not
device, ok = wrapper.Device(device3)
if ok {
t.Error(device3, "should not exist")
}
if device.DeviceID == device3 {
t.Error("Should not returned ID", device3)
}
}
func TestSharesRemovedOnDeviceRemoval(t *testing.T) {
wrapper, err := Load("testdata/example.xml", device1)
if err != nil {
t.Errorf("Failed: %s", err)
}
raw := wrapper.Raw()
raw.Devices = raw.Devices[:len(raw.Devices)-1]
if len(raw.Folders[0].Devices) <= len(raw.Devices) {
t.Error("Should have less devices")
}
err = wrapper.Replace(raw)
if err != nil {
t.Errorf("Failed: %s", err)
}
raw = wrapper.Raw()
if len(raw.Folders[0].Devices) > len(raw.Devices) {
t.Error("Unexpected extra device")
}
}

View File

@@ -13,14 +13,16 @@ import (
)
type GUIConfiguration struct {
Enabled bool `xml:"enabled,attr" json:"enabled" default:"true"`
RawAddress string `xml:"address" json:"address" default:"127.0.0.1:8384"`
User string `xml:"user,omitempty" json:"user"`
Password string `xml:"password,omitempty" json:"password"`
RawUseTLS bool `xml:"tls,attr" json:"useTLS"`
APIKey string `xml:"apikey,omitempty" json:"apiKey"`
InsecureAdminAccess bool `xml:"insecureAdminAccess,omitempty" json:"insecureAdminAccess"`
Theme string `xml:"theme" json:"theme" default:"default"`
Enabled bool `xml:"enabled,attr" json:"enabled" default:"true"`
RawAddress string `xml:"address" json:"address" default:"127.0.0.1:8384"`
User string `xml:"user,omitempty" json:"user"`
Password string `xml:"password,omitempty" json:"password"`
RawUseTLS bool `xml:"tls,attr" json:"useTLS"`
APIKey string `xml:"apikey,omitempty" json:"apiKey"`
InsecureAdminAccess bool `xml:"insecureAdminAccess,omitempty" json:"insecureAdminAccess"`
Theme string `xml:"theme" json:"theme" default:"default"`
Debugging bool `xml:"debugging,attr" json:"debugging"`
InsecureSkipHostCheck bool `xml:"insecureSkipHostcheck,omitempty" json:"insecureSkipHostcheck"`
}
func (c GUIConfiguration) Address() string {

View File

@@ -40,6 +40,7 @@ type OptionsConfiguration struct {
AlwaysLocalNets []string `xml:"alwaysLocalNet" json:"alwaysLocalNets"`
OverwriteRemoteDevNames bool `xml:"overwriteRemoteDeviceNamesOnConnect" json:"overwriteRemoteDeviceNamesOnConnect" default:"false"`
TempIndexMinBlocks int `xml:"tempIndexMinBlocks" json:"tempIndexMinBlocks" default:"10"`
UnackedNotificationIDs []string `xml:"unackedNotificationID" json:"unackedNotificationIDs"`
DeprecatedUPnPEnabled bool `xml:"upnpEnabled,omitempty" json:"-"`
DeprecatedUPnPLeaseM int `xml:"upnpLeaseMinutes,omitempty" json:"-"`
@@ -56,5 +57,7 @@ func (orig OptionsConfiguration) Copy() OptionsConfiguration {
copy(c.GlobalAnnServers, orig.GlobalAnnServers)
c.AlwaysLocalNets = make([]string, len(orig.AlwaysLocalNets))
copy(c.AlwaysLocalNets, orig.AlwaysLocalNets)
c.UnackedNotificationIDs = make([]string, len(orig.UnackedNotificationIDs))
copy(c.UnackedNotificationIDs, orig.UnackedNotificationIDs)
return c
}

10
lib/config/testdata/ignoreddevices.xml vendored Normal file
View File

@@ -0,0 +1,10 @@
<configuration version="15">
<device id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR">
<address>dynamic</address>
</device>
<device id="GYRZZQB-IRNPV4Z-T7TC52W-EQYJ3TT-FDQW6MW-DFLMU42-SSSU6EM-FBK2VAY">
<address>dynamic</address>
</device>
<ignoredDevice>AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR</ignoredDevice>
<ignoredDevice>LGFPDIT-7SKNNJL-VJZA4FC-7QNCRKA-CE753K7-2BW5QDK-2FOZ7FR-FEP57QJ</ignoredDevice>
</configuration>

14
lib/config/testdata/v16.xml vendored Normal file
View File

@@ -0,0 +1,14 @@
<configuration version="16">
<folder id="test" path="testdata" type="readonly" ignorePerms="false" rescanIntervalS="600" autoNormalize="true">
<device id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR"></device>
<device id="P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2"></device>
<minDiskFreePct>1</minDiskFreePct>
<maxConflicts>-1</maxConflicts>
</folder>
<device id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR" name="node one" compression="metadata">
<address>tcp://a</address>
</device>
<device id="P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2" name="node two" compression="metadata">
<address>tcp://b</address>
</device>
</configuration>

View File

@@ -136,6 +136,10 @@ func (w *Wrapper) Replace(cfg Configuration) error {
func (w *Wrapper) replaceLocked(to Configuration) error {
from := w.cfg
if err := to.clean(); err != nil {
return err
}
for _, sub := range w.subs {
l.Debugln(sub, "verifying configuration")
if err := sub.VerifyConfiguration(from, to); err != nil {
@@ -284,6 +288,18 @@ func (w *Wrapper) IgnoredDevice(id protocol.DeviceID) bool {
return false
}
// Device returns the configuration for the given device and an "ok" bool.
func (w *Wrapper) Device(id protocol.DeviceID) (DeviceConfiguration, bool) {
w.mut.Lock()
defer w.mut.Unlock()
for _, device := range w.cfg.Devices {
if device.DeviceID == id {
return device, true
}
}
return DeviceConfiguration{}, false
}
// Save writes the configuration to disk, and generates a ConfigSaved event.
func (w *Wrapper) Save() error {
fd, err := osutil.CreateAtomic(w.path, 0600)

View File

@@ -0,0 +1,10 @@
// 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 http://mozilla.org/MPL/2.0/.
// The existence of this file means we get 0% test coverage rather than no
// test coverage at all. Remove when implementing an actual test.
package connections

View File

@@ -183,19 +183,26 @@ next:
}
c.SetDeadline(time.Time{})
s.model.OnHello(remoteID, c.RemoteAddr(), hello)
// The Model will return an error for devices that we don't want to
// have a connection with for whatever reason, for example unknown devices.
if err := s.model.OnHello(remoteID, c.RemoteAddr(), hello); err != nil {
l.Infof("Connection from %s at %s (%s) rejected: %v", remoteID, c.RemoteAddr(), c.Type, err)
c.Close()
continue
}
// If we have a relay connection, and the new incoming connection is
// not a relay connection, we should drop that, and prefer the this one.
connected := s.model.ConnectedTo(remoteID)
s.curConMut.Lock()
ct, ok := s.currentConnection[remoteID]
s.curConMut.Unlock()
priorityKnown := ok && connected
// Lower priority is better, just like nice etc.
if ok && ct.Priority > c.Priority {
if priorityKnown && ct.Priority > c.Priority {
l.Debugln("Switching connections", remoteID)
s.model.Close(remoteID, protocol.ErrSwitchingConnections)
} else if s.model.ConnectedTo(remoteID) {
} else if connected {
// We should not already be connected to the other party. TODO: This
// could use some better handling. If the old connection is dead but
// hasn't timed out yet we may want to drop *that* connection and keep
@@ -205,63 +212,56 @@ next:
l.Infof("Connected to already connected device (%s)", remoteID)
c.Close()
continue
} else if s.model.IsPaused(remoteID) {
l.Infof("Connection from paused device (%s)", remoteID)
}
deviceCfg, ok := s.cfg.Device(remoteID)
if !ok {
panic("bug: unknown device should already have been rejected")
}
// Verify the name on the certificate. By default we set it to
// "syncthing" when generating, but the user may have replaced
// the certificate and used another name.
certName := deviceCfg.CertName
if certName == "" {
certName = s.tlsDefaultCommonName
}
if err := remoteCert.VerifyHostname(certName); err != nil {
// Incorrect certificate name is something the user most
// likely wants to know about, since it's an advanced
// config. Warn instead of Info.
l.Warnf("Bad certificate from %s (%v): %v", remoteID, c.RemoteAddr(), err)
c.Close()
continue
continue next
}
for deviceID, deviceCfg := range s.cfg.Devices() {
if deviceID == remoteID {
// Verify the name on the certificate. By default we set it to
// "syncthing" when generating, but the user may have replaced
// the certificate and used another name.
certName := deviceCfg.CertName
if certName == "" {
certName = s.tlsDefaultCommonName
}
err := remoteCert.VerifyHostname(certName)
if err != nil {
// Incorrect certificate name is something the user most
// likely wants to know about, since it's an advanced
// config. Warn instead of Info.
l.Warnf("Bad certificate from %s (%v): %v", remoteID, c.RemoteAddr(), err)
c.Close()
continue next
}
// If rate limiting is set, and based on the address we should
// limit the connection, then we wrap it in a limiter.
// If rate limiting is set, and based on the address we should
// limit the connection, then we wrap it in a limiter.
limit := s.shouldLimit(c.RemoteAddr())
limit := s.shouldLimit(c.RemoteAddr())
wr := io.Writer(c)
if limit && s.writeRateLimit != nil {
wr = NewWriteLimiter(c, s.writeRateLimit)
}
rd := io.Reader(c)
if limit && s.readRateLimit != nil {
rd = NewReadLimiter(c, s.readRateLimit)
}
name := fmt.Sprintf("%s-%s (%s)", c.LocalAddr(), c.RemoteAddr(), c.Type)
protoConn := protocol.NewConnection(remoteID, rd, wr, s.model, name, deviceCfg.Compression)
modelConn := Connection{c, protoConn}
l.Infof("Established secure connection to %s at %s", remoteID, name)
l.Debugf("cipher suite: %04X in lan: %t", c.ConnectionState().CipherSuite, !limit)
s.model.AddConnection(modelConn, hello)
s.curConMut.Lock()
s.currentConnection[remoteID] = modelConn
s.curConMut.Unlock()
continue next
}
wr := io.Writer(c)
if limit && s.writeRateLimit != nil {
wr = NewWriteLimiter(c, s.writeRateLimit)
}
l.Infof("Connection from %s (%s) with ignored device ID %s", c.RemoteAddr(), c.Type, remoteID)
c.Close()
rd := io.Reader(c)
if limit && s.readRateLimit != nil {
rd = NewReadLimiter(c, s.readRateLimit)
}
name := fmt.Sprintf("%s-%s (%s)", c.LocalAddr(), c.RemoteAddr(), c.Type)
protoConn := protocol.NewConnection(remoteID, rd, wr, s.model, name, deviceCfg.Compression)
modelConn := Connection{c, protoConn}
l.Infof("Established secure connection to %s at %s", remoteID, name)
l.Debugf("cipher suite: %04X in lan: %t", c.ConnectionState().CipherSuite, !limit)
s.model.AddConnection(modelConn, hello)
s.curConMut.Lock()
s.currentConnection[remoteID] = modelConn
s.curConMut.Unlock()
continue next
}
}
@@ -307,10 +307,11 @@ func (s *Service) connect() {
connected := s.model.ConnectedTo(deviceID)
s.curConMut.Lock()
ct := s.currentConnection[deviceID]
ct, ok := s.currentConnection[deviceID]
s.curConMut.Unlock()
priorityKnown := ok && connected
if connected && ct.Priority == bestDialerPrio {
if priorityKnown && ct.Priority == bestDialerPrio {
// Things are already as good as they can get.
continue
}
@@ -358,7 +359,7 @@ func (s *Service) connect() {
continue
}
if connected && dialerFactory.Priority() >= ct.Priority {
if priorityKnown && dialerFactory.Priority() >= ct.Priority {
l.Debugf("Not dialing using %s as priority is less than current connection (%d >= %d)", dialerFactory, dialerFactory.Priority(), ct.Priority)
continue
}
@@ -373,10 +374,6 @@ func (s *Service) connect() {
continue
}
if connected {
s.model.Close(deviceID, protocol.ErrSwitchingConnections)
}
s.conns <- conn
continue nextDevice
}
@@ -437,10 +434,6 @@ func (s *Service) VerifyConfiguration(from, to config.Configuration) error {
}
func (s *Service) CommitConfiguration(from, to config.Configuration) bool {
// We require a restart if a device as been removed.
restart := false
newDevices := make(map[protocol.DeviceID]bool, len(to.Devices))
for _, dev := range to.Devices {
newDevices[dev.DeviceID] = true
@@ -448,7 +441,12 @@ func (s *Service) CommitConfiguration(from, to config.Configuration) bool {
for _, dev := range from.Devices {
if !newDevices[dev.DeviceID] {
restart = true
s.curConMut.Lock()
delete(s.currentConnection, dev.DeviceID)
s.curConMut.Unlock()
warningLimitersMut.Lock()
delete(warningLimiters, dev.DeviceID)
warningLimitersMut.Unlock()
}
}
@@ -500,7 +498,7 @@ func (s *Service) CommitConfiguration(from, to config.Configuration) bool {
s.natServiceToken = nil
}
return !restart
return true
}
func (s *Service) AllAddresses() []string {

View File

@@ -8,6 +8,7 @@ package connections
import (
"crypto/tls"
"fmt"
"net"
"net/url"
"time"
@@ -28,6 +29,10 @@ type Connection struct {
protocol.Connection
}
func (c Connection) String() string {
return fmt.Sprintf("%s-%s/%s", c.LocalAddr(), c.RemoteAddr(), c.Type)
}
type dialerFactory interface {
New(*config.Wrapper, *tls.Config) genericDialer
Priority() int
@@ -69,7 +74,7 @@ type Model interface {
AddConnection(conn Connection, hello protocol.HelloResult)
ConnectedTo(remoteID protocol.DeviceID) bool
IsPaused(remoteID protocol.DeviceID) bool
OnHello(protocol.DeviceID, net.Addr, protocol.HelloResult)
OnHello(protocol.DeviceID, net.Addr, protocol.HelloResult) error
GetHello(protocol.DeviceID) protocol.HelloIntf
}

View File

@@ -29,6 +29,7 @@ type deletionHandler func(t readWriteTransaction, folder, device, name []byte, d
type Instance struct {
committed int64 // this must be the first attribute in the struct to ensure 64 bit alignment on 32 bit plaforms
*leveldb.DB
location string
folderIdx *smallIndex
deviceIdx *smallIndex
}
@@ -64,17 +65,18 @@ func Open(file string) (*Instance, error) {
return nil, err
}
return newDBInstance(db), nil
return newDBInstance(db, file), nil
}
func OpenMemory() *Instance {
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
return newDBInstance(db)
return newDBInstance(db, "<memory>")
}
func newDBInstance(db *leveldb.DB) *Instance {
func newDBInstance(db *leveldb.DB, location string) *Instance {
i := &Instance{
DB: db,
DB: db,
location: location,
}
i.folderIdx = newSmallIndex(i, []byte{KeyTypeFolderIdx})
i.deviceIdx = newSmallIndex(i, []byte{KeyTypeDeviceIdx})
@@ -86,6 +88,11 @@ func (db *Instance) Committed() int64 {
return atomic.LoadInt64(&db.committed)
}
// Location returns the filesystem path where the database is stored
func (db *Instance) Location() string {
return db.location
}
func (db *Instance) genericReplace(folder, device []byte, fs []protocol.FileInfo, localSize, globalSize *sizeTracker, deleteFn deletionHandler) {
sort.Sort(fileList(fs)) // sort list on name, same as in the database
@@ -712,6 +719,35 @@ func (db *Instance) indexIDKey(device, folder []byte) []byte {
return k
}
func (db *Instance) mtimesKey(folder []byte) []byte {
prefix := make([]byte, 5) // key type + 4 bytes folder idx number
prefix[0] = KeyTypeVirtualMtime
binary.BigEndian.PutUint32(prefix[1:], db.folderIdx.ID(folder))
return prefix
}
// DropDeltaIndexIDs removes all index IDs from the database. This will
// cause a full index transmission on the next connection.
func (db *Instance) DropDeltaIndexIDs() {
db.dropPrefix([]byte{KeyTypeIndexID})
}
func (db *Instance) dropMtimes(folder []byte) {
db.dropPrefix(db.mtimesKey(folder))
}
func (db *Instance) dropPrefix(prefix []byte) {
t := db.newReadWriteTransaction()
defer t.close()
dbi := t.NewIterator(util.BytesPrefix(prefix), nil)
defer dbi.Release()
for dbi.Next() {
t.Delete(dbi.Key())
}
}
func unmarshalTrunc(bs []byte, truncate bool) (FileIntf, error) {
if truncate {
var tf FileInfoTruncated

View File

@@ -16,21 +16,22 @@ import (
stdsync "sync"
"sync/atomic"
"github.com/syncthing/syncthing/lib/fs"
"github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/sync"
)
type FileSet struct {
localVersion int64 // Our local version counter
folder string
db *Instance
blockmap *BlockMap
localSize sizeTracker
globalSize sizeTracker
sequence int64 // Our local sequence number
folder string
db *Instance
blockmap *BlockMap
localSize sizeTracker
globalSize sizeTracker
remoteLocalVersion map[protocol.DeviceID]int64 // Highest seen local versions for other devices
updateMutex sync.Mutex // protects remoteLocalVersion and database updates
remoteSequence map[protocol.DeviceID]int64 // Highest seen sequence numbers for other devices
updateMutex sync.Mutex // protects remoteSequence and database updates
}
// FileIntf is the set of methods implemented by both protocol.FileInfo and
@@ -98,11 +99,11 @@ func (s *sizeTracker) Size() (files, deleted int, bytes int64) {
func NewFileSet(folder string, db *Instance) *FileSet {
var s = FileSet{
remoteLocalVersion: make(map[protocol.DeviceID]int64),
folder: folder,
db: db,
blockmap: NewBlockMap(db, db.folderIdx.ID([]byte(folder))),
updateMutex: sync.NewMutex(),
remoteSequence: make(map[protocol.DeviceID]int64),
folder: folder,
db: db,
blockmap: NewBlockMap(db, db.folderIdx.ID([]byte(folder))),
updateMutex: sync.NewMutex(),
}
s.db.checkGlobals([]byte(folder), &s.globalSize)
@@ -111,16 +112,16 @@ func NewFileSet(folder string, db *Instance) *FileSet {
s.db.withAllFolderTruncated([]byte(folder), func(device []byte, f FileInfoTruncated) bool {
copy(deviceID[:], device)
if deviceID == protocol.LocalDeviceID {
if f.LocalVersion > s.localVersion {
s.localVersion = f.LocalVersion
if f.Sequence > s.sequence {
s.sequence = f.Sequence
}
s.localSize.addFile(f)
} else if f.LocalVersion > s.remoteLocalVersion[deviceID] {
s.remoteLocalVersion[deviceID] = f.LocalVersion
} else if f.Sequence > s.remoteSequence[deviceID] {
s.remoteSequence[deviceID] = f.Sequence
}
return true
})
l.Debugf("loaded localVersion for %q: %#v", folder, s.localVersion)
l.Debugf("loaded sequence for %q: %#v", folder, s.sequence)
return &s
}
@@ -134,17 +135,17 @@ func (s *FileSet) Replace(device protocol.DeviceID, fs []protocol.FileInfo) {
if device == protocol.LocalDeviceID {
if len(fs) == 0 {
s.localVersion = 0
s.sequence = 0
} else {
// Always overwrite LocalVersion on updated files to ensure
// Always overwrite Sequence on updated files to ensure
// correct ordering. The caller is supposed to leave it set to
// zero anyhow.
for i := range fs {
fs[i].LocalVersion = atomic.AddInt64(&s.localVersion, 1)
fs[i].Sequence = atomic.AddInt64(&s.sequence, 1)
}
}
} else {
s.remoteLocalVersion[device] = maxLocalVersion(fs)
s.remoteSequence[device] = maxSequence(fs)
}
s.db.replace([]byte(s.folder), device[:], fs, &s.localSize, &s.globalSize)
if device == protocol.LocalDeviceID {
@@ -164,7 +165,7 @@ func (s *FileSet) Update(device protocol.DeviceID, fs []protocol.FileInfo) {
discards := make([]protocol.FileInfo, 0, len(fs))
updates := make([]protocol.FileInfo, 0, len(fs))
for i, newFile := range fs {
fs[i].LocalVersion = atomic.AddInt64(&s.localVersion, 1)
fs[i].Sequence = atomic.AddInt64(&s.sequence, 1)
existingFile, ok := s.db.getFile([]byte(s.folder), device[:], []byte(newFile.Name))
if !ok || !existingFile.Version.Equal(newFile.Version) {
discards = append(discards, existingFile)
@@ -174,7 +175,7 @@ func (s *FileSet) Update(device protocol.DeviceID, fs []protocol.FileInfo) {
s.blockmap.Discard(discards)
s.blockmap.Update(updates)
} else {
s.remoteLocalVersion[device] = maxLocalVersion(fs)
s.remoteSequence[device] = maxSequence(fs)
}
s.db.updateFiles([]byte(s.folder), device[:], fs, &s.localSize, &s.globalSize)
}
@@ -248,14 +249,14 @@ func (s *FileSet) Availability(file string) []protocol.DeviceID {
return s.db.availability([]byte(s.folder), []byte(osutil.NormalizedFilename(file)))
}
func (s *FileSet) LocalVersion(device protocol.DeviceID) int64 {
func (s *FileSet) Sequence(device protocol.DeviceID) int64 {
if device == protocol.LocalDeviceID {
return atomic.LoadInt64(&s.localVersion)
return atomic.LoadInt64(&s.sequence)
}
s.updateMutex.Lock()
defer s.updateMutex.Unlock()
return s.remoteLocalVersion[device]
return s.remoteSequence[device]
}
func (s *FileSet) LocalSize() (files, deleted int, bytes int64) {
@@ -283,15 +284,33 @@ func (s *FileSet) SetIndexID(device protocol.DeviceID, id protocol.IndexID) {
s.db.setIndexID(device[:], []byte(s.folder), id)
}
// maxLocalVersion returns the highest of the LocalVersion numbers found in
// the given slice of FileInfos. This should really be the LocalVersion of
func (s *FileSet) MtimeFS() *fs.MtimeFS {
prefix := s.db.mtimesKey([]byte(s.folder))
kv := NewNamespacedKV(s.db, string(prefix))
return fs.NewMtimeFS(kv)
}
func (s *FileSet) ListDevices() []protocol.DeviceID {
s.updateMutex.Lock()
devices := make([]protocol.DeviceID, 0, len(s.remoteSequence))
for id, seq := range s.remoteSequence {
if seq > 0 {
devices = append(devices, id)
}
}
s.updateMutex.Unlock()
return devices
}
// maxSequence returns the highest of the Sequence numbers found in
// the given slice of FileInfos. This should really be the Sequence of
// the last item, but Syncthing v0.14.0 and other implementations may not
// implement update sorting....
func maxLocalVersion(fs []protocol.FileInfo) int64 {
func maxSequence(fs []protocol.FileInfo) int64 {
var max int64
for _, f := range fs {
if f.LocalVersion > max {
max = f.LocalVersion
if f.Sequence > max {
max = f.Sequence
}
}
return max
@@ -301,12 +320,12 @@ func maxLocalVersion(fs []protocol.FileInfo) int64 {
// database.
func DropFolder(db *Instance, folder string) {
db.dropFolder([]byte(folder))
db.dropMtimes([]byte(folder))
bm := &BlockMap{
db: db,
folder: db.folderIdx.ID([]byte(folder)),
}
bm.Drop()
NewVirtualMtimeRepo(db, folder).Drop()
}
func normalizeFilenames(fs []protocol.FileInfo) {

View File

@@ -100,11 +100,11 @@ func TestGlobalSet(t *testing.T) {
m := db.NewFileSet("test", ldb)
local0 := fileList{
protocol.FileInfo{Name: "a", LocalVersion: 1, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(1)},
protocol.FileInfo{Name: "b", LocalVersion: 2, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(2)},
protocol.FileInfo{Name: "c", LocalVersion: 3, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(3)},
protocol.FileInfo{Name: "d", LocalVersion: 4, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(4)},
protocol.FileInfo{Name: "z", LocalVersion: 5, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(8)},
protocol.FileInfo{Name: "a", Sequence: 1, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(1)},
protocol.FileInfo{Name: "b", Sequence: 2, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(2)},
protocol.FileInfo{Name: "c", Sequence: 3, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(3)},
protocol.FileInfo{Name: "d", Sequence: 4, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(4)},
protocol.FileInfo{Name: "z", Sequence: 5, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(8)},
}
local1 := fileList{
protocol.FileInfo{Name: "a", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(1)},
@@ -480,7 +480,7 @@ func TestNeed(t *testing.T) {
}
}
func TestLocalVersion(t *testing.T) {
func TestSequence(t *testing.T) {
ldb := db.OpenMemory()
m := db.NewFileSet("test", ldb)
@@ -501,10 +501,10 @@ func TestLocalVersion(t *testing.T) {
}
m.Replace(protocol.LocalDeviceID, local1)
c0 := m.LocalVersion(protocol.LocalDeviceID)
c0 := m.Sequence(protocol.LocalDeviceID)
m.Replace(protocol.LocalDeviceID, local2)
c1 := m.LocalVersion(protocol.LocalDeviceID)
c1 := m.Sequence(protocol.LocalDeviceID)
if !(c1 > c0) {
t.Fatal("Local version number should have incremented")
}

View File

@@ -11,13 +11,14 @@ package db
import (
"fmt"
"time"
"github.com/syncthing/syncthing/lib/protocol"
)
func (f FileInfoTruncated) String() string {
return fmt.Sprintf("File{Name:%q, Permissions:0%o, Modified:%d, Version:%v, Length:%d, Deleted:%v, Invalid:%v, NoPermissions:%v}",
f.Name, f.Permissions, f.Modified, f.Version, f.Size, f.Deleted, f.Invalid, f.NoPermissions)
return fmt.Sprintf("File{Name:%q, Permissions:0%o, Modified:%v, Version:%v, Length:%d, Deleted:%v, Invalid:%v, NoPermissions:%v}",
f.Name, f.Permissions, f.ModTime(), f.Version, f.Size, f.Deleted, f.Invalid, f.NoPermissions)
}
func (f FileInfoTruncated) IsDeleted() bool {
@@ -46,8 +47,11 @@ func (f FileInfoTruncated) HasPermissionBits() bool {
}
func (f FileInfoTruncated) FileSize() int64 {
if f.IsDirectory() || f.IsDeleted() {
return 128
if f.Deleted {
return 0
}
if f.IsDirectory() {
return protocol.SyntheticDirectorySize
}
return f.Size
}
@@ -55,3 +59,7 @@ func (f FileInfoTruncated) FileSize() int64 {
func (f FileInfoTruncated) FileName() string {
return f.Name
}
func (f FileInfoTruncated) ModTime() time.Time {
return time.Unix(f.ModifiedS, int64(f.ModifiedNs))
}

View File

@@ -56,12 +56,13 @@ type FileInfoTruncated struct {
Type protocol.FileInfoType `protobuf:"varint,2,opt,name=type,proto3,enum=protocol.FileInfoType" json:"type,omitempty"`
Size int64 `protobuf:"varint,3,opt,name=size,proto3" json:"size,omitempty"`
Permissions uint32 `protobuf:"varint,4,opt,name=permissions,proto3" json:"permissions,omitempty"`
Modified int64 `protobuf:"varint,5,opt,name=modified,proto3" json:"modified,omitempty"`
ModifiedS int64 `protobuf:"varint,5,opt,name=modified_s,json=modifiedS,proto3" json:"modified_s,omitempty"`
ModifiedNs int32 `protobuf:"varint,11,opt,name=modified_ns,json=modifiedNs,proto3" json:"modified_ns,omitempty"`
Deleted bool `protobuf:"varint,6,opt,name=deleted,proto3" json:"deleted,omitempty"`
Invalid bool `protobuf:"varint,7,opt,name=invalid,proto3" json:"invalid,omitempty"`
NoPermissions bool `protobuf:"varint,8,opt,name=no_permissions,json=noPermissions,proto3" json:"no_permissions,omitempty"`
Version protocol.Vector `protobuf:"bytes,9,opt,name=version" json:"version"`
LocalVersion int64 `protobuf:"varint,10,opt,name=local_version,json=localVersion,proto3" json:"local_version,omitempty"`
Sequence int64 `protobuf:"varint,10,opt,name=sequence,proto3" json:"sequence,omitempty"`
}
func (m *FileInfoTruncated) Reset() { *m = FileInfoTruncated{} }
@@ -171,10 +172,10 @@ func (m *FileInfoTruncated) MarshalTo(data []byte) (int, error) {
i++
i = encodeVarintStructs(data, i, uint64(m.Permissions))
}
if m.Modified != 0 {
if m.ModifiedS != 0 {
data[i] = 0x28
i++
i = encodeVarintStructs(data, i, uint64(m.Modified))
i = encodeVarintStructs(data, i, uint64(m.ModifiedS))
}
if m.Deleted {
data[i] = 0x30
@@ -214,10 +215,15 @@ func (m *FileInfoTruncated) MarshalTo(data []byte) (int, error) {
return 0, err
}
i += n2
if m.LocalVersion != 0 {
if m.Sequence != 0 {
data[i] = 0x50
i++
i = encodeVarintStructs(data, i, uint64(m.LocalVersion))
i = encodeVarintStructs(data, i, uint64(m.Sequence))
}
if m.ModifiedNs != 0 {
data[i] = 0x58
i++
i = encodeVarintStructs(data, i, uint64(m.ModifiedNs))
}
return i, nil
}
@@ -289,8 +295,8 @@ func (m *FileInfoTruncated) ProtoSize() (n int) {
if m.Permissions != 0 {
n += 1 + sovStructs(uint64(m.Permissions))
}
if m.Modified != 0 {
n += 1 + sovStructs(uint64(m.Modified))
if m.ModifiedS != 0 {
n += 1 + sovStructs(uint64(m.ModifiedS))
}
if m.Deleted {
n += 2
@@ -303,8 +309,11 @@ func (m *FileInfoTruncated) ProtoSize() (n int) {
}
l = m.Version.ProtoSize()
n += 1 + l + sovStructs(uint64(l))
if m.LocalVersion != 0 {
n += 1 + sovStructs(uint64(m.LocalVersion))
if m.Sequence != 0 {
n += 1 + sovStructs(uint64(m.Sequence))
}
if m.ModifiedNs != 0 {
n += 1 + sovStructs(uint64(m.ModifiedNs))
}
return n
}
@@ -631,9 +640,9 @@ func (m *FileInfoTruncated) Unmarshal(data []byte) error {
}
case 5:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field Modified", wireType)
return fmt.Errorf("proto: wrong wireType = %d for field ModifiedS", wireType)
}
m.Modified = 0
m.ModifiedS = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowStructs
@@ -643,7 +652,7 @@ func (m *FileInfoTruncated) Unmarshal(data []byte) error {
}
b := data[iNdEx]
iNdEx++
m.Modified |= (int64(b) & 0x7F) << shift
m.ModifiedS |= (int64(b) & 0x7F) << shift
if b < 0x80 {
break
}
@@ -740,9 +749,9 @@ func (m *FileInfoTruncated) Unmarshal(data []byte) error {
iNdEx = postIndex
case 10:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field LocalVersion", wireType)
return fmt.Errorf("proto: wrong wireType = %d for field Sequence", wireType)
}
m.LocalVersion = 0
m.Sequence = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowStructs
@@ -752,7 +761,26 @@ func (m *FileInfoTruncated) Unmarshal(data []byte) error {
}
b := data[iNdEx]
iNdEx++
m.LocalVersion |= (int64(b) & 0x7F) << shift
m.Sequence |= (int64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
case 11:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field ModifiedNs", wireType)
}
m.ModifiedNs = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowStructs
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := data[iNdEx]
iNdEx++
m.ModifiedNs |= (int32(b) & 0x7F) << shift
if b < 0x80 {
break
}
@@ -884,31 +912,32 @@ var (
)
var fileDescriptorStructs = []byte{
// 401 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x8c, 0x51, 0x4f, 0xcb, 0xd3, 0x30,
0x1c, 0x6e, 0xb7, 0xba, 0xf5, 0x4d, 0xdf, 0x4e, 0x0d, 0x32, 0xca, 0x0e, 0xdd, 0x98, 0x08, 0x22,
0xd8, 0xe9, 0xc4, 0x8b, 0xc7, 0x1d, 0x06, 0x82, 0x07, 0x29, 0x32, 0x8f, 0xa3, 0x4d, 0xb2, 0x2e,
0xd0, 0x26, 0xa5, 0x49, 0x07, 0xf3, 0x93, 0x78, 0xdc, 0xc7, 0xd9, 0xd1, 0x2f, 0xa0, 0xe8, 0xfc,
0x22, 0x66, 0x49, 0x3b, 0x7b, 0x7c, 0x0f, 0x81, 0xdf, 0x93, 0xe7, 0xcf, 0xef, 0x21, 0x01, 0xbe,
0x90, 0x55, 0x8d, 0xa4, 0x88, 0xca, 0x8a, 0x4b, 0x0e, 0x7b, 0x38, 0x9d, 0xbc, 0xce, 0xa8, 0xdc,
0xd7, 0x69, 0x84, 0x78, 0xb1, 0xc8, 0x78, 0xc6, 0x17, 0x9a, 0x4a, 0xeb, 0x9d, 0x46, 0x1a, 0xe8,
0xc9, 0x58, 0x26, 0xef, 0x3b, 0x72, 0x71, 0x64, 0x48, 0xee, 0x29, 0xcb, 0x3a, 0x53, 0x4e, 0x53,
0x93, 0x80, 0x78, 0xbe, 0x48, 0x49, 0x69, 0x6c, 0xf3, 0xaf, 0xc0, 0x5b, 0xd3, 0x9c, 0x6c, 0x48,
0x25, 0x28, 0x67, 0xf0, 0x0d, 0x18, 0x1e, 0xcc, 0x18, 0xd8, 0x33, 0xfb, 0xa5, 0xb7, 0x7c, 0x12,
0xb5, 0xa6, 0x68, 0x43, 0x90, 0xe4, 0xd5, 0xca, 0x39, 0xff, 0x9a, 0x5a, 0x71, 0x2b, 0x83, 0x63,
0x30, 0xc0, 0xe4, 0x40, 0x11, 0x09, 0x7a, 0xca, 0x70, 0x1f, 0x37, 0x68, 0xbe, 0x06, 0x5e, 0x13,
0xfa, 0x89, 0x0a, 0x09, 0xdf, 0x02, 0xb7, 0x71, 0x08, 0x95, 0xdc, 0x57, 0xc9, 0x8f, 0x23, 0x9c,
0x46, 0x9d, 0xdd, 0x4d, 0xf0, 0x4d, 0xf6, 0xc1, 0xf9, 0x7e, 0x9a, 0x5a, 0xf3, 0x9f, 0x3d, 0xf0,
0xf4, 0xaa, 0xfa, 0xc8, 0x76, 0xfc, 0x4b, 0x55, 0x33, 0x94, 0x48, 0x82, 0x21, 0x04, 0x0e, 0x4b,
0x0a, 0xa2, 0x4b, 0xde, 0xc5, 0x7a, 0x86, 0xaf, 0x80, 0x23, 0x8f, 0xa5, 0xe9, 0x31, 0x5a, 0x8e,
0xff, 0x17, 0xbf, 0xd9, 0x15, 0x1b, 0x6b, 0xcd, 0xd5, 0x2f, 0xe8, 0x37, 0x12, 0xf4, 0x95, 0xb6,
0x1f, 0xeb, 0x19, 0xce, 0x80, 0x57, 0x92, 0xaa, 0xa0, 0xc2, 0xb4, 0x74, 0x14, 0xe5, 0xc7, 0xdd,
0x2b, 0x38, 0x01, 0x6e, 0xc1, 0x31, 0xdd, 0x51, 0x82, 0x83, 0x47, 0xda, 0x79, 0xc3, 0x30, 0x00,
0x43, 0x4c, 0x72, 0xa2, 0xca, 0x05, 0x03, 0x45, 0xb9, 0x71, 0x0b, 0xaf, 0x0c, 0x65, 0x87, 0x24,
0xa7, 0x38, 0x18, 0x1a, 0xa6, 0x81, 0xf0, 0x05, 0x18, 0x31, 0xbe, 0xed, 0x2e, 0x75, 0xb5, 0xc0,
0x67, 0xfc, 0x73, 0x67, 0x6d, 0xe7, 0x53, 0xee, 0x1e, 0xf6, 0x29, 0xcf, 0x81, 0x9f, 0x73, 0x94,
0xe4, 0xdb, 0xd6, 0x07, 0x74, 0xdb, 0x7b, 0x7d, 0xd9, 0xbc, 0xb7, 0x79, 0xdf, 0xd5, 0xb3, 0xf3,
0x9f, 0xd0, 0x3a, 0x5f, 0x42, 0xfb, 0x87, 0x3a, 0xbf, 0x2f, 0xa1, 0x75, 0xfa, 0x1b, 0xda, 0xe9,
0x40, 0x2f, 0x78, 0xf7, 0x2f, 0x00, 0x00, 0xff, 0xff, 0xe4, 0xb1, 0x7f, 0x07, 0x98, 0x02, 0x00,
0x00,
// 419 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x8c, 0x51, 0xcd, 0xaa, 0xd3, 0x40,
0x18, 0x4d, 0xda, 0xdc, 0x36, 0xfd, 0x62, 0xaf, 0x3a, 0xc8, 0x25, 0x14, 0x4c, 0x2f, 0x05, 0x41,
0x04, 0x53, 0xbd, 0xe2, 0xc6, 0x65, 0x17, 0x05, 0x41, 0x44, 0x46, 0xa9, 0xcb, 0xd2, 0x64, 0xa6,
0xe9, 0x40, 0x32, 0x13, 0x33, 0x93, 0x42, 0x7d, 0x12, 0x97, 0x7d, 0x9c, 0x2e, 0x7d, 0x02, 0xd1,
0xfa, 0x12, 0x2e, 0x9d, 0x4e, 0x7e, 0xcc, 0xd2, 0x45, 0xe0, 0x3b, 0x73, 0xce, 0xf9, 0xce, 0x99,
0x0c, 0x8c, 0xa5, 0x2a, 0xca, 0x58, 0xc9, 0x30, 0x2f, 0x84, 0x12, 0xa8, 0x47, 0xa2, 0xc9, 0xf3,
0x84, 0xa9, 0x5d, 0x19, 0x85, 0xb1, 0xc8, 0xe6, 0x89, 0x48, 0xc4, 0xdc, 0x50, 0x51, 0xb9, 0x35,
0xc8, 0x00, 0x33, 0x55, 0x96, 0xc9, 0xeb, 0x8e, 0x5c, 0x1e, 0x78, 0xac, 0x76, 0x8c, 0x27, 0x9d,
0x29, 0x65, 0x51, 0xb5, 0x21, 0x16, 0xe9, 0x3c, 0xa2, 0x79, 0x65, 0x9b, 0x7d, 0x06, 0x6f, 0xc9,
0x52, 0xba, 0xa2, 0x85, 0x64, 0x82, 0xa3, 0x17, 0x30, 0xdc, 0x57, 0xa3, 0x6f, 0xdf, 0xda, 0x4f,
0xbd, 0xbb, 0x07, 0x61, 0x63, 0x0a, 0x57, 0x34, 0x56, 0xa2, 0x58, 0x38, 0xa7, 0x1f, 0x53, 0x0b,
0x37, 0x32, 0x74, 0x03, 0x03, 0x42, 0xf7, 0x2c, 0xa6, 0x7e, 0x4f, 0x1b, 0xee, 0xe1, 0x1a, 0xcd,
0x96, 0xe0, 0xd5, 0x4b, 0xdf, 0x31, 0xa9, 0xd0, 0x4b, 0x70, 0x6b, 0x87, 0xd4, 0x9b, 0xfb, 0x7a,
0xf3, 0xfd, 0x90, 0x44, 0x61, 0x27, 0xbb, 0x5e, 0xdc, 0xca, 0xde, 0x38, 0xdf, 0x8e, 0x53, 0x6b,
0xf6, 0xa7, 0x07, 0x0f, 0x2f, 0xaa, 0xb7, 0x7c, 0x2b, 0x3e, 0x15, 0x25, 0x8f, 0x37, 0x8a, 0x12,
0x84, 0xc0, 0xe1, 0x9b, 0x8c, 0x9a, 0x92, 0x23, 0x6c, 0x66, 0xf4, 0x0c, 0x1c, 0x75, 0xc8, 0xab,
0x1e, 0xd7, 0x77, 0x37, 0xff, 0x8a, 0xb7, 0x76, 0xcd, 0x62, 0xa3, 0xb9, 0xf8, 0x25, 0xfb, 0x4a,
0xfd, 0xbe, 0xd6, 0xf6, 0xb1, 0x99, 0xd1, 0x2d, 0x78, 0x39, 0x2d, 0x32, 0x26, 0xab, 0x96, 0x8e,
0xa6, 0xc6, 0xb8, 0x7b, 0x84, 0x1e, 0x03, 0x64, 0x82, 0xb0, 0x2d, 0xa3, 0x64, 0x2d, 0xfd, 0x2b,
0xe3, 0x1d, 0x35, 0x27, 0x1f, 0x91, 0x0f, 0x43, 0x42, 0x53, 0xaa, 0xfb, 0xf9, 0x03, 0xcd, 0xb9,
0xb8, 0x81, 0x17, 0x86, 0xf1, 0xfd, 0x26, 0x65, 0xc4, 0x1f, 0x56, 0x4c, 0x0d, 0xd1, 0x13, 0xb8,
0xe6, 0x62, 0xdd, 0xcd, 0x75, 0x8d, 0x60, 0xcc, 0xc5, 0x87, 0x4e, 0x72, 0xe7, 0x5d, 0x46, 0xff,
0xf7, 0x2e, 0x13, 0x70, 0x25, 0xfd, 0x52, 0x52, 0xae, 0x5f, 0x06, 0x4c, 0xd3, 0x16, 0xa3, 0x29,
0x78, 0xed, 0x3d, 0x74, 0xa2, 0xa7, 0xe9, 0x2b, 0xdc, 0x5e, 0xed, 0x7d, 0xfd, 0xeb, 0x17, 0x8f,
0x4e, 0xbf, 0x02, 0xeb, 0x74, 0x0e, 0xec, 0xef, 0xfa, 0xfb, 0x79, 0x0e, 0xac, 0xe3, 0xef, 0xc0,
0x8e, 0x06, 0x26, 0xf8, 0xd5, 0xdf, 0x00, 0x00, 0x00, 0xff, 0xff, 0x2a, 0xae, 0x24, 0x77, 0xb3,
0x02, 0x00, 0x00,
}

View File

@@ -26,10 +26,11 @@ message FileInfoTruncated {
protocol.FileInfoType type = 2;
int64 size = 3;
uint32 permissions = 4;
int64 modified = 5;
int64 modified_s = 5;
int32 modified_ns = 11;
bool deleted = 6;
bool invalid = 7;
bool no_permissions = 8;
protocol.Vector version = 9 [(gogoproto.nullable) = false];
int64 local_version = 10;
int64 sequence = 10;
}

View File

@@ -1,79 +0,0 @@
// Copyright (C) 2015 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package db
import (
"encoding/binary"
"fmt"
"time"
)
// This type encapsulates a repository of mtimes for platforms where file mtimes
// can't be set to arbitrary values. For this to work, we need to store both
// the mtime we tried to set (the "actual" mtime) as well as the mtime the file
// has when we're done touching it (the "disk" mtime) so that we can tell if it
// was changed. So in GetMtime(), it's not sufficient that the record exists --
// the argument must also equal the "disk" mtime in the record, otherwise it's
// been touched locally and the "disk" mtime is actually correct.
type VirtualMtimeRepo struct {
ns *NamespacedKV
}
func NewVirtualMtimeRepo(ldb *Instance, folder string) *VirtualMtimeRepo {
var prefix [5]byte // key type + 4 bytes folder idx number
prefix[0] = KeyTypeVirtualMtime
binary.BigEndian.PutUint32(prefix[1:], ldb.folderIdx.ID([]byte(folder)))
return &VirtualMtimeRepo{
ns: NewNamespacedKV(ldb, string(prefix[:])),
}
}
func (r *VirtualMtimeRepo) UpdateMtime(path string, diskMtime, actualMtime time.Time) {
l.Debugf("virtual mtime: storing values for path:%s disk:%v actual:%v", path, diskMtime, actualMtime)
diskBytes, _ := diskMtime.MarshalBinary()
actualBytes, _ := actualMtime.MarshalBinary()
data := append(diskBytes, actualBytes...)
r.ns.PutBytes(path, data)
}
func (r *VirtualMtimeRepo) GetMtime(path string, diskMtime time.Time) time.Time {
data, exists := r.ns.Bytes(path)
if !exists {
// Absence of debug print is significant enough in itself here
return diskMtime
}
var mtime time.Time
if err := mtime.UnmarshalBinary(data[:len(data)/2]); err != nil {
panic(fmt.Sprintf("Can't unmarshal stored mtime at path %s: %v", path, err))
}
if mtime.Equal(diskMtime) {
if err := mtime.UnmarshalBinary(data[len(data)/2:]); err != nil {
panic(fmt.Sprintf("Can't unmarshal stored mtime at path %s: %v", path, err))
}
l.Debugf("virtual mtime: return %v instead of %v for path: %s", mtime, diskMtime, path)
return mtime
}
l.Debugf("virtual mtime: record exists, but mismatch inDisk: %v dbDisk: %v for path: %s", diskMtime, mtime, path)
return diskMtime
}
func (r *VirtualMtimeRepo) DeleteMtime(path string) {
r.ns.Delete(path)
}
func (r *VirtualMtimeRepo) Drop() {
r.ns.Reset()
}

View File

@@ -1,74 +0,0 @@
// Copyright (C) 2015 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package db
import (
"testing"
"time"
)
func TestVirtualMtimeRepo(t *testing.T) {
ldb := OpenMemory()
// A few repos so we can ensure they don't pollute each other
repo1 := NewVirtualMtimeRepo(ldb, "folder1")
repo2 := NewVirtualMtimeRepo(ldb, "folder2")
// Since GetMtime() returns its argument if the key isn't found or is outdated, we need a dummy to test with.
dummyTime := time.Date(2001, time.February, 3, 4, 5, 6, 0, time.UTC)
// Some times to test with
time1 := time.Date(2001, time.February, 3, 4, 5, 7, 0, time.UTC)
time2 := time.Date(2010, time.February, 3, 4, 5, 6, 0, time.UTC)
file1 := "file1.txt"
// Files are not present at the start
if v := repo1.GetMtime(file1, dummyTime); !v.Equal(dummyTime) {
t.Errorf("Mtime should be missing (%v) from repo 1 but it's %v", dummyTime, v)
}
if v := repo2.GetMtime(file1, dummyTime); !v.Equal(dummyTime) {
t.Errorf("Mtime should be missing (%v) from repo 2 but it's %v", dummyTime, v)
}
repo1.UpdateMtime(file1, time1, time2)
// Now it should return time2 only when time1 is passed as the argument
if v := repo1.GetMtime(file1, time1); !v.Equal(time2) {
t.Errorf("Mtime should be %v for disk time %v but we got %v", time2, time1, v)
}
if v := repo1.GetMtime(file1, dummyTime); !v.Equal(dummyTime) {
t.Errorf("Mtime should be %v for disk time %v but we got %v", dummyTime, dummyTime, v)
}
// repo2 shouldn't know about this file
if v := repo2.GetMtime(file1, time1); !v.Equal(time1) {
t.Errorf("Mtime should be %v for disk time %v in repo 2 but we got %v", time1, time1, v)
}
repo1.DeleteMtime(file1)
// Now it should be gone
if v := repo1.GetMtime(file1, time1); !v.Equal(time1) {
t.Errorf("Mtime should be %v for disk time %v but we got %v", time1, time1, v)
}
// Try again but with Drop()
repo1.UpdateMtime(file1, time1, time2)
repo1.Drop()
if v := repo1.GetMtime(file1, time1); !v.Equal(time1) {
t.Errorf("Mtime should be %v for disk time %v but we got %v", time1, time1, v)
}
}

10
lib/dialer/empty_test.go Normal file
View File

@@ -0,0 +1,10 @@
// 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 http://mozilla.org/MPL/2.0/.
// The existence of this file means we get 0% test coverage rather than no
// test coverage at all. Remove when implementing an actual test.
package dialer

View File

@@ -9,6 +9,7 @@ package events
import (
"errors"
"runtime"
stdsync "sync"
"time"
@@ -47,6 +48,8 @@ const (
AllEvents = (1 << iota) - 1
)
var runningTests = false
func (t EventType) String() string {
switch t {
case Ping:
@@ -186,6 +189,13 @@ func (l *Logger) Subscribe(mask EventType) *Subscription {
// We need to create the timeout timer in the stopped, non-fired state so
// that Subscription.Poll() can safely reset it and select on the timeout
// channel. This ensures the timer is stopped and the channel drained.
if runningTests {
// Make the behavior stable when running tests to avoid randomly
// varying test coverage. This ensures, in practice if not in
// theory, that the timer fires and we take the true branch of the
// next if.
runtime.Gosched()
}
if !s.timeout.Stop() {
<-s.timeout.C
}
@@ -231,6 +241,14 @@ func (s *Subscription) Poll(timeout time.Duration) (Event, error) {
if !ok {
return e, ErrClosed
}
if runningTests {
// Make the behavior stable when running tests to avoid randomly
// varying test coverage. This ensures, in practice if not in
// theory, that the timer fires and we take the true branch of
// the next if.
s.timeout.Reset(0)
runtime.Gosched()
}
if !s.timeout.Stop() {
// The timeout must be stopped and possibly drained to be ready
// for reuse in the next call.

View File

@@ -4,27 +4,29 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package events_test
package events
import (
"fmt"
"testing"
"time"
"github.com/syncthing/syncthing/lib/events"
)
const timeout = 100 * time.Millisecond
func init() {
runningTests = true
}
func TestNewLogger(t *testing.T) {
l := events.NewLogger()
l := NewLogger()
if l == nil {
t.Fatal("Unexpected nil Logger")
}
}
func TestSubscriber(t *testing.T) {
l := events.NewLogger()
l := NewLogger()
s := l.Subscribe(0)
defer l.Unsubscribe(s)
if s == nil {
@@ -33,41 +35,41 @@ func TestSubscriber(t *testing.T) {
}
func TestTimeout(t *testing.T) {
l := events.NewLogger()
l := NewLogger()
s := l.Subscribe(0)
defer l.Unsubscribe(s)
_, err := s.Poll(timeout)
if err != events.ErrTimeout {
if err != ErrTimeout {
t.Fatal("Unexpected non-Timeout error:", err)
}
}
func TestEventBeforeSubscribe(t *testing.T) {
l := events.NewLogger()
l := NewLogger()
l.Log(events.DeviceConnected, "foo")
l.Log(DeviceConnected, "foo")
s := l.Subscribe(0)
defer l.Unsubscribe(s)
_, err := s.Poll(timeout)
if err != events.ErrTimeout {
if err != ErrTimeout {
t.Fatal("Unexpected non-Timeout error:", err)
}
}
func TestEventAfterSubscribe(t *testing.T) {
l := events.NewLogger()
l := NewLogger()
s := l.Subscribe(events.AllEvents)
s := l.Subscribe(AllEvents)
defer l.Unsubscribe(s)
l.Log(events.DeviceConnected, "foo")
l.Log(DeviceConnected, "foo")
ev, err := s.Poll(timeout)
if err != nil {
t.Fatal("Unexpected error:", err)
}
if ev.Type != events.DeviceConnected {
if ev.Type != DeviceConnected {
t.Error("Incorrect event type", ev.Type)
}
switch v := ev.Data.(type) {
@@ -81,27 +83,27 @@ func TestEventAfterSubscribe(t *testing.T) {
}
func TestEventAfterSubscribeIgnoreMask(t *testing.T) {
l := events.NewLogger()
l := NewLogger()
s := l.Subscribe(events.DeviceDisconnected)
s := l.Subscribe(DeviceDisconnected)
defer l.Unsubscribe(s)
l.Log(events.DeviceConnected, "foo")
l.Log(DeviceConnected, "foo")
_, err := s.Poll(timeout)
if err != events.ErrTimeout {
if err != ErrTimeout {
t.Fatal("Unexpected non-Timeout error:", err)
}
}
func TestBufferOverflow(t *testing.T) {
l := events.NewLogger()
l := NewLogger()
s := l.Subscribe(events.AllEvents)
s := l.Subscribe(AllEvents)
defer l.Unsubscribe(s)
t0 := time.Now()
for i := 0; i < events.BufferSize*2; i++ {
l.Log(events.DeviceConnected, "foo")
for i := 0; i < BufferSize*2; i++ {
l.Log(DeviceConnected, "foo")
}
if time.Since(t0) > timeout {
t.Fatalf("Logging took too long")
@@ -109,10 +111,10 @@ func TestBufferOverflow(t *testing.T) {
}
func TestUnsubscribe(t *testing.T) {
l := events.NewLogger()
l := NewLogger()
s := l.Subscribe(events.AllEvents)
l.Log(events.DeviceConnected, "foo")
s := l.Subscribe(AllEvents)
l.Log(DeviceConnected, "foo")
_, err := s.Poll(timeout)
if err != nil {
@@ -120,22 +122,22 @@ func TestUnsubscribe(t *testing.T) {
}
l.Unsubscribe(s)
l.Log(events.DeviceConnected, "foo")
l.Log(DeviceConnected, "foo")
_, err = s.Poll(timeout)
if err != events.ErrClosed {
if err != ErrClosed {
t.Fatal("Unexpected non-Closed error:", err)
}
}
func TestGlobalIDs(t *testing.T) {
l := events.NewLogger()
l := NewLogger()
s := l.Subscribe(events.AllEvents)
s := l.Subscribe(AllEvents)
defer l.Unsubscribe(s)
l.Log(events.DeviceConnected, "foo")
_ = l.Subscribe(events.AllEvents)
l.Log(events.DeviceConnected, "bar")
l.Log(DeviceConnected, "foo")
_ = l.Subscribe(AllEvents)
l.Log(DeviceConnected, "bar")
ev, err := s.Poll(timeout)
if err != nil {
@@ -159,15 +161,15 @@ func TestGlobalIDs(t *testing.T) {
}
func TestSubscriptionIDs(t *testing.T) {
l := events.NewLogger()
l := NewLogger()
s := l.Subscribe(events.DeviceConnected)
s := l.Subscribe(DeviceConnected)
defer l.Unsubscribe(s)
l.Log(events.DeviceDisconnected, "a")
l.Log(events.DeviceConnected, "b")
l.Log(events.DeviceConnected, "c")
l.Log(events.DeviceDisconnected, "d")
l.Log(DeviceDisconnected, "a")
l.Log(DeviceConnected, "b")
l.Log(DeviceConnected, "c")
l.Log(DeviceDisconnected, "d")
ev, err := s.Poll(timeout)
if err != nil {
@@ -193,21 +195,21 @@ func TestSubscriptionIDs(t *testing.T) {
}
ev, err = s.Poll(timeout)
if err != events.ErrTimeout {
if err != ErrTimeout {
t.Fatal("Unexpected error:", err)
}
}
func TestBufferedSub(t *testing.T) {
l := events.NewLogger()
l := NewLogger()
s := l.Subscribe(events.AllEvents)
s := l.Subscribe(AllEvents)
defer l.Unsubscribe(s)
bs := events.NewBufferedSubscription(s, 10*events.BufferSize)
bs := NewBufferedSubscription(s, 10*BufferSize)
go func() {
for i := 0; i < 10*events.BufferSize; i++ {
l.Log(events.DeviceConnected, fmt.Sprintf("event-%d", i))
for i := 0; i < 10*BufferSize; i++ {
l.Log(DeviceConnected, fmt.Sprintf("event-%d", i))
if i%30 == 0 {
// Give the buffer routine time to pick up the events
time.Sleep(20 * time.Millisecond)
@@ -216,7 +218,7 @@ func TestBufferedSub(t *testing.T) {
}()
recv := 0
for recv < 10*events.BufferSize {
for recv < 10*BufferSize {
evs := bs.Since(recv, nil)
for _, ev := range evs {
if ev.GlobalID != recv+1 {
@@ -228,12 +230,12 @@ func TestBufferedSub(t *testing.T) {
}
func BenchmarkBufferedSub(b *testing.B) {
l := events.NewLogger()
l := NewLogger()
s := l.Subscribe(events.AllEvents)
s := l.Subscribe(AllEvents)
defer l.Unsubscribe(s)
bufferSize := events.BufferSize
bs := events.NewBufferedSubscription(s, bufferSize)
bufferSize := BufferSize
bs := NewBufferedSubscription(s, bufferSize)
// The coord channel paces the sender according to the receiver,
// ensuring that no events are dropped. The benchmark measures sending +
@@ -249,7 +251,7 @@ func BenchmarkBufferedSub(b *testing.B) {
go func() {
defer close(done)
recv := 0
var evs []events.Event
var evs []Event
for i := 0; i < b.N; {
evs = bs.Since(recv, evs[:0])
for _, ev := range evs {
@@ -270,7 +272,7 @@ func BenchmarkBufferedSub(b *testing.B) {
"and": "something else",
}
for i := 0; i < b.N; i++ {
l.Log(events.DeviceConnected, eventData)
l.Log(DeviceConnected, eventData)
<-coord
}
@@ -279,16 +281,16 @@ func BenchmarkBufferedSub(b *testing.B) {
}
func TestSinceUsesSubscriptionId(t *testing.T) {
l := events.NewLogger()
l := NewLogger()
s := l.Subscribe(events.DeviceConnected)
s := l.Subscribe(DeviceConnected)
defer l.Unsubscribe(s)
bs := events.NewBufferedSubscription(s, 10*events.BufferSize)
bs := NewBufferedSubscription(s, 10*BufferSize)
l.Log(events.DeviceConnected, "a") // SubscriptionID = 1
l.Log(events.DeviceDisconnected, "b")
l.Log(events.DeviceDisconnected, "c")
l.Log(events.DeviceConnected, "d") // SubscriptionID = 2
l.Log(DeviceConnected, "a") // SubscriptionID = 1
l.Log(DeviceDisconnected, "b")
l.Log(DeviceDisconnected, "c")
l.Log(DeviceConnected, "d") // SubscriptionID = 2
// We need to loop for the events, as they may not all have been
// delivered to the buffered subscription when we get here.

136
lib/fs/mtimefs.go Normal file
View File

@@ -0,0 +1,136 @@
// 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 http://mozilla.org/MPL/2.0/.
package fs
import (
"os"
"time"
"github.com/syncthing/syncthing/lib/osutil"
)
// The database is where we store the virtual mtimes
type database interface {
Bytes(key string) (data []byte, ok bool)
PutBytes(key string, data []byte)
Delete(key string)
}
// variable so that we can mock it for testing
var osChtimes = os.Chtimes
// The MtimeFS is a filesystem with nanosecond mtime precision, regardless
// of what shenanigans the underlying filesystem gets up to.
type MtimeFS struct {
db database
}
func NewMtimeFS(db database) *MtimeFS {
return &MtimeFS{
db: db,
}
}
func (f *MtimeFS) Chtimes(name string, atime, mtime time.Time) error {
// Do a normal Chtimes call, don't care if it succeeds or not.
osChtimes(name, atime, mtime)
// Stat the file to see what happened. Here we *do* return an error,
// because it might be "does not exist" or similar. osutil.Lstat is the
// souped up version to account for Android breakage.
info, err := osutil.Lstat(name)
if err != nil {
return err
}
f.save(name, info.ModTime(), mtime)
return nil
}
func (f *MtimeFS) Lstat(name string) (os.FileInfo, error) {
info, err := osutil.Lstat(name)
if err != nil {
return nil, err
}
real, virtual := f.load(name)
if real == info.ModTime() {
info = mtimeFileInfo{
FileInfo: info,
mtime: virtual,
}
}
return info, nil
}
// "real" is the on disk timestamp
// "virtual" is what want the timestamp to be
func (f *MtimeFS) save(name string, real, virtual time.Time) {
if real.Equal(virtual) {
// If the virtual time and the real on disk time are equal we don't
// need to store anything.
f.db.Delete(name)
return
}
mtime := dbMtime{
real: real,
virtual: virtual,
}
bs, _ := mtime.Marshal() // Can't fail
f.db.PutBytes(name, bs)
}
func (f *MtimeFS) load(name string) (real, virtual time.Time) {
data, exists := f.db.Bytes(name)
if !exists {
return
}
var mtime dbMtime
if err := mtime.Unmarshal(data); err != nil {
return
}
return mtime.real, mtime.virtual
}
// The mtimeFileInfo is an os.FileInfo that lies about the ModTime().
type mtimeFileInfo struct {
os.FileInfo
mtime time.Time
}
func (m mtimeFileInfo) ModTime() time.Time {
return m.mtime
}
// The dbMtime is our database representation
type dbMtime struct {
real time.Time
virtual time.Time
}
func (t *dbMtime) Marshal() ([]byte, error) {
bs0, _ := t.real.MarshalBinary()
bs1, _ := t.virtual.MarshalBinary()
return append(bs0, bs1...), nil
}
func (t *dbMtime) Unmarshal(bs []byte) error {
if err := t.real.UnmarshalBinary(bs[:len(bs)/2]); err != nil {
return err
}
if err := t.virtual.UnmarshalBinary(bs[len(bs)/2:]); err != nil {
return err
}
return nil
}

109
lib/fs/mtimefs_test.go Normal file
View File

@@ -0,0 +1,109 @@
// 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 http://mozilla.org/MPL/2.0/.
package fs
import (
"errors"
"io/ioutil"
"os"
"testing"
"time"
)
func TestMtimeFS(t *testing.T) {
os.RemoveAll("testdata")
defer os.RemoveAll("testdata")
os.Mkdir("testdata", 0755)
ioutil.WriteFile("testdata/exists0", []byte("hello"), 0644)
ioutil.WriteFile("testdata/exists1", []byte("hello"), 0644)
ioutil.WriteFile("testdata/exists2", []byte("hello"), 0644)
// a random time with nanosecond precision
testTime := time.Unix(1234567890, 123456789)
mtimefs := NewMtimeFS(make(mapStore))
// Do one Chtimes call that will go through to the normal filesystem
osChtimes = os.Chtimes
if err := mtimefs.Chtimes("testdata/exists0", testTime, testTime); err != nil {
t.Error("Should not have failed:", err)
}
// Do one call that gets an error back from the underlying Chtimes
osChtimes = failChtimes
if err := mtimefs.Chtimes("testdata/exists1", testTime, testTime); err != nil {
t.Error("Should not have failed:", err)
}
// Do one call that gets struck by an exceptionally evil Chtimes
osChtimes = evilChtimes
if err := mtimefs.Chtimes("testdata/exists2", testTime, testTime); err != nil {
t.Error("Should not have failed:", err)
}
// All of the calls were successfull, so an Lstat on them should return
// the test timestamp.
for _, file := range []string{"testdata/exists0", "testdata/exists1", "testdata/exists2"} {
if info, err := mtimefs.Lstat(file); err != nil {
t.Error("Lstat shouldn't fail:", err)
} else if !info.ModTime().Equal(testTime) {
t.Errorf("Time mismatch; %v != expected %v", info.ModTime(), testTime)
}
}
// The two last files should certainly not have the correct timestamp
// when looking directly on disk though.
for _, file := range []string{"testdata/exists1", "testdata/exists2"} {
if info, err := os.Lstat(file); err != nil {
t.Error("Lstat shouldn't fail:", err)
} else if info.ModTime().Equal(testTime) {
t.Errorf("Unexpected time match; %v == %v", info.ModTime(), testTime)
}
}
// Changing the timestamp on disk should be reflected in a new Lstat
// call. Choose a time that is likely to be able to be on all reasonable
// filesystems.
testTime = time.Now().Add(5 * time.Hour).Truncate(time.Minute)
os.Chtimes("testdata/exists0", testTime, testTime)
if info, err := mtimefs.Lstat("testdata/exists0"); err != nil {
t.Error("Lstat shouldn't fail:", err)
} else if !info.ModTime().Equal(testTime) {
t.Errorf("Time mismatch; %v != expected %v", info.ModTime(), testTime)
}
}
// The mapStore is a simple database
type mapStore map[string][]byte
func (s mapStore) PutBytes(key string, data []byte) {
s[key] = data
}
func (s mapStore) Bytes(key string) (data []byte, ok bool) {
data, ok = s[key]
return
}
func (s mapStore) Delete(key string) {
delete(s, key)
}
// failChtimes does nothing, and fails
func failChtimes(name string, mtime, atime time.Time) error {
return errors.New("no")
}
// evilChtimes will set an mtime that's 300 days in the future of what was
// asked for, and truncate the time to the closest hour.
func evilChtimes(name string, mtime, atime time.Time) error {
return os.Chtimes(name, mtime.Add(300*time.Hour).Truncate(time.Hour), atime.Add(300*time.Hour).Truncate(time.Hour))
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -54,22 +54,22 @@ func init() {
var testDataExpected = map[string]protocol.FileInfo{
"foo": {
Name: "foo",
Type: protocol.FileInfoTypeFile,
Modified: 0,
Blocks: []protocol.BlockInfo{{Offset: 0x0, Size: 0x7, Hash: []uint8{0xae, 0xc0, 0x70, 0x64, 0x5f, 0xe5, 0x3e, 0xe3, 0xb3, 0x76, 0x30, 0x59, 0x37, 0x61, 0x34, 0xf0, 0x58, 0xcc, 0x33, 0x72, 0x47, 0xc9, 0x78, 0xad, 0xd1, 0x78, 0xb6, 0xcc, 0xdf, 0xb0, 0x1, 0x9f}}},
Name: "foo",
Type: protocol.FileInfoTypeFile,
ModifiedS: 0,
Blocks: []protocol.BlockInfo{{Offset: 0x0, Size: 0x7, Hash: []uint8{0xae, 0xc0, 0x70, 0x64, 0x5f, 0xe5, 0x3e, 0xe3, 0xb3, 0x76, 0x30, 0x59, 0x37, 0x61, 0x34, 0xf0, 0x58, 0xcc, 0x33, 0x72, 0x47, 0xc9, 0x78, 0xad, 0xd1, 0x78, 0xb6, 0xcc, 0xdf, 0xb0, 0x1, 0x9f}}},
},
"empty": {
Name: "empty",
Type: protocol.FileInfoTypeFile,
Modified: 0,
Blocks: []protocol.BlockInfo{{Offset: 0x0, Size: 0x0, Hash: []uint8{0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24, 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55}}},
Name: "empty",
Type: protocol.FileInfoTypeFile,
ModifiedS: 0,
Blocks: []protocol.BlockInfo{{Offset: 0x0, Size: 0x0, Hash: []uint8{0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24, 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55}}},
},
"bar": {
Name: "bar",
Type: protocol.FileInfoTypeFile,
Modified: 0,
Blocks: []protocol.BlockInfo{{Offset: 0x0, Size: 0xa, Hash: []uint8{0x2f, 0x72, 0xcc, 0x11, 0xa6, 0xfc, 0xd0, 0x27, 0x1e, 0xce, 0xf8, 0xc6, 0x10, 0x56, 0xee, 0x1e, 0xb1, 0x24, 0x3b, 0xe3, 0x80, 0x5b, 0xf9, 0xa9, 0xdf, 0x98, 0xf9, 0x2f, 0x76, 0x36, 0xb0, 0x5c}}},
Name: "bar",
Type: protocol.FileInfoTypeFile,
ModifiedS: 0,
Blocks: []protocol.BlockInfo{{Offset: 0x0, Size: 0xa, Hash: []uint8{0x2f, 0x72, 0xcc, 0x11, 0xa6, 0xfc, 0xd0, 0x27, 0x1e, 0xce, 0xf8, 0xc6, 0x10, 0x56, 0xee, 0x1e, 0xb1, 0x24, 0x3b, 0xe3, 0x80, 0x5b, 0xf9, 0xa9, 0xdf, 0x98, 0xf9, 0x2f, 0x76, 0x36, 0xb0, 0x5c}}},
},
}
@@ -78,7 +78,7 @@ func init() {
for n, f := range testDataExpected {
fi, _ := os.Stat("testdata/" + n)
f.Permissions = uint32(fi.Mode())
f.Modified = fi.ModTime().Unix()
f.ModifiedS = fi.ModTime().Unix()
f.Size = fi.Size()
testDataExpected[n] = f
}
@@ -93,6 +93,7 @@ func TestRequest(t *testing.T) {
m.AddFolder(defaultFolderConfig)
m.StartFolder("default")
m.ServeBackground()
defer m.Stop()
m.ScanFolder("default")
bs := make([]byte, protocol.BlockSize)
@@ -144,9 +145,10 @@ func genFiles(n int) []protocol.FileInfo {
t := time.Now().Unix()
for i := 0; i < n; i++ {
files[i] = protocol.FileInfo{
Name: fmt.Sprintf("file%d", i),
Modified: t,
Blocks: []protocol.BlockInfo{{Offset: 0, Size: 100, Hash: []byte("some hash bytes")}},
Name: fmt.Sprintf("file%d", i),
ModifiedS: t,
Sequence: int64(i + 1),
Blocks: []protocol.BlockInfo{{Offset: 0, Size: 100, Hash: []byte("some hash bytes")}},
}
}
@@ -167,6 +169,7 @@ func benchmarkIndex(b *testing.B, nfiles int) {
m.AddFolder(defaultFolderConfig)
m.StartFolder("default")
m.ServeBackground()
defer m.Stop()
files := genFiles(nfiles)
m.Index(device1, "default", files)
@@ -196,6 +199,7 @@ func benchmarkIndexUpdate(b *testing.B, nfiles, nufiles int) {
m.AddFolder(defaultFolderConfig)
m.StartFolder("default")
m.ServeBackground()
defer m.Stop()
files := genFiles(nfiles)
ufiles := genFiles(nufiles)
@@ -277,18 +281,11 @@ func BenchmarkRequest(b *testing.B) {
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
m.AddFolder(defaultFolderConfig)
m.ServeBackground()
defer m.Stop()
m.ScanFolder("default")
const n = 1000
files := make([]protocol.FileInfo, n)
t := time.Now().Unix()
for i := 0; i < n; i++ {
files[i] = protocol.FileInfo{
Name: fmt.Sprintf("file%d", i),
Modified: t,
Blocks: []protocol.BlockInfo{{Offset: 0, Size: 100, Hash: []byte("some hash bytes")}},
}
}
files := genFiles(n)
fc := &FakeConnection{
id: device1,
@@ -353,12 +350,13 @@ func TestDeviceRename(t *testing.T) {
m.AddConnection(conn, hello)
m.ServeBackground()
defer m.Stop()
if cfg.Devices()[device1].Name != "" {
t.Errorf("Device already has a name")
}
m.Close(device1, protocol.ErrTimeout)
m.Closed(conn, protocol.ErrTimeout)
hello.DeviceName = "tester"
m.AddConnection(conn, hello)
@@ -366,7 +364,7 @@ func TestDeviceRename(t *testing.T) {
t.Errorf("Device did not get a name")
}
m.Close(device1, protocol.ErrTimeout)
m.Closed(conn, protocol.ErrTimeout)
hello.DeviceName = "tester2"
m.AddConnection(conn, hello)
@@ -383,7 +381,7 @@ func TestDeviceRename(t *testing.T) {
t.Errorf("Device name not saved in config")
}
m.Close(device1, protocol.ErrTimeout)
m.Closed(conn, protocol.ErrTimeout)
opts := cfg.Options()
opts.OverwriteRemoteDevNames = true
@@ -431,6 +429,7 @@ func TestClusterConfig(t *testing.T) {
m.AddFolder(cfg.Folders[0])
m.AddFolder(cfg.Folders[1])
m.ServeBackground()
defer m.Stop()
cm := m.generateClusterConfig(device2)
@@ -502,6 +501,7 @@ func TestIgnores(t *testing.T) {
m.AddFolder(defaultFolderConfig)
m.StartFolder("default")
m.ServeBackground()
defer m.Stop()
expected := []string{
".*",
@@ -597,6 +597,7 @@ func TestROScanRecovery(t *testing.T) {
m.AddFolder(fcfg)
m.StartFolder("default")
m.ServeBackground()
defer m.Stop()
waitFor := func(status string) error {
timeout := time.Now().Add(2 * time.Second)
@@ -683,6 +684,7 @@ func TestRWScanRecovery(t *testing.T) {
m.AddFolder(fcfg)
m.StartFolder("default")
m.ServeBackground()
defer m.Stop()
waitFor := func(status string) error {
timeout := time.Now().Add(2 * time.Second)
@@ -746,6 +748,7 @@ func TestGlobalDirectoryTree(t *testing.T) {
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
m.AddFolder(defaultFolderConfig)
m.ServeBackground()
defer m.Stop()
b := func(isfile bool, path ...string) protocol.FileInfo {
typ := protocol.FileInfoTypeDirectory
@@ -755,11 +758,11 @@ func TestGlobalDirectoryTree(t *testing.T) {
blocks = []protocol.BlockInfo{{Offset: 0x0, Size: 0xa, Hash: []uint8{0x2f, 0x72, 0xcc, 0x11, 0xa6, 0xfc, 0xd0, 0x27, 0x1e, 0xce, 0xf8, 0xc6, 0x10, 0x56, 0xee, 0x1e, 0xb1, 0x24, 0x3b, 0xe3, 0x80, 0x5b, 0xf9, 0xa9, 0xdf, 0x98, 0xf9, 0x2f, 0x76, 0x36, 0xb0, 0x5c}}}
}
return protocol.FileInfo{
Name: filepath.Join(path...),
Type: typ,
Modified: 0x666,
Blocks: blocks,
Size: 0xa,
Name: filepath.Join(path...),
Type: typ,
ModifiedS: 0x666,
Blocks: blocks,
Size: 0xa,
}
}
@@ -1006,11 +1009,11 @@ func TestGlobalDirectorySelfFixing(t *testing.T) {
blocks = []protocol.BlockInfo{{Offset: 0x0, Size: 0xa, Hash: []uint8{0x2f, 0x72, 0xcc, 0x11, 0xa6, 0xfc, 0xd0, 0x27, 0x1e, 0xce, 0xf8, 0xc6, 0x10, 0x56, 0xee, 0x1e, 0xb1, 0x24, 0x3b, 0xe3, 0x80, 0x5b, 0xf9, 0xa9, 0xdf, 0x98, 0xf9, 0x2f, 0x76, 0x36, 0xb0, 0x5c}}}
}
return protocol.FileInfo{
Name: filepath.Join(path...),
Type: typ,
Modified: 0x666,
Blocks: blocks,
Size: 0xa,
Name: filepath.Join(path...),
Type: typ,
ModifiedS: 0x666,
Blocks: blocks,
Size: 0xa,
}
}
@@ -1148,7 +1151,7 @@ func genDeepFiles(n, d int) []protocol.FileInfo {
i++
}
files[i].Modified = t
files[i].ModifiedS = t
files[i].Blocks = []protocol.BlockInfo{{Offset: 0, Size: 100, Hash: []byte("some hash bytes")}}
}
@@ -1185,43 +1188,6 @@ func benchmarkTree(b *testing.B, n1, n2 int) {
b.ReportAllocs()
}
func TestIgnoreDelete(t *testing.T) {
db := db.OpenMemory()
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
// This folder should ignore external deletes
cfg := defaultFolderConfig
cfg.IgnoreDelete = true
m.AddFolder(cfg)
m.ServeBackground()
m.StartFolder("default")
m.ScanFolder("default")
// Get a currently existing file
f, ok := m.CurrentGlobalFile("default", "foo")
if !ok {
t.Fatal("foo should exist")
}
// Mark it for deletion
f.Deleted = true
f.Version = f.Version.Update(142) // arbitrary short remote ID
f.Blocks = nil
// Send the index
m.Index(device1, "default", []protocol.FileInfo{f})
// Make sure we ignored it
f, ok = m.CurrentGlobalFile("default", "foo")
if !ok {
t.Fatal("foo should exist")
}
if f.IsDeleted() {
t.Fatal("foo should not be marked for deletion")
}
}
func TestUnifySubs(t *testing.T) {
cases := []struct {
in []string // input to unifySubs
@@ -1387,8 +1353,8 @@ func TestIssue3028(t *testing.T) {
}
func TestIssue3164(t *testing.T) {
osutil.RemoveAll("testdata/issue3164")
defer osutil.RemoveAll("testdata/issue3164")
os.RemoveAll("testdata/issue3164")
defer os.RemoveAll("testdata/issue3164")
if err := os.MkdirAll("testdata/issue3164/oktodelete/foobar", 0777); err != nil {
t.Fatal(err)
@@ -1489,7 +1455,7 @@ func TestIssue2782(t *testing.T) {
testName := ".syncthing-test." + srand.String(16)
testDir := filepath.Join(home, testName)
if err := osutil.RemoveAll(testDir); err != nil {
if err := os.RemoveAll(testDir); err != nil {
t.Skip(err)
}
if err := osutil.MkdirAll(testDir+"/syncdir", 0755); err != nil {
@@ -1501,7 +1467,7 @@ func TestIssue2782(t *testing.T) {
if err := os.Symlink("syncdir", testDir+"/synclink"); err != nil {
t.Skip(err)
}
defer osutil.RemoveAll(testDir)
defer os.RemoveAll(testDir)
db := db.OpenMemory()
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
@@ -1519,6 +1485,280 @@ func TestIssue2782(t *testing.T) {
}
}
func TestIndexesForUnknownDevicesDropped(t *testing.T) {
dbi := db.OpenMemory()
files := db.NewFileSet("default", dbi)
files.Replace(device1, genFiles(1))
files.Replace(device2, genFiles(1))
if len(files.ListDevices()) != 2 {
t.Error("expected two devices")
}
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", dbi, nil)
m.AddFolder(defaultFolderConfig)
m.StartFolder("default")
// Remote sequence is cached, hence need to recreated.
files = db.NewFileSet("default", dbi)
if len(files.ListDevices()) != 1 {
t.Error("Expected one device")
}
}
func TestSharedWithClearedOnDisconnect(t *testing.T) {
dbi := db.OpenMemory()
fcfg := config.NewFolderConfiguration("default", "testdata")
fcfg.Devices = []config.FolderDeviceConfiguration{
{DeviceID: device1},
{DeviceID: device2},
}
cfg := config.Configuration{
Folders: []config.FolderConfiguration{fcfg},
Devices: []config.DeviceConfiguration{
config.NewDeviceConfiguration(device1, "device1"),
config.NewDeviceConfiguration(device2, "device2"),
},
Options: config.OptionsConfiguration{
// Don't remove temporaries directly on startup
KeepTemporariesH: 1,
},
}
wcfg := config.Wrap("/tmp/test", cfg)
d2c := &fakeConn{}
m := NewModel(wcfg, protocol.LocalDeviceID, "device", "syncthing", "dev", dbi, nil)
m.AddFolder(fcfg)
m.StartFolder(fcfg.ID)
m.ServeBackground()
conn1 := connections.Connection{
IntermediateConnection: connections.IntermediateConnection{
Conn: tls.Client(&fakeConn{}, nil),
Type: "foo",
Priority: 10,
},
Connection: &FakeConnection{
id: device1,
},
}
m.AddConnection(conn1, protocol.HelloResult{})
conn2 := connections.Connection{
IntermediateConnection: connections.IntermediateConnection{
Conn: tls.Client(d2c, nil),
Type: "foo",
Priority: 10,
},
Connection: &FakeConnection{
id: device2,
},
}
m.AddConnection(conn2, protocol.HelloResult{})
m.ClusterConfig(device1, protocol.ClusterConfig{
Folders: []protocol.Folder{
{
ID: "default",
Devices: []protocol.Device{
{ID: device1[:]},
{ID: device2[:]},
},
},
},
})
m.ClusterConfig(device2, protocol.ClusterConfig{
Folders: []protocol.Folder{
{
ID: "default",
Devices: []protocol.Device{
{ID: device1[:]},
{ID: device2[:]},
},
},
},
})
if !m.folderSharedWith("default", device1) {
t.Error("not shared with device1")
}
if !m.folderSharedWith("default", device2) {
t.Error("not shared with device2")
}
if d2c.closed {
t.Error("conn already closed")
}
cfg = cfg.Copy()
cfg.Devices = cfg.Devices[:1]
if err := wcfg.Replace(cfg); err != nil {
t.Error(err)
}
time.Sleep(100 * time.Millisecond) // Committer notification happens in a separate routine
if !m.folderSharedWith("default", device1) {
t.Error("not shared with device1")
}
if m.folderSharedWith("default", device2) { // checks m.deviceFolders
t.Error("shared with device2")
}
if !d2c.closed {
t.Error("connection not closed")
}
if _, ok := wcfg.Devices()[device2]; ok {
t.Error("device still in config")
}
fdevs, ok := m.folderDevices["default"]
if !ok {
t.Error("folder missing?")
}
for _, id := range fdevs {
if id == device2 {
t.Error("still there")
}
}
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")
}
if _, ok := m.helloMessages[device2]; ok {
t.Error("hello not missing")
}
if _, ok := m.deviceDownloads[device2]; ok {
t.Error("downloads not missing")
}
}
func TestIssue3496(t *testing.T) {
// It seems like lots of deleted files can cause negative completion
// percentages. Lets make sure that doesn't happen. Also do some general
// checks on the completion calculation stuff.
dbi := db.OpenMemory()
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", dbi, nil)
m.AddFolder(defaultFolderConfig)
m.StartFolder("default")
m.ServeBackground()
defer m.Stop()
m.ScanFolder("default")
addFakeConn(m, device1)
addFakeConn(m, device2)
// Reach into the model and grab the current file list...
m.fmut.RLock()
fs := m.folderFiles["default"]
m.fmut.RUnlock()
var localFiles []protocol.FileInfo
fs.WithHave(protocol.LocalDeviceID, func(i db.FileIntf) bool {
localFiles = append(localFiles, i.(protocol.FileInfo))
return true
})
// Mark all files as deleted and fake it as update from device1
for i := range localFiles {
localFiles[i].Deleted = true
localFiles[i].Version = localFiles[i].Version.Update(device1.Short())
localFiles[i].Blocks = nil
}
// Also add a small file that we're supposed to need, or the global size
// stuff will bail out early due to the entire folder being zero size.
localFiles = append(localFiles, protocol.FileInfo{
Name: "fake",
Size: 1234,
Type: protocol.FileInfoTypeFile,
Version: protocol.Vector{Counters: []protocol.Counter{{ID: device1.Short(), Value: 42}}},
})
m.IndexUpdate(device1, "default", localFiles)
// Check that the completion percentage for us makes sense
comp := m.Completion(protocol.LocalDeviceID, "default")
if comp.NeedBytes > comp.GlobalBytes {
t.Errorf("Need more bytes than exist, not possible: %d > %d", comp.NeedBytes, comp.GlobalBytes)
}
if comp.CompletionPct < 0 {
t.Errorf("Less than zero percent complete, not possible: %.02f%%", comp.CompletionPct)
}
if comp.NeedBytes == 0 {
t.Error("Need no bytes even though some files are deleted")
}
if comp.CompletionPct == 100 {
t.Errorf("Fully complete, not possible: %.02f%%", comp.CompletionPct)
}
t.Log(comp)
// Check that NeedSize does the correct thing
files, deletes, bytes := m.NeedSize("default")
if files != 1 || bytes != 1234 {
// The one we added synthetically above
t.Errorf("Incorrect need size; %d, %d != 1, 1234", files, bytes)
}
if deletes != len(localFiles)-1 {
// The rest
t.Errorf("Incorrect need deletes; %d != %d", deletes, len(localFiles)-1)
}
}
func addFakeConn(m *Model, dev protocol.DeviceID) {
conn1 := connections.Connection{
IntermediateConnection: connections.IntermediateConnection{
Conn: tls.Client(&fakeConn{}, nil),
Type: "foo",
Priority: 10,
},
Connection: &FakeConnection{
id: dev,
},
}
m.AddConnection(conn1, protocol.HelloResult{})
m.ClusterConfig(device1, protocol.ClusterConfig{
Folders: []protocol.Folder{
{
ID: "default",
Devices: []protocol.Device{
{ID: device1[:]},
{ID: device2[:]},
},
},
},
})
}
type fakeAddr struct{}
func (fakeAddr) Network() string {
@@ -1529,9 +1769,12 @@ func (fakeAddr) String() string {
return "address"
}
type fakeConn struct{}
type fakeConn struct {
closed bool
}
func (fakeConn) Close() error {
func (c *fakeConn) Close() error {
c.closed = true
return nil
}

View File

@@ -190,6 +190,9 @@ func (t *ProgressEmitter) CommitConfiguration(from, to config.Configuration) boo
defer t.mut.Unlock()
t.interval = time.Duration(to.Options.ProgressUpdateIntervalS) * time.Second
if t.interval < time.Second {
t.interval = time.Second
}
t.minBlocks = to.Options.TempIndexMinBlocks
l.Debugln("progress emitter: updated interval", t.interval)

View File

@@ -60,6 +60,7 @@ func TestProgressEmitter(t *testing.T) {
p := NewProgressEmitter(c)
go p.Serve()
p.interval = 0
expectTimeout(w, t)

View File

@@ -9,6 +9,7 @@ package model
import (
"math/rand"
"sort"
"time"
"github.com/syncthing/syncthing/lib/sync"
)
@@ -22,7 +23,7 @@ type jobQueue struct {
type jobQueueEntry struct {
name string
size int64
modified int64
modified time.Time
}
func newJobQueue() *jobQueue {
@@ -31,7 +32,7 @@ func newJobQueue() *jobQueue {
}
}
func (q *jobQueue) Push(file string, size, modified int64) {
func (q *jobQueue) Push(file string, size int64, modified time.Time) {
q.mut.Lock()
q.queued = append(q.queued, jobQueueEntry{file, size, modified})
q.mut.Unlock()
@@ -160,5 +161,5 @@ func (q smallestFirst) Swap(a, b int) { q[a], q[b] = q[b], q[a] }
type oldestFirst []jobQueueEntry
func (q oldestFirst) Len() int { return len(q) }
func (q oldestFirst) Less(a, b int) bool { return q[a].modified < q[b].modified }
func (q oldestFirst) Less(a, b int) bool { return q[a].modified.Before(q[b].modified) }
func (q oldestFirst) Swap(a, b int) { q[a], q[b] = q[b], q[a] }

View File

@@ -9,6 +9,7 @@ package model
import (
"fmt"
"testing"
"time"
"github.com/d4l3k/messagediff"
)
@@ -16,10 +17,10 @@ import (
func TestJobQueue(t *testing.T) {
// Some random actions
q := newJobQueue()
q.Push("f1", 0, 0)
q.Push("f2", 0, 0)
q.Push("f3", 0, 0)
q.Push("f4", 0, 0)
q.Push("f1", 0, time.Time{})
q.Push("f2", 0, time.Time{})
q.Push("f3", 0, time.Time{})
q.Push("f4", 0, time.Time{})
progress, queued := q.Jobs()
if len(progress) != 0 || len(queued) != 4 {
@@ -44,7 +45,7 @@ func TestJobQueue(t *testing.T) {
t.Fatal("Wrong length", len(progress), len(queued))
}
q.Push(n, 0, 0)
q.Push(n, 0, time.Time{})
progress, queued = q.Jobs()
if len(progress) != 0 || len(queued) != 4 {
t.Fatal("Wrong length")
@@ -121,10 +122,10 @@ func TestJobQueue(t *testing.T) {
func TestBringToFront(t *testing.T) {
q := newJobQueue()
q.Push("f1", 0, 0)
q.Push("f2", 0, 0)
q.Push("f3", 0, 0)
q.Push("f4", 0, 0)
q.Push("f1", 0, time.Time{})
q.Push("f2", 0, time.Time{})
q.Push("f3", 0, time.Time{})
q.Push("f4", 0, time.Time{})
_, queued := q.Jobs()
if diff, equal := messagediff.PrettyDiff([]string{"f1", "f2", "f3", "f4"}, queued); !equal {
@@ -162,10 +163,10 @@ func TestBringToFront(t *testing.T) {
func TestShuffle(t *testing.T) {
q := newJobQueue()
q.Push("f1", 0, 0)
q.Push("f2", 0, 0)
q.Push("f3", 0, 0)
q.Push("f4", 0, 0)
q.Push("f1", 0, time.Time{})
q.Push("f2", 0, time.Time{})
q.Push("f3", 0, time.Time{})
q.Push("f4", 0, time.Time{})
// This test will fail once in eight million times (1 / (4!)^5) :)
for i := 0; i < 5; i++ {
@@ -187,10 +188,10 @@ func TestShuffle(t *testing.T) {
func TestSortBySize(t *testing.T) {
q := newJobQueue()
q.Push("f1", 20, 0)
q.Push("f2", 40, 0)
q.Push("f3", 30, 0)
q.Push("f4", 10, 0)
q.Push("f1", 20, time.Time{})
q.Push("f2", 40, time.Time{})
q.Push("f3", 30, time.Time{})
q.Push("f4", 10, time.Time{})
q.SortSmallestFirst()
@@ -219,10 +220,10 @@ func TestSortBySize(t *testing.T) {
func TestSortByAge(t *testing.T) {
q := newJobQueue()
q.Push("f1", 0, 20)
q.Push("f2", 0, 40)
q.Push("f3", 0, 30)
q.Push("f4", 0, 10)
q.Push("f1", 0, time.Unix(20, 0))
q.Push("f2", 0, time.Unix(40, 0))
q.Push("f3", 0, time.Unix(30, 0))
q.Push("f4", 0, time.Unix(10, 0))
q.SortOldestFirst()
@@ -254,7 +255,7 @@ func BenchmarkJobQueueBump(b *testing.B) {
q := newJobQueue()
for _, f := range files {
q.Push(f.Name, 0, 0)
q.Push(f.Name, 0, time.Time{})
}
b.ResetTimer()
@@ -270,9 +271,9 @@ func BenchmarkJobQueuePushPopDone10k(b *testing.B) {
for i := 0; i < b.N; i++ {
q := newJobQueue()
for _, f := range files {
q.Push(f.Name, 0, 0)
q.Push(f.Name, 0, time.Time{})
}
for _ = range files {
for range files {
n, _ := q.Pop()
q.Done(n)
}

View File

@@ -10,6 +10,7 @@ import (
"fmt"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/fs"
"github.com/syncthing/syncthing/lib/versioner"
)
@@ -21,7 +22,7 @@ type roFolder struct {
folder
}
func newROFolder(model *Model, config config.FolderConfiguration, ver versioner.Versioner) service {
func newROFolder(model *Model, config config.FolderConfiguration, _ versioner.Versioner, _ *fs.MtimeFS) service {
return &roFolder{
folder: folder{
stateTracker: newStateTracker(config.ID),

View File

@@ -13,6 +13,7 @@ import (
"math/rand"
"os"
"path/filepath"
"runtime"
"sort"
"strings"
"time"
@@ -20,6 +21,7 @@ import (
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/db"
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/fs"
"github.com/syncthing/syncthing/lib/ignore"
"github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/protocol"
@@ -29,8 +31,6 @@ import (
"github.com/syncthing/syncthing/lib/versioner"
)
// TODO: Stop on errors
func init() {
folderFactories[config.FolderTypeReadWrite] = newRWFolder
}
@@ -80,18 +80,20 @@ type dbUpdateJob struct {
type rwFolder struct {
folder
virtualMtimeRepo *db.VirtualMtimeRepo
dir string
versioner versioner.Versioner
ignorePerms bool
copiers int
pullers int
order config.PullOrder
maxConflicts int
sleep time.Duration
pause time.Duration
allowSparse bool
checkFreeSpace bool
mtimeFS *fs.MtimeFS
dir string
versioner versioner.Versioner
ignorePerms bool
order config.PullOrder
maxConflicts int
sleep time.Duration
pause time.Duration
allowSparse bool
checkFreeSpace bool
ignoreDelete bool
copiers int
pullers int
queue *jobQueue
dbUpdates chan dbUpdateJob
@@ -104,7 +106,7 @@ type rwFolder struct {
initialScanCompleted chan (struct{}) // exposed for testing
}
func newRWFolder(model *Model, cfg config.FolderConfiguration, ver versioner.Versioner) service {
func newRWFolder(model *Model, cfg config.FolderConfiguration, ver versioner.Versioner, mtimeFS *fs.MtimeFS) service {
f := &rwFolder{
folder: folder{
stateTracker: newStateTracker(cfg.ID),
@@ -113,16 +115,17 @@ func newRWFolder(model *Model, cfg config.FolderConfiguration, ver versioner.Ver
model: model,
},
virtualMtimeRepo: db.NewVirtualMtimeRepo(model.db, cfg.ID),
dir: cfg.Path(),
ignorePerms: cfg.IgnorePerms,
copiers: cfg.Copiers,
pullers: cfg.Pullers,
order: cfg.Order,
maxConflicts: cfg.MaxConflicts,
allowSparse: !cfg.DisableSparseFiles,
checkFreeSpace: cfg.MinDiskFreePct != 0,
versioner: ver,
mtimeFS: mtimeFS,
dir: cfg.Path(),
versioner: ver,
ignorePerms: cfg.IgnorePerms,
copiers: cfg.Copiers,
pullers: cfg.Pullers,
order: cfg.Order,
maxConflicts: cfg.MaxConflicts,
allowSparse: !cfg.DisableSparseFiles,
checkFreeSpace: cfg.MinDiskFreePct != 0,
ignoreDelete: cfg.IgnoreDelete,
queue: newJobQueue(),
pullTimer: time.NewTimer(time.Second),
@@ -179,7 +182,7 @@ func (f *rwFolder) Serve() {
f.setState(FolderIdle)
}()
var prevVer int64
var prevSec int64
var prevIgnoreHash string
for {
@@ -188,7 +191,7 @@ func (f *rwFolder) Serve() {
return
case <-f.remoteIndex:
prevVer = 0
prevSec = 0
f.pullTimer.Reset(0)
l.Debugln(f, "remote index updated, rescheduling pull")
@@ -210,14 +213,14 @@ func (f *rwFolder) Serve() {
// The ignore patterns have changed. We need to re-evaluate if
// there are files we need now that were ignored before.
l.Debugln(f, "ignore patterns have changed, resetting prevVer")
prevVer = 0
prevSec = 0
prevIgnoreHash = newHash
}
// RemoteLocalVersion() is a fast call, doesn't touch the database.
curVer, ok := f.model.RemoteLocalVersion(f.folderID)
if !ok || curVer == prevVer {
l.Debugln(f, "skip (curVer == prevVer)", prevVer, ok)
// RemoteSequence() is a fast call, doesn't touch the database.
curSeq, ok := f.model.RemoteSequence(f.folderID)
if !ok || curSeq == prevSec {
l.Debugln(f, "skip (curSeq == prevSeq)", prevSec, ok)
f.pullTimer.Reset(f.sleep)
continue
}
@@ -228,7 +231,7 @@ func (f *rwFolder) Serve() {
continue
}
l.Debugln(f, "pulling", prevVer, curVer)
l.Debugln(f, "pulling", prevSec, curSeq)
f.setState(FolderSyncing)
f.clearErrors()
@@ -245,19 +248,19 @@ func (f *rwFolder) Serve() {
// sync. Remember the local version number and
// schedule a resync a little bit into the future.
if lv, ok := f.model.RemoteLocalVersion(f.folderID); ok && lv < curVer {
if lv, ok := f.model.RemoteSequence(f.folderID); ok && lv < curSeq {
// There's a corner case where the device we needed
// files from disconnected during the puller
// iteration. The files will have been removed from
// the index, so we've concluded that we don't need
// them, but at the same time we have the local
// version that includes those files in curVer. So we
// catch the case that localVersion might have
// catch the case that sequence might have
// decreased here.
l.Debugln(f, "adjusting curVer", lv)
curVer = lv
curSeq = lv
}
prevVer = curVer
prevSec = curSeq
l.Debugln(f, "next pull in", f.sleep)
f.pullTimer.Reset(f.sleep)
break
@@ -423,15 +426,20 @@ func (f *rwFolder) pullerIteration(ignores *ignore.Matcher) int {
// directories as they come along, so parents before children. Files
// are queued and the order may be changed later.
file := intf.(protocol.FileInfo)
if ignores.Match(file.Name).IsIgnored() {
// This is an ignored file. Skip it, continue iteration.
if shouldIgnore(intf, ignores, f.ignoreDelete) {
return true
}
l.Debugln(f, "handling", file.Name)
if err := fileValid(intf); err != nil {
// The file isn't valid so we can't process it. Pretend that we
// tried and set the error for the file.
f.newError(intf.FileName(), err)
changed++
return true
}
file := intf.(protocol.FileInfo)
l.Debugln(f, "handling", file.Name)
if !handleFile(file) {
// A new or changed file or symlink. This is the only case where
// we do stuff concurrently in the background. We only queue
@@ -441,7 +449,7 @@ func (f *rwFolder) pullerIteration(ignores *ignore.Matcher) int {
devices := folderFiles.Availability(file.Name)
for _, dev := range devices {
if f.model.ConnectedTo(dev) {
f.queue.Push(file.Name, file.Size, file.Modified)
f.queue.Push(file.Name, file.Size, file.ModTime())
changed++
break
}
@@ -588,13 +596,13 @@ func (f *rwFolder) handleDir(file protocol.FileInfo) {
l.Debugf("need dir\n\t%v\n\t%v", file, curFile)
}
info, err := osutil.Lstat(realName)
info, err := f.mtimeFS.Lstat(realName)
switch {
// There is already something under that name, but it's a file/link.
// Most likely a file/link is getting replaced with a directory.
// Remove the file/link and fall through to directory creation.
case err == nil && (!info.IsDir() || info.Mode()&os.ModeSymlink != 0):
err = osutil.InWritableDir(osutil.Remove, realName)
err = osutil.InWritableDir(os.Remove, realName)
if err != nil {
l.Infof("Puller (folder %q, dir %q): %v", f.folderID, file.Name, err)
f.newError(file.Name, err)
@@ -614,7 +622,7 @@ func (f *rwFolder) handleDir(file protocol.FileInfo) {
}
// Stat the directory so we can check its permissions.
info, err := osutil.Lstat(path)
info, err := f.mtimeFS.Lstat(path)
if err != nil {
return err
}
@@ -679,17 +687,17 @@ func (f *rwFolder) deleteDir(file protocol.FileInfo, matcher *ignore.Matcher) {
for _, dirFile := range files {
fullDirFile := filepath.Join(file.Name, dirFile)
if defTempNamer.IsTemporary(dirFile) || (matcher != nil && matcher.Match(fullDirFile).IsDeletable()) {
osutil.RemoveAll(filepath.Join(f.dir, fullDirFile))
os.RemoveAll(filepath.Join(f.dir, fullDirFile))
}
}
dir.Close()
}
err = osutil.InWritableDir(osutil.Remove, realName)
err = osutil.InWritableDir(os.Remove, realName)
if err == nil || os.IsNotExist(err) {
// It was removed or it doesn't exist to start with
f.dbUpdates <- dbUpdateJob{file, dbUpdateDeleteDir}
} else if _, serr := os.Lstat(realName); serr != nil && !os.IsPermission(serr) {
} else if _, serr := f.mtimeFS.Lstat(realName); serr != nil && !os.IsPermission(serr) {
// We get an error just looking at the directory, and it's not a
// permission problem. Lets assume the error is in fact some variant
// of "file does not exist" (possibly expressed as some parent being a
@@ -732,13 +740,13 @@ func (f *rwFolder) deleteFile(file protocol.FileInfo) {
} else if f.versioner != nil {
err = osutil.InWritableDir(f.versioner.Archive, realName)
} else {
err = osutil.InWritableDir(osutil.Remove, realName)
err = osutil.InWritableDir(os.Remove, realName)
}
if err == nil || os.IsNotExist(err) {
// It was removed or it doesn't exist to start with
f.dbUpdates <- dbUpdateJob{file, dbUpdateDeleteFile}
} else if _, serr := os.Lstat(realName); serr != nil && !os.IsPermission(serr) {
} else if _, serr := f.mtimeFS.Lstat(realName); serr != nil && !os.IsPermission(serr) {
// We get an error just looking at the file, and it's not a permission
// problem. Lets assume the error is in fact some variant of "file
// does not exist" (possibly expressed as some parent being a file and
@@ -817,7 +825,7 @@ func (f *rwFolder) renameFile(source, target protocol.FileInfo) {
// get rid of. Attempt to delete it instead so that we make *some*
// progress. The target is unhandled.
err = osutil.InWritableDir(osutil.Remove, from)
err = osutil.InWritableDir(os.Remove, from)
if err != nil {
l.Infof("Puller (folder %q, file %q): delete %q after failed rename: %v", f.folderID, target.Name, source.Name, err)
f.newError(target.Name, err)
@@ -916,9 +924,8 @@ func (f *rwFolder) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocks
// the database. If there's a mismatch here, there might be local
// changes that we don't know about yet and we should scan before
// touching the file. If we can't stat the file we'll just pull it.
if info, err := osutil.Lstat(realName); err == nil {
mtime := f.virtualMtimeRepo.GetMtime(file.Name, info.ModTime())
if mtime.Unix() != curFile.Modified || info.Size() != curFile.Size {
if info, err := f.mtimeFS.Lstat(realName); err == nil {
if !info.ModTime().Equal(curFile.ModTime()) || info.Size() != curFile.Size {
l.Debugln("file modified but not rescanned; not pulling:", realName)
// Scan() is synchronous (i.e. blocks until the scan is
// completed and returns an error), but a scan can't happen
@@ -969,7 +976,7 @@ func (f *rwFolder) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocks
// Otherwise, discard the file ourselves in order for the
// sharedpuller not to panic when it fails to exclusively create a
// file which already exists
osutil.InWritableDir(osutil.Remove, tempName)
osutil.InWritableDir(os.Remove, tempName)
}
} else {
// Copy the blocks, as we don't want to shuffle them on the FileInfo
@@ -1037,18 +1044,7 @@ func (f *rwFolder) shortcutFile(file protocol.FileInfo) error {
}
}
t := time.Unix(file.Modified, 0)
if err := os.Chtimes(realName, t, t); err != nil {
// Try using virtual mtimes
info, err := os.Stat(realName)
if err != nil {
l.Infof("Puller (folder %q, file %q): shortcut: unable to stat file: %v", f.folderID, file.Name, err)
f.newError(file.Name, err)
return err
}
f.virtualMtimeRepo.UpdateMtime(file.Name, info.ModTime(), t)
}
f.mtimeFS.Chtimes(realName, file.ModTime(), file.ModTime()) // never fails
// This may have been a conflict. We should merge the version vectors so
// that our clock doesn't move backwards.
@@ -1249,18 +1245,7 @@ func (f *rwFolder) performFinish(state *sharedPullerState) error {
}
}
// Set the correct timestamp on the new file
t := time.Unix(state.file.Modified, 0)
if err := os.Chtimes(state.tempName, t, t); err != nil {
// Try using virtual mtimes instead
info, err := os.Stat(state.tempName)
if err != nil {
return err
}
f.virtualMtimeRepo.UpdateMtime(state.file.Name, info.ModTime(), t)
}
if stat, err := osutil.Lstat(state.realName); err == nil {
if stat, err := f.mtimeFS.Lstat(state.realName); err == nil {
// There is an old file or directory already in place. We need to
// handle that.
@@ -1274,7 +1259,7 @@ func (f *rwFolder) performFinish(state *sharedPullerState) error {
// and future hard ignores before attempting a directory delete.
// Should share code with f.deletDir().
if err = osutil.InWritableDir(osutil.Remove, state.realName); err != nil {
if err = osutil.InWritableDir(os.Remove, state.realName); err != nil {
return err
}
@@ -1306,6 +1291,9 @@ func (f *rwFolder) performFinish(state *sharedPullerState) error {
return err
}
// Set the correct timestamp on the new file
f.mtimeFS.Chtimes(state.realName, state.file.ModTime(), state.file.ModTime()) // never fails
// If it's a symlink, the target of the symlink is inside the file.
if state.file.IsSymlink() {
content, err := ioutil.ReadFile(state.realName)
@@ -1422,7 +1410,7 @@ loop:
break loop
}
job.file.LocalVersion = 0
job.file.Sequence = 0
batch = append(batch, job)
if len(batch) == maxBatchSize {
@@ -1470,14 +1458,14 @@ func removeAvailability(availabilities []Availability, availability Availability
func (f *rwFolder) moveForConflict(name string) error {
if strings.Contains(filepath.Base(name), ".sync-conflict-") {
l.Infoln("Conflict for", name, "which is already a conflict copy; not copying again.")
if err := osutil.Remove(name); err != nil && !os.IsNotExist(err) {
if err := os.Remove(name); err != nil && !os.IsNotExist(err) {
return err
}
return nil
}
if f.maxConflicts == 0 {
if err := osutil.Remove(name); err != nil && !os.IsNotExist(err) {
if err := os.Remove(name); err != nil && !os.IsNotExist(err) {
return err
}
return nil
@@ -1499,7 +1487,7 @@ func (f *rwFolder) moveForConflict(name string) error {
if gerr == nil && len(matches) > f.maxConflicts {
sort.Sort(sort.Reverse(sort.StringSlice(matches)))
for _, match := range matches[f.maxConflicts:] {
gerr = osutil.Remove(match)
gerr = os.Remove(match)
if gerr != nil {
l.Debugln(f, "removing extra conflict", gerr)
}
@@ -1561,3 +1549,44 @@ func (l fileErrorList) Less(a, b int) bool {
func (l fileErrorList) Swap(a, b int) {
l[a], l[b] = l[b], l[a]
}
// fileValid returns nil when the file is valid for processing, or an error if it's not
func fileValid(file db.FileIntf) error {
switch {
case file.IsDeleted():
// We don't care about file validity if we're not supposed to have it
return nil
case !symlinks.Supported && file.IsSymlink():
return errUnsupportedSymlink
case runtime.GOOS == "windows" && windowsInvalidFilename(file.FileName()):
return errInvalidFilename
}
return nil
}
var windowsDisallowedCharacters = string([]rune{
'<', '>', ':', '"', '|', '?', '*',
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
31,
})
func windowsInvalidFilename(name string) bool {
// None of the path components should end in space
for _, part := range strings.Split(name, `\`) {
if len(part) == 0 {
continue
}
if part[len(part)-1] == ' ' {
// Names ending in space are not valid.
return true
}
}
// The path must not contain any disallowed characters
return strings.ContainsAny(name, windowsDisallowedCharacters)
}

View File

@@ -332,7 +332,7 @@ func TestDeregisterOnFailInCopy(t *testing.T) {
f := setUpRwFolder(m)
// queue.Done should be called by the finisher routine
f.queue.Push("filex", 0, 0)
f.queue.Push("filex", 0, time.Time{})
f.queue.Pop()
if f.queue.lenProgress() != 1 {
@@ -405,7 +405,7 @@ func TestDeregisterOnFailInPull(t *testing.T) {
f := setUpRwFolder(m)
// queue.Done should be called by the finisher routine
f.queue.Push("filex", 0, 0)
f.queue.Push("filex", 0, time.Time{})
f.queue.Pop()
if f.queue.lenProgress() != 1 {

View File

@@ -9,9 +9,9 @@ package model
import (
"encoding/binary"
"io/ioutil"
"os"
"sort"
"github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/opt"
@@ -21,7 +21,7 @@ const (
maxBytesInMemory = 512 << 10
)
// The IndexSorter sorts FileInfos based on their LocalVersion. You use it
// The IndexSorter sorts FileInfos based on their Sequence. You use it
// by first Append()ing all entries to be sorted, then calling Sorted()
// which will iterate over all the items in correctly sorted order.
type IndexSorter interface {
@@ -39,9 +39,10 @@ type internalIndexSorter interface {
// NewIndexSorter returns a new IndexSorter that will start out in memory
// for efficiency but switch to on disk storage once the amount of data
// becomes large.
func NewIndexSorter() IndexSorter {
func NewIndexSorter(location string) IndexSorter {
return &autoSwitchingIndexSorter{
internalIndexSorter: newInMemoryIndexSorter(),
location: location,
}
}
@@ -49,6 +50,7 @@ func NewIndexSorter() IndexSorter {
// onDiskSorter when the in memory sorter is full().
type autoSwitchingIndexSorter struct {
internalIndexSorter
location string
}
func (s *autoSwitchingIndexSorter) Append(f protocol.FileInfo) {
@@ -58,7 +60,7 @@ func (s *autoSwitchingIndexSorter) Append(f protocol.FileInfo) {
// which case we *don't* need to spill. An example of this would be
// an index containing just a single large file.
l.Debugf("sorter %p spills to disk", s)
next := newOnDiskIndexSorter()
next := newOnDiskIndexSorter(s.location)
s.internalIndexSorter.copyTo(next)
s.internalIndexSorter = next
}
@@ -86,7 +88,7 @@ func (s *inMemoryIndexSorter) Append(f protocol.FileInfo) {
}
func (s *inMemoryIndexSorter) Sorted(fn func(protocol.FileInfo) bool) {
sort.Sort(byLocalVersion(s.files))
sort.Sort(bySequence(s.files))
for _, f := range s.files {
if !fn(f) {
break
@@ -107,29 +109,29 @@ func (s *inMemoryIndexSorter) copyTo(dst IndexSorter) {
}
}
// byLocalVersion sorts FileInfos by LocalVersion
type byLocalVersion []protocol.FileInfo
// bySequence sorts FileInfos by Sequence
type bySequence []protocol.FileInfo
func (l byLocalVersion) Len() int {
func (l bySequence) Len() int {
return len(l)
}
func (l byLocalVersion) Swap(a, b int) {
func (l bySequence) Swap(a, b int) {
l[a], l[b] = l[b], l[a]
}
func (l byLocalVersion) Less(a, b int) bool {
return l[a].LocalVersion < l[b].LocalVersion
func (l bySequence) Less(a, b int) bool {
return l[a].Sequence < l[b].Sequence
}
// An onDiskIndexSorter is backed by a LevelDB database in the temporary
// directory. It relies on the fact that iterating over the database is done
// in key order and uses the LocalVersion as key. When done with an
// in key order and uses the Sequence as key. When done with an
// onDiskIndexSorter you must call Close() to remove the temporary database.
type onDiskIndexSorter struct {
db *leveldb.DB
dir string
}
func newOnDiskIndexSorter() *onDiskIndexSorter {
func newOnDiskIndexSorter(location string) *onDiskIndexSorter {
// Set options to minimize resource usage.
opts := &opt.Options{
OpenFilesCacheCapacity: 10,
@@ -137,7 +139,7 @@ func newOnDiskIndexSorter() *onDiskIndexSorter {
}
// Use a temporary database directory.
tmp, err := ioutil.TempDir("", "syncthing-db.")
tmp, err := ioutil.TempDir(location, "tmp-index-sorter.")
if err != nil {
panic("creating temporary directory: " + err.Error())
}
@@ -156,7 +158,7 @@ func newOnDiskIndexSorter() *onDiskIndexSorter {
func (s *onDiskIndexSorter) Append(f protocol.FileInfo) {
key := make([]byte, 8)
binary.BigEndian.PutUint64(key[:], uint64(f.LocalVersion))
binary.BigEndian.PutUint64(key[:], uint64(f.Sequence))
data, err := f.Marshal()
if err != nil {
panic("bug: marshalling FileInfo should never fail: " + err.Error())
@@ -184,7 +186,7 @@ func (s *onDiskIndexSorter) Sorted(fn func(protocol.FileInfo) bool) {
func (s *onDiskIndexSorter) Close() {
l.Debugf("onDiskIndexSorter %p closes", s)
s.db.Close()
osutil.RemoveAll(s.dir)
os.RemoveAll(s.dir)
}
func (s *onDiskIndexSorter) full() bool {

View File

@@ -30,7 +30,7 @@ func TestOnDiskIndexSorter(t *testing.T) {
// An onDiskSorter should be able to absorb a few files in unsorted
// order, and return them sorted.
s := newOnDiskIndexSorter()
s := newOnDiskIndexSorter("testdata")
addFiles(50, s)
verifySorted(t, s, 50)
verifyBreak(t, s, 50)
@@ -58,7 +58,7 @@ func TestIndexSorter(t *testing.T) {
// An default IndexSorter should be able to absorb files, have them in
// memory, and at some point switch to an on disk database.
s := NewIndexSorter()
s := NewIndexSorter("testdata")
defer s.Close()
// We should start out as an in memory store.
@@ -95,17 +95,18 @@ func TestIndexSorter(t *testing.T) {
verifySorted(t, s, nFiles+1)
}
// addFiles adds files with random LocalVersion to the Sorter.
// addFiles adds files with random Sequence to the Sorter.
func addFiles(n int, s IndexSorter) {
for i := 0; i < n; i++ {
rnd := rand.Int63()
f := protocol.FileInfo{
Name: fmt.Sprintf("file-%d", rnd),
Size: rand.Int63(),
Permissions: uint32(rand.Intn(0777)),
Modified: rand.Int63(),
LocalVersion: rnd,
Version: protocol.Vector{Counters: []protocol.Counter{{ID: 42, Value: uint64(rand.Int63())}}},
Name: fmt.Sprintf("file-%d", rnd),
Size: rand.Int63(),
Permissions: uint32(rand.Intn(0777)),
ModifiedS: rand.Int63(),
ModifiedNs: int32(rand.Int63()),
Sequence: rnd,
Version: protocol.Vector{Counters: []protocol.Counter{{ID: 42, Value: uint64(rand.Int63())}}},
Blocks: []protocol.BlockInfo{{
Size: int32(rand.Intn(128 << 10)),
Hash: []byte(rand.String(32)),
@@ -115,15 +116,15 @@ func addFiles(n int, s IndexSorter) {
}
}
// verifySorted checks that the files are returned sorted by LocalVersion.
// verifySorted checks that the files are returned sorted by Sequence.
func verifySorted(t *testing.T, s IndexSorter, expected int) {
prevLocalVer := int64(-1)
prevSequence := int64(-1)
seen := 0
s.Sorted(func(f protocol.FileInfo) bool {
if f.LocalVersion <= prevLocalVer {
t.Fatalf("Unsorted LocalVer, %d <= %d", f.LocalVersion, prevLocalVer)
if f.Sequence <= prevSequence {
t.Fatalf("Unsorted Sequence, %d <= %d", f.Sequence, prevSequence)
}
prevLocalVer = f.LocalVersion
prevSequence = f.Sequence
seen++
return true
})
@@ -134,11 +135,11 @@ func verifySorted(t *testing.T, s IndexSorter, expected int) {
// verifyBreak checks that the Sorter stops iteration once we return false.
func verifyBreak(t *testing.T, s IndexSorter, expected int) {
prevLocalVer := int64(-1)
prevSequence := int64(-1)
seen := 0
s.Sorted(func(f protocol.FileInfo) bool {
if f.LocalVersion <= prevLocalVer {
t.Fatalf("Unsorted LocalVer, %d <= %d", f.LocalVersion, prevLocalVer)
if f.Sequence <= prevSequence {
t.Fatalf("Unsorted Sequence, %d <= %d", f.Sequence, prevSequence)
}
if len(f.Blocks) != 1 {
t.Fatalf("incorrect number of blocks %d != 1", len(f.Blocks))
@@ -146,7 +147,7 @@ func verifyBreak(t *testing.T, s IndexSorter, expected int) {
if len(f.Version.Counters) != 1 {
t.Fatalf("incorrect number of version counters %d != 1", len(f.Version.Counters))
}
prevLocalVer = f.LocalVersion
prevSequence = f.Sequence
seen++
return seen < expected/2
})

View File

@@ -15,7 +15,6 @@ import (
"path/filepath"
"runtime"
"strings"
"syscall"
"github.com/calmh/du"
"github.com/syncthing/syncthing/lib/sync"
@@ -93,93 +92,6 @@ func InWritableDir(fn func(string) error, path string) error {
return fn(path)
}
// Remove removes the given path. On Windows, removes the read-only attribute
// from the target prior to deletion.
func Remove(path string) error {
if runtime.GOOS == "windows" {
info, err := os.Stat(path)
if err != nil {
return err
}
if info.Mode()&0200 == 0 {
os.Chmod(path, 0700)
}
}
return os.Remove(path)
}
// RemoveAll is a copy of os.RemoveAll, but uses osutil.Remove.
// RemoveAll removes path and any children it contains.
// It removes everything it can but returns the first error
// it encounters. If the path does not exist, RemoveAll
// returns nil (no error).
func RemoveAll(path string) error {
// Simple case: if Remove works, we're done.
err := Remove(path)
if err == nil || os.IsNotExist(err) {
return nil
}
// Otherwise, is this a directory we need to recurse into?
dir, serr := os.Lstat(path)
if serr != nil {
if serr, ok := serr.(*os.PathError); ok && (os.IsNotExist(serr.Err) || serr.Err == syscall.ENOTDIR) {
return nil
}
return serr
}
if !dir.IsDir() {
// Not a directory; return the error from Remove.
return err
}
// Directory.
fd, err := os.Open(path)
if err != nil {
if os.IsNotExist(err) {
// Race. It was deleted between the Lstat and Open.
// Return nil per RemoveAll's docs.
return nil
}
return err
}
// Remove contents & return first error.
err = nil
for {
names, err1 := fd.Readdirnames(100)
for _, name := range names {
err1 := RemoveAll(path + string(os.PathSeparator) + name)
if err == nil {
err = err1
}
}
if err1 == io.EOF {
break
}
// If Readdirnames returned an error, use it.
if err == nil {
err = err1
}
if len(names) == 0 {
break
}
}
// Close directory, because windows won't remove opened directory.
fd.Close()
// Remove directory.
err1 := Remove(path)
if err1 == nil || os.IsNotExist(err1) {
return nil
}
if err == nil {
err = err1
}
return err
}
func ExpandTilde(path string) (string, error) {
if path == "~" {
return getHomeDir()

View File

@@ -71,6 +71,8 @@ func TestInWriteableDir(t *testing.T) {
}
func TestInWritableDirWindowsRemove(t *testing.T) {
// os.Remove should remove read only things on windows
if runtime.GOOS != "windows" {
t.Skipf("Tests not required")
return
@@ -100,20 +102,49 @@ func TestInWritableDirWindowsRemove(t *testing.T) {
os.Chmod("testdata/windows/ro/readonly", 0500)
for _, path := range []string{"testdata/windows/ro/readonly", "testdata/windows/ro", "testdata/windows"} {
err := os.Remove(path)
if err == nil {
t.Errorf("Expected error %s", path)
}
}
for _, path := range []string{"testdata/windows/ro/readonly", "testdata/windows/ro", "testdata/windows"} {
err := osutil.InWritableDir(osutil.Remove, path)
err := osutil.InWritableDir(os.Remove, path)
if err != nil {
t.Errorf("Unexpected error %s: %s", path, err)
}
}
}
func TestInWritableDirWindowsRemoveAll(t *testing.T) {
// os.RemoveAll should remove read only things on windows
if runtime.GOOS != "windows" {
t.Skipf("Tests not required")
return
}
err := os.RemoveAll("testdata")
if err != nil {
t.Fatal(err)
}
defer os.Chmod("testdata/windows/ro/readonlynew", 0700)
defer os.RemoveAll("testdata")
create := func(name string) error {
fd, err := os.Create(name)
if err != nil {
return err
}
fd.Close()
return nil
}
os.Mkdir("testdata", 0700)
os.Mkdir("testdata/windows", 0500)
os.Mkdir("testdata/windows/ro", 0500)
create("testdata/windows/ro/readonly")
os.Chmod("testdata/windows/ro/readonly", 0500)
if err := os.RemoveAll("testdata/windows"); err != nil {
t.Errorf("Unexpected error: %s", err)
}
}
func TestInWritableDirWindowsRename(t *testing.T) {
if runtime.GOOS != "windows" {
t.Skipf("Tests not required")

10
lib/pmp/empty_test.go Normal file
View File

@@ -0,0 +1,10 @@
// 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 http://mozilla.org/MPL/2.0/.
// The existence of this file means we get 0% test coverage rather than no
// test coverage at all. Remove when implementing an actual test.
package pmp

View File

@@ -1,4 +0,0 @@
# This is the official list of Protocol Authors for copyright purposes.
Audrius Butkevicius <audrius.butkevicius@gmail.com>
Jakob Borg <jakob@nym.se>

View File

@@ -1,76 +0,0 @@
## Reporting Bugs
Please file bugs in the [Github Issue
Tracker](https://github.com/syncthing/protocol/issues).
## Contributing Code
Every contribution is welcome. Following the points below will make this
a smoother process.
Individuals making significant and valuable contributions are given
commit-access to the project. If you make a significant contribution and
are not considered for commit-access, please contact any of the
Syncthing core team members.
All nontrivial contributions should go through the pull request
mechanism for internal review. Determining what is "nontrivial" is left
at the discretion of the contributor.
### Authorship
All code authors are listed in the AUTHORS file. Commits must be made
with the same name and email as listed in the AUTHORS file. To
accomplish this, ensure that your git configuration is set correctly
prior to making your first commit;
$ git config --global user.name "Jane Doe"
$ git config --global user.email janedoe@example.com
You must be reachable on the given email address. If you do not wish to
use your real name for whatever reason, using a nickname or pseudonym is
perfectly acceptable.
## Coding Style
- Follow the conventions laid out in [Effective Go](https://golang.org/doc/effective_go.html)
as much as makes sense.
- All text files use Unix line endings.
- Each commit should be `go fmt` clean.
- The commit message subject should be a single short sentence
describing the change, starting with a capital letter.
- Commits that resolve an existing issue must include the issue number
as `(fixes #123)` at the end of the commit message subject.
- Imports are grouped per `goimports` standard; that is, standard
library first, then third party libraries after a blank line.
- A contribution solving a single issue or introducing a single new
feature should probably be a single commit based on the current
`master` branch. You may be asked to "rebase" or "squash" your pull
request to make sure this is the case, especially if there have been
amendments during review.
## Licensing
All contributions are made under the same MIT license as the rest of the
project, except documentation, user interface text and translation
strings which are licensed under the Creative Commons Attribution 4.0
International License. You retain the copyright to code you have
written.
When accepting your first contribution, the maintainer of the project
will ensure that you are added to the AUTHORS file. You are welcome to
add yourself as a separate commit in your first pull request.
## Tests
Yes please!
## License
MIT

View File

@@ -1,13 +0,0 @@
The BEPv1 Protocol
==================
[![Latest Build](http://img.shields.io/jenkins/s/http/build.syncthing.net/protocol.svg?style=flat-square)](http://build.syncthing.net/job/protocol/lastBuild/)
[![API Documentation](http://img.shields.io/badge/api-Godoc-blue.svg?style=flat-square)](http://godoc.org/github.com/syncthing/protocol)
[![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](http://opensource.org/licenses/MIT)
This is the protocol implementation used by Syncthing.
License
=======
MIT

View File

@@ -181,7 +181,7 @@ func (m *fakeModel) Request(deviceID DeviceID, folder string, name string, offse
func (m *fakeModel) ClusterConfig(deviceID DeviceID, config ClusterConfig) {
}
func (m *fakeModel) Close(deviceID DeviceID, err error) {
func (m *fakeModel) Closed(conn Connection, err error) {
}
func (m *fakeModel) DownloadProgress(deviceID DeviceID, folder string, updates []FileDownloadProgressUpdate) {

View File

@@ -255,14 +255,14 @@ func (*Folder) ProtoMessage() {}
func (*Folder) Descriptor() ([]byte, []int) { return fileDescriptorBep, []int{3} }
type Device struct {
ID []byte `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
Addresses []string `protobuf:"bytes,3,rep,name=addresses" json:"addresses,omitempty"`
Compression Compression `protobuf:"varint,4,opt,name=compression,proto3,enum=protocol.Compression" json:"compression,omitempty"`
CertName string `protobuf:"bytes,5,opt,name=cert_name,json=certName,proto3" json:"cert_name,omitempty"`
MaxLocalVersion int64 `protobuf:"varint,6,opt,name=max_local_version,json=maxLocalVersion,proto3" json:"max_local_version,omitempty"`
Introducer bool `protobuf:"varint,7,opt,name=introducer,proto3" json:"introducer,omitempty"`
IndexID IndexID `protobuf:"varint,8,opt,name=index_id,json=indexId,proto3,customtype=IndexID" json:"index_id"`
ID []byte `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
Addresses []string `protobuf:"bytes,3,rep,name=addresses" json:"addresses,omitempty"`
Compression Compression `protobuf:"varint,4,opt,name=compression,proto3,enum=protocol.Compression" json:"compression,omitempty"`
CertName string `protobuf:"bytes,5,opt,name=cert_name,json=certName,proto3" json:"cert_name,omitempty"`
MaxSequence int64 `protobuf:"varint,6,opt,name=max_sequence,json=maxSequence,proto3" json:"max_sequence,omitempty"`
Introducer bool `protobuf:"varint,7,opt,name=introducer,proto3" json:"introducer,omitempty"`
IndexID IndexID `protobuf:"varint,8,opt,name=index_id,json=indexId,proto3,customtype=IndexID" json:"index_id"`
}
func (m *Device) Reset() { *m = Device{} }
@@ -295,12 +295,13 @@ type FileInfo struct {
Type FileInfoType `protobuf:"varint,2,opt,name=type,proto3,enum=protocol.FileInfoType" json:"type,omitempty"`
Size int64 `protobuf:"varint,3,opt,name=size,proto3" json:"size,omitempty"`
Permissions uint32 `protobuf:"varint,4,opt,name=permissions,proto3" json:"permissions,omitempty"`
Modified int64 `protobuf:"varint,5,opt,name=modified,proto3" json:"modified,omitempty"`
ModifiedS int64 `protobuf:"varint,5,opt,name=modified_s,json=modifiedS,proto3" json:"modified_s,omitempty"`
ModifiedNs int32 `protobuf:"varint,11,opt,name=modified_ns,json=modifiedNs,proto3" json:"modified_ns,omitempty"`
Deleted bool `protobuf:"varint,6,opt,name=deleted,proto3" json:"deleted,omitempty"`
Invalid bool `protobuf:"varint,7,opt,name=invalid,proto3" json:"invalid,omitempty"`
NoPermissions bool `protobuf:"varint,8,opt,name=no_permissions,json=noPermissions,proto3" json:"no_permissions,omitempty"`
Version Vector `protobuf:"bytes,9,opt,name=version" json:"version"`
LocalVersion int64 `protobuf:"varint,10,opt,name=local_version,json=localVersion,proto3" json:"local_version,omitempty"`
Sequence int64 `protobuf:"varint,10,opt,name=sequence,proto3" json:"sequence,omitempty"`
Blocks []BlockInfo `protobuf:"bytes,16,rep,name=Blocks,json=blocks" json:"Blocks"`
}
@@ -658,10 +659,10 @@ func (m *Device) MarshalTo(data []byte) (int, error) {
i = encodeVarintBep(data, i, uint64(len(m.CertName)))
i += copy(data[i:], m.CertName)
}
if m.MaxLocalVersion != 0 {
if m.MaxSequence != 0 {
data[i] = 0x30
i++
i = encodeVarintBep(data, i, uint64(m.MaxLocalVersion))
i = encodeVarintBep(data, i, uint64(m.MaxSequence))
}
if m.Introducer {
data[i] = 0x38
@@ -789,10 +790,10 @@ func (m *FileInfo) MarshalTo(data []byte) (int, error) {
i++
i = encodeVarintBep(data, i, uint64(m.Permissions))
}
if m.Modified != 0 {
if m.ModifiedS != 0 {
data[i] = 0x28
i++
i = encodeVarintBep(data, i, uint64(m.Modified))
i = encodeVarintBep(data, i, uint64(m.ModifiedS))
}
if m.Deleted {
data[i] = 0x30
@@ -832,10 +833,15 @@ func (m *FileInfo) MarshalTo(data []byte) (int, error) {
return 0, err
}
i += n1
if m.LocalVersion != 0 {
if m.Sequence != 0 {
data[i] = 0x50
i++
i = encodeVarintBep(data, i, uint64(m.LocalVersion))
i = encodeVarintBep(data, i, uint64(m.Sequence))
}
if m.ModifiedNs != 0 {
data[i] = 0x58
i++
i = encodeVarintBep(data, i, uint64(m.ModifiedNs))
}
if len(m.Blocks) > 0 {
for _, msg := range m.Blocks {
@@ -1288,8 +1294,8 @@ func (m *Device) ProtoSize() (n int) {
if l > 0 {
n += 1 + l + sovBep(uint64(l))
}
if m.MaxLocalVersion != 0 {
n += 1 + sovBep(uint64(m.MaxLocalVersion))
if m.MaxSequence != 0 {
n += 1 + sovBep(uint64(m.MaxSequence))
}
if m.Introducer {
n += 2
@@ -1348,8 +1354,8 @@ func (m *FileInfo) ProtoSize() (n int) {
if m.Permissions != 0 {
n += 1 + sovBep(uint64(m.Permissions))
}
if m.Modified != 0 {
n += 1 + sovBep(uint64(m.Modified))
if m.ModifiedS != 0 {
n += 1 + sovBep(uint64(m.ModifiedS))
}
if m.Deleted {
n += 2
@@ -1362,8 +1368,11 @@ func (m *FileInfo) ProtoSize() (n int) {
}
l = m.Version.ProtoSize()
n += 1 + l + sovBep(uint64(l))
if m.LocalVersion != 0 {
n += 1 + sovBep(uint64(m.LocalVersion))
if m.Sequence != 0 {
n += 1 + sovBep(uint64(m.Sequence))
}
if m.ModifiedNs != 0 {
n += 1 + sovBep(uint64(m.ModifiedNs))
}
if len(m.Blocks) > 0 {
for _, e := range m.Blocks {
@@ -2218,9 +2227,9 @@ func (m *Device) Unmarshal(data []byte) error {
iNdEx = postIndex
case 6:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field MaxLocalVersion", wireType)
return fmt.Errorf("proto: wrong wireType = %d for field MaxSequence", wireType)
}
m.MaxLocalVersion = 0
m.MaxSequence = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowBep
@@ -2230,7 +2239,7 @@ func (m *Device) Unmarshal(data []byte) error {
}
b := data[iNdEx]
iNdEx++
m.MaxLocalVersion |= (int64(b) & 0x7F) << shift
m.MaxSequence |= (int64(b) & 0x7F) << shift
if b < 0x80 {
break
}
@@ -2632,9 +2641,9 @@ func (m *FileInfo) Unmarshal(data []byte) error {
}
case 5:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field Modified", wireType)
return fmt.Errorf("proto: wrong wireType = %d for field ModifiedS", wireType)
}
m.Modified = 0
m.ModifiedS = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowBep
@@ -2644,7 +2653,7 @@ func (m *FileInfo) Unmarshal(data []byte) error {
}
b := data[iNdEx]
iNdEx++
m.Modified |= (int64(b) & 0x7F) << shift
m.ModifiedS |= (int64(b) & 0x7F) << shift
if b < 0x80 {
break
}
@@ -2741,9 +2750,9 @@ func (m *FileInfo) Unmarshal(data []byte) error {
iNdEx = postIndex
case 10:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field LocalVersion", wireType)
return fmt.Errorf("proto: wrong wireType = %d for field Sequence", wireType)
}
m.LocalVersion = 0
m.Sequence = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowBep
@@ -2753,7 +2762,26 @@ func (m *FileInfo) Unmarshal(data []byte) error {
}
b := data[iNdEx]
iNdEx++
m.LocalVersion |= (int64(b) & 0x7F) << shift
m.Sequence |= (int64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
case 11:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field ModifiedNs", wireType)
}
m.ModifiedNs = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowBep
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := data[iNdEx]
iNdEx++
m.ModifiedNs |= (int32(b) & 0x7F) << shift
if b < 0x80 {
break
}
@@ -3926,104 +3954,105 @@ var (
)
var fileDescriptorBep = []byte{
// 1569 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xac, 0x56, 0x4f, 0x6f, 0x1b, 0x45,
0x14, 0x8f, 0xed, 0xf5, 0xbf, 0x89, 0x93, 0x3a, 0xd3, 0x34, 0x35, 0xdb, 0x90, 0x84, 0x6d, 0x2b,
0x82, 0x45, 0x53, 0x68, 0x81, 0x4a, 0x48, 0x20, 0x39, 0xf6, 0x26, 0xb5, 0xea, 0xac, 0xdd, 0xb5,
0x9d, 0x52, 0x0e, 0x58, 0x6b, 0xef, 0xd8, 0x59, 0x75, 0xbd, 0x63, 0x76, 0xd7, 0x6d, 0xc3, 0x17,
0x40, 0x82, 0x2f, 0xc0, 0x05, 0xa9, 0x57, 0xc4, 0x95, 0x0f, 0xd1, 0x63, 0xd5, 0x23, 0x87, 0x0a,
0xc2, 0x85, 0x2f, 0xc0, 0x9d, 0x37, 0x33, 0xbb, 0xde, 0x75, 0xfe, 0xa0, 0x1e, 0x38, 0x58, 0x9e,
0x79, 0xef, 0x37, 0x6f, 0x66, 0x7e, 0xef, 0xf7, 0xde, 0x2c, 0xca, 0xf7, 0xc9, 0x64, 0x67, 0xe2,
0x52, 0x9f, 0xe2, 0x1c, 0xff, 0x1b, 0x50, 0x5b, 0xbe, 0x35, 0xb2, 0xfc, 0xa3, 0x69, 0x7f, 0x67,
0x40, 0xc7, 0xb7, 0x47, 0x74, 0x44, 0x6f, 0x73, 0x4f, 0x7f, 0x3a, 0xe4, 0x33, 0x3e, 0xe1, 0x23,
0xb1, 0x50, 0x99, 0xa0, 0xf4, 0x7d, 0x62, 0xdb, 0x14, 0x6f, 0xa2, 0x45, 0x93, 0x3c, 0xb5, 0x06,
0xa4, 0xe7, 0x18, 0x63, 0x52, 0x4a, 0x6c, 0x25, 0xb6, 0xf3, 0x3a, 0x12, 0x26, 0x0d, 0x2c, 0x0c,
0x30, 0xb0, 0x2d, 0xe2, 0xf8, 0x02, 0x90, 0x14, 0x00, 0x61, 0xe2, 0x80, 0x9b, 0x68, 0x39, 0x00,
0x3c, 0x25, 0xae, 0x67, 0x51, 0xa7, 0x94, 0xe2, 0x98, 0x25, 0x61, 0x3d, 0x14, 0x46, 0xc5, 0x43,
0x99, 0xfb, 0xc4, 0x30, 0x89, 0x8b, 0x3f, 0x40, 0x92, 0x7f, 0x3c, 0x11, 0x7b, 0x2d, 0xdf, 0xb9,
0xb2, 0x13, 0xde, 0x61, 0xe7, 0x80, 0x78, 0x9e, 0x31, 0x22, 0x1d, 0x70, 0xea, 0x1c, 0x82, 0xbf,
0x84, 0xcd, 0xe9, 0x78, 0xe2, 0x82, 0x83, 0x05, 0x4e, 0xf2, 0x15, 0xeb, 0x67, 0x56, 0x54, 0x23,
0x8c, 0x1e, 0x5f, 0xa0, 0x54, 0xd0, 0x52, 0xd5, 0x9e, 0x7a, 0x3e, 0x71, 0xab, 0xd4, 0x19, 0x5a,
0x23, 0xfc, 0x11, 0xca, 0x0e, 0xa9, 0x0d, 0xa7, 0xf0, 0x60, 0xfb, 0xd4, 0xf6, 0xe2, 0x9d, 0x62,
0x14, 0x6c, 0x8f, 0x3b, 0x76, 0xa5, 0x97, 0x6f, 0x36, 0x17, 0xf4, 0x10, 0xa6, 0xfc, 0x98, 0x44,
0x19, 0xe1, 0xc1, 0x6b, 0x28, 0x69, 0x99, 0x82, 0xa2, 0xdd, 0xcc, 0xc9, 0x9b, 0xcd, 0x64, 0xbd,
0xa6, 0x83, 0x05, 0xaf, 0xa2, 0xb4, 0x6d, 0xf4, 0x89, 0x1d, 0x90, 0x23, 0x26, 0xf8, 0x1a, 0xca,
0xbb, 0x70, 0xe1, 0x1e, 0x75, 0xec, 0x63, 0x4e, 0x49, 0x4e, 0xcf, 0x31, 0x43, 0x13, 0xe6, 0xf8,
0x16, 0xc2, 0xd6, 0xc8, 0xa1, 0x2e, 0xe9, 0x4d, 0x88, 0x3b, 0xb6, 0xf8, 0x69, 0xbd, 0x92, 0xc4,
0x51, 0x2b, 0xc2, 0xd3, 0x8a, 0x1c, 0xf8, 0x3a, 0x5a, 0x0a, 0xe0, 0x26, 0xb1, 0x89, 0x4f, 0x4a,
0x69, 0x8e, 0x2c, 0x08, 0x63, 0x8d, 0xdb, 0xe0, 0x6e, 0xab, 0xa6, 0xe5, 0x19, 0x7d, 0x9b, 0xf4,
0x7c, 0x32, 0x9e, 0xf4, 0x2c, 0xc7, 0x24, 0xcf, 0x89, 0x57, 0xca, 0x70, 0x2c, 0x0e, 0x7c, 0x1d,
0x70, 0xd5, 0x85, 0x87, 0xb1, 0x21, 0x32, 0xed, 0x95, 0x8a, 0xa7, 0xd9, 0xa8, 0x71, 0x47, 0xc8,
0x46, 0x00, 0x53, 0x7e, 0x05, 0x36, 0x84, 0x27, 0xc6, 0x46, 0x61, 0x8e, 0x0d, 0x8c, 0xa4, 0x98,
0x52, 0xf8, 0x18, 0xaf, 0xa3, 0xbc, 0x61, 0x9a, 0x2c, 0x2b, 0xb0, 0x55, 0x0a, 0xb6, 0xca, 0xeb,
0x91, 0x01, 0xdf, 0x9b, 0xcf, 0xb2, 0x74, 0x5a, 0x17, 0x17, 0xa5, 0x97, 0x51, 0x3c, 0x20, 0x6e,
0xa0, 0xcc, 0x34, 0xdf, 0x2f, 0xc7, 0x0c, 0x5c, 0x97, 0x65, 0xb4, 0x32, 0x36, 0x9e, 0xf7, 0x6c,
0x3a, 0x30, 0xec, 0x99, 0x34, 0x19, 0x17, 0x29, 0xfd, 0x12, 0x38, 0x1a, 0xcc, 0x1e, 0x88, 0x13,
0x6f, 0x20, 0x64, 0x39, 0xbe, 0x4b, 0xcd, 0x29, 0x2c, 0x2f, 0x65, 0x39, 0x61, 0x31, 0x0b, 0xfe,
0x14, 0xe5, 0x38, 0x9b, 0x3d, 0xb8, 0x71, 0x0e, 0xbc, 0xd2, 0xae, 0xcc, 0x78, 0xf9, 0xfd, 0xcd,
0x66, 0x96, 0x73, 0x59, 0xaf, 0x9d, 0x44, 0x43, 0x3d, 0xcb, 0xb1, 0x75, 0x53, 0x69, 0xa2, 0x34,
0xb7, 0x01, 0x57, 0x19, 0xa1, 0xa7, 0xa0, 0xc0, 0x82, 0x19, 0xde, 0x41, 0xe9, 0xa1, 0x65, 0x03,
0x27, 0x49, 0x4e, 0x3f, 0x8e, 0x89, 0x11, 0xcc, 0x75, 0x67, 0x48, 0x83, 0x04, 0x08, 0x98, 0xd2,
0x45, 0x8b, 0x3c, 0x60, 0x77, 0x62, 0x1a, 0x3e, 0xf9, 0xdf, 0xc2, 0x7e, 0x9f, 0x42, 0xb9, 0xd0,
0x33, 0xcb, 0x5f, 0x22, 0x96, 0xbf, 0x72, 0x50, 0xb2, 0xa2, 0x00, 0xd7, 0xce, 0xc6, 0x8b, 0xd5,
0x2c, 0xac, 0xf7, 0xac, 0xef, 0x08, 0x97, 0x7c, 0x4a, 0xe7, 0x63, 0xbc, 0x85, 0x16, 0x4f, 0xeb,
0x7c, 0x49, 0x8f, 0x9b, 0xb0, 0x8c, 0x72, 0x63, 0x6a, 0x5a, 0x43, 0x8b, 0x98, 0x3c, 0x93, 0x29,
0x7d, 0x36, 0xc7, 0x25, 0x26, 0x53, 0x26, 0x71, 0x33, 0xd0, 0x72, 0x38, 0x65, 0x1e, 0xcb, 0x79,
0x6a, 0xd8, 0x90, 0x16, 0x91, 0xb4, 0x70, 0xca, 0xba, 0x92, 0x43, 0xe7, 0x8a, 0x2b, 0xc7, 0x01,
0x4b, 0x0e, 0x8d, 0x17, 0x16, 0x54, 0x40, 0x28, 0x8d, 0x3c, 0xf8, 0xe7, 0x2a, 0xe0, 0x90, 0x0c,
0x7c, 0x3a, 0xeb, 0x07, 0x01, 0x8c, 0x95, 0xe2, 0xbc, 0xa4, 0x10, 0x3f, 0x6d, 0xc1, 0x8e, 0xeb,
0xe9, 0x63, 0x94, 0xd9, 0x05, 0xc3, 0x93, 0xb0, 0xae, 0x2e, 0x47, 0x51, 0xb9, 0x3d, 0x96, 0x82,
0x4c, 0x9f, 0x03, 0x3f, 0x97, 0x7e, 0x7a, 0xb1, 0xb9, 0xa0, 0x3c, 0x44, 0xf9, 0x19, 0x80, 0xa5,
0x97, 0x0e, 0x87, 0x1e, 0xf1, 0x79, 0x2e, 0x52, 0x7a, 0x30, 0x9b, 0x31, 0xcc, 0xb2, 0x91, 0x0e,
0x18, 0x06, 0xdb, 0x91, 0xe1, 0x1d, 0x71, 0xd6, 0x0b, 0x3a, 0x1f, 0x07, 0x21, 0xbf, 0x40, 0x19,
0x71, 0x13, 0x7c, 0x17, 0xe5, 0x06, 0x74, 0xea, 0xf8, 0x51, 0xf7, 0x5b, 0x89, 0x17, 0x19, 0xf7,
0x04, 0xa7, 0x9a, 0x01, 0x95, 0x3d, 0x94, 0x0d, 0x5c, 0xc0, 0x69, 0x58, 0xf1, 0xd2, 0xee, 0x95,
0x50, 0xff, 0xed, 0x23, 0xea, 0xfa, 0x5c, 0xff, 0xb1, 0x76, 0x08, 0x39, 0x98, 0x8a, 0xf3, 0x49,
0xba, 0x98, 0x28, 0xbf, 0x25, 0x50, 0x56, 0x27, 0xdf, 0x4e, 0x89, 0xe7, 0xc7, 0x5a, 0x47, 0x7a,
0xae, 0x75, 0x44, 0x7a, 0x4e, 0xce, 0xe9, 0x39, 0x94, 0x64, 0x2a, 0x26, 0xc9, 0x88, 0x1c, 0xe9,
0x5c, 0x72, 0xd2, 0xe7, 0x90, 0x93, 0x89, 0xc8, 0x61, 0x02, 0x19, 0xba, 0x74, 0xcc, 0x5b, 0x25,
0x75, 0x0d, 0xf7, 0x38, 0x50, 0xd0, 0x12, 0xb3, 0x76, 0x42, 0xa3, 0xd2, 0x43, 0x39, 0x9d, 0x78,
0x13, 0xd0, 0x0a, 0xb9, 0xf0, 0xd8, 0x10, 0x1e, 0xca, 0xd1, 0xe0, 0x87, 0x86, 0xf0, 0x6c, 0x8c,
0xdf, 0x47, 0xd2, 0x80, 0x9a, 0xe2, 0xc8, 0xcb, 0xf1, 0xfc, 0xab, 0xae, 0x4b, 0xe1, 0x35, 0x32,
0xa1, 0x5c, 0x18, 0x00, 0x5e, 0xe2, 0x62, 0x8d, 0x3e, 0x73, 0x6c, 0x6a, 0x98, 0x2d, 0x97, 0x8e,
0x58, 0x6b, 0xbb, 0xb0, 0xae, 0x6b, 0x28, 0x3b, 0xe5, 0x95, 0x1f, 0x56, 0xf6, 0x8d, 0xf9, 0x4a,
0x3c, 0x1d, 0x48, 0xb4, 0x89, 0x50, 0xc1, 0xc1, 0x52, 0xe5, 0x75, 0x02, 0xc9, 0x17, 0xa3, 0x71,
0x1d, 0x2d, 0x0a, 0x64, 0x2f, 0xf6, 0x4a, 0x6f, 0xbf, 0xcd, 0x46, 0xbc, 0x09, 0xa0, 0xe9, 0x6c,
0x7c, 0xee, 0x53, 0x10, 0xab, 0xb8, 0xd4, 0x5b, 0x57, 0x1c, 0xaf, 0x91, 0xd9, 0x83, 0x26, 0xc1,
0xdd, 0xd3, 0x7a, 0xa1, 0x2f, 0x0a, 0x85, 0xdb, 0x94, 0x0c, 0x92, 0x5a, 0x96, 0x33, 0x52, 0x36,
0x51, 0xba, 0x6a, 0x53, 0x9e, 0xac, 0x0c, 0xbc, 0xb6, 0x1e, 0x6c, 0x13, 0x70, 0x28, 0x66, 0xe5,
0xd7, 0x49, 0xb4, 0x18, 0xfb, 0xd0, 0x80, 0xf3, 0x2c, 0x57, 0x1b, 0xdd, 0x76, 0x47, 0xd5, 0x7b,
0xd5, 0xa6, 0xb6, 0x57, 0xdf, 0x2f, 0x2e, 0xc8, 0xeb, 0x3f, 0xfc, 0xbc, 0x55, 0x1a, 0x47, 0xa0,
0xf9, 0x6f, 0x08, 0xd8, 0xa2, 0xae, 0xd5, 0xd4, 0xaf, 0x8a, 0x09, 0x79, 0x15, 0x80, 0xc5, 0x18,
0x50, 0x74, 0xfb, 0x0f, 0x51, 0x81, 0x03, 0x7a, 0xdd, 0x56, 0xad, 0xd2, 0x51, 0x8b, 0x49, 0x59,
0x06, 0xdc, 0xda, 0x69, 0x5c, 0xc0, 0xf7, 0x75, 0xa8, 0x0b, 0xf5, 0x61, 0x57, 0x6d, 0x77, 0x8a,
0x29, 0x79, 0x0d, 0x80, 0x38, 0x06, 0x0c, 0x2b, 0xe6, 0x26, 0xc8, 0x50, 0x6d, 0xb7, 0x9a, 0x5a,
0x5b, 0x2d, 0x4a, 0xf2, 0x55, 0x40, 0x5d, 0x9e, 0x43, 0x05, 0x0a, 0xfd, 0x0c, 0xad, 0xd4, 0x9a,
0x8f, 0xb4, 0x46, 0xb3, 0x52, 0xeb, 0xb5, 0xf4, 0xe6, 0x3e, 0xac, 0x69, 0x17, 0xd3, 0xf2, 0x26,
0xe0, 0xaf, 0xc5, 0xf0, 0x67, 0x04, 0xf7, 0x2e, 0xb0, 0x57, 0xd7, 0xf6, 0x8b, 0x19, 0xf9, 0x32,
0x40, 0x2f, 0xc5, 0xa0, 0x8c, 0x54, 0x76, 0xe3, 0x6a, 0xa3, 0x09, 0x5b, 0x67, 0xcf, 0xdc, 0x98,
0x93, 0x5d, 0xfe, 0x06, 0xe1, 0xb3, 0x9f, 0x62, 0xf8, 0x06, 0x92, 0xb4, 0xa6, 0xa6, 0x02, 0xa1,
0xfc, 0xfe, 0x67, 0x11, 0x1a, 0x75, 0x08, 0x56, 0x50, 0xaa, 0xf1, 0xf5, 0x27, 0x40, 0xe6, 0x3b,
0x00, 0xba, 0x72, 0x16, 0x04, 0xce, 0x32, 0x45, 0x8b, 0xf1, 0xc0, 0x0a, 0xca, 0x1d, 0xa8, 0x9d,
0x0a, 0x90, 0x5b, 0x81, 0xe0, 0xfc, 0x48, 0xa1, 0xfb, 0x80, 0xf8, 0x06, 0x2f, 0xc0, 0x75, 0x94,
0xd6, 0xd4, 0x43, 0x55, 0x87, 0xc0, 0x2b, 0x00, 0x58, 0x0a, 0x01, 0x1a, 0x01, 0x5d, 0xc1, 0x83,
0x9f, 0xa9, 0x34, 0x1e, 0x55, 0x1e, 0xb7, 0x21, 0x39, 0x18, 0xdc, 0xcb, 0xa1, 0xbb, 0x62, 0x3f,
0x33, 0x8e, 0xbd, 0xf2, 0x3f, 0x09, 0x54, 0x88, 0xbf, 0x6d, 0xb0, 0x40, 0xda, 0xab, 0x37, 0xd4,
0x70, 0xbb, 0xb8, 0x8f, 0x8d, 0xf1, 0x36, 0xca, 0xd7, 0xea, 0xba, 0x5a, 0xed, 0x34, 0xf5, 0xc7,
0xe1, 0x5d, 0xe2, 0xa0, 0x9a, 0xe5, 0x72, 0x71, 0xb3, 0x4f, 0xbf, 0x42, 0xfb, 0xf1, 0x41, 0xa3,
0xae, 0x3d, 0xe8, 0xf1, 0x88, 0x49, 0xf9, 0x1a, 0x80, 0xaf, 0xc6, 0xc1, 0xed, 0xe3, 0xb1, 0x6d,
0x39, 0x4f, 0x78, 0xe0, 0x7b, 0x68, 0x25, 0x84, 0x47, 0x1b, 0xa4, 0xe4, 0x2d, 0x58, 0xb3, 0x7e,
0xce, 0x9a, 0x68, 0x9f, 0xbb, 0xe8, 0x52, 0xb8, 0xb0, 0xab, 0x3d, 0xd0, 0x40, 0x16, 0xa0, 0x9c,
0x0d, 0x58, 0x26, 0x9f, 0xb3, 0xac, 0xeb, 0x3c, 0x71, 0x40, 0x14, 0xe5, 0x5f, 0x12, 0x28, 0x3f,
0xeb, 0x50, 0x8c, 0x67, 0xad, 0xd9, 0x53, 0x75, 0xbd, 0xa9, 0x87, 0x17, 0x9f, 0x39, 0x35, 0xca,
0x87, 0xf8, 0x3d, 0x94, 0xdd, 0x57, 0x35, 0x55, 0xaf, 0x57, 0xc3, 0x7a, 0x98, 0x41, 0xf6, 0x89,
0x43, 0x5c, 0x6b, 0x00, 0x1f, 0xfc, 0x05, 0x08, 0xd3, 0xee, 0x56, 0xef, 0x87, 0x37, 0xe6, 0x02,
0x8e, 0x85, 0x6a, 0x4f, 0x07, 0x47, 0xfc, 0xb6, 0x65, 0x56, 0x3a, 0x87, 0x95, 0x46, 0xbd, 0x26,
0xa0, 0x29, 0xb9, 0x04, 0xd0, 0xd5, 0x19, 0xb4, 0x2e, 0x9e, 0x77, 0x86, 0x2d, 0x9b, 0x68, 0xe3,
0xbf, 0x7b, 0x11, 0x7c, 0x76, 0x64, 0x2a, 0xad, 0x96, 0xaa, 0xd5, 0xc2, 0xd3, 0x47, 0xbe, 0xca,
0x64, 0x42, 0x1c, 0x93, 0x21, 0xf6, 0x9a, 0xfa, 0xbe, 0xda, 0x09, 0x0f, 0x1f, 0x21, 0xf6, 0xa8,
0x3b, 0x22, 0xfe, 0xee, 0xfa, 0xcb, 0x3f, 0x37, 0x16, 0x5e, 0xc1, 0xef, 0xe5, 0xc9, 0x46, 0xe2,
0x15, 0xfc, 0xfe, 0x38, 0xd9, 0x58, 0xf8, 0x1b, 0xfe, 0x5f, 0xfc, 0xb5, 0x91, 0xe8, 0x67, 0x78,
0xef, 0xba, 0xfb, 0x6f, 0x00, 0x00, 0x00, 0xff, 0xff, 0x5f, 0x7a, 0x46, 0xf6, 0x94, 0x0d, 0x00,
0x00,
// 1589 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xac, 0x57, 0x4f, 0x6f, 0xdb, 0x46,
0x16, 0xb7, 0x24, 0xea, 0xdf, 0x48, 0x76, 0xe4, 0x89, 0xe3, 0x68, 0x19, 0xc7, 0xf6, 0x32, 0x09,
0xd6, 0x2b, 0x6c, 0x9c, 0xdd, 0x64, 0x77, 0x03, 0x14, 0x68, 0x01, 0x59, 0xa2, 0x1d, 0x21, 0x32,
0xa5, 0x50, 0x92, 0xd3, 0xf4, 0x50, 0x41, 0x12, 0x47, 0x32, 0x11, 0x8a, 0xa3, 0x92, 0x54, 0x12,
0xf7, 0x23, 0xb4, 0x5f, 0xa0, 0x97, 0x02, 0x41, 0x6e, 0xbd, 0xf7, 0x43, 0xe4, 0x18, 0xe4, 0xd8,
0x43, 0xd0, 0xba, 0x97, 0x7e, 0x81, 0xde, 0xfb, 0xf8, 0x86, 0x94, 0x28, 0xff, 0x29, 0x72, 0xe8,
0xc1, 0xd0, 0xcc, 0x7b, 0xbf, 0x99, 0x37, 0xf3, 0x7b, 0xbf, 0xf7, 0x86, 0x26, 0xd9, 0x3e, 0x9b,
0xec, 0x4e, 0x1c, 0xee, 0x71, 0x9a, 0xc1, 0x9f, 0x01, 0xb7, 0xe4, 0xbb, 0x23, 0xd3, 0x3b, 0x9e,
0xf6, 0x77, 0x07, 0x7c, 0x7c, 0x6f, 0xc4, 0x47, 0xfc, 0x1e, 0x7a, 0xfa, 0xd3, 0x21, 0xce, 0x70,
0x82, 0x23, 0xb1, 0x50, 0x99, 0x90, 0xe4, 0x23, 0x66, 0x59, 0x9c, 0x6e, 0x91, 0x9c, 0xc1, 0x5e,
0x98, 0x03, 0xd6, 0xb5, 0x7b, 0x63, 0x56, 0x8c, 0x6d, 0xc7, 0x76, 0xb2, 0x3a, 0x11, 0x26, 0x0d,
0x2c, 0x3e, 0x60, 0x60, 0x99, 0xcc, 0xf6, 0x04, 0x20, 0x2e, 0x00, 0xc2, 0x84, 0x80, 0x3b, 0x64,
0x25, 0x00, 0xbc, 0x60, 0x8e, 0x6b, 0x72, 0xbb, 0x98, 0x40, 0xcc, 0xb2, 0xb0, 0x1e, 0x09, 0xa3,
0xe2, 0x92, 0xd4, 0x23, 0xd6, 0x33, 0x98, 0x43, 0xff, 0x49, 0x24, 0xef, 0x64, 0x22, 0x62, 0xad,
0xdc, 0xbf, 0xb6, 0x1b, 0xde, 0x61, 0xf7, 0x90, 0xb9, 0x6e, 0x6f, 0xc4, 0xda, 0xe0, 0xd4, 0x11,
0x42, 0x3f, 0x83, 0xe0, 0x7c, 0x3c, 0x71, 0xc0, 0xe1, 0x6f, 0x1c, 0xc7, 0x15, 0x1b, 0xe7, 0x56,
0x54, 0xe6, 0x18, 0x3d, 0xba, 0x40, 0x29, 0x93, 0xe5, 0x8a, 0x35, 0x75, 0x3d, 0xe6, 0x54, 0xb8,
0x3d, 0x34, 0x47, 0xf4, 0xdf, 0x24, 0x3d, 0xe4, 0x16, 0x9c, 0xc2, 0x85, 0xf0, 0x89, 0x9d, 0xdc,
0xfd, 0xc2, 0x7c, 0xb3, 0x7d, 0x74, 0xec, 0x49, 0x6f, 0x3f, 0x6c, 0x2d, 0xe9, 0x21, 0x4c, 0xf9,
0x36, 0x4e, 0x52, 0xc2, 0x43, 0xd7, 0x49, 0xdc, 0x34, 0x04, 0x45, 0x7b, 0xa9, 0xd3, 0x0f, 0x5b,
0xf1, 0x5a, 0x55, 0x07, 0x0b, 0x5d, 0x23, 0x49, 0xab, 0xd7, 0x67, 0x56, 0x40, 0x8e, 0x98, 0xd0,
0x1b, 0x24, 0xeb, 0xc0, 0x85, 0xbb, 0xdc, 0xb6, 0x4e, 0x90, 0x92, 0x8c, 0x9e, 0xf1, 0x0d, 0x0d,
0x98, 0xd3, 0xbb, 0x84, 0x9a, 0x23, 0x9b, 0x3b, 0xac, 0x3b, 0x61, 0xce, 0xd8, 0xc4, 0xd3, 0xba,
0x45, 0x09, 0x51, 0xab, 0xc2, 0xd3, 0x9c, 0x3b, 0xe8, 0x2d, 0xb2, 0x1c, 0xc0, 0x0d, 0x66, 0x31,
0x8f, 0x15, 0x93, 0x88, 0xcc, 0x0b, 0x63, 0x15, 0x6d, 0x70, 0xb7, 0x35, 0xc3, 0x74, 0x7b, 0x7d,
0x8b, 0x75, 0x3d, 0x36, 0x9e, 0x74, 0x4d, 0xdb, 0x60, 0xaf, 0x98, 0x5b, 0x4c, 0x21, 0x96, 0x06,
0xbe, 0x36, 0xb8, 0x6a, 0xc2, 0xe3, 0xb3, 0x21, 0x32, 0xed, 0x16, 0x0b, 0x67, 0xd9, 0xa8, 0xa2,
0x23, 0x64, 0x23, 0x80, 0x29, 0x6f, 0x80, 0x0d, 0xe1, 0x89, 0xb0, 0x91, 0x5f, 0x60, 0x83, 0x12,
0x29, 0xa2, 0x14, 0x1c, 0xd3, 0x0d, 0x92, 0xed, 0x19, 0x86, 0x9f, 0x15, 0x08, 0x95, 0x80, 0x50,
0x59, 0x7d, 0x6e, 0xa0, 0x0f, 0x17, 0xb3, 0x2c, 0x9d, 0xd5, 0xc5, 0x65, 0xe9, 0xf5, 0x29, 0x1e,
0x30, 0x27, 0x50, 0x66, 0x12, 0xe3, 0x65, 0x7c, 0x03, 0xea, 0xf2, 0xef, 0x24, 0x3f, 0xee, 0xbd,
0xea, 0xba, 0xec, 0xab, 0x29, 0xb3, 0x07, 0x0c, 0x69, 0x48, 0xe8, 0x39, 0xb0, 0xb5, 0x02, 0x13,
0xdd, 0x24, 0xc4, 0xb4, 0x3d, 0x87, 0x1b, 0x53, 0x58, 0x55, 0x4c, 0x23, 0x4f, 0x11, 0x0b, 0xfd,
0x1f, 0xc9, 0x20, 0x89, 0x5d, 0xb8, 0x68, 0x06, 0xbc, 0xd2, 0x9e, 0xec, 0xd3, 0xf1, 0xd3, 0x87,
0xad, 0x34, 0x52, 0x58, 0xab, 0x9e, 0xce, 0x87, 0x7a, 0x1a, 0xb1, 0x35, 0x43, 0x69, 0x90, 0x24,
0xda, 0x80, 0xa2, 0x94, 0x90, 0x51, 0x50, 0x57, 0xc1, 0x8c, 0xee, 0x92, 0xe4, 0xd0, 0xb4, 0x80,
0x8a, 0x38, 0xb2, 0x4e, 0x23, 0x1a, 0x04, 0x73, 0xcd, 0x1e, 0xf2, 0x80, 0x77, 0x01, 0x53, 0x3a,
0x24, 0x87, 0x1b, 0x76, 0x26, 0x46, 0xcf, 0x63, 0x7f, 0xd9, 0xb6, 0x6f, 0x12, 0x24, 0x13, 0x7a,
0x66, 0x69, 0x8b, 0x45, 0xd2, 0x56, 0x0a, 0x2a, 0x55, 0xd4, 0xdd, 0xfa, 0xf9, 0xfd, 0x22, 0xa5,
0x0a, 0xeb, 0x5d, 0xf3, 0x6b, 0x86, 0x4a, 0x4f, 0xe8, 0x38, 0xa6, 0xdb, 0x24, 0x77, 0x56, 0xde,
0xcb, 0x7a, 0xd4, 0x44, 0x6f, 0x12, 0x32, 0xe6, 0x86, 0x39, 0x34, 0x99, 0xd1, 0x75, 0x31, 0x85,
0x09, 0x3d, 0x1b, 0x5a, 0x5a, 0xb4, 0xe8, 0x0b, 0xd4, 0x17, 0xb7, 0x11, 0xa8, 0x38, 0x9c, 0xfa,
0x1e, 0xd3, 0x7e, 0xd1, 0xb3, 0x20, 0x33, 0x22, 0x6f, 0xe1, 0xd4, 0xef, 0x47, 0x36, 0x5f, 0x28,
0xab, 0x0c, 0x02, 0x96, 0x6d, 0x1e, 0x2d, 0x29, 0xd0, 0x7e, 0xd8, 0xaf, 0xb2, 0xe0, 0x5f, 0xd0,
0xfe, 0x11, 0x1b, 0x78, 0x7c, 0xd6, 0x09, 0x02, 0x18, 0x95, 0x49, 0x66, 0x26, 0x26, 0x82, 0x27,
0x9d, 0xcd, 0xfd, 0x2e, 0x39, 0xbb, 0x07, 0x44, 0xcc, 0x81, 0x3b, 0xa9, 0xcf, 0xae, 0xa6, 0xb9,
0xf4, 0x3f, 0x24, 0xb5, 0x67, 0xf1, 0xc1, 0xf3, 0xb0, 0xd2, 0xae, 0xce, 0xa3, 0xa1, 0x3d, 0x92,
0x9d, 0x54, 0x1f, 0x81, 0x9f, 0x48, 0xdf, 0xbd, 0xde, 0x5a, 0x52, 0x9e, 0x90, 0xec, 0x0c, 0xe0,
0x67, 0x9e, 0x0f, 0x87, 0x2e, 0xf3, 0x30, 0x4d, 0x09, 0x3d, 0x98, 0xcd, 0xc8, 0x8f, 0x63, 0x5c,
0x41, 0x3e, 0xd8, 0x8e, 0x7b, 0xee, 0x31, 0x26, 0x24, 0xaf, 0xe3, 0x38, 0xd8, 0xf2, 0x53, 0x92,
0x12, 0x37, 0xa4, 0x0f, 0x48, 0x66, 0xc0, 0xa7, 0xb6, 0x37, 0xef, 0x87, 0xab, 0xd1, 0xb2, 0x43,
0x4f, 0x70, 0xaa, 0x19, 0x50, 0xd9, 0x27, 0xe9, 0xc0, 0x05, 0x5c, 0x87, 0x3d, 0x40, 0xda, 0xbb,
0x16, 0x96, 0x46, 0xeb, 0x98, 0x3b, 0x1e, 0x96, 0x46, 0xa4, 0x41, 0x42, 0x6e, 0xa6, 0xe2, 0x7c,
0x92, 0x2e, 0x26, 0xca, 0x8f, 0x31, 0x92, 0xd6, 0x7d, 0x02, 0x5d, 0x2f, 0xd2, 0x4c, 0x92, 0x0b,
0xcd, 0x64, 0x2e, 0xf5, 0xf8, 0x82, 0xd4, 0x43, 0xb5, 0x26, 0x22, 0x6a, 0x9d, 0x93, 0x23, 0x5d,
0x48, 0x4e, 0xf2, 0x02, 0x72, 0x52, 0x73, 0x72, 0x7c, 0xe1, 0x0c, 0x1d, 0x3e, 0xc6, 0xe6, 0xc9,
0x9d, 0x9e, 0x73, 0x12, 0x28, 0x6b, 0xd9, 0xb7, 0xb6, 0x43, 0xa3, 0xd2, 0x25, 0x19, 0x9d, 0xb9,
0x13, 0xd0, 0x10, 0xbb, 0xf4, 0xd8, 0xb0, 0x3d, 0x54, 0x6a, 0x0f, 0x0f, 0x0d, 0xdb, 0xfb, 0x63,
0xfa, 0x0f, 0x22, 0x0d, 0xb8, 0x21, 0x8e, 0xbc, 0x12, 0xcd, 0xbf, 0xea, 0x38, 0x1c, 0xde, 0x27,
0x03, 0x2a, 0xc9, 0x07, 0xc0, 0xdb, 0x5c, 0xa8, 0xf2, 0x97, 0xb6, 0xc5, 0x7b, 0x46, 0xd3, 0xe1,
0x23, 0xbf, 0xd9, 0x5d, 0x5a, 0xf2, 0x55, 0x92, 0x9e, 0x62, 0x53, 0x08, 0x8b, 0xfe, 0xf6, 0x62,
0x91, 0x9e, 0xdd, 0x48, 0x74, 0x90, 0x50, 0xd9, 0xc1, 0x52, 0xe5, 0x7d, 0x8c, 0xc8, 0x97, 0xa3,
0x69, 0x8d, 0xe4, 0x04, 0xb2, 0x1b, 0x79, 0xb7, 0x77, 0x3e, 0x26, 0x10, 0xf6, 0x07, 0x32, 0x9d,
0x8d, 0x2f, 0x7c, 0x1c, 0x22, 0x95, 0x98, 0xf8, 0xb8, 0x4a, 0x84, 0xe7, 0x10, 0x6b, 0x64, 0xf6,
0xc4, 0x49, 0x70, 0xf7, 0xa4, 0x9e, 0xef, 0x8b, 0x42, 0x41, 0x9b, 0x92, 0x22, 0x52, 0xd3, 0xb4,
0x47, 0xca, 0x16, 0x49, 0x56, 0x2c, 0x8e, 0xc9, 0x4a, 0xc1, 0xfb, 0xeb, 0x42, 0x98, 0x80, 0x43,
0x31, 0x2b, 0xbd, 0x8f, 0x93, 0x5c, 0xe4, 0xd3, 0x03, 0xce, 0xb3, 0x52, 0xa9, 0x77, 0x5a, 0x6d,
0x55, 0xef, 0x56, 0x1a, 0xda, 0x7e, 0xed, 0xa0, 0xb0, 0x24, 0x6f, 0x7c, 0xf3, 0xfd, 0x76, 0x71,
0x3c, 0x07, 0x2d, 0x7e, 0x55, 0x40, 0x88, 0x9a, 0x56, 0x55, 0x3f, 0x2f, 0xc4, 0xe4, 0x35, 0x00,
0x16, 0x22, 0x40, 0xf1, 0x10, 0xfc, 0x8b, 0xe4, 0x11, 0xd0, 0xed, 0x34, 0xab, 0xe5, 0xb6, 0x5a,
0x88, 0xcb, 0x32, 0xe0, 0xd6, 0xcf, 0xe2, 0x02, 0xbe, 0x6f, 0x41, 0x5d, 0xa8, 0x4f, 0x3a, 0x6a,
0xab, 0x5d, 0x48, 0xc8, 0xeb, 0x00, 0xa4, 0x11, 0x60, 0x58, 0x31, 0x77, 0x40, 0x86, 0x6a, 0xab,
0xd9, 0xd0, 0x5a, 0x6a, 0x41, 0x92, 0xaf, 0x03, 0xea, 0xea, 0x02, 0x2a, 0x50, 0xe8, 0xff, 0xc9,
0x6a, 0xb5, 0xf1, 0x54, 0xab, 0x37, 0xca, 0xd5, 0x6e, 0x53, 0x6f, 0x1c, 0xc0, 0x9a, 0x56, 0x21,
0x29, 0x6f, 0x01, 0xfe, 0x46, 0x04, 0x7f, 0x4e, 0x70, 0x37, 0x81, 0xbd, 0x9a, 0x76, 0x50, 0x48,
0xc9, 0x57, 0x01, 0x7a, 0x25, 0x02, 0xf5, 0x49, 0xf5, 0x6f, 0x5c, 0xa9, 0x37, 0x20, 0x74, 0xfa,
0xdc, 0x8d, 0x91, 0xec, 0xd2, 0x97, 0x84, 0x9e, 0xff, 0x38, 0xa3, 0xb7, 0x89, 0xa4, 0x35, 0x34,
0x15, 0x08, 0xc5, 0xfb, 0x9f, 0x47, 0x68, 0xdc, 0x66, 0x54, 0x21, 0x89, 0xfa, 0x17, 0xff, 0x05,
0x32, 0xff, 0x06, 0xa0, 0x6b, 0xe7, 0x41, 0xe0, 0x2c, 0x71, 0x92, 0x8b, 0x6e, 0xac, 0x90, 0xcc,
0xa1, 0xda, 0x2e, 0x03, 0xb9, 0x65, 0xd8, 0x1c, 0x8f, 0x14, 0xba, 0x0f, 0x99, 0xd7, 0xc3, 0x02,
0xdc, 0x20, 0x49, 0x4d, 0x3d, 0x52, 0x75, 0xd8, 0x78, 0x15, 0x00, 0xcb, 0x21, 0x40, 0x63, 0xa0,
0x2b, 0xf8, 0x16, 0x48, 0x95, 0xeb, 0x4f, 0xcb, 0xcf, 0x5a, 0x90, 0x1c, 0x0a, 0xee, 0x95, 0xd0,
0x5d, 0xb6, 0x5e, 0xf6, 0x4e, 0xdc, 0xd2, 0xef, 0x31, 0x92, 0x8f, 0x3e, 0x7b, 0xb0, 0x40, 0xda,
0xaf, 0xd5, 0xd5, 0x30, 0x5c, 0xd4, 0xe7, 0x8f, 0xe9, 0x0e, 0xc9, 0x56, 0x6b, 0xba, 0x5a, 0x69,
0x37, 0xf4, 0x67, 0xe1, 0x5d, 0xa2, 0xa0, 0xaa, 0xe9, 0xa0, 0xb8, 0xfd, 0x8f, 0xc1, 0x7c, 0xeb,
0xd9, 0x61, 0xbd, 0xa6, 0x3d, 0xee, 0xe2, 0x8e, 0x71, 0xf9, 0x06, 0x80, 0xaf, 0x47, 0xc1, 0xad,
0x93, 0xb1, 0x65, 0xda, 0xcf, 0x71, 0xe3, 0x87, 0x64, 0x35, 0x84, 0xcf, 0x03, 0x24, 0xe4, 0x6d,
0x58, 0xb3, 0x71, 0xc1, 0x9a, 0x79, 0x9c, 0x07, 0xe4, 0x4a, 0xb8, 0xb0, 0xa3, 0x3d, 0xd6, 0x40,
0x16, 0xa0, 0x9c, 0x4d, 0x58, 0x26, 0x5f, 0xb0, 0xac, 0x63, 0x3f, 0xb7, 0x41, 0x14, 0xa5, 0x1f,
0x62, 0x24, 0x3b, 0xeb, 0x50, 0x3e, 0xcf, 0x5a, 0xa3, 0xab, 0xea, 0x7a, 0x43, 0x0f, 0x2f, 0x3e,
0x73, 0x6a, 0x1c, 0x87, 0xf0, 0xe1, 0x95, 0x3e, 0x50, 0x35, 0x55, 0xaf, 0x55, 0xc2, 0x7a, 0x98,
0x41, 0x0e, 0x98, 0xcd, 0x1c, 0x73, 0x00, 0xff, 0x02, 0xe4, 0x61, 0x9b, 0x56, 0xa7, 0xf2, 0x28,
0xbc, 0x31, 0x0a, 0x38, 0xb2, 0x55, 0x6b, 0x3a, 0x38, 0xc6, 0xdb, 0x96, 0xfc, 0xd2, 0x39, 0x2a,
0xd7, 0x6b, 0x55, 0x01, 0x4d, 0xc8, 0x45, 0x80, 0xae, 0xcd, 0xa0, 0x35, 0xf1, 0xec, 0xfb, 0xd8,
0x92, 0x41, 0x36, 0xff, 0xbc, 0x17, 0xc1, 0x17, 0x49, 0xaa, 0xdc, 0x6c, 0xaa, 0x5a, 0x35, 0x3c,
0xfd, 0xdc, 0x57, 0x9e, 0x4c, 0x98, 0x6d, 0xf8, 0x88, 0xfd, 0x86, 0x7e, 0xa0, 0xb6, 0xc3, 0xc3,
0xcf, 0x11, 0xfb, 0xdc, 0x19, 0x31, 0x6f, 0x6f, 0xe3, 0xed, 0x2f, 0x9b, 0x4b, 0xef, 0xe0, 0xef,
0xed, 0xe9, 0x66, 0xec, 0x1d, 0xfc, 0xfd, 0x7c, 0xba, 0xb9, 0xf4, 0x1b, 0xfc, 0xbe, 0xfe, 0x75,
0x33, 0xd6, 0x4f, 0x61, 0xef, 0x7a, 0xf0, 0x47, 0x00, 0x00, 0x00, 0xff, 0xff, 0x83, 0x88, 0xa1,
0x6a, 0xa6, 0x0d, 0x00, 0x00,
}

View File

@@ -63,14 +63,14 @@ message Folder {
}
message Device {
bytes id = 1 [(gogoproto.customname) = "ID"];
string name = 2;
repeated string addresses = 3;
Compression compression = 4;
string cert_name = 5;
int64 max_local_version = 6;
bool introducer = 7;
uint64 index_id = 8 [(gogoproto.customname) = "IndexID", (gogoproto.customtype) = "IndexID", (gogoproto.nullable) = false];
bytes id = 1 [(gogoproto.customname) = "ID"];
string name = 2;
repeated string addresses = 3;
Compression compression = 4;
string cert_name = 5;
int64 max_sequence = 6;
bool introducer = 7;
uint64 index_id = 8 [(gogoproto.customname) = "IndexID", (gogoproto.customtype) = "IndexID", (gogoproto.nullable) = false];
}
enum Compression {
@@ -97,12 +97,13 @@ message FileInfo {
FileInfoType type = 2;
int64 size = 3;
uint32 permissions = 4;
int64 modified = 5;
int64 modified_s = 5;
int32 modified_ns = 11;
bool deleted = 6;
bool invalid = 7;
bool no_permissions = 8;
Vector version = 9 [(gogoproto.nullable) = false];
int64 local_version = 10;
int64 sequence = 10;
repeated BlockInfo Blocks = 16 [(gogoproto.nullable) = false];
}

View File

@@ -7,14 +7,20 @@ package protocol
import (
"bytes"
"crypto/sha256"
"encoding/binary"
"errors"
"fmt"
"time"
"github.com/minio/sha256-simd"
"github.com/syncthing/syncthing/lib/rand"
)
const (
SyntheticDirectorySize = 128
)
var (
sha256OfEmptyBlock = sha256.Sum256(make([]byte, BlockSize))
HelloMessageMagic = uint32(0x2EA7D90B)
@@ -25,8 +31,8 @@ func (m Hello) Magic() uint32 {
}
func (f FileInfo) String() string {
return fmt.Sprintf("File{Name:%q, Type:%v, LocalVersion:%d, Permissions:0%o, Modified:%d, Version:%v, Length:%d, Deleted:%v, Invalid:%v, NoPermissions:%v, Blocks:%v}",
f.Name, f.Type, f.LocalVersion, f.Permissions, f.Modified, f.Version, f.Size, f.Deleted, f.Invalid, f.NoPermissions, f.Blocks)
return fmt.Sprintf("File{Name:%q, Type:%v, Sequence:%d, Permissions:0%o, ModTime:%v, Version:%v, Length:%d, Deleted:%v, Invalid:%v, NoPermissions:%v, Blocks:%v}",
f.Name, f.Type, f.Sequence, f.Permissions, f.ModTime(), f.Version, f.Size, f.Deleted, f.Invalid, f.NoPermissions, f.Blocks)
}
func (f FileInfo) IsDeleted() bool {
@@ -55,8 +61,11 @@ func (f FileInfo) HasPermissionBits() bool {
}
func (f FileInfo) FileSize() int64 {
if f.IsDirectory() || f.IsDeleted() {
return 128
if f.Deleted {
return 0
}
if f.IsDirectory() {
return SyntheticDirectorySize
}
return f.Size
}
@@ -65,6 +74,10 @@ func (f FileInfo) FileName() string {
return f.Name
}
func (f FileInfo) ModTime() time.Time {
return time.Unix(f.ModifiedS, int64(f.ModifiedNs))
}
// WinsConflict returns true if "f" is the one to choose when it is in
// conflict with "other".
func (f FileInfo) WinsConflict(other FileInfo) bool {
@@ -78,10 +91,10 @@ func (f FileInfo) WinsConflict(other FileInfo) bool {
}
// The one with the newer modification time wins.
if f.Modified > other.Modified {
if f.ModTime().After(other.ModTime()) {
return true
}
if f.Modified < other.Modified {
if f.ModTime().Before(other.ModTime()) {
return false
}

View File

@@ -39,7 +39,7 @@ func (t *TestModel) Request(deviceID DeviceID, folder, name string, offset int64
return nil
}
func (t *TestModel) Close(deviceID DeviceID, err error) {
func (t *TestModel) Closed(conn Connection, err error) {
t.closedErr = err
close(t.closedCh)
}

View File

@@ -7,9 +7,9 @@ import "testing"
func TestWinsConflict(t *testing.T) {
testcases := [][2]FileInfo{
// The first should always win over the second
{{Modified: 42}, {Modified: 41}},
{{Modified: 41}, {Modified: 42, Deleted: true}},
{{Modified: 41, Version: Vector{[]Counter{{42, 2}, {43, 1}}}}, {Modified: 41, Version: Vector{[]Counter{{42, 1}, {43, 2}}}}},
{{ModifiedS: 42}, {ModifiedS: 41}},
{{ModifiedS: 41}, {ModifiedS: 42, Deleted: true}},
{{ModifiedS: 41, Version: Vector{[]Counter{{42, 2}, {43, 1}}}}, {ModifiedS: 41, Version: Vector{[]Counter{{42, 1}, {43, 2}}}}},
}
for _, tc := range testcases {

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