Compare commits
69 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 66a506e72b | |||
| 25a7b0a6f8 | |||
| 7aaa1dd8a3 | |||
| 2a6f164923 | |||
| 0f28626bb4 | |||
| 6ed22d0885 | |||
| 6715b91a6c | |||
| 694da60659 | |||
| 47fa4b0a2c | |||
| 8ab6b60778 | |||
| e1a4f81e50 | |||
| 7b7e35d339 | |||
| 3176629410 | |||
| e3ccc45d19 | |||
| beec9e834e | |||
| f6f0486ff9 | |||
| 518f446d31 | |||
| fbbd510088 | |||
| e440d30028 | |||
| 44d30c83bf | |||
| 7ff7b55732 | |||
| 44346b3a5a | |||
| 23a538d61a | |||
| dcb5026f33 | |||
| 778ff9daa9 | |||
| ce9dc809bc | |||
| 59370588dd | |||
| 7d434aa9c4 | |||
| 59ce7c0424 | |||
| 9a0e5a7c18 | |||
| 8d0019595f | |||
| 6ff74cfcab | |||
| aa50ef4069 | |||
| e9063c639a | |||
| 8d6dedc15b | |||
| 1bc4c1a8ac | |||
| 1a35c440e8 | |||
| 2c6c84ac61 | |||
| bd666daf82 | |||
| ca3831c4f5 | |||
| bbe0d34f43 | |||
| dd364c962f | |||
| 50068b0b0f | |||
| 175769b53e | |||
| 07722dc33d | |||
| f39f816a98 | |||
| 845f31b98f | |||
| 89b6c32cee | |||
| 6ee36fe361 | |||
| b61d7c2428 | |||
| bcc5d7c00f | |||
| 925f60d9c3 | |||
| 8b3f5fda07 | |||
| ac17b2c584 | |||
| c67c861dc6 | |||
| 09ba9e6259 | |||
| 0e167f5c24 | |||
| c885903ff2 | |||
| a91a836224 | |||
| 8450ab8dab | |||
| 168889d999 | |||
| e1339628d9 | |||
| 425f61cf34 | |||
| 7d9df5abc6 | |||
| 118cba4d9b | |||
| 3cacb48f3c | |||
| 6965812d79 | |||
| 78fb7fe9f9 | |||
| d7c8075862 |
+2
-2
@@ -1,7 +1,7 @@
|
||||
/syncthing
|
||||
/discosrv
|
||||
/stdiscosrv
|
||||
syncthing.exe
|
||||
discosrv.exe
|
||||
stdiscosrv.exe
|
||||
*.tar.gz
|
||||
*.zip
|
||||
*.asc
|
||||
|
||||
@@ -27,6 +27,11 @@ There are a few examples for keeping Syncthing running in the background
|
||||
on your system in [the etc directory][3]. There are also several [GUI
|
||||
implementations][11] for Windows, Mac and Linux.
|
||||
|
||||
## Vote on features/bugs
|
||||
|
||||
We'd like to encourage you to [vote][12] on issues that matter to you.
|
||||
This helps the team understand what are the biggest pain points for our users, and could potentially influence what is being worked on next.
|
||||
|
||||
## Getting in Touch
|
||||
|
||||
The first and best point of contact is the [Forum][8]. There is also an IRC
|
||||
@@ -66,3 +71,4 @@ All code is licensed under the [MPLv2 License][7].
|
||||
[9]: https://kiwiirc.com/client/irc.freenode.net/#syncthing
|
||||
[10]: https://github.com/syncthing/syncthing/issues
|
||||
[11]: http://docs.syncthing.net/users/contrib.html#gui-wrappers
|
||||
[12]: https://www.bountysource.com/teams/syncthing/issues
|
||||
|
||||
@@ -96,38 +96,55 @@ var targets = map[string]target{
|
||||
{src: "etc/linux-systemd/user/syncthing.service", dst: "deb/usr/lib/systemd/user/syncthing.service", perm: 0644},
|
||||
},
|
||||
},
|
||||
"discosrv": {
|
||||
name: "discosrv",
|
||||
buildPkg: "./cmd/discosrv",
|
||||
binaryName: "discosrv", // .exe will be added automatically for Windows builds
|
||||
"stdiscosrv": {
|
||||
name: "stdiscosrv",
|
||||
buildPkg: "./cmd/stdiscosrv",
|
||||
binaryName: "stdiscosrv", // .exe will be added automatically for Windows builds
|
||||
archiveFiles: []archiveFile{
|
||||
{src: "{{binary}}", dst: "{{binary}}", perm: 0755},
|
||||
{src: "cmd/discosrv/README.md", dst: "README.txt", perm: 0644},
|
||||
{src: "cmd/discosrv/LICENSE", dst: "LICENSE.txt", perm: 0644},
|
||||
{src: "cmd/stdiscosrv/README.md", dst: "README.txt", perm: 0644},
|
||||
{src: "cmd/stdiscosrv/LICENSE", dst: "LICENSE.txt", perm: 0644},
|
||||
{src: "AUTHORS", dst: "AUTHORS.txt", perm: 0644},
|
||||
},
|
||||
debianFiles: []archiveFile{
|
||||
{src: "{{binary}}", dst: "deb/usr/bin/{{binary}}", perm: 0755},
|
||||
{src: "cmd/discosrv/README.md", dst: "deb/usr/share/doc/discosrv/README.txt", perm: 0644},
|
||||
{src: "cmd/discosrv/LICENSE", dst: "deb/usr/share/doc/discosrv/LICENSE.txt", perm: 0644},
|
||||
{src: "AUTHORS", dst: "deb/usr/share/doc/discosrv/AUTHORS.txt", perm: 0644},
|
||||
{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},
|
||||
},
|
||||
tags: []string{"purego"},
|
||||
},
|
||||
"relaysrv": {
|
||||
name: "relaysrv",
|
||||
buildPkg: "./cmd/relaysrv",
|
||||
binaryName: "relaysrv", // .exe will be added automatically for Windows builds
|
||||
"strelaysrv": {
|
||||
name: "strelaysrv",
|
||||
buildPkg: "./cmd/strelaysrv",
|
||||
binaryName: "strelaysrv", // .exe will be added automatically for Windows builds
|
||||
archiveFiles: []archiveFile{
|
||||
{src: "{{binary}}", dst: "{{binary}}", perm: 0755},
|
||||
{src: "cmd/relaysrv/README.md", dst: "README.txt", perm: 0644},
|
||||
{src: "cmd/relaysrv/LICENSE", dst: "LICENSE.txt", perm: 0644},
|
||||
{src: "cmd/strelaysrv/README.md", dst: "README.txt", perm: 0644},
|
||||
{src: "cmd/strelaysrv/LICENSE", dst: "LICENSE.txt", perm: 0644},
|
||||
{src: "AUTHORS", dst: "AUTHORS.txt", perm: 0644},
|
||||
},
|
||||
debianFiles: []archiveFile{
|
||||
{src: "{{binary}}", dst: "deb/usr/bin/{{binary}}", perm: 0755},
|
||||
{src: "cmd/relaysrv/README.md", dst: "deb/usr/share/doc/relaysrv/README.txt", perm: 0644},
|
||||
{src: "cmd/relaysrv/LICENSE", dst: "deb/usr/share/doc/relaysrv/LICENSE.txt", perm: 0644},
|
||||
{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},
|
||||
},
|
||||
},
|
||||
"strelaypoolsrv": {
|
||||
name: "strelaypoolsrv",
|
||||
buildPkg: "./cmd/strelaypoolsrv",
|
||||
binaryName: "strelaypoolsrv", // .exe will be added automatically for Windows builds
|
||||
archiveFiles: []archiveFile{
|
||||
{src: "{{binary}}", dst: "{{binary}}", perm: 0755},
|
||||
{src: "cmd/strelaypoolsrv/README.md", dst: "README.txt", perm: 0644},
|
||||
{src: "cmd/strelaypoolsrv/LICENSE", dst: "LICENSE.txt", perm: 0644},
|
||||
{src: "AUTHORS", dst: "AUTHORS.txt", perm: 0644},
|
||||
},
|
||||
debianFiles: []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},
|
||||
{src: "AUTHORS", dst: "deb/usr/share/doc/relaysrv/AUTHORS.txt", perm: 0644},
|
||||
},
|
||||
},
|
||||
@@ -546,16 +563,17 @@ func listFiles(dir string) []string {
|
||||
|
||||
func rebuildAssets() {
|
||||
runPipe("lib/auto/gui.files.go", "go", "run", "script/genassets.go", "gui")
|
||||
runPipe("cmd/strelaypoolsrv/auto/gui.go", "go", "run", "script/genassets.go", "cmd/strelaypoolsrv/gui")
|
||||
}
|
||||
|
||||
func lazyRebuildAssets() {
|
||||
if shouldRebuildAssets() {
|
||||
if shouldRebuildAssets("lib/auto/gui.files.go", "gui") || shouldRebuildAssets("cmd/strelaypoolsrv/auto/gui.go", "cmd/strelaypoolsrv/auto/gui") {
|
||||
rebuildAssets()
|
||||
}
|
||||
}
|
||||
|
||||
func shouldRebuildAssets() bool {
|
||||
info, err := os.Stat("lib/auto/gui.files.go")
|
||||
func shouldRebuildAssets(target, srcdir string) bool {
|
||||
info, err := os.Stat(target)
|
||||
if err != nil {
|
||||
// If the file doesn't exist, we must rebuild it
|
||||
return true
|
||||
@@ -565,7 +583,7 @@ func shouldRebuildAssets() bool {
|
||||
// so we should rebuild it.
|
||||
currentBuild := info.ModTime()
|
||||
assetsAreNewer := false
|
||||
filepath.Walk("gui", func(path string, info os.FileInfo, err error) error {
|
||||
filepath.Walk(srcdir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -131,55 +131,6 @@ case "${1:-default}" in
|
||||
go2xunit -output tests.xml -fail < tests.out
|
||||
;;
|
||||
|
||||
docker-all)
|
||||
img=${DOCKERIMG:-syncthing/build:latest}
|
||||
docker run --rm -h syncthing-builder -u $(id -u) -t \
|
||||
-v $(pwd):/go/src/github.com/syncthing/syncthing \
|
||||
-w /go/src/github.com/syncthing/syncthing \
|
||||
-e "STTRACE=$STTRACE" \
|
||||
"$img" \
|
||||
sh -c './build.sh clean \
|
||||
&& ./build.sh test-cov \
|
||||
&& ./build.sh bench \
|
||||
&& ./build.sh all'
|
||||
;;
|
||||
|
||||
docker-test)
|
||||
img=${DOCKERIMG:-syncthing/build:latest}
|
||||
docker run --rm -h syncthing-builder -u $(id -u) -t \
|
||||
-v $(pwd):/go/src/github.com/syncthing/syncthing \
|
||||
-w /go/src/github.com/syncthing/syncthing \
|
||||
-e "STTRACE=$STTRACE" \
|
||||
"$img" \
|
||||
sh -euxc './build.sh clean \
|
||||
&& go run build.go -race \
|
||||
&& export GOPATH=$(pwd)/Godeps/_workspace:$GOPATH \
|
||||
&& cd test \
|
||||
&& go test -tags integration -v -timeout 90m -short \
|
||||
&& git clean -fxd .'
|
||||
;;
|
||||
|
||||
docker-lint)
|
||||
img=${DOCKERIMG:-syncthing/build:latest}
|
||||
docker run --rm -h syncthing-builder -u $(id -u) -t \
|
||||
-v $(pwd):/go/src/github.com/syncthing/syncthing \
|
||||
-w /go/src/github.com/syncthing/syncthing \
|
||||
-e "STTRACE=$STTRACE" \
|
||||
"$img" \
|
||||
sh -euxc 'go run build.go lint'
|
||||
;;
|
||||
|
||||
|
||||
docker-vet)
|
||||
img=${DOCKERIMG:-syncthing/build:latest}
|
||||
docker run --rm -h syncthing-builder -u $(id -u) -t \
|
||||
-v $(pwd):/go/src/github.com/syncthing/syncthing \
|
||||
-w /go/src/github.com/syncthing/syncthing \
|
||||
-e "STTRACE=$STTRACE" \
|
||||
"$img" \
|
||||
sh -euxc 'go run build.go vet'
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Unknown build command $1"
|
||||
;;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
discosrv
|
||||
========
|
||||
stdiscosrv
|
||||
==========
|
||||
|
||||
[](http://build.syncthing.net/job/discosrv/lastBuild/)
|
||||
[](http://build.syncthing.net/job/stdiscosrv/lastBuild/)
|
||||
|
||||
This is the global discovery server for the `syncthing` project.
|
||||
|
||||
To get it, run `go get github.com/syncthing/discosrv` or download the
|
||||
[latest build](http://build.syncthing.net/job/discosrv/lastSuccessfulBuild/artifact/)
|
||||
To get it, run `go get github.com/syncthing/stdiscosrv` or download the
|
||||
[latest build](http://build.syncthing.net/job/stdiscosrv/lastSuccessfulBuild/artifact/)
|
||||
from the build server.
|
||||
|
||||
Usage
|
||||
@@ -19,15 +19,15 @@ By default it will use in-memory `ql` backend. If you wish to persist the
|
||||
information on disk between restarts in `ql`, specify a file DSN:
|
||||
|
||||
```bash
|
||||
$ discosrv -db-dsn="file:///var/run/discosrv.db"
|
||||
$ stdiscosrv -db-dsn="file:///var/run/stdiscosrv.db"
|
||||
```
|
||||
|
||||
For `postgres`, you will need to create a database and a user with permissions
|
||||
to create tables in it, then start the discosrv as follows:
|
||||
to create tables in it, then start the stdiscosrv as follows:
|
||||
|
||||
```bash
|
||||
$ export DISCOSRV_DB_DSN="postgres://user:password@localhost/databasename"
|
||||
$ discosrv -db-backend="postgres"
|
||||
$ export STDISCOSRV_DB_DSN="postgres://user:password@localhost/databasename"
|
||||
$ stdiscosrv -db-backend="postgres"
|
||||
```
|
||||
|
||||
You can pass the DSN as command line option, but the value what you pass in will
|
||||
@@ -37,4 +37,4 @@ to other users.
|
||||
In all cases, the appropriate tables and indexes will be created at first
|
||||
startup. If it doesn't exit with an error, you're fine.
|
||||
|
||||
See `discosrv -help` for other options.
|
||||
See `stdiscosrv -help` for other options.
|
||||
@@ -38,7 +38,7 @@ func init() {
|
||||
BuildDate = time.Unix(int64(stamp), 0)
|
||||
|
||||
date := BuildDate.UTC().Format("2006-01-02 15:04:05 MST")
|
||||
LongVersion = fmt.Sprintf(`discosrv %s (%s %s-%s) %s@%s %s`, Version, runtime.Version(), runtime.GOOS, runtime.GOARCH, BuildUser, BuildHost, date)
|
||||
LongVersion = fmt.Sprintf(`stdiscosrv %s (%s %s-%s) %s@%s %s`, Version, runtime.Version(), runtime.GOOS, runtime.GOARCH, BuildUser, BuildHost, date)
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -48,7 +48,7 @@ var (
|
||||
globalStats stats
|
||||
statsFile string
|
||||
backend = "ql"
|
||||
dsn = getEnvDefault("DISCOSRV_DB_DSN", "memory://discosrv")
|
||||
dsn = getEnvDefault("STDISCOSRV_DB_DSN", "memory://stdiscosrv")
|
||||
certFile = "cert.pem"
|
||||
keyFile = "key.pem"
|
||||
debug = false
|
||||
@@ -28,7 +28,7 @@ func postgresSetup(db *sql.DB) error {
|
||||
}
|
||||
|
||||
row := db.QueryRow(`SELECT 'DevicesDeviceIDIndex'::regclass`)
|
||||
if err := row.Scan(nil); err != nil {
|
||||
if err = row.Scan(nil); err != nil {
|
||||
_, err = db.Exec(`CREATE INDEX DevicesDeviceIDIndex ON Devices (DeviceID)`)
|
||||
}
|
||||
if err != nil {
|
||||
@@ -36,7 +36,7 @@ func postgresSetup(db *sql.DB) error {
|
||||
}
|
||||
|
||||
row = db.QueryRow(`SELECT 'DevicesSeenIndex'::regclass`)
|
||||
if err := row.Scan(nil); err != nil {
|
||||
if err = row.Scan(nil); err != nil {
|
||||
_, err = db.Exec(`CREATE INDEX DevicesSeenIndex ON Devices (Seen)`)
|
||||
}
|
||||
if err != nil {
|
||||
@@ -53,7 +53,7 @@ func postgresSetup(db *sql.DB) error {
|
||||
}
|
||||
|
||||
row = db.QueryRow(`SELECT 'AddressesDeviceIDSeenIndex'::regclass`)
|
||||
if err := row.Scan(nil); err != nil {
|
||||
if err = row.Scan(nil); err != nil {
|
||||
_, err = db.Exec(`CREATE INDEX AddressesDeviceIDSeenIndex ON Addresses (DeviceID, Seen)`)
|
||||
}
|
||||
if err != nil {
|
||||
@@ -61,7 +61,7 @@ func postgresSetup(db *sql.DB) error {
|
||||
}
|
||||
|
||||
row = db.QueryRow(`SELECT 'AddressesDeviceIDAddressIndex'::regclass`)
|
||||
if err := row.Scan(nil); err != nil {
|
||||
if err = row.Scan(nil); err != nil {
|
||||
_, err = db.Exec(`CREATE INDEX AddressesDeviceIDAddressIndex ON Addresses (DeviceID, Address)`)
|
||||
}
|
||||
if err != nil {
|
||||
@@ -0,0 +1,15 @@
|
||||
# relaypoolsrv
|
||||
|
||||
[](http://build.syncthing.net/job/relaypoolsrv/lastBuild/)
|
||||
|
||||
This is the relay pool server for the `syncthing` project, which allows community hosted [relaysrv](https://github.com/syncthing/relaysrv)'s to join the public pool.
|
||||
|
||||
Servers that join the pool are then advertised to users of `syncthing` as potential connection points for those who are unable to connect directly due to NAT or firewall issues.
|
||||
|
||||
There is very little reason why you'd want to run this yourself, as `relaypoolsrv` is just used for announcement and lookup of public relay servers. If you are looking to setup a private or a public relay, please check the documentation for [relaysrv](https://github.com/syncthing/relaysrv), which also explains how to join the default public pool.
|
||||
|
||||
If you still want to run it, you can run `go get github.com/syncthing/relaypoolsrv` download it or download the
|
||||
[latest build](http://build.syncthing.net/job/relaypoolsrv/lastSuccessfulBuild/artifact/)
|
||||
from the build server.
|
||||
|
||||
See `relaypoolsrv -help` for configuration options.
|
||||
@@ -0,0 +1 @@
|
||||
gui.go
|
||||
@@ -0,0 +1,395 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en" ng-app="syncthing" ng-controller="relayDataController">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="">
|
||||
<meta name="author" content="">
|
||||
|
||||
<title>Relay stats</title>
|
||||
<link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.6.1/css/font-awesome.min.css">
|
||||
|
||||
<style>
|
||||
#map {
|
||||
height: 600px;
|
||||
}
|
||||
.ng-cloak {
|
||||
display: none;
|
||||
}
|
||||
table {
|
||||
font-size: 11px !important;
|
||||
width: 100%;
|
||||
border: 1px;
|
||||
|
||||
}
|
||||
td {
|
||||
padding: 0px !important;
|
||||
}
|
||||
tfoot td {
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="ng-cloak">
|
||||
<div class="container">
|
||||
<h1>Relay Pool Data</h2>
|
||||
<div ng-if="relays === undefined" class="text-center">
|
||||
<img src="//cdnjs.cloudflare.com/ajax/libs/galleriffic/2.0.1/css/loader.gif"/>
|
||||
<p>Please wait while we gather data</p>
|
||||
</div>
|
||||
<div>
|
||||
<div ng-show="relays !== undefined" class="ng-hide">
|
||||
<p>
|
||||
Currently {{ relays.length }} relays online ({{ totals.goMaxProcs }} cores in total).
|
||||
</p>
|
||||
</div>
|
||||
<div id="map"></div> <!-- Can't hide the map, otherwise it freaks out -->
|
||||
<p>The circle size represents how much bytes the relay transfered relative to other relays</p>
|
||||
</div>
|
||||
<div>
|
||||
<table class="table table-striped table-condensed table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th rowspan="2">Address</td>
|
||||
<th rowspan="2">
|
||||
<a ng-click="sortType = 'status.numActiveSessions || -1'; sortReverse = !sortReverse">
|
||||
Sessions
|
||||
<span ng-show="sortType == 'status.numActiveSessions || -1' && !sortReverse" class="fa fa-caret-down"></span>
|
||||
<span ng-show="sortType == 'status.numActiveSessions || -1' && sortReverse" class="fa fa-caret-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th rowspan="2">
|
||||
<a ng-click="sortType = 'status.numConnections || -1'; sortReverse = !sortReverse">
|
||||
Connections
|
||||
<span ng-show="sortType == 'status.numConnections || -1' && !sortReverse" class="fa fa-caret-down"></span>
|
||||
<span ng-show="sortType == 'status.numConnections || -1' && sortReverse" class="fa fa-caret-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th rowspan="2">
|
||||
<a ng-click="sortType = 'status.bytesProxied || -1'; sortReverse = !sortReverse">
|
||||
Data relayed
|
||||
<span ng-show="sortType == 'status.bytesProxied || -1' && !sortReverse" class="fa fa-caret-down"></span>
|
||||
<span ng-show="sortType == 'status.bytesProxied || -1' && sortReverse" class="fa fa-caret-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th colspan="6" class="text-center">Transfer rate in the last period</th>
|
||||
<th rowspan="2">
|
||||
<a ng-click="sortType = 'status.uptimeSeconds || -1'; sortReverse = !sortReverse">
|
||||
Uptime hours
|
||||
<span ng-show="sortType == 'status.uptimeSeconds || -1' && !sortReverse" class="fa fa-caret-down"></span>
|
||||
<span ng-show="sortType == 'status.uptimeSeconds || -1' && sortReverse" class="fa fa-caret-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th rowspan="2">
|
||||
<a ng-click="sortType = 'status.options[\'provided-by\'] || \'\''; sortReverse = !sortReverse">
|
||||
Provided by
|
||||
<span ng-show="sortType == 'status.options[\'provided-by\'] || \'\'' && !sortReverse" class="fa fa-caret-down"></span>
|
||||
<span ng-show="sortType == 'status.options[\'provided-by\'] || \'\'' && sortReverse" class="fa fa-caret-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
<a ng-click="sortType = 'status.kbps10s1m5m15m30m60m[0] || -1'; sortReverse = !sortReverse">
|
||||
10s
|
||||
<span ng-show="sortType == 'status.kbps10s1m5m15m30m60m[0] || -1' && !sortReverse" class="fa fa-caret-down"></span>
|
||||
<span ng-show="sortType == 'status.kbps10s1m5m15m30m60m[0] || -1' && sortReverse" class="fa fa-caret-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="sortType = 'status.kbps10s1m5m15m30m60m[1] || -1'; sortReverse = !sortReverse">
|
||||
1m
|
||||
<span ng-show="sortType == 'status.kbps10s1m5m15m30m60m[1] || -1' && !sortReverse" class="fa fa-caret-down"></span>
|
||||
<span ng-show="sortType == 'status.kbps10s1m5m15m30m60m[1] || -1' && sortReverse" class="fa fa-caret-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="sortType = 'status.kbps10s1m5m15m30m60m[2] || -1'; sortReverse = !sortReverse">
|
||||
5m
|
||||
<span ng-show="sortType == 'status.kbps10s1m5m15m30m60m[2] || -1' && !sortReverse" class="fa fa-caret-down"></span>
|
||||
<span ng-show="sortType == 'status.kbps10s1m5m15m30m60m[2] || -1' && sortReverse" class="fa fa-caret-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="sortType = 'status.kbps10s1m5m15m30m60m[3] || -1'; sortReverse = !sortReverse">
|
||||
15m
|
||||
<span ng-show="sortType == 'status.kbps10s1m5m15m30m60m[3] || -1' && !sortReverse" class="fa fa-caret-down"></span>
|
||||
<span ng-show="sortType == 'status.kbps10s1m5m15m30m60m[3] || -1' && sortReverse" class="fa fa-caret-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="sortType = 'status.kbps10s1m5m15m30m60m[4] || -1'; sortReverse = !sortReverse">
|
||||
30m
|
||||
<span ng-show="sortType == 'status.kbps10s1m5m15m30m60m[4] || -1' && !sortReverse" class="fa fa-caret-down"></span>
|
||||
<span ng-show="sortType == 'status.kbps10s1m5m15m30m60m[4] || -1' && sortReverse" class="fa fa-caret-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="sortType = 'status.kbps10s1m5m15m30m60m[5] || -1'; sortReverse = !sortReverse">
|
||||
60m
|
||||
<span ng-show="sortType == 'status.kbps10s1m5m15m30m60m[5] || -1' && !sortReverse" class="fa fa-caret-down"></span>
|
||||
<span ng-show="sortType == 'status.kbps10s1m5m15m30m60m[5] || -1' && sortReverse" class="fa fa-caret-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="relay in relays | orderBy:sortType:sortReverse ">
|
||||
<td>{{ relay.address }}</td>
|
||||
<td ng-if="relay.status === undefined" colspan="11" class="text-center">Looking up...</td>
|
||||
<td ng-if-start="relay.status !== undefined">{{ relay.status.numActiveSessions }}</td>
|
||||
<td>{{ relay.status.numConnections }}</td>
|
||||
<td>{{ relay.status.bytesProxied | bytes }}</td>
|
||||
<td>{{ relay.status.kbps10s1m5m15m30m60m[0] * 128 | bytes }}/s</td>
|
||||
<td>{{ relay.status.kbps10s1m5m15m30m60m[1] * 128 | bytes }}/s</td>
|
||||
<td>{{ relay.status.kbps10s1m5m15m30m60m[2] * 128 | bytes }}/s</td>
|
||||
<td>{{ relay.status.kbps10s1m5m15m30m60m[3] * 128 | bytes }}/s</td>
|
||||
<td>{{ relay.status.kbps10s1m5m15m30m60m[4] * 128 | bytes }}/s</td>
|
||||
<td>{{ relay.status.kbps10s1m5m15m30m60m[5] * 128 | bytes }}/s</td>
|
||||
<td ng-if="relay.status.uptimeSeconds != undefined">{{ relay.status.uptimeSeconds/60/60 | number:0 }}</td>
|
||||
<td ng-if="relay.status.uptimeSeconds == undefined"></td>
|
||||
<td title="{{ relay.status.options['provided-by'] || '' }}" ng-if-end>
|
||||
{{ relay.status.options['provided-by'] || '' | limitTo:50 }}
|
||||
<span ng-if="(relay.status.options['provided-by'] || '').length > 50">…
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td>Totals</td>
|
||||
<td>{{ totals.numActiveSessions }}</td>
|
||||
<td>{{ totals.numConnections }}</td>
|
||||
<td>{{ totals.bytesProxied | bytes }}</td>
|
||||
<td>{{ totals.kbps10s1m5m15m30m60m[0] * 128 | bytes }}/s</td>
|
||||
<td>{{ totals.kbps10s1m5m15m30m60m[1] * 128 | bytes }}/s</td>
|
||||
<td>{{ totals.kbps10s1m5m15m30m60m[2] * 128 | bytes }}/s</td>
|
||||
<td>{{ totals.kbps10s1m5m15m30m60m[3] * 128 | bytes }}/s</td>
|
||||
<td>{{ totals.kbps10s1m5m15m30m60m[4] * 128 | bytes }}/s</td>
|
||||
<td>{{ totals.kbps10s1m5m15m30m60m[5] * 128 | bytes }}/s</td>
|
||||
<td>{{ totals.uptimeSeconds/60/60 | number:0 }} hours</td>
|
||||
<td>{{ relays.length }} relays</td>
|
||||
</tr>
|
||||
</tfoor>
|
||||
</table>
|
||||
</div>
|
||||
<hr>
|
||||
<p>
|
||||
This product includes GeoLite2 data created by MaxMind, available from
|
||||
<a href="http://www.maxmind.com">http://www.maxmind.com</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<script src="//code.jquery.com/jquery-2.1.4.min.js"></script>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.7/angular.min.js"></script>
|
||||
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
|
||||
<script src="//maps.googleapis.com/maps/api/js"></script>
|
||||
</body>
|
||||
|
||||
<script>
|
||||
angular.module('syncthing', [
|
||||
])
|
||||
.config(function($httpProvider) {
|
||||
$httpProvider.defaults.timeout = 5000;
|
||||
})
|
||||
.filter('bytes', function() {
|
||||
return function(bytes, precision) {
|
||||
if (isNaN(parseFloat(bytes)) || !isFinite(bytes)) return '-';
|
||||
if (typeof precision === 'undefined') precision = 1;
|
||||
|
||||
var units = ['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'],
|
||||
number = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
|
||||
var value = (bytes / Math.pow(1000, Math.floor(number)));
|
||||
if (!isFinite(value)) {
|
||||
value = 0;
|
||||
precision = 0;
|
||||
}
|
||||
if (!isFinite(number)) {
|
||||
units = 'bytes';
|
||||
} else {
|
||||
units = units[number];
|
||||
}
|
||||
return value.toFixed(precision) + ' ' + units;
|
||||
}
|
||||
})
|
||||
.controller('relayDataController', ['$scope', '$rootScope', '$http', '$q', '$compile', '$timeout', function($scope, $rootScope, $http, $q, $compile, $timeout) {
|
||||
$scope.totals = {
|
||||
bytesProxied: 0,
|
||||
goMaxProcs: 0,
|
||||
kbps10s1m5m15m30m60m: [0, 0, 0, 0, 0, 0],
|
||||
numActiveSessions: 0,
|
||||
numConnections: 0,
|
||||
numPendingSessionKeys: 0,
|
||||
numProxies: 0,
|
||||
uptimeSeconds: 0,
|
||||
};
|
||||
$scope.map = new google.maps.Map(document.getElementById('map'), {
|
||||
zoom: 1,
|
||||
mapTypeId: google.maps.MapTypeId.ROADMAP
|
||||
});
|
||||
$scope.mapBounds = new google.maps.LatLngBounds();
|
||||
$scope.tooltipTemplate = $('#infoTemplate').html();
|
||||
$scope.usedLocations = {};
|
||||
$scope.sortType = 'status.numActiveSessions || -1';
|
||||
$scope.sortReverse = true;
|
||||
|
||||
$http.get("/endpoint").then(function(response) {
|
||||
$scope.relays = response.data.relays;
|
||||
var promises = [];
|
||||
angular.forEach($scope.relays, function(relay) {
|
||||
|
||||
relay.uri = constructURI(relay.url);
|
||||
relay.address = relay.url.split('/')[2];
|
||||
|
||||
addMarkerToMap(relay);
|
||||
|
||||
promises.push(getRelayStatus(relay));
|
||||
});
|
||||
|
||||
// Can only add circles once we know the totals for transfers, which means
|
||||
// we need to resolve all statuses.
|
||||
$q.all(promises).then(function() {
|
||||
angular.forEach($scope.relays, function(relay) {
|
||||
if (relay.status) {
|
||||
addCircleToMap(relay);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$scope.map.fitBounds($scope.mapBounds);
|
||||
if ($scope.relays.length == 1) {
|
||||
$scope.map.setZoom(13);
|
||||
}
|
||||
});
|
||||
|
||||
function addMarkerToMap(relay) {
|
||||
var loc = relay.location.latitude + "," + relay.location.longitude;
|
||||
|
||||
// Deal with overlapping markers
|
||||
while (loc in $scope.usedLocations) {
|
||||
var locParts = loc.split(',');
|
||||
locParts = [parseFloat(locParts[0]), parseFloat(locParts[1])];
|
||||
locParts[Math.round(Math.random())] += 0.5 * (Math.random() >= 0.5 ? 1 : -1);
|
||||
loc = locParts.join(',');
|
||||
}
|
||||
|
||||
$scope.usedLocations[loc] = true;
|
||||
|
||||
var locParts = loc.split(',');
|
||||
|
||||
relay.marker = new google.maps.Marker({
|
||||
map: $scope.map,
|
||||
position: new google.maps.LatLng(locParts[0], locParts[1]),
|
||||
title: relay.url,
|
||||
});
|
||||
|
||||
var scope = $rootScope.$new(true);
|
||||
scope.relay = relay;
|
||||
|
||||
relay.marker.info = new google.maps.InfoWindow({
|
||||
content: $compile($scope.tooltipTemplate)(scope)[0],
|
||||
});
|
||||
|
||||
relay.marker.addListener('mouseover', function() {
|
||||
relay.marker.info.open($scope.map, relay.marker);
|
||||
});
|
||||
|
||||
relay.marker.addListener('mouseout', function() {
|
||||
relay.marker.info.close();
|
||||
});
|
||||
|
||||
$scope.mapBounds.extend(relay.marker.position);
|
||||
}
|
||||
|
||||
function addCircleToMap(relay) {
|
||||
relay.marker.circle = new google.maps.Circle({
|
||||
strokeColor: '#FF0000',
|
||||
strokeOpacity: 0.8,
|
||||
strokeWeight: 2,
|
||||
fillColor: '#FF0000',
|
||||
fillOpacity: 0.35,
|
||||
map: $scope.map,
|
||||
center: relay.marker.position,
|
||||
radius: ((relay.status.bytesProxied * 100) / $scope.totals.bytesProxied) * 10000
|
||||
});
|
||||
}
|
||||
|
||||
function getRelayStatus(relay) {
|
||||
// Normal timeout doesn't deal with relays which accept the TCP connection
|
||||
// but don't respond (some firewalls do that), so deal with it this way.
|
||||
var timeoutRequest = $q.defer();
|
||||
var resolveStatus = $q.defer();
|
||||
|
||||
$http.get("http://" + relay.uri.hostname + (relay.uri.args.statusAddr || ":22070") + "/status", { timeout: timeoutRequest.promise }).then(function (response) {
|
||||
relay.status = response.data;
|
||||
resolveStatus.resolve();
|
||||
angular.forEach($scope.totals, function(value, key) {
|
||||
if (typeof $scope.totals[key] == 'number') {
|
||||
$scope.totals[key] += response.data[key];
|
||||
} else if (typeof $scope.totals[key] == 'object' && $scope.totals[key] instanceof Array) {
|
||||
angular.forEach($scope.totals[key], function(value, index) {
|
||||
$scope.totals[key][index] += response.data[key][index];
|
||||
});
|
||||
}
|
||||
});
|
||||
}, function() {
|
||||
relay.status = null;
|
||||
resolveStatus.resolve();
|
||||
});
|
||||
|
||||
$timeout(function() {
|
||||
timeoutRequest.resolve();
|
||||
}, 5000);
|
||||
|
||||
return resolveStatus.promise;
|
||||
}
|
||||
|
||||
function constructURI(url) {
|
||||
var uri = document.createElement('a');
|
||||
|
||||
// HAX, otherwise doesn't work
|
||||
uri.href = url.replace('relay://', 'http://');
|
||||
|
||||
// Convert query string to object
|
||||
uri.args = {};
|
||||
angular.forEach(uri.search.replace(/^\?/, '').split('&'), function(query) {
|
||||
var split = query.split('=');
|
||||
uri.args[split[0]] = split[1];
|
||||
});
|
||||
|
||||
return uri;
|
||||
}
|
||||
}]);
|
||||
</script>
|
||||
|
||||
<script type="text/template" id="infoTemplate">
|
||||
<div>
|
||||
<p><b>{{ relay.uri.hostname }}</b> <span ng-if="relay.status.options['provided-by']">provided by <u>{{ relay.status.options['provided-by'] }}</u></span></p>
|
||||
<div ng-if="relay.status">
|
||||
<span ng-if="relay.status.startTime">Start time: {{ relay.status.startTime | date:"medium" }}</br></span>
|
||||
<span ng-if="relay.status.bytesProxied != undefined">Proxied: {{ relay.status.bytesProxied | bytes }}</br></span>
|
||||
<span ng-if="relay.status.numActiveSessions != undefined">Sessions: {{ relay.status.numActiveSessions }}</br></span>
|
||||
<span ng-if="relay.status.numConnections != undefined">Clients: {{ relay.status.numConnections }}</br></span>
|
||||
<span ng-if="relay.status.options.pools">Pools: {{ relay.status.options.pools.join(', ') }}</br></span>
|
||||
<span ng-if="relay.status.options['global-rate'] != undefined">
|
||||
<span ng-if="relay.status.options['global-rate'] > 0">Global rate limit: {{ relay.status.options['global-rate'] | bytes }}/s</span>
|
||||
<span ng-if="relay.status.options['global-rate'] == 0">Global rate limit: unlimited</span>
|
||||
</br>
|
||||
</span>
|
||||
<span ng-if="relay.status.options['per-session-rate'] != undefined">
|
||||
<span ng-if="relay.status.options['per-session-rate'] > 0">Session rate limit: {{ relay.status.options['per-session-rate'] | bytes }}/s</span>
|
||||
<span ng-if="relay.status.options['per-session-rate'] == 0">Session rate limit: unlimited</span>
|
||||
</br>
|
||||
</span>
|
||||
</div>
|
||||
<div ng-if="!relay.status">
|
||||
Data unavailable.
|
||||
<div>
|
||||
</div>
|
||||
</script>
|
||||
</html>
|
||||
@@ -0,0 +1,536 @@
|
||||
// Copyright (C) 2015 Audrius Butkevicius and Contributors (see the CONTRIBUTORS file).
|
||||
|
||||
//go:generate go run genassets.go gui auto/gui.go
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math/rand"
|
||||
"mime"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang/groupcache/lru"
|
||||
"github.com/juju/ratelimit"
|
||||
|
||||
"github.com/oschwald/geoip2-golang"
|
||||
|
||||
"github.com/syncthing/syncthing/cmd/strelaypoolsrv/auto"
|
||||
"github.com/syncthing/syncthing/lib/relay/client"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
"github.com/syncthing/syncthing/lib/tlsutil"
|
||||
)
|
||||
|
||||
type location struct {
|
||||
Latitude float64 `json:"latitude"`
|
||||
Longitude float64 `json:"longitude"`
|
||||
}
|
||||
|
||||
type relay struct {
|
||||
URL string `json:"url"`
|
||||
Location location `json:"location"`
|
||||
uri *url.URL
|
||||
}
|
||||
|
||||
func (r relay) String() string {
|
||||
return r.URL
|
||||
}
|
||||
|
||||
type request struct {
|
||||
relay relay
|
||||
uri *url.URL
|
||||
result chan result
|
||||
}
|
||||
|
||||
type result struct {
|
||||
err error
|
||||
eviction time.Duration
|
||||
}
|
||||
|
||||
var (
|
||||
testCert tls.Certificate
|
||||
listen = ":80"
|
||||
dir string
|
||||
evictionTime = time.Hour
|
||||
debug bool
|
||||
getLRUSize = 10 << 10
|
||||
getLimitBurst int64 = 10
|
||||
getLimitAvg = 1
|
||||
postLRUSize = 1 << 10
|
||||
postLimitBurst int64 = 2
|
||||
postLimitAvg = 1
|
||||
getLimit time.Duration
|
||||
postLimit time.Duration
|
||||
permRelaysFile string
|
||||
ipHeader string
|
||||
geoipPath string
|
||||
|
||||
getMut = sync.NewRWMutex()
|
||||
getLRUCache *lru.Cache
|
||||
|
||||
postMut = sync.NewRWMutex()
|
||||
postLRUCache *lru.Cache
|
||||
|
||||
requests = make(chan request, 10)
|
||||
|
||||
mut = sync.NewRWMutex()
|
||||
knownRelays = make([]relay, 0)
|
||||
permanentRelays = make([]relay, 0)
|
||||
evictionTimers = make(map[string]*time.Timer)
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.StringVar(&listen, "listen", listen, "Listen address")
|
||||
flag.StringVar(&dir, "keys", dir, "Directory where http-cert.pem and http-key.pem is stored for TLS listening")
|
||||
flag.BoolVar(&debug, "debug", debug, "Enable debug output")
|
||||
flag.DurationVar(&evictionTime, "eviction", evictionTime, "After how long the relay is evicted")
|
||||
flag.IntVar(&getLRUSize, "get-limit-cache", getLRUSize, "Get request limiter cache size")
|
||||
flag.IntVar(&getLimitAvg, "get-limit-avg", 2, "Allowed average get request rate, per 10 s")
|
||||
flag.Int64Var(&getLimitBurst, "get-limit-burst", getLimitBurst, "Allowed burst get requests")
|
||||
flag.IntVar(&postLRUSize, "post-limit-cache", postLRUSize, "Post request limiter cache size")
|
||||
flag.IntVar(&postLimitAvg, "post-limit-avg", 2, "Allowed average post request rate, per minute")
|
||||
flag.Int64Var(&postLimitBurst, "post-limit-burst", postLimitBurst, "Allowed burst post requests")
|
||||
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.Parse()
|
||||
|
||||
getLimit = 10 * time.Second / time.Duration(getLimitAvg)
|
||||
postLimit = time.Minute / time.Duration(postLimitAvg)
|
||||
|
||||
getLRUCache = lru.New(getLRUSize)
|
||||
postLRUCache = lru.New(postLRUSize)
|
||||
|
||||
var listener net.Listener
|
||||
var err error
|
||||
|
||||
if permRelaysFile != "" {
|
||||
loadPermanentRelays(permRelaysFile)
|
||||
}
|
||||
|
||||
testCert = createTestCertificate()
|
||||
|
||||
go requestProcessor()
|
||||
|
||||
if dir != "" {
|
||||
if debug {
|
||||
log.Println("Starting TLS listener on", listen)
|
||||
}
|
||||
certFile, keyFile := filepath.Join(dir, "http-cert.pem"), filepath.Join(dir, "http-key.pem")
|
||||
var cert tls.Certificate
|
||||
cert, err = tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to load HTTP X509 key pair:", err)
|
||||
}
|
||||
|
||||
tlsCfg := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
MinVersion: tls.VersionTLS10, // No SSLv3
|
||||
CipherSuites: []uint16{
|
||||
// No RC4
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
|
||||
tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
|
||||
},
|
||||
}
|
||||
|
||||
listener, err = tls.Listen("tcp", listen, tlsCfg)
|
||||
} else {
|
||||
if debug {
|
||||
log.Println("Starting plain listener on", listen)
|
||||
}
|
||||
listener, err = net.Listen("tcp", listen)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Fatalln("listen:", err)
|
||||
}
|
||||
|
||||
handler := http.NewServeMux()
|
||||
handler.HandleFunc("/", handleAssets)
|
||||
handler.HandleFunc("/endpoint", handleRequest)
|
||||
|
||||
srv := http.Server{
|
||||
Handler: handler,
|
||||
ReadTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
err = srv.Serve(listener)
|
||||
if err != nil {
|
||||
log.Fatalln("serve:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func handleAssets(w http.ResponseWriter, r *http.Request) {
|
||||
assets := auto.Assets()
|
||||
path := r.URL.Path[1:]
|
||||
if path == "" {
|
||||
path = "index.html"
|
||||
}
|
||||
|
||||
bs, ok := assets[path]
|
||||
if !ok {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
mtype := mimeTypeForFile(path)
|
||||
if len(mtype) != 0 {
|
||||
w.Header().Set("Content-Type", mtype)
|
||||
}
|
||||
|
||||
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||
w.Header().Set("Content-Encoding", "gzip")
|
||||
} else {
|
||||
// ungzip if browser not send gzip accepted header
|
||||
var gr *gzip.Reader
|
||||
gr, _ = gzip.NewReader(bytes.NewReader(bs))
|
||||
bs, _ = ioutil.ReadAll(gr)
|
||||
gr.Close()
|
||||
}
|
||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(bs)))
|
||||
|
||||
w.Write(bs)
|
||||
}
|
||||
|
||||
func mimeTypeForFile(file string) string {
|
||||
// We use a built in table of the common types since the system
|
||||
// TypeByExtension might be unreliable. But if we don't know, we delegate
|
||||
// to the system.
|
||||
ext := filepath.Ext(file)
|
||||
switch ext {
|
||||
case ".htm", ".html":
|
||||
return "text/html"
|
||||
case ".css":
|
||||
return "text/css"
|
||||
case ".js":
|
||||
return "application/javascript"
|
||||
case ".json":
|
||||
return "application/json"
|
||||
case ".png":
|
||||
return "image/png"
|
||||
case ".ttf":
|
||||
return "application/x-font-ttf"
|
||||
case ".woff":
|
||||
return "application/x-font-woff"
|
||||
case ".svg":
|
||||
return "image/svg+xml"
|
||||
default:
|
||||
return mime.TypeByExtension(ext)
|
||||
}
|
||||
}
|
||||
|
||||
func handleRequest(w http.ResponseWriter, r *http.Request) {
|
||||
if ipHeader != "" {
|
||||
r.RemoteAddr = r.Header.Get(ipHeader)
|
||||
}
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
if limit(r.RemoteAddr, getLRUCache, getMut, getLimit, int64(getLimitBurst)) {
|
||||
w.WriteHeader(429)
|
||||
return
|
||||
}
|
||||
handleGetRequest(w, r)
|
||||
case "POST":
|
||||
if limit(r.RemoteAddr, postLRUCache, postMut, postLimit, int64(postLimitBurst)) {
|
||||
w.WriteHeader(429)
|
||||
return
|
||||
}
|
||||
handlePostRequest(w, r)
|
||||
default:
|
||||
if debug {
|
||||
log.Println("Unhandled HTTP method", r.Method)
|
||||
}
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetRequest(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
mut.RLock()
|
||||
relays := append(permanentRelays, knownRelays...)
|
||||
mut.RUnlock()
|
||||
|
||||
// Shuffle
|
||||
for i := range relays {
|
||||
j := rand.Intn(i + 1)
|
||||
relays[i], relays[j] = relays[j], relays[i]
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string][]relay{
|
||||
"relays": relays,
|
||||
})
|
||||
}
|
||||
|
||||
func handlePostRequest(w http.ResponseWriter, r *http.Request) {
|
||||
var newRelay relay
|
||||
err := json.NewDecoder(r.Body).Decode(&newRelay)
|
||||
r.Body.Close()
|
||||
|
||||
if err != nil {
|
||||
if debug {
|
||||
log.Println("Failed to parse payload")
|
||||
}
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
uri, err := url.Parse(newRelay.URL)
|
||||
if err != nil {
|
||||
if debug {
|
||||
log.Println("Failed to parse URI", newRelay.URL)
|
||||
}
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
host, port, err := net.SplitHostPort(uri.Host)
|
||||
if err != nil {
|
||||
if debug {
|
||||
log.Println("Failed to split URI", newRelay.URL)
|
||||
}
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the IP address of the client
|
||||
rhost, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
if debug {
|
||||
log.Println("Failed to split remote address", r.RemoteAddr)
|
||||
}
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
// The client did not provide an IP address, use the IP address of the client.
|
||||
if host == "" {
|
||||
uri.Host = net.JoinHostPort(rhost, port)
|
||||
newRelay.URL = uri.String()
|
||||
} else if host != rhost {
|
||||
if debug {
|
||||
log.Println("IP address advertised does not match client IP address", r.RemoteAddr, uri)
|
||||
}
|
||||
http.Error(w, "IP address does not match client IP", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
newRelay.uri = uri
|
||||
newRelay.Location = getLocation(uri.Host)
|
||||
|
||||
for _, current := range permanentRelays {
|
||||
if current.uri.Host == newRelay.uri.Host {
|
||||
if debug {
|
||||
log.Println("Asked to add a relay", newRelay, "which exists in permanent list")
|
||||
}
|
||||
http.Error(w, "Invalid request", 500)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
reschan := make(chan result)
|
||||
|
||||
select {
|
||||
case requests <- request{newRelay, uri, reschan}:
|
||||
result := <-reschan
|
||||
if result.err != nil {
|
||||
http.Error(w, result.err.Error(), 500)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(map[string]time.Duration{
|
||||
"evictionIn": result.eviction,
|
||||
})
|
||||
|
||||
default:
|
||||
if debug {
|
||||
log.Println("Dropping request")
|
||||
}
|
||||
w.WriteHeader(429)
|
||||
}
|
||||
}
|
||||
|
||||
func requestProcessor() {
|
||||
for request := range requests {
|
||||
if debug {
|
||||
log.Println("Request for", request.relay)
|
||||
}
|
||||
if !client.TestRelay(request.uri, []tls.Certificate{testCert}, time.Second, 2*time.Second, 3) {
|
||||
if debug {
|
||||
log.Println("Test for relay", request.relay, "failed")
|
||||
}
|
||||
request.result <- result{fmt.Errorf("test failed"), 0}
|
||||
continue
|
||||
}
|
||||
|
||||
mut.Lock()
|
||||
timer, ok := evictionTimers[request.relay.uri.Host]
|
||||
if ok {
|
||||
if debug {
|
||||
log.Println("Stopping existing timer for", request.relay)
|
||||
}
|
||||
timer.Stop()
|
||||
}
|
||||
|
||||
for i, current := range knownRelays {
|
||||
if current.uri.Host == request.relay.uri.Host {
|
||||
if debug {
|
||||
log.Println("Relay", request.relay, "already exists")
|
||||
}
|
||||
|
||||
// Evict the old entry anyway, as configuration might have changed.
|
||||
last := len(knownRelays) - 1
|
||||
knownRelays[i] = knownRelays[last]
|
||||
knownRelays = knownRelays[:last]
|
||||
|
||||
goto found
|
||||
}
|
||||
}
|
||||
|
||||
if debug {
|
||||
log.Println("Adding new relay", request.relay)
|
||||
}
|
||||
|
||||
found:
|
||||
|
||||
knownRelays = append(knownRelays, request.relay)
|
||||
|
||||
evictionTimers[request.relay.uri.Host] = time.AfterFunc(evictionTime, evict(request.relay))
|
||||
mut.Unlock()
|
||||
request.result <- result{nil, evictionTime}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func evict(relay relay) func() {
|
||||
return func() {
|
||||
mut.Lock()
|
||||
defer mut.Unlock()
|
||||
if debug {
|
||||
log.Println("Evicting", relay)
|
||||
}
|
||||
for i, current := range knownRelays {
|
||||
if current.uri.Host == relay.uri.Host {
|
||||
if debug {
|
||||
log.Println("Evicted", relay)
|
||||
}
|
||||
last := len(knownRelays) - 1
|
||||
knownRelays[i] = knownRelays[last]
|
||||
knownRelays = knownRelays[:last]
|
||||
}
|
||||
}
|
||||
delete(evictionTimers, relay.uri.Host)
|
||||
}
|
||||
}
|
||||
|
||||
func limit(addr string, cache *lru.Cache, lock sync.RWMutex, rate time.Duration, burst int64) bool {
|
||||
host, _, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
lock.RLock()
|
||||
bkt, ok := cache.Get(host)
|
||||
lock.RUnlock()
|
||||
if ok {
|
||||
bkt := bkt.(*ratelimit.Bucket)
|
||||
if bkt.TakeAvailable(1) != 1 {
|
||||
// Rate limit
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
lock.Lock()
|
||||
cache.Add(host, ratelimit.NewBucket(rate, burst))
|
||||
lock.Unlock()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func loadPermanentRelays(file string) {
|
||||
content, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
for _, line := range strings.Split(string(content), "\n") {
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
uri, err := url.Parse(line)
|
||||
if err != nil {
|
||||
if debug {
|
||||
log.Println("Skipping permanent relay", line, "due to parse error", err)
|
||||
}
|
||||
continue
|
||||
|
||||
}
|
||||
|
||||
permanentRelays = append(permanentRelays, relay{
|
||||
URL: line,
|
||||
Location: getLocation(uri.Host),
|
||||
uri: uri,
|
||||
})
|
||||
if debug {
|
||||
log.Println("Adding permanent relay", line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func createTestCertificate() tls.Certificate {
|
||||
tmpDir, err := ioutil.TempDir("", "relaypoolsrv")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
certFile, keyFile := filepath.Join(tmpDir, "cert.pem"), filepath.Join(tmpDir, "key.pem")
|
||||
cert, err := tlsutil.NewCertificate(certFile, keyFile, "relaypoolsrv", 3072)
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to create test X509 key pair:", err)
|
||||
}
|
||||
|
||||
return cert
|
||||
}
|
||||
|
||||
func getLocation(host string) location {
|
||||
db, err := geoip2.Open(geoipPath)
|
||||
if err != nil {
|
||||
return location{}
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
addr, err := net.ResolveTCPAddr("tcp", host)
|
||||
if err != nil {
|
||||
return location{}
|
||||
}
|
||||
|
||||
city, err := db.City(addr.IP)
|
||||
if err != nil {
|
||||
return location{}
|
||||
}
|
||||
|
||||
return location{
|
||||
Latitude: city.Location.Latitude,
|
||||
Longitude: city.Location.Longitude,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 The Syncthing Project
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
relaysrv
|
||||
========
|
||||
strelaysrv
|
||||
==========
|
||||
|
||||
[](http://build.syncthing.net/job/relaysrv/lastBuild/)
|
||||
[](http://build.syncthing.net/job/strelaysrv/lastBuild/)
|
||||
|
||||
This is the relay server for the `syncthing` project.
|
||||
|
||||
To get it, run `go get github.com/syncthing/relaysrv` or download the
|
||||
[latest build](http://build.syncthing.net/job/relaysrv/lastSuccessfulBuild/artifact/)
|
||||
To get it, run `go get github.com/syncthing/strelaysrv` or download the
|
||||
[latest build](http://build.syncthing.net/job/strelaysrv/lastSuccessfulBuild/artifact/)
|
||||
from the build server.
|
||||
|
||||
:exclamation:Warnings:exclamation: - Read or regret
|
||||
@@ -16,13 +16,13 @@ By default, all relay servers will join the default public relay pool, which mea
|
||||
|
||||
If you wish to disable this behaviour, please specify `-pools=""` argument.
|
||||
|
||||
Please note that `relaysrv` is only usable by `syncthing` **version v0.12 and onwards**.
|
||||
Please note that `strelaysrv` is only usable by `syncthing` **version v0.12 and onwards**.
|
||||
|
||||
To run `relaysrv` you need to have port 22067 available to the internet, which means you might need to allow it through your firewall if you **have a public IP, or setup a port-forwarding** (22067 to 22067) if you are behind a router.
|
||||
To run `strelaysrv` you need to have port 22067 available to the internet, which means you might need to allow it through your firewall if you **have a public IP, or setup a port-forwarding** (22067 to 22067) if you are behind a router.
|
||||
|
||||
Furthermore, **by default relaysrv will also expose a /status HTTP endpoint on port 22070**, which is used by the pool servers to peek at metrics of the relaysrv, such as what are the current transfer rates, how many clients are connected, etc, etc. If you wish this information to be available, similarlly you might want to allow it through your firewall, or port-forward it (22070 to 22070) on your NAT device.
|
||||
Furthermore, **by default strelaysrv will also expose a /status HTTP endpoint on port 22070**, which is used by the pool servers to peek at metrics of the strelaysrv, such as what are the current transfer rates, how many clients are connected, etc, etc. If you wish this information to be available, similarlly you might want to allow it through your firewall, or port-forward it (22070 to 22070) on your NAT device.
|
||||
|
||||
This is **not mandatory** for the relaysrv to function, and is used only to gather metrics and present them in the overview page of the pool server, displaying stats about the specific relay.
|
||||
This is **not mandatory** for the strelaysrv to function, and is used only to gather metrics and present them in the overview page of the pool server, displaying stats about the specific relay.
|
||||
|
||||
At the point of writing the endpoint output looks as follows:
|
||||
|
||||
@@ -62,31 +62,31 @@ At the point of writing the endpoint output looks as follows:
|
||||
}
|
||||
```
|
||||
|
||||
If you wish to disable the /status endpoint, provide `-status-srv=""` as one of the arguments when starting the relaysrv.
|
||||
If you wish to disable the /status endpoint, provide `-status-srv=""` as one of the arguments when starting the strelaysrv.
|
||||
|
||||
Running for public use
|
||||
----
|
||||
Make sure you have a public IP with port 22067 open, or make sure you have port-forwarding (22067 to 22067) if you are behind a router.
|
||||
|
||||
Run the `relaysrv` with no arguments (or `-debug` if you want more output), and that should be enough for the server to join the public relay pool.
|
||||
Run the `strelaysrv` with no arguments (or `-debug` if you want more output), and that should be enough for the server to join the public relay pool.
|
||||
You should see a message saying:
|
||||
```
|
||||
2015/09/21 22:45:46 pool.go:60: Joined https://relays.syncthing.net/endpoint rejoining in 48m0s
|
||||
```
|
||||
|
||||
See `relaysrv -help` for other options, such as rate limits, timeout intervals, etc.
|
||||
See `strelaysrv -help` for other options, such as rate limits, timeout intervals, etc.
|
||||
|
||||
Running for private use
|
||||
-----
|
||||
|
||||
Once you've started the `relaysrv`, it will generate a key pair and print an URI:
|
||||
Once you've started the `strelaysrv`, it will generate a key pair and print an URI:
|
||||
```bash
|
||||
relay://:22067/?id=EZQOIDM-6DDD4ZI-DJ65NSM-4OQWRAT-EIKSMJO-OZ552BO-WQZEGYY-STS5RQM&pingInterval=1m0s&networkTimeout=2m0s&sessionLimitBps=0&globalLimitBps=0&statusAddr=:22070
|
||||
```
|
||||
|
||||
This URI contains partial address of the relay server, as well as it's options which in the future may be taken into account when choosing the best suitable relay out of multiple available.
|
||||
|
||||
Because `-listen` option was not used, the `relaysrv` does not know it's external IP, therefore you should replace the host part of the URI with your public IP address on which the `relaysrv` will be available:
|
||||
Because `-listen` option was not used, the `strelaysrv` does not know it's external IP, therefore you should replace the host part of the URI with your public IP address on which the `strelaysrv` will be available:
|
||||
|
||||
```bash
|
||||
relay://123.123.123.123:22067/?id=EZQOIDM-6DDD4ZI-DJ65NSM-4OQWRAT-EIKSMJO-OZ552BO-WQZEGYY-STS5RQM&pingInterval=1m0s&networkTimeout=2m0s&sessionLimitBps=0&globalLimitBps=0&statusAddr=:22070
|
||||
@@ -100,7 +100,7 @@ relay://123.123.123.123:22067
|
||||
|
||||
This URI can then be used in `syncthing` as one of the relay servers.
|
||||
|
||||
See `relaysrv -help` for other options, such as rate limits, timeout intervals, etc.
|
||||
See `strelaysrv -help` for other options, such as rate limits, timeout intervals, etc.
|
||||
|
||||
Other items available in this repo
|
||||
----
|
||||
+4
-4
@@ -3,10 +3,10 @@ Description=Syncthing relay server
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=syncthing-relaysrv
|
||||
Group=syncthing-relaysrv
|
||||
ExecStart=/usr/bin/relaysrv
|
||||
WorkingDirectory=/var/lib/syncthing-relaysrv
|
||||
User=strelaysrv
|
||||
Group=strelaysrv
|
||||
ExecStart=/usr/bin/strelaysrv
|
||||
WorkingDirectory=/var/lib/strelaysrv
|
||||
|
||||
PrivateTmp=true
|
||||
ProtectSystem=full
|
||||
@@ -42,7 +42,7 @@ func init() {
|
||||
BuildDate = time.Unix(int64(stamp), 0)
|
||||
|
||||
date := BuildDate.UTC().Format("2006-01-02 15:04:05 MST")
|
||||
LongVersion = fmt.Sprintf(`relaysrv %s (%s %s-%s) %s@%s %s`, Version, runtime.Version(), runtime.GOOS, runtime.GOARCH, BuildUser, BuildHost, date)
|
||||
LongVersion = fmt.Sprintf(`strelaysrv %s (%s %s-%s) %s@%s %s`, Version, runtime.Version(), runtime.GOOS, runtime.GOARCH, BuildUser, BuildHost, date)
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -121,7 +121,7 @@ func main() {
|
||||
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if err != nil {
|
||||
log.Println("Failed to load keypair. Generating one, this might take a while...")
|
||||
cert, err = tlsutil.NewCertificate(certFile, keyFile, "relaysrv", 3072)
|
||||
cert, err = tlsutil.NewCertificate(certFile, keyFile, "strelaysrv", 3072)
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to generate X509 key pair:", err)
|
||||
}
|
||||
+13
-7
@@ -41,8 +41,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
configInSync = true
|
||||
startTime = time.Now()
|
||||
startTime = time.Now()
|
||||
)
|
||||
|
||||
type apiService struct {
|
||||
@@ -100,12 +99,13 @@ type configIntf interface {
|
||||
GUI() config.GUIConfiguration
|
||||
Raw() config.Configuration
|
||||
Options() config.OptionsConfiguration
|
||||
Replace(cfg config.Configuration) config.CommitResponse
|
||||
Replace(cfg config.Configuration) error
|
||||
Subscribe(c config.Committer)
|
||||
Folders() map[string]config.FolderConfiguration
|
||||
Devices() map[protocol.DeviceID]config.DeviceConfiguration
|
||||
Save() error
|
||||
ListenAddresses() []string
|
||||
RequiresRestart() bool
|
||||
}
|
||||
|
||||
type connectionsIntf interface {
|
||||
@@ -722,13 +722,19 @@ func (s *apiService) postSystemConfig(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Activate and save
|
||||
|
||||
resp := s.cfg.Replace(to)
|
||||
configInSync = !resp.RequiresRestart
|
||||
s.cfg.Save()
|
||||
if err := s.cfg.Replace(to); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.cfg.Save(); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (s *apiService) getSystemConfigInsync(w http.ResponseWriter, r *http.Request) {
|
||||
sendJSON(w, map[string]bool{"configInSync": configInSync})
|
||||
sendJSON(w, map[string]bool{"configInSync": !s.cfg.RequiresRestart()})
|
||||
}
|
||||
|
||||
func (s *apiService) postSystemRestart(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
+5
-12
@@ -51,7 +51,7 @@ import (
|
||||
|
||||
var (
|
||||
Version = "unknown-dev"
|
||||
Codename = "Copper Cockroach"
|
||||
Codename = "Dysprosium Dragonfly"
|
||||
BuildStamp = "0"
|
||||
BuildDate time.Time
|
||||
BuildHost = "unknown"
|
||||
@@ -539,8 +539,9 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
|
||||
errors := logger.NewRecorder(l, logger.LevelWarn, maxSystemErrors, 0)
|
||||
systemLog := logger.NewRecorder(l, logger.LevelDebug, maxSystemLog, initialSystemLog)
|
||||
|
||||
// Event subscription for the API; must start early to catch the early events. The LocalChangeDetected
|
||||
// event might overwhelm the event reciever in some situations so we will not subscribe to it here.
|
||||
// Event subscription for the API; must start early to catch the early
|
||||
// events. The LocalChangeDetected event might overwhelm the event
|
||||
// receiver in some situations so we will not subscribe to it here.
|
||||
apiSub := events.NewBufferedSubscription(events.Default.Subscribe(events.AllEvents&^events.LocalChangeDetected), 1000)
|
||||
|
||||
if len(os.Getenv("GOMAXPROCS")) == 0 {
|
||||
@@ -681,17 +682,9 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
// Clear out old indexes for other devices. Otherwise we'll start up and
|
||||
// start needing a bunch of files which are nowhere to be found. This
|
||||
// needs to be changed when we correctly do persistent indexes.
|
||||
// Add and start folders
|
||||
for _, folderCfg := range cfg.Folders() {
|
||||
m.AddFolder(folderCfg)
|
||||
for _, device := range folderCfg.DeviceIDs() {
|
||||
if device == myID {
|
||||
continue
|
||||
}
|
||||
m.Index(device, folderCfg.ID, nil)
|
||||
}
|
||||
m.StartFolder(folderCfg.ID)
|
||||
}
|
||||
|
||||
|
||||
@@ -31,8 +31,8 @@ func (c *mockedConfig) Options() config.OptionsConfiguration {
|
||||
return config.OptionsConfiguration{}
|
||||
}
|
||||
|
||||
func (c *mockedConfig) Replace(cfg config.Configuration) config.CommitResponse {
|
||||
return config.CommitResponse{}
|
||||
func (c *mockedConfig) Replace(cfg config.Configuration) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *mockedConfig) Subscribe(cm config.Committer) {}
|
||||
@@ -48,3 +48,7 @@ func (c *mockedConfig) Devices() map[protocol.DeviceID]config.DeviceConfiguratio
|
||||
func (c *mockedConfig) Save() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *mockedConfig) RequiresRestart() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
"github.com/thejerf/suture"
|
||||
)
|
||||
@@ -59,7 +60,7 @@ func (c *folderSummaryService) Stop() {
|
||||
// listenForUpdates subscribes to the event bus and makes note of folders that
|
||||
// need their data recalculated.
|
||||
func (c *folderSummaryService) listenForUpdates() {
|
||||
sub := events.Default.Subscribe(events.LocalIndexUpdated | events.RemoteIndexUpdated | events.StateChanged | events.RemoteDownloadProgress)
|
||||
sub := events.Default.Subscribe(events.LocalIndexUpdated | events.RemoteIndexUpdated | events.StateChanged | events.RemoteDownloadProgress | events.DeviceConnected)
|
||||
defer events.Default.Unsubscribe(sub)
|
||||
|
||||
for {
|
||||
@@ -67,8 +68,31 @@ func (c *folderSummaryService) listenForUpdates() {
|
||||
|
||||
select {
|
||||
case ev := <-sub.C():
|
||||
// Whenever the local or remote index is updated for a given
|
||||
// folder we make a note of it.
|
||||
if ev.Type == events.DeviceConnected {
|
||||
// When a device connects we schedule a refresh of all
|
||||
// folders shared with that device.
|
||||
|
||||
data := ev.Data.(map[string]string)
|
||||
deviceID, _ := protocol.DeviceIDFromString(data["id"])
|
||||
|
||||
c.foldersMut.Lock()
|
||||
nextFolder:
|
||||
for _, folder := range c.cfg.Folders() {
|
||||
for _, dev := range folder.Devices {
|
||||
if dev.DeviceID == deviceID {
|
||||
c.folders[folder.ID] = struct{}{}
|
||||
continue nextFolder
|
||||
}
|
||||
}
|
||||
}
|
||||
c.foldersMut.Unlock()
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// The other events all have a "folder" attribute that they
|
||||
// affect. Whenever the local or remote index is updated for a
|
||||
// given folder we make a note of it.
|
||||
|
||||
data := ev.Data.(map[string]interface{})
|
||||
folder := data["folder"].(string)
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
"Add": "Προσθήκη",
|
||||
"Add Device": "Προσθήκη συσκευής",
|
||||
"Add Folder": "Προσθήκη φακέλου",
|
||||
"Add Remote Device": "Add Remote Device",
|
||||
"Add Remote Device": "Προσθήκη Απομακρυσμένης Συσκευής",
|
||||
"Add new folder?": "Προσθήκη νέου φακέλου;",
|
||||
"Address": "Διεύθυνση",
|
||||
"Addresses": "Διευθύνσεις",
|
||||
"Advanced": "Προχωρημένες",
|
||||
"Advanced Configuration": "Προχωρημένες ρυθμίσεις",
|
||||
"Advanced settings": "Advanced settings",
|
||||
"Advanced settings": "Προχωρημένες ρυθμίσεις",
|
||||
"All Data": "Όλα τα δεδομένα",
|
||||
"Allow Anonymous Usage Reporting?": "Να επιτρέπεται η αποστολή ανώνυμων στοιχείων χρήσης;",
|
||||
"Alphabetic": "Αλφαβητικά",
|
||||
@@ -32,15 +32,15 @@
|
||||
"Comment, when used at the start of a line": "Σχόλιο, όταν χρησιμοποιείται στην αρχή μιας γραμμής",
|
||||
"Compression": "Συμπίεση",
|
||||
"Connection Error": "Σφάλμα σύνδεσης",
|
||||
"Connection Type": "Connection Type",
|
||||
"Connection Type": "Τύπος Σύνδεσης",
|
||||
"Copied from elsewhere": "Έχει αντιγραφεί από κάπου αλλού",
|
||||
"Copied from original": "Έχει αντιγραφεί από το πρωτότυπο",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 οι παρακάτω Συνεισφέροντες:",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 από τους παρακάτω συνεισφορείς:",
|
||||
"Danger!": "Προσοχή!",
|
||||
"Delete": "Διαγραφή",
|
||||
"Deleted": "Διαγραμμένα",
|
||||
"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?": "Η συσκευή \"{{name}}\" ({{device}} στη {{address}}) επιθυμεί να συνδεθεί. Προσθήκη της νέας συσκευής?",
|
||||
"Device ID": "Ταυτότητα συσκευής",
|
||||
"Device Identification": "Ταυτότητα συσκευής",
|
||||
"Device Name": "Όνομα συσκευής",
|
||||
@@ -98,7 +98,7 @@
|
||||
"Keep Versions": "Διατήρηση εκδόσεων",
|
||||
"Largest First": "Το μεγαλύτερο πρώτα",
|
||||
"Last File Received": "Πιο πρόσφατο αρχείο",
|
||||
"Last Scan": "Last Scan",
|
||||
"Last Scan": "Τελευταία Σάρωση",
|
||||
"Last seen": "Τελευταία φορά συνδεδεμένος",
|
||||
"Later": "Αργότερα",
|
||||
"Listeners": "Listeners",
|
||||
@@ -193,7 +193,7 @@
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Το Syncthing φαίνεται πως είναι απενεργοποιημένο ή υπάρχει πρόβλημα στη σύνδεσή σου στο διαδίκτυο. Προσπαθώ πάλι…",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Το Syncthing φαίνεται να αντιμετωπίζει ένα πρόβλημα με την επεξεργασία του αιτήματός σου. Παρακαλούμε, αν το πρόβλημα συνεχίζει, ανανέωσε την σελίδα ή επανεκκίνησε το Syncthing.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "Η διεπαφή διαχείρισης του Syncthing είναι ρυθμισμένη να επιτρέπει την πρόσβαση χωρίς κωδικό.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "The 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 {%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.": "Η ταυτότητα της συσκευής δεν μπορεί να είναι κενή",
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
{
|
||||
"A device with that ID is already added.": "A device with that ID is already added.",
|
||||
"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 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 un périphérique",
|
||||
"Add Folder": "Ajouter un répertoire",
|
||||
"Add Remote Device": "Add Remote Device",
|
||||
"Add new folder?": "Ajouter un nouveau dossier ?",
|
||||
"Add Device": "Ajouter une machine",
|
||||
"Add Folder": "Ajouter un partage",
|
||||
"Add Remote Device": "Ajouter une machine",
|
||||
"Add new folder?": "Ajouter un nouveau partage ?",
|
||||
"Address": "Adresse",
|
||||
"Addresses": "Adresses",
|
||||
"Advanced": "Avancé",
|
||||
"Advanced Configuration": "Configuration avancée",
|
||||
"Advanced settings": "Advanced settings",
|
||||
"Advanced settings": "Réglages experts",
|
||||
"All Data": "Toutes les données",
|
||||
"Allow Anonymous Usage Reporting?": "Autoriser le rapport anonyme de statistiques d'utilisation ?",
|
||||
"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 introductrice sera aussi ajoutée sur cette machine.",
|
||||
"Automatic upgrades": "Mises à jour automatiques",
|
||||
@@ -32,32 +32,32 @@
|
||||
"Comment, when used at the start of a line": "Commentaire lorsque utilisé en début de ligne",
|
||||
"Compression": "Compression",
|
||||
"Connection Error": "Erreur de connexion",
|
||||
"Connection Type": "Connection Type",
|
||||
"Connection Type": "Type de connexion",
|
||||
"Copied from elsewhere": "Copié d'ailleurs",
|
||||
"Copied from original": "Copié depuis l'original",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Les contributeurs suivants, Copyright © 2014-2016 :",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 Les contributeurs suivants:",
|
||||
"Danger!": "Danger!",
|
||||
"Danger!": "Attention !",
|
||||
"Delete": "Supprimer",
|
||||
"Deleted": "Supprimé",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Device \"{{name}}\" ({{device}} at {{address}}) wants to connect. Add new device?",
|
||||
"Device ID": "ID du périphérique",
|
||||
"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 Identification": "Identification de l'appareil",
|
||||
"Device Name": "Nom du périphérique",
|
||||
"Device Name": "Nom de la machine",
|
||||
"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é",
|
||||
"Discovery": "Discovery",
|
||||
"Discovery": "Découverte",
|
||||
"Documentation": "Documentation",
|
||||
"Download Rate": "Débit de réception",
|
||||
"Downloaded": "Téléchargé",
|
||||
"Downloading": "En cours de téléchargement",
|
||||
"Edit": "Éditer",
|
||||
"Edit Device": "Éditer le périphérique",
|
||||
"Edit Folder": "Éditer le répertoire",
|
||||
"Edit": "Modifier",
|
||||
"Edit Device": "Modifier la machine",
|
||||
"Edit Folder": "Modifier le partage",
|
||||
"Editing": "Édition",
|
||||
"Enable NAT traversal": "Enable NAT traversal",
|
||||
"Enable Relaying": "Enable Relaying",
|
||||
"Enable NAT traversal": "Activer transfert d'adresses NAT",
|
||||
"Enable Relaying": "Activer le relayage",
|
||||
"Enable UPnP": "Activer l'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.",
|
||||
@@ -67,16 +67,16 @@
|
||||
"File Pull Order": "Ordre de récupération de fichier",
|
||||
"File Versioning": "Versions de fichier",
|
||||
"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 moved to .stversions folder when replaced or deleted by Syncthing.": "Les fichiers sont déplacés vers le répertoire .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 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 cet appareil seront transférés aux autres appareils.",
|
||||
"Folder": "Dossier",
|
||||
"Folder ID": "ID du répertoire",
|
||||
"Folder Label": "Folder Label",
|
||||
"Folder Master": "Répertoire maître",
|
||||
"Folder Path": "Chemin du répertoire",
|
||||
"Folder Type": "Folder Type",
|
||||
"Folders": "Dossiers",
|
||||
"Folder": "Partage",
|
||||
"Folder ID": "ID du partage",
|
||||
"Folder Label": "Étiquette du partage",
|
||||
"Folder Master": "Partage maître",
|
||||
"Folder Path": "Chemin racine du partage",
|
||||
"Folder Type": "Type de partage",
|
||||
"Folders": "Partages",
|
||||
"GUI": "GUI",
|
||||
"GUI Authentication Password": "Mot de passe d'authentification GUI",
|
||||
"GUI Authentication User": "Utilisateur autorisé GUI",
|
||||
@@ -84,29 +84,29 @@
|
||||
"Generate": "Générer",
|
||||
"Global Discovery": "Recherche globale",
|
||||
"Global Discovery Server": "Serveur global de recherche",
|
||||
"Global Discovery Servers": "Global Discovery Servers",
|
||||
"Global Discovery Servers": "Serveurs de découverte globale",
|
||||
"Global State": "État global",
|
||||
"Help": "Aide",
|
||||
"Home page": "Page d'accueil",
|
||||
"Ignore": "Ignorer",
|
||||
"Ignore Patterns": "Modèles à éviter",
|
||||
"Ignore Patterns": "Masques 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 dossiers et mettre hors-service Syncthing",
|
||||
"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",
|
||||
"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",
|
||||
"Last File Received": "Dernier fichier reçu",
|
||||
"Last Scan": "Last Scan",
|
||||
"Last Scan": "Dernière analyse",
|
||||
"Last seen": "Dernière apparition",
|
||||
"Later": "Plus tard",
|
||||
"Listeners": "Listeners",
|
||||
"Listeners": "Systèmes à l'écoute",
|
||||
"Local Discovery": "Recherche locale",
|
||||
"Local State": "État local",
|
||||
"Local State (Total)": "État local (Total)",
|
||||
"Major Upgrade": "Mise à jour majeure",
|
||||
"Master": "Master",
|
||||
"Master": "Maître",
|
||||
"Maximum Age": "Ancienneté maximum",
|
||||
"Metadata Only": "Métadonnées uniquement",
|
||||
"Minimum Free Disk Space": "Espace disque libre minimum",
|
||||
@@ -114,7 +114,7 @@
|
||||
"Multi level wildcard (matches multiple directory levels)": "Astérisque à plusieurs niveaux (correspond aux répertoires et sous-répertoires)",
|
||||
"Never": "Jamais",
|
||||
"New Device": "Nouvel appareil",
|
||||
"New Folder": "Nouveau dossier",
|
||||
"New Folder": "Nouveau partage",
|
||||
"Newest First": "Les plus récents en premier",
|
||||
"No": "Non",
|
||||
"No File Versioning": "Pas de version de fichier",
|
||||
@@ -123,60 +123,60 @@
|
||||
"OK": "OK",
|
||||
"Off": "Éteint",
|
||||
"Oldest First": "Les plus anciens en premier",
|
||||
"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.": "Étiquette conviviale pour le partage. Elle peut être différente sur chaque machine.",
|
||||
"Options": "Options",
|
||||
"Out of Sync": "Désynchronisé",
|
||||
"Out of Sync Items": "Objets non synchronisés",
|
||||
"Outgoing Rate Limit (KiB/s)": "Limite du débit sortant (KiB/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 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 répertoire 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)",
|
||||
"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.",
|
||||
"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.": "Veuillez définir un nom d'utilisateur et un mot de passe dans les réglages.",
|
||||
"Please wait": "Merci de patienter",
|
||||
"Preview": "Aperçu",
|
||||
"Preview Usage Report": "Aperçu du rapport de statistiques d'utilisation",
|
||||
"Quick guide to supported patterns": "Guide rapide des masques supportés",
|
||||
"RAM Utilization": "Utilisation de la RAM",
|
||||
"Random": "Aléatoire",
|
||||
"Relay Servers": "Relay Servers",
|
||||
"Relay Servers": "Serveurs relais",
|
||||
"Relayed via": "Relayée par",
|
||||
"Relays": "Relais",
|
||||
"Release Notes": "Notes de version",
|
||||
"Remote Devices": "Remote Devices",
|
||||
"Remote Devices": "Machines distantes",
|
||||
"Remove": "Enlever",
|
||||
"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.",
|
||||
"Rescan": "Rescanner",
|
||||
"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",
|
||||
"Rescan All": "Réanalyser tout",
|
||||
"Rescan Interval": "Intervalle de scan",
|
||||
"Rescan Interval": "Intervalle d'analyse",
|
||||
"Restart": "Redémarrer",
|
||||
"Restart Needed": "Redémarrage nécessaire",
|
||||
"Restarting": "Redémarrage",
|
||||
"Resume": "Résumer",
|
||||
"Reused": "Réutilisé",
|
||||
"Save": "Sauver",
|
||||
"Scan Time Remaining": "Scan Time Remaining",
|
||||
"Scanning": "En cours de scan",
|
||||
"Select the devices to share this folder with.": "Sélectionner les appareils avec qui partager ce dossier.",
|
||||
"Select the folders to share with this device.": "Sélectionner les dossiers à partager avec cet appareil.",
|
||||
"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.",
|
||||
"Settings": "Configuration",
|
||||
"Share": "Partager",
|
||||
"Share Folder": "Partager le dossier",
|
||||
"Share Folders With Device": "Partager des dossiers avec des appareils",
|
||||
"Share Folder": "Partager",
|
||||
"Share Folders With Device": "Partages avec cette machine",
|
||||
"Share With Devices": "Partage avec des appareils",
|
||||
"Share this folder?": "Voulez-vous partager ce dossier ?",
|
||||
"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 dossier. Il doit être le même sur l'ensemble des appareils du groupe.",
|
||||
"Show ID": "Montrer l'ID",
|
||||
"Show QR": "Show QR",
|
||||
"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.",
|
||||
"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": "Éteindre",
|
||||
"Shutdown Complete": "Extinction terminée",
|
||||
"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 à l’intérieur du dossier)",
|
||||
"Single level wildcard (matches within a directory only)": "Astérisque à un seul niveau (correspond uniquement à l’intérieur du répertoire)",
|
||||
"Smallest First": "Les plus petits en premier",
|
||||
"Source Code": "Code source",
|
||||
"Staggered File Versioning": "Versions échelonnées de fichier",
|
||||
@@ -192,20 +192,20 @@
|
||||
"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.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "The Syncthing admin interface is configured to allow remote access without a password.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "The aggregated statistics are publicly available at the URL below.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "L'interface d'administration de Syncthing est configuré pour accepter l'accès distant sans mot de passe !",
|
||||
"The aggregated statistics are publicly available at the URL below.": "Les statistiques aggrégées sont publiquement disponibles à 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 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).": "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 \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "L'ID de l'appareil à entrer peut être trouvé dans le menu \"Éditer > Montrer l'ID\" des autres appareils. 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 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 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 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 répertoire doit être unique.",
|
||||
"The folder path cannot be blank.": "Le chemin du répertoire ne peut pas être vide.",
|
||||
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "Le premier paramètre de ligne de commande est le chemin du répertoire partagé, et le second est le chemin relatif dans le répertoire.",
|
||||
"The folder ID cannot be blank.": "L'identifiant du partage ne peut être vide.",
|
||||
"The folder ID must be 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) comprenant uniquement des lettres, chiffre, points (.), traits d'union (-) et tirets bas (_).",
|
||||
"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 éléments 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,8 +219,8 @@
|
||||
"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": "This Device",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "This can easily give hackers access to read and change any files on your computer.",
|
||||
"This Device": "Cette machine",
|
||||
"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.",
|
||||
"Unknown": "Inconnu",
|
||||
@@ -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%}\".": "Warning, this path is a subdirectory of an existing folder \"{{otherFolder}}\".",
|
||||
"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 répertoire est ajouté, gardez à l'esprit que son ID est utilisé pour lier les répertoires à travers les appareils. Les ID sont sensibles à la casse et doivent être identiques à travers tous les nœuds.",
|
||||
"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.",
|
||||
"Yes": "Oui",
|
||||
"You must keep at least one version.": "Vous devez garder au minimum une version.",
|
||||
"days": "Jours",
|
||||
"full documentation": "documentation complète",
|
||||
"items": "éléments",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} veut partager le dossier \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
|
||||
"{%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}})."
|
||||
}
|
||||
@@ -40,7 +40,7 @@
|
||||
"Danger!": "Attention !",
|
||||
"Delete": "Supprimer",
|
||||
"Deleted": "Supprimé",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "La machine \"{{name}}\" ({{device}} sur {{address}}) veut se connecter. Ajouter cette nouvelle machine ?",
|
||||
"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",
|
||||
@@ -52,12 +52,12 @@
|
||||
"Download Rate": "Vitesse de réception",
|
||||
"Downloaded": "Téléchargé",
|
||||
"Downloading": "En cours de téléchargement",
|
||||
"Edit": "Éditer",
|
||||
"Edit Device": "Éditer la machine",
|
||||
"Edit Folder": "Éditer le dossier",
|
||||
"Editing": "Édition",
|
||||
"Enable NAT traversal": "Activer transfert d'adresses (NAT)",
|
||||
"Enable Relaying": "Activer le relayage",
|
||||
"Edit": "Modifier",
|
||||
"Edit Device": "Modifier la machine",
|
||||
"Edit Folder": "Modifier le dossier",
|
||||
"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.",
|
||||
@@ -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.": "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",
|
||||
"Folder Label": "Étiquette du dossier",
|
||||
"Folder Label": "Étiquette du partage",
|
||||
"Folder Master": "Dossier maître",
|
||||
"Folder Path": "Chemin du dossier",
|
||||
"Folder Type": "Type de répertoire",
|
||||
"Folder Type": "Type de partage",
|
||||
"Folders": "Dossiers",
|
||||
"GUI": "GUI",
|
||||
"GUI Authentication Password": "Mot de passe d'authentification GUI",
|
||||
@@ -89,7 +89,7 @@
|
||||
"Help": "Aide",
|
||||
"Home page": "Page d'accueil",
|
||||
"Ignore": "Ignorer",
|
||||
"Ignore Patterns": "Modèles à éviter",
|
||||
"Ignore Patterns": "Masques d'exclusion",
|
||||
"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",
|
||||
@@ -106,7 +106,7 @@
|
||||
"Local State": "État local",
|
||||
"Local State (Total)": "État local (Total)",
|
||||
"Major Upgrade": "Mise à jour majeure",
|
||||
"Master": "Maitre",
|
||||
"Master": "Maître",
|
||||
"Maximum Age": "Ancienneté maximum",
|
||||
"Metadata Only": "Métadonnées uniquement",
|
||||
"Minimum Free Disk Space": "Espace disque libre minimum",
|
||||
@@ -123,7 +123,7 @@
|
||||
"OK": "OK",
|
||||
"Off": "Éteint",
|
||||
"Oldest First": "Les plus anciens en premier",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Étiquette optionnelle pour le dossier. Peut être différente pour 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 machine.",
|
||||
"Options": "Options",
|
||||
"Out of Sync": "Désynchronisé",
|
||||
"Out of Sync Items": "Fichiers non synchronisés",
|
||||
@@ -134,7 +134,7 @@
|
||||
"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.",
|
||||
"Please set a GUI Authentication User and Password in the Settings dialog.": "SVP, mettez un nom d'utilisateur et un mot de passe dans la fenêtre de paramétrage.",
|
||||
"Please set a GUI Authentication User and Password in the Settings dialog.": "Veuillez définir un nom d'utilisateur et un mot de passe dans la fenêtre de Configuration.",
|
||||
"Please wait": "Merci de patienter",
|
||||
"Preview": "Aperçu",
|
||||
"Preview Usage Report": "Aperçu du rapport de statistiques d'utilisation",
|
||||
@@ -147,7 +147,7 @@
|
||||
"Release Notes": "Notes de version",
|
||||
"Remote Devices": "Machines distantes",
|
||||
"Remove": "Enlever",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "Identifiant pour le dossier. Doit être le même sur l'ensemble des machines du cluster.",
|
||||
"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",
|
||||
"Rescan All": "Réanalyser tout",
|
||||
"Rescan Interval": "Intervalle d'analyse",
|
||||
@@ -157,7 +157,7 @@
|
||||
"Resume": "Résumer",
|
||||
"Reused": "Réutilisé",
|
||||
"Save": "Sauver",
|
||||
"Scan Time Remaining": "Intervalle entre chaque analyse",
|
||||
"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.",
|
||||
@@ -169,12 +169,12 @@
|
||||
"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.",
|
||||
"Show ID": "Afficher l'ID",
|
||||
"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.",
|
||||
"Shutdown": "Éteindre",
|
||||
"Shutdown Complete": "Extinction terminée",
|
||||
"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 à l’intérieur du dossier)",
|
||||
"Smallest First": "Les plus petits en premier",
|
||||
@@ -192,13 +192,13 @@
|
||||
"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.",
|
||||
"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 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 la machine à indiquer ici se trouve dans \"Actions > Afficher ID\" sur l'autre machine. 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 la machine à entrer peut être trouvé dans le menu \"Éditer > Montrer l'ID\" des autres machines. 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 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.",
|
||||
@@ -220,7 +220,7 @@
|
||||
"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 can easily give hackers access to read and change any files on your computer.": "Cela permet facilement aux pirates de lire et modifier n'importe quel fichier de votre machine.",
|
||||
"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.",
|
||||
"Unknown": "Inconnu",
|
||||
@@ -237,7 +237,7 @@
|
||||
"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%}\".": "Attention, ce chemin est un sous-répertoire du dossier existant \"{{otherFolder}}\".",
|
||||
"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.",
|
||||
"Yes": "Oui",
|
||||
@@ -246,6 +246,6 @@
|
||||
"full documentation": "documentation complète",
|
||||
"items": "fichiers",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} veut partager le dossier \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} veut partager le dossier \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} veut partager le dossier \"{{folderLabel}}\" ({{folder}})."
|
||||
"{%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}})."
|
||||
}
|
||||
@@ -33,8 +33,8 @@
|
||||
"Compression": "Tömörítés",
|
||||
"Connection Error": "Kapcsolódási hiba",
|
||||
"Connection Type": "Kapcsolat típus",
|
||||
"Copied from elsewhere": "Másolva máshonnan",
|
||||
"Copied from original": "Másolva az eredetiről",
|
||||
"Copied from elsewhere": "Máshonnan másolva",
|
||||
"Copied from original": "Eredetiről másolva",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Szerzői jog © 2014-2016 az alábbi közreműködők:",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 az alábbi közreműködők:",
|
||||
"Danger!": "Veszély!",
|
||||
@@ -210,7 +210,7 @@
|
||||
"The following items could not be synchronized.": "A következő elemek nem szinkronizálhatóak.",
|
||||
"The maximum age must be a number and cannot be blank.": "A maximális kornak számnak kell lenni és nem lehet üres",
|
||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "A verziók megtartásának maximális ideje (napokban, ha 0-t adsz meg örökre megmaradnak).",
|
||||
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "A minimális szabad terület százalékos. nem-negatív érték 0 és 100 között",
|
||||
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "A minimális szabad terület százalékos, nem-negatív értéke 0 és 100 között",
|
||||
"The number of days must be a number and cannot be blank.": "A napok száma szám kell legyen és nem lehet üres.",
|
||||
"The number of days to keep files in the trash can. Zero means forever.": "A napok száma ameddig a fájlok meg lesznek tartva a szemetesben. A 0 azt jelenti örökre.",
|
||||
"The number of old versions to keep, per file.": "A megtartott régi verziók száma, fájlonként.",
|
||||
@@ -220,7 +220,7 @@
|
||||
"The rescan interval must be a non-negative number of seconds.": "Az átnézési intervallum nullánál nagyobb másodperc érték kell legyen",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "A hiba javítása után automatikusan újra megpróbálja a szinkronizálást.",
|
||||
"This Device": "Ez az eszköz",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "Így a hekkerek könnyedén hozzáférhetnek a számítógépen található fájlokhoz. ",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "Így a hekkerek könnyedén hozzáférést szerezhetnek a gépeden tárolt fájlok olvasásához és módosításához.",
|
||||
"This is a major version upgrade.": "Ez egy főverzió frissítés.",
|
||||
"Trash Can File Versioning": "Szemetes fájl verziókövetés",
|
||||
"Unknown": "Ismeretlen",
|
||||
@@ -241,7 +241,7 @@
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Amikor új eszközt adsz hozzá, tartsd észben, hogy a másik oldalon ezt az eszközt is hozzá kell adni.",
|
||||
"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.": "Amikor új mappát adsz hozzá, tartsd észben, hogy a mappa azonosító arra való hogy összekösd a mappákat az eszközeiden. Az azonosító kisbetű-nagybetű érzékeny és pontosan egyeznie kell az eszközökön.",
|
||||
"Yes": "Igen",
|
||||
"You must keep at least one version.": "Legalább egy verziót meg kell tartanod",
|
||||
"You must keep at least one version.": "Legalább egy verziót meg kell tartanod.",
|
||||
"days": "nap",
|
||||
"full documentation": "teljes dokumentáció",
|
||||
"items": "elem",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"A negative number of days doesn't make sense.": "음수로는 지정할 수 없습니다.",
|
||||
"A new major version may not be compatible with previous versions.": "새로운 메이저 버전은 이전 버전과 호환되지 않을 수 있습니다.",
|
||||
"API Key": "API 키",
|
||||
"About": " 정보",
|
||||
"About": "정보",
|
||||
"Actions": "동작",
|
||||
"Add": "추가",
|
||||
"Add Device": "기기 추가",
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
"Enable Relaying": "Habilitar retransmissão",
|
||||
"Enable UPnP": "Habilitar UPnP",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Insira endereços (\"tcp://ip:porta\", \"tcp://host:porta\") separados por vírgula ou \"dynamic\" para executar a descoberta automática do endereço.",
|
||||
"Enter ignore patterns, one per line.": "Insira os padrões de exclusão, um por linha.",
|
||||
"Enter ignore patterns, one per line.": "Insira os filtros, um por linha.",
|
||||
"Error": "Erro",
|
||||
"External File Versioning": "Versionamento externo de arquivo",
|
||||
"Failed Items": "Itens com falha",
|
||||
@@ -89,7 +89,7 @@
|
||||
"Help": "Ajuda",
|
||||
"Home page": "Página inicial",
|
||||
"Ignore": "Ignorar",
|
||||
"Ignore Patterns": "Padrões de exclusão",
|
||||
"Ignore Patterns": "Filtros",
|
||||
"Ignore Permissions": "Ignorar permissões",
|
||||
"Incoming Rate Limit (KiB/s)": "Limite de velocidade de recepção (KiB/s)",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "A configuração incorreta poderá causar danos aos seus dados e tornar o Syncthing inoperante.",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"A device with that ID is already added.": "En enhet med det ID är redan tillagt.",
|
||||
"A negative number of days doesn't make sense.": "Ett negativt antal dagar är inte troligt.",
|
||||
"A device with that ID is already added.": "En enhet med det ID:t är redan tillagt.",
|
||||
"A negative number of days doesn't make sense.": "Ett negativt antal dagar är inte rimligt.",
|
||||
"A new major version may not be compatible with previous versions.": "En ny huvudversion kan eventuellt vara inkompatibel med tidigare versioner.",
|
||||
"API Key": "API-nyckel",
|
||||
"About": "Om",
|
||||
@@ -18,8 +18,8 @@
|
||||
"All Data": "All data",
|
||||
"Allow Anonymous Usage Reporting?": "Tillåt anonym användarstatistiksrapportering?",
|
||||
"Alphabetic": "Alfabetisk",
|
||||
"An external command handles the versioning. It has to remove the file from the synced folder.": "Ett externt kommando sköter versionshanteringen. Det måste ta bort filen från den synkroniserade mappen.",
|
||||
"Anonymous Usage Reporting": "Anonym användarstatistik",
|
||||
"An external command handles the versioning. It has to remove the file from the synced folder.": "Ett externt kommando sköter versionshanteringen. Den behöver ta bort filen från den synkroniserade mappen.",
|
||||
"Anonymous Usage Reporting": "Anonym användarstatistik rapportering",
|
||||
"Any devices configured on an introducer device will be added to this device as well.": "Enheter konfigurerade på en introduktörsenhet kommer också att läggas till den här enheten.",
|
||||
"Automatic upgrades": "Automatiska uppgraderingar",
|
||||
"Be careful!": "Var aktsam!",
|
||||
@@ -29,87 +29,87 @@
|
||||
"Clean out after": "Rensa efteråt",
|
||||
"Close": "Stäng",
|
||||
"Command": "Kommando",
|
||||
"Comment, when used at the start of a line": "Kommentar, vid början av en rad.",
|
||||
"Comment, when used at the start of a line": "Kommentar, vid användning i början av en rad.",
|
||||
"Compression": "Komprimering",
|
||||
"Connection Error": "Anslutningsproblem",
|
||||
"Connection Type": "Anslutningstyp",
|
||||
"Copied from elsewhere": "Kopierat utifrån",
|
||||
"Copied from original": "Oförändrat",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 följande bidragande:",
|
||||
"Copied from original": "Kopierat från original",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 följande bidragare:",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 följande medverkande:",
|
||||
"Danger!": "Fara!",
|
||||
"Delete": "Ta bort",
|
||||
"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 ID": "Enhets ID",
|
||||
"Device Identification": "Enhetsidentifikation",
|
||||
"Device Name": "Enhetsnamn",
|
||||
"Device Name": "Enhetens namn",
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Enheten {{device}} ({{address}}) vill ansluta. Lägg till ny enhet?",
|
||||
"Devices": "Enheter",
|
||||
"Disconnected": "Ej ansluten",
|
||||
"Discovery": "Uppslagning",
|
||||
"Disconnected": "Frånkopplad",
|
||||
"Discovery": "Upptäckt",
|
||||
"Documentation": "Dokumentation",
|
||||
"Download Rate": "Nedladdningshastighet",
|
||||
"Downloaded": "Nerladdat",
|
||||
"Downloading": "Laddar ner",
|
||||
"Downloaded": "Hämtat",
|
||||
"Downloading": "Hämtar",
|
||||
"Edit": "Redigera",
|
||||
"Edit Device": "Redigera enhet",
|
||||
"Edit Folder": "Redigera katalog",
|
||||
"Editing": "Redigerar",
|
||||
"Enable NAT traversal": "Aktivera NAT traversering",
|
||||
"Enable Relaying": "Aktivera reläa",
|
||||
"Enable UPnP": "Använd UPnP",
|
||||
"Enable UPnP": "Aktivera UPnP",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Ange kommaseparerade (\"tcp://ip:port\", \"tcp://host:port\")-adresser eller ordet \"dynamic\" för att använda automatisk uppslagning.",
|
||||
"Enter ignore patterns, one per line.": "Ange filmönster, ett per rad.",
|
||||
"Enter ignore patterns, one per line.": "Ange ignorera mönster, en per rad.",
|
||||
"Error": "Fel",
|
||||
"External File Versioning": "Extern versionshantering",
|
||||
"Failed Items": "Misslyckade filer",
|
||||
"File Pull Order": "Hämtningsprioritering av filer",
|
||||
"File Versioning": "Versionshantering",
|
||||
"External File Versioning": "Extern filversionshantering",
|
||||
"Failed Items": "Misslyckade objekt",
|
||||
"File Pull Order": "Filhämtningsprioritering",
|
||||
"File Versioning": "Filversionshantering",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Filrättigheter ignoreras vid sökning efter förändringar. Används på FAT-filsystem.",
|
||||
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Filer flyttas till katalogen .stversions om de ersätts eller raderas av Syncthing.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Filer flyttas till datummärkta versioner i en .stversions-mapp när de ersatts eller raderats av 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.": "Filer skyddas från ändringar gjorda på andra enheter, men ändringar som görs på den här noden skickas till de andra klustermedlemmarna.",
|
||||
"Folder": "Katalog",
|
||||
"Folder ID": "Katalog-ID",
|
||||
"Folder Label": "Katalog etikett",
|
||||
"Folder Master": "Huvudlagring",
|
||||
"Folder Path": "Sökväg",
|
||||
"Folder Label": "Katalog-etikett",
|
||||
"Folder Master": "Huvudkatalog",
|
||||
"Folder Path": "Katalog-sökväg",
|
||||
"Folder Type": "Katalogtyp",
|
||||
"Folders": "Kataloger",
|
||||
"GUI": "GUI",
|
||||
"GUI Authentication Password": "GUI-lösenord",
|
||||
"GUI Authentication User": "GUI-användare",
|
||||
"GUI Listen Addresses": "GUI-adress",
|
||||
"Generate": "Skapa",
|
||||
"Global Discovery": "Global uppslagning",
|
||||
"Global Discovery Server": "Global uppslagningsserver",
|
||||
"Global Discovery Servers": "Globala uppslagningsservrar",
|
||||
"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",
|
||||
"Generate": "Generera",
|
||||
"Global Discovery": "Global upptäckt",
|
||||
"Global Discovery Server": "Global upptäcktsserver",
|
||||
"Global Discovery Servers": "Globala upptäcktsservrar",
|
||||
"Global State": "Global status",
|
||||
"Help": "Hjälp",
|
||||
"Home page": "Hemsida",
|
||||
"Ignore": "Ignorera",
|
||||
"Ignore Patterns": "Ignorerade filmönster",
|
||||
"Ignore Permissions": "Ignorera filrättigheter",
|
||||
"Incoming Rate Limit (KiB/s)": "Max nedladdningshastighet (KiB/s)",
|
||||
"Ignore Patterns": "Ignorera mönster",
|
||||
"Ignore Permissions": "Ignorera rättigheter",
|
||||
"Incoming Rate Limit (KiB/s)": "Nedladdningshastighetsgräns (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.",
|
||||
"Keep Versions": "Behåll versioner",
|
||||
"Largest First": "Störst först",
|
||||
"Last File Received": "Senast Mottagna Fil",
|
||||
"Last Scan": "Senast skanning",
|
||||
"Last seen": "Senast online",
|
||||
"Largest First": "Största först",
|
||||
"Last File Received": "Senaste fil mottagen",
|
||||
"Last Scan": "Senaste skanning",
|
||||
"Last seen": "Senast sedd",
|
||||
"Later": "Senare",
|
||||
"Listeners": "Lyssnare",
|
||||
"Local Discovery": "Lokal uppslagning",
|
||||
"Local Discovery": "Lokal upptäckt",
|
||||
"Local State": "Lokal status",
|
||||
"Local State (Total)": "Lokal status (Total)",
|
||||
"Local State (Total)": "Lokal status (totalt)",
|
||||
"Major Upgrade": "Stor uppgradering",
|
||||
"Master": "Huvud",
|
||||
"Maximum Age": "Högsta ålder",
|
||||
"Maximum Age": "Maximum ålder",
|
||||
"Metadata Only": "Endast metadata",
|
||||
"Minimum Free Disk Space": "Minimum ledigt diskutrymme",
|
||||
"Minimum Free Disk Space": "Minsta ledigt diskutrymme",
|
||||
"Move to top of queue": "Flytta till överst i kön",
|
||||
"Multi level wildcard (matches multiple directory levels)": "Jokertecken som representerar noll eller fler godtyckliga tecken, även över kataloggränser.",
|
||||
"Never": "Aldrig",
|
||||
@@ -117,7 +117,7 @@
|
||||
"New Folder": "Ny katalog",
|
||||
"Newest First": "Nyast först",
|
||||
"No": "Nej",
|
||||
"No File Versioning": "Ingen versionshantering",
|
||||
"No File Versioning": "Ingen filversionshantering",
|
||||
"Normal": "Normal",
|
||||
"Notice": "Observera",
|
||||
"OK": "OK",
|
||||
@@ -126,39 +126,39 @@
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Valfri beskrivande etikett för katalogen. Kan vara olika på varje enhet.",
|
||||
"Options": "Alternativ",
|
||||
"Out of Sync": "Osynkroniserad",
|
||||
"Out of Sync Items": "Osynkroniserade poster",
|
||||
"Outgoing Rate Limit (KiB/s)": "Max uppladdningshastighet (KiB/s)",
|
||||
"Override Changes": "Skriv över ändringar",
|
||||
"Out of Sync Items": "Osynkroniserade objekt",
|
||||
"Outgoing Rate Limit (KiB/s)": "Uppladdningshastighetsgräns (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).",
|
||||
"Pause": "Paus",
|
||||
"Paused": "Pausad",
|
||||
"Please consult the release notes before performing a major upgrade.": "Läs igenom versionsnyheterna innan den stora uppgraderingen.",
|
||||
"Please set a GUI Authentication User and Password in the Settings dialog.": "Ställ in ett grafiskt användarautentisering och lösenord i dialogrutan Inställningar.",
|
||||
"Please set a GUI Authentication User and Password in the Settings dialog.": "Ställ in ett grafiska gränssnittets användarautentisering och lösenord i inställningsdialogrutan.",
|
||||
"Please wait": "Var god vänta",
|
||||
"Preview": "Förhandsgranska",
|
||||
"Preview Usage Report": "Förhandsgranska statistik",
|
||||
"Quick guide to supported patterns": "Snabb guide till filmönster som stöds",
|
||||
"RAM Utilization": "Minnesanvändning",
|
||||
"Quick guide to supported patterns": "Snabb handledning till mönster som stöds",
|
||||
"RAM Utilization": "RAM-användning",
|
||||
"Random": "Slumpmässig",
|
||||
"Relay Servers": "Reläservrar",
|
||||
"Relayed via": "Vidarbefordras via",
|
||||
"Relays": "Vidarbefordringar",
|
||||
"Relays": "Reläservrar",
|
||||
"Release Notes": "Versionsanteckningar",
|
||||
"Remote Devices": "Fjärrenheter",
|
||||
"Remove": "Ta bort",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "Krävs identifierare för katalogen. Måste vara densamma på alla kluster enheter.",
|
||||
"Rescan": "Uppdatera",
|
||||
"Rescan All": "Uppdatera alla",
|
||||
"Rescan Interval": "Uppdateringsintervall",
|
||||
"Rescan": "Skanna om",
|
||||
"Rescan All": "Skanna om alla",
|
||||
"Rescan Interval": "Omskanningsintervall",
|
||||
"Restart": "Starta om",
|
||||
"Restart Needed": "Omstart behövs",
|
||||
"Restarting": "Startar om",
|
||||
"Resume": "Återuppta",
|
||||
"Reused": "Återanvänt",
|
||||
"Reused": "Återanvänd",
|
||||
"Save": "Spara",
|
||||
"Scan Time Remaining": "Granska återstående tid",
|
||||
"Scanning": "Uppdaterar",
|
||||
"Scan Time Remaining": "Återstående skanningstid",
|
||||
"Scanning": "Skannar",
|
||||
"Select the devices to share this folder with.": "Ange enheterna att dela den här katalogen med.",
|
||||
"Select the folders to share with this device.": "Välj kataloger att dela med den här enheten.",
|
||||
"Settings": "Inställningar",
|
||||
@@ -166,67 +166,67 @@
|
||||
"Share Folder": "Dela katalog",
|
||||
"Share Folders With Device": "Dela kataloger med enhet",
|
||||
"Share With Devices": "Dela med enheter",
|
||||
"Share this folder?": "Dela den här katalogen?",
|
||||
"Share this folder?": "Dela denna katalog?",
|
||||
"Shared With": "Delad med",
|
||||
"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 enhets-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 enhets-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 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.",
|
||||
"Shutdown": "Stäng av",
|
||||
"Shutdown Complete": "Avstängning klar",
|
||||
"Simple File Versioning": "Enkel versionshantering",
|
||||
"Simple File Versioning": "Enkel filversionshantering",
|
||||
"Single level wildcard (matches within a directory only)": "Jokertecken som representerar noll eller fler godtyckliga tecken i ett filnamn.",
|
||||
"Smallest First": "Minst först",
|
||||
"Source Code": "Källkod",
|
||||
"Staggered File Versioning": "Versionshantering i intervall",
|
||||
"Start Browser": "Starta browser",
|
||||
"Staggered File Versioning": "Filversionshantering i intervall",
|
||||
"Start Browser": "Starta webbläsare",
|
||||
"Statistics": "Statistik",
|
||||
"Stopped": "Stoppad",
|
||||
"Support": "Support",
|
||||
"Sync Protocol Listen Addresses": "Address för inkommande anslutningar",
|
||||
"Sync Protocol Listen Addresses": "Synkroniseringsprotokollets lyssningsadresser",
|
||||
"Syncing": "Synkroniserar",
|
||||
"Syncthing has been shut down.": "Syncthing har stängts.",
|
||||
"Syncthing includes the following software or portions thereof:": "Syncthing innehåller följande mjukvarupaket eller delar av dem:",
|
||||
"Syncthing is restarting.": "Syncthing startar om.",
|
||||
"Syncthing is upgrading.": "Syncthing uppgraderas.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing verkar avstängd, eller finns det problem med din Internetanslutning. Försöker igen...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing verkar ha drabbats av ett problem. Uppdatera sidan eller starta om Syncthing om problemet kvarstår.",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing verkar ha drabbats av ett problem med behandlingen av din begäran. Uppdatera sidan eller starta om Syncthing om problemet kvarstår.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "Syncthing administratör gränssnittet är konfigurerat för att tillåta fjärrtillträde utan ett lösenord.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "Den aggregerade statistiken är offentligt tillgä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.": "Enhets-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).": "Enhets-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).": "Enhets-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.": "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 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 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 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 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.": "Ange ett enhets-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.": "Katalog-ID:t måste vara en kort sträng (64 tecken eller mindre), bestående av endast bokstäver, siffror, punkt (.), bindestreck (-) och understreck (_).",
|
||||
"The folder ID must be unique.": "Katalog-ID:t måste vara unikt.",
|
||||
"The folder path cannot be blank.": "Ange en sökväg.",
|
||||
"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 (_).",
|
||||
"The folder ID must be unique.": "Katalogens ID måste vara unikt.",
|
||||
"The folder path cannot be blank.": "Katalogsökvägen får inte vara tom.",
|
||||
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "De följande intervallen används: varje 30 sekunder under den första timmen; varje timme under den första dagen; varje dag för de första 30 dagarna; varje vecka tills den maximala åldersgränsen uppnås.",
|
||||
"The following items could not be synchronized.": "Följande filer kunde inte synkroniseras.",
|
||||
"The following items could not be synchronized.": "Följande objekt kunde inte synkroniseras.",
|
||||
"The maximum age must be a number and cannot be blank.": "Åldersgränsen måste vara ett tal och kan inte lämnas tomt.",
|
||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Den längsta tiden att behålla en version (i dagar, sätt till 0 för att behålla versioner för evigt).",
|
||||
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "Minimum ledigt diskutrymme i procent måste vara en icke negativ siffra mellan 0 och 100 (inklusive).",
|
||||
"The number of days must be a number and cannot be blank.": "Antalet dagar måste vara en siffra och får inte vara tomt.",
|
||||
"The number of days to keep files in the trash can. Zero means forever.": "Antal dagar som filer ligger kvar i papperskorgen. Noll betyder för alltid.",
|
||||
"The number of days to keep files in the trash can. Zero means forever.": "Antalet dagar som filer ligger kvar i papperskorgen. Noll betyder för alltid.",
|
||||
"The number of old versions to keep, per file.": "Antalet gamla versioner som ska behållas, per fil.",
|
||||
"The number of versions must be a number and cannot be blank.": "Antalet versioner måste vara ett nummer och kan inte lämnas tomt.",
|
||||
"The path cannot be blank.": "Ange en sökväg",
|
||||
"The path cannot be blank.": "Sökvägen kan inte vara tom.",
|
||||
"The rate limit must be a non-negative number (0: no limit)": "Frekvensgränsen måste vara ett icke-negativt tal (0: ingen gräns)",
|
||||
"The rescan interval must be a non-negative number of seconds.": "Förnyelseintervallet måste vara ett positivt antal sekunder",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "De omprövas automatiskt och kommer att synkroniseras när felet är löst.",
|
||||
"This Device": "Denna enhet",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "Detta kan lätt ge hackare tillgång till att läsa och ändra några filer på datorn.",
|
||||
"This is a major version upgrade.": "Det här är en stor uppgradering.",
|
||||
"Trash Can File Versioning": "Versionshantering på filer i papperskorgen",
|
||||
"Unknown": "Okänt",
|
||||
"Trash Can File Versioning": "Papperskorgs filversionshantering",
|
||||
"Unknown": "Okänd",
|
||||
"Unshared": "Inte delad",
|
||||
"Unused": "Oanvänd",
|
||||
"Up to Date": "Helt uppdaterad",
|
||||
"Up to Date": "Uppdaterad",
|
||||
"Updated": "Uppdaterad",
|
||||
"Upgrade": "Uppgradering",
|
||||
"Upgrade To {%version%}": "Uppgradera till {{version}}",
|
||||
@@ -235,16 +235,16 @@
|
||||
"Uptime": "Tid sedan start",
|
||||
"Use HTTPS for GUI": "Använd HTTPS för GUI",
|
||||
"Version": "Version",
|
||||
"Versions Path": "Katalog för versioner",
|
||||
"Versions Path": "Sökväg för versioner",
|
||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versioner tas bort automatiskt när de är äldre än den maximala åldersgränsen eller överstiger frekvensen i sitt interval.",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Varning, denna sökväg är en underkatalog till en befintlig katalog \"{{otherFolder}}\".",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "När du lägger till en ny enhet, kom ihåg att den här enheten måste läggas till på den andra enheten också.",
|
||||
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "När du lägger till ny katalog, tänk på att katalog-ID:t knyter ihop katalogen mellan olika noder. De måste vara exakt desamma mellan noder och stora eller små bokstäver har betydelse.",
|
||||
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "När du lägger till ny katalog, tänk på att katalogens ID knyter ihop katalogen mellan olika noder. De måste vara exakt desamma mellan noder och stora eller små bokstäver har betydelse.",
|
||||
"Yes": "Ja",
|
||||
"You must keep at least one version.": "Du måste behålla åtminstone en version.",
|
||||
"days": "dagar",
|
||||
"full documentation": "fullständig dokumentation",
|
||||
"items": "poster",
|
||||
"items": "objekt",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vill dela katalogen \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} vill dela katalogen \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} vill dela katalogen \"{{folderlabel}}\" ({{folder}})."
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
<p translate>
|
||||
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.
|
||||
</p>
|
||||
<p translate translate-value-url="<a href="https://data.syncthing.net" target="_blank">https://data.syncthing.net</a>">
|
||||
The aggregated statistics are publicly available at {%url%}.
|
||||
</p>
|
||||
<p translate>The aggregated statistics are publicly available at the URL below.</p>
|
||||
<p><a href="https://data.syncthing.net/" target="_blank">https://data.syncthing.net/</a></p>
|
||||
<form>
|
||||
<textarea class="form-control" rows="20">{{reportData | json}}</textarea>
|
||||
</form>
|
||||
|
||||
+25
-16
@@ -11,12 +11,18 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
type requiresRestart struct{}
|
||||
type requiresRestart struct {
|
||||
committed chan struct{}
|
||||
}
|
||||
|
||||
func (requiresRestart) VerifyConfiguration(_, _ Configuration) error {
|
||||
return nil
|
||||
}
|
||||
func (requiresRestart) CommitConfiguration(_, _ Configuration) bool {
|
||||
func (c requiresRestart) CommitConfiguration(_, _ Configuration) bool {
|
||||
select {
|
||||
case c.committed <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
return false
|
||||
}
|
||||
func (requiresRestart) String() string {
|
||||
@@ -28,7 +34,7 @@ type validationError struct{}
|
||||
func (validationError) VerifyConfiguration(_, _ Configuration) error {
|
||||
return errors.New("some error")
|
||||
}
|
||||
func (validationError) CommitConfiguration(_, _ Configuration) bool {
|
||||
func (c validationError) CommitConfiguration(_, _ Configuration) bool {
|
||||
return true
|
||||
}
|
||||
func (validationError) String() string {
|
||||
@@ -44,11 +50,11 @@ func TestReplaceCommit(t *testing.T) {
|
||||
// Replace config. We should get back a clean response and the config
|
||||
// should change.
|
||||
|
||||
resp := w.Replace(Configuration{Version: 1})
|
||||
if resp.ValidationError != nil {
|
||||
t.Fatal("Should not have a validation error")
|
||||
err := w.Replace(Configuration{Version: 1})
|
||||
if err != nil {
|
||||
t.Fatal("Should not have a validation error:", err)
|
||||
}
|
||||
if resp.RequiresRestart {
|
||||
if w.RequiresRestart() {
|
||||
t.Fatal("Should not require restart")
|
||||
}
|
||||
if w.Raw().Version != 1 {
|
||||
@@ -58,13 +64,16 @@ func TestReplaceCommit(t *testing.T) {
|
||||
// Now with a subscriber requiring restart. We should get a clean response
|
||||
// but with the restart flag set, and the config should change.
|
||||
|
||||
w.Subscribe(requiresRestart{})
|
||||
sub0 := requiresRestart{committed: make(chan struct{}, 1)}
|
||||
w.Subscribe(sub0)
|
||||
|
||||
resp = w.Replace(Configuration{Version: 2})
|
||||
if resp.ValidationError != nil {
|
||||
t.Fatal("Should not have a validation error")
|
||||
err = w.Replace(Configuration{Version: 2})
|
||||
if err != nil {
|
||||
t.Fatal("Should not have a validation error:", err)
|
||||
}
|
||||
if !resp.RequiresRestart {
|
||||
|
||||
<-sub0.committed
|
||||
if !w.RequiresRestart() {
|
||||
t.Fatal("Should require restart")
|
||||
}
|
||||
if w.Raw().Version != 2 {
|
||||
@@ -76,12 +85,12 @@ func TestReplaceCommit(t *testing.T) {
|
||||
|
||||
w.Subscribe(validationError{})
|
||||
|
||||
resp = w.Replace(Configuration{Version: 3})
|
||||
if resp.ValidationError == nil {
|
||||
err = w.Replace(Configuration{Version: 3})
|
||||
if err == nil {
|
||||
t.Fatal("Should have a validation error")
|
||||
}
|
||||
if resp.RequiresRestart {
|
||||
t.Fatal("Should not require restart")
|
||||
if !w.RequiresRestart() {
|
||||
t.Fatal("Should still require restart")
|
||||
}
|
||||
if w.Raw().Version != 2 {
|
||||
t.Fatal("Config should not have changed")
|
||||
|
||||
@@ -105,7 +105,9 @@ func ReadJSON(r io.Reader, myID protocol.DeviceID) (Configuration, error) {
|
||||
return Configuration{}, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(bs, &cfg)
|
||||
if err := json.Unmarshal(bs, &cfg); err != nil {
|
||||
return Configuration{}, err
|
||||
}
|
||||
cfg.OriginalVersion = cfg.Version
|
||||
|
||||
if err := cfg.prepare(myID); err != nil {
|
||||
|
||||
@@ -549,6 +549,9 @@ func TestPullOrder(t *testing.T) {
|
||||
t.Logf("%s", buf.Bytes())
|
||||
|
||||
cfg, err = ReadXML(buf, device1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wrapper = Wrap("testdata/pullorder.xml", cfg)
|
||||
folders = wrapper.Folders()
|
||||
|
||||
|
||||
+35
-26
@@ -8,6 +8,7 @@ package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
@@ -41,11 +42,6 @@ type Committer interface {
|
||||
String() string
|
||||
}
|
||||
|
||||
type CommitResponse struct {
|
||||
ValidationError error
|
||||
RequiresRestart bool
|
||||
}
|
||||
|
||||
// A wrapper around a Configuration that manages loads, saves and published
|
||||
// notifications of changes to registered Handlers
|
||||
|
||||
@@ -58,6 +54,8 @@ type Wrapper struct {
|
||||
replaces chan Configuration
|
||||
subs []Committer
|
||||
mut sync.Mutex
|
||||
|
||||
requiresRestart uint32 // an atomic bool
|
||||
}
|
||||
|
||||
// Wrap wraps an existing Configuration structure and ties it to a file on
|
||||
@@ -128,32 +126,21 @@ func (w *Wrapper) Raw() Configuration {
|
||||
}
|
||||
|
||||
// Replace swaps the current configuration object for the given one.
|
||||
func (w *Wrapper) Replace(cfg Configuration) CommitResponse {
|
||||
func (w *Wrapper) Replace(cfg Configuration) error {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
|
||||
return w.replaceLocked(cfg)
|
||||
}
|
||||
|
||||
func (w *Wrapper) replaceLocked(to Configuration) CommitResponse {
|
||||
func (w *Wrapper) replaceLocked(to Configuration) error {
|
||||
from := w.cfg
|
||||
|
||||
for _, sub := range w.subs {
|
||||
l.Debugln(sub, "verifying configuration")
|
||||
if err := sub.VerifyConfiguration(from, to); err != nil {
|
||||
l.Debugln(sub, "rejected config:", err)
|
||||
return CommitResponse{
|
||||
ValidationError: err,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
allOk := true
|
||||
for _, sub := range w.subs {
|
||||
l.Debugln(sub, "committing configuration")
|
||||
ok := sub.CommitConfiguration(from, to)
|
||||
if !ok {
|
||||
l.Debugln(sub, "requires restart")
|
||||
allOk = false
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,8 +148,22 @@ func (w *Wrapper) replaceLocked(to Configuration) CommitResponse {
|
||||
w.deviceMap = nil
|
||||
w.folderMap = nil
|
||||
|
||||
return CommitResponse{
|
||||
RequiresRestart: !allOk,
|
||||
w.notifyListeners(from, to)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *Wrapper) notifyListeners(from, to Configuration) {
|
||||
for _, sub := range w.subs {
|
||||
go w.notifyListener(sub, from, to)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Wrapper) notifyListener(sub Committer, from, to Configuration) {
|
||||
l.Debugln(sub, "committing configuration")
|
||||
if !sub.CommitConfiguration(from, to) {
|
||||
l.Debugln(sub, "requires restart")
|
||||
w.setRequiresRestart()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,7 +183,7 @@ func (w *Wrapper) Devices() map[protocol.DeviceID]DeviceConfiguration {
|
||||
|
||||
// SetDevice adds a new device to the configuration, or overwrites an existing
|
||||
// device with the same ID.
|
||||
func (w *Wrapper) SetDevice(dev DeviceConfiguration) CommitResponse {
|
||||
func (w *Wrapper) SetDevice(dev DeviceConfiguration) error {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
|
||||
@@ -218,7 +219,7 @@ func (w *Wrapper) Folders() map[string]FolderConfiguration {
|
||||
|
||||
// SetFolder adds a new folder to the configuration, or overwrites an existing
|
||||
// folder with the same ID.
|
||||
func (w *Wrapper) SetFolder(fld FolderConfiguration) CommitResponse {
|
||||
func (w *Wrapper) SetFolder(fld FolderConfiguration) error {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
|
||||
@@ -246,7 +247,7 @@ func (w *Wrapper) Options() OptionsConfiguration {
|
||||
}
|
||||
|
||||
// SetOptions replaces the current options configuration object.
|
||||
func (w *Wrapper) SetOptions(opts OptionsConfiguration) CommitResponse {
|
||||
func (w *Wrapper) SetOptions(opts OptionsConfiguration) error {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
newCfg := w.cfg.Copy()
|
||||
@@ -262,7 +263,7 @@ func (w *Wrapper) GUI() GUIConfiguration {
|
||||
}
|
||||
|
||||
// SetGUI replaces the current GUI configuration object.
|
||||
func (w *Wrapper) SetGUI(gui GUIConfiguration) CommitResponse {
|
||||
func (w *Wrapper) SetGUI(gui GUIConfiguration) error {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
newCfg := w.cfg.Copy()
|
||||
@@ -332,3 +333,11 @@ func (w *Wrapper) ListenAddresses() []string {
|
||||
}
|
||||
return util.UniqueStrings(addresses)
|
||||
}
|
||||
|
||||
func (w *Wrapper) RequiresRestart() bool {
|
||||
return atomic.LoadUint32(&w.requiresRestart) != 0
|
||||
}
|
||||
|
||||
func (w *Wrapper) setRequiresRestart() {
|
||||
atomic.StoreUint32(&w.requiresRestart, 1)
|
||||
}
|
||||
|
||||
+1
-17
@@ -11,27 +11,10 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/opt"
|
||||
)
|
||||
|
||||
var (
|
||||
clockTick int64
|
||||
clockMut = sync.NewMutex()
|
||||
)
|
||||
|
||||
func clock(v int64) int64 {
|
||||
clockMut.Lock()
|
||||
defer clockMut.Unlock()
|
||||
if v > clockTick {
|
||||
clockTick = v + 1
|
||||
} else {
|
||||
clockTick++
|
||||
}
|
||||
return clockTick
|
||||
}
|
||||
|
||||
const (
|
||||
KeyTypeDevice = iota
|
||||
KeyTypeGlobal
|
||||
@@ -41,6 +24,7 @@ const (
|
||||
KeyTypeVirtualMtime
|
||||
KeyTypeFolderIdx
|
||||
KeyTypeDeviceIdx
|
||||
KeyTypeIndexID
|
||||
)
|
||||
|
||||
func (l VersionList) String() string {
|
||||
|
||||
@@ -24,7 +24,7 @@ import (
|
||||
"github.com/syndtr/goleveldb/leveldb/util"
|
||||
)
|
||||
|
||||
type deletionHandler func(t readWriteTransaction, folder, device, name []byte, dbi iterator.Iterator) int64
|
||||
type deletionHandler func(t readWriteTransaction, folder, device, name []byte, dbi iterator.Iterator)
|
||||
|
||||
type Instance struct {
|
||||
committed int64 // this must be the first attribute in the struct to ensure 64 bit alignment on 32 bit plaforms
|
||||
@@ -86,7 +86,7 @@ func (db *Instance) Committed() int64 {
|
||||
return atomic.LoadInt64(&db.committed)
|
||||
}
|
||||
|
||||
func (db *Instance) genericReplace(folder, device []byte, fs []protocol.FileInfo, localSize, globalSize *sizeTracker, deleteFn deletionHandler) int64 {
|
||||
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
|
||||
|
||||
t := db.newReadWriteTransaction()
|
||||
@@ -97,7 +97,6 @@ func (db *Instance) genericReplace(folder, device []byte, fs []protocol.FileInfo
|
||||
|
||||
moreDb := dbi.Next()
|
||||
fsi := 0
|
||||
var maxLocalVer int64
|
||||
|
||||
isLocalDevice := bytes.Equal(device, protocol.LocalDeviceID[:])
|
||||
for {
|
||||
@@ -124,9 +123,7 @@ func (db *Instance) genericReplace(folder, device []byte, fs []protocol.FileInfo
|
||||
case moreFs && (!moreDb || cmp == -1):
|
||||
l.Debugln("generic replace; missing - insert")
|
||||
// Database is missing this file. Insert it.
|
||||
if lv := t.insertFile(folder, device, fs[fsi]); lv > maxLocalVer {
|
||||
maxLocalVer = lv
|
||||
}
|
||||
t.insertFile(folder, device, fs[fsi])
|
||||
if isLocalDevice {
|
||||
localSize.addFile(fs[fsi])
|
||||
}
|
||||
@@ -146,9 +143,7 @@ func (db *Instance) genericReplace(folder, device []byte, fs []protocol.FileInfo
|
||||
ef.Unmarshal(dbi.Value())
|
||||
if !fs[fsi].Version.Equal(ef.Version) || fs[fsi].Invalid != ef.Invalid {
|
||||
l.Debugln("generic replace; differs - insert")
|
||||
if lv := t.insertFile(folder, device, fs[fsi]); lv > maxLocalVer {
|
||||
maxLocalVer = lv
|
||||
}
|
||||
t.insertFile(folder, device, fs[fsi])
|
||||
if isLocalDevice {
|
||||
localSize.removeFile(ef)
|
||||
localSize.addFile(fs[fsi])
|
||||
@@ -167,9 +162,7 @@ func (db *Instance) genericReplace(folder, device []byte, fs []protocol.FileInfo
|
||||
|
||||
case moreDb && (!moreFs || cmp == 1):
|
||||
l.Debugln("generic replace; exists - remove")
|
||||
if lv := deleteFn(t, folder, device, oldName, dbi); lv > maxLocalVer {
|
||||
maxLocalVer = lv
|
||||
}
|
||||
deleteFn(t, folder, device, oldName, dbi)
|
||||
moreDb = dbi.Next()
|
||||
}
|
||||
|
||||
@@ -177,26 +170,21 @@ func (db *Instance) genericReplace(folder, device []byte, fs []protocol.FileInfo
|
||||
// growing too large and thus allocating unnecessarily much memory.
|
||||
t.checkFlush()
|
||||
}
|
||||
|
||||
return maxLocalVer
|
||||
}
|
||||
|
||||
func (db *Instance) replace(folder, device []byte, fs []protocol.FileInfo, localSize, globalSize *sizeTracker) int64 {
|
||||
// TODO: Return the remaining maxLocalVer?
|
||||
return db.genericReplace(folder, device, fs, localSize, globalSize, func(t readWriteTransaction, folder, device, name []byte, dbi iterator.Iterator) int64 {
|
||||
func (db *Instance) replace(folder, device []byte, fs []protocol.FileInfo, localSize, globalSize *sizeTracker) {
|
||||
db.genericReplace(folder, device, fs, localSize, globalSize, func(t readWriteTransaction, folder, device, name []byte, dbi iterator.Iterator) {
|
||||
// Database has a file that we are missing. Remove it.
|
||||
l.Debugf("delete; folder=%q device=%v name=%q", folder, protocol.DeviceIDFromBytes(device), name)
|
||||
t.removeFromGlobal(folder, device, name, globalSize)
|
||||
t.Delete(dbi.Key())
|
||||
return 0
|
||||
})
|
||||
}
|
||||
|
||||
func (db *Instance) updateFiles(folder, device []byte, fs []protocol.FileInfo, localSize, globalSize *sizeTracker) int64 {
|
||||
func (db *Instance) updateFiles(folder, device []byte, fs []protocol.FileInfo, localSize, globalSize *sizeTracker) {
|
||||
t := db.newReadWriteTransaction()
|
||||
defer t.close()
|
||||
|
||||
var maxLocalVer int64
|
||||
var fk []byte
|
||||
isLocalDevice := bytes.Equal(device, protocol.LocalDeviceID[:])
|
||||
for _, f := range fs {
|
||||
@@ -208,9 +196,7 @@ func (db *Instance) updateFiles(folder, device []byte, fs []protocol.FileInfo, l
|
||||
localSize.addFile(f)
|
||||
}
|
||||
|
||||
if lv := t.insertFile(folder, device, f); lv > maxLocalVer {
|
||||
maxLocalVer = lv
|
||||
}
|
||||
t.insertFile(folder, device, f)
|
||||
if f.IsInvalid() {
|
||||
t.removeFromGlobal(folder, device, name, globalSize)
|
||||
} else {
|
||||
@@ -231,9 +217,7 @@ func (db *Instance) updateFiles(folder, device []byte, fs []protocol.FileInfo, l
|
||||
localSize.addFile(f)
|
||||
}
|
||||
|
||||
if lv := t.insertFile(folder, device, f); lv > maxLocalVer {
|
||||
maxLocalVer = lv
|
||||
}
|
||||
t.insertFile(folder, device, f)
|
||||
if f.IsInvalid() {
|
||||
t.removeFromGlobal(folder, device, name, globalSize)
|
||||
} else {
|
||||
@@ -245,8 +229,6 @@ func (db *Instance) updateFiles(folder, device []byte, fs []protocol.FileInfo, l
|
||||
// growing too large and thus allocating unnecessarily much memory.
|
||||
t.checkFlush()
|
||||
}
|
||||
|
||||
return maxLocalVer
|
||||
}
|
||||
|
||||
func (db *Instance) withHave(folder, device, prefix []byte, truncate bool, fn Iterator) {
|
||||
@@ -699,6 +681,37 @@ func (db *Instance) globalKeyFolder(key []byte) []byte {
|
||||
return folder
|
||||
}
|
||||
|
||||
func (db *Instance) getIndexID(device, folder []byte) protocol.IndexID {
|
||||
key := db.indexIDKey(device, folder)
|
||||
cur, err := db.Get(key, nil)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
var id protocol.IndexID
|
||||
if err := id.Unmarshal(cur); err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
func (db *Instance) setIndexID(device, folder []byte, id protocol.IndexID) {
|
||||
key := db.indexIDKey(device, folder)
|
||||
bs, _ := id.Marshal() // marshalling can't fail
|
||||
if err := db.Put(key, bs, nil); err != nil {
|
||||
panic("storing index ID: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (db *Instance) indexIDKey(device, folder []byte) []byte {
|
||||
k := make([]byte, keyPrefixLen+keyDeviceLen+keyFolderLen)
|
||||
k[0] = KeyTypeIndexID
|
||||
binary.BigEndian.PutUint32(k[keyPrefixLen:], db.deviceIdx.ID(device))
|
||||
binary.BigEndian.PutUint32(k[keyPrefixLen+keyDeviceLen:], db.folderIdx.ID(folder))
|
||||
return k
|
||||
}
|
||||
|
||||
func unmarshalTrunc(bs []byte, truncate bool) (FileIntf, error) {
|
||||
if truncate {
|
||||
var tf FileInfoTruncated
|
||||
|
||||
@@ -74,18 +74,12 @@ func (t readWriteTransaction) flush() {
|
||||
atomic.AddInt64(&t.db.committed, int64(t.Batch.Len()))
|
||||
}
|
||||
|
||||
func (t readWriteTransaction) insertFile(folder, device []byte, file protocol.FileInfo) int64 {
|
||||
func (t readWriteTransaction) insertFile(folder, device []byte, file protocol.FileInfo) {
|
||||
l.Debugf("insert; folder=%q device=%v %v", folder, protocol.DeviceIDFromBytes(device), file)
|
||||
|
||||
if file.LocalVersion == 0 {
|
||||
file.LocalVersion = clock(0)
|
||||
}
|
||||
|
||||
name := []byte(file.Name)
|
||||
nk := t.db.deviceKey(folder, device, name)
|
||||
t.Put(nk, mustMarshal(&file))
|
||||
|
||||
return file.LocalVersion
|
||||
}
|
||||
|
||||
// updateGlobal adds this device+version to the version list for the given
|
||||
|
||||
+80
-26
@@ -14,6 +14,7 @@ package db
|
||||
|
||||
import (
|
||||
stdsync "sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
@@ -21,13 +22,15 @@ import (
|
||||
)
|
||||
|
||||
type FileSet struct {
|
||||
localVersion map[protocol.DeviceID]int64
|
||||
mutex sync.Mutex
|
||||
localVersion int64 // Our local version counter
|
||||
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
|
||||
}
|
||||
|
||||
// FileIntf is the set of methods implemented by both protocol.FileInfo and
|
||||
@@ -95,11 +98,11 @@ func (s *sizeTracker) Size() (files, deleted int, bytes int64) {
|
||||
|
||||
func NewFileSet(folder string, db *Instance) *FileSet {
|
||||
var s = FileSet{
|
||||
localVersion: make(map[protocol.DeviceID]int64),
|
||||
folder: folder,
|
||||
db: db,
|
||||
blockmap: NewBlockMap(db, db.folderIdx.ID([]byte(folder))),
|
||||
mutex: sync.NewMutex(),
|
||||
remoteLocalVersion: 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)
|
||||
@@ -107,16 +110,17 @@ func NewFileSet(folder string, db *Instance) *FileSet {
|
||||
var deviceID protocol.DeviceID
|
||||
s.db.withAllFolderTruncated([]byte(folder), func(device []byte, f FileInfoTruncated) bool {
|
||||
copy(deviceID[:], device)
|
||||
if f.LocalVersion > s.localVersion[deviceID] {
|
||||
s.localVersion[deviceID] = f.LocalVersion
|
||||
}
|
||||
if deviceID == protocol.LocalDeviceID {
|
||||
if f.LocalVersion > s.localVersion {
|
||||
s.localVersion = f.LocalVersion
|
||||
}
|
||||
s.localSize.addFile(f)
|
||||
} else if f.LocalVersion > s.remoteLocalVersion[deviceID] {
|
||||
s.remoteLocalVersion[deviceID] = f.LocalVersion
|
||||
}
|
||||
return true
|
||||
})
|
||||
l.Debugf("loaded localVersion for %q: %#v", folder, s.localVersion)
|
||||
clock(s.localVersion[protocol.LocalDeviceID])
|
||||
|
||||
return &s
|
||||
}
|
||||
@@ -124,13 +128,25 @@ func NewFileSet(folder string, db *Instance) *FileSet {
|
||||
func (s *FileSet) Replace(device protocol.DeviceID, fs []protocol.FileInfo) {
|
||||
l.Debugf("%s Replace(%v, [%d])", s.folder, device, len(fs))
|
||||
normalizeFilenames(fs)
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
s.localVersion[device] = s.db.replace([]byte(s.folder), device[:], fs, &s.localSize, &s.globalSize)
|
||||
if len(fs) == 0 {
|
||||
// Reset the local version if all files were removed.
|
||||
s.localVersion[device] = 0
|
||||
|
||||
s.updateMutex.Lock()
|
||||
defer s.updateMutex.Unlock()
|
||||
|
||||
if device == protocol.LocalDeviceID {
|
||||
if len(fs) == 0 {
|
||||
s.localVersion = 0
|
||||
} else {
|
||||
// Always overwrite LocalVersion 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)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
s.remoteLocalVersion[device] = maxLocalVersion(fs)
|
||||
}
|
||||
s.db.replace([]byte(s.folder), device[:], fs, &s.localSize, &s.globalSize)
|
||||
if device == protocol.LocalDeviceID {
|
||||
s.blockmap.Drop()
|
||||
s.blockmap.Add(fs)
|
||||
@@ -140,12 +156,15 @@ func (s *FileSet) Replace(device protocol.DeviceID, fs []protocol.FileInfo) {
|
||||
func (s *FileSet) Update(device protocol.DeviceID, fs []protocol.FileInfo) {
|
||||
l.Debugf("%s Update(%v, [%d])", s.folder, device, len(fs))
|
||||
normalizeFilenames(fs)
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
s.updateMutex.Lock()
|
||||
defer s.updateMutex.Unlock()
|
||||
|
||||
if device == protocol.LocalDeviceID {
|
||||
discards := make([]protocol.FileInfo, 0, len(fs))
|
||||
updates := make([]protocol.FileInfo, 0, len(fs))
|
||||
for _, newFile := range fs {
|
||||
for i, newFile := range fs {
|
||||
fs[i].LocalVersion = atomic.AddInt64(&s.localVersion, 1)
|
||||
existingFile, ok := s.db.getFile([]byte(s.folder), device[:], []byte(newFile.Name))
|
||||
if !ok || !existingFile.Version.Equal(newFile.Version) {
|
||||
discards = append(discards, existingFile)
|
||||
@@ -154,10 +173,10 @@ func (s *FileSet) Update(device protocol.DeviceID, fs []protocol.FileInfo) {
|
||||
}
|
||||
s.blockmap.Discard(discards)
|
||||
s.blockmap.Update(updates)
|
||||
} else {
|
||||
s.remoteLocalVersion[device] = maxLocalVersion(fs)
|
||||
}
|
||||
if lv := s.db.updateFiles([]byte(s.folder), device[:], fs, &s.localSize, &s.globalSize); lv > s.localVersion[device] {
|
||||
s.localVersion[device] = lv
|
||||
}
|
||||
s.db.updateFiles([]byte(s.folder), device[:], fs, &s.localSize, &s.globalSize)
|
||||
}
|
||||
|
||||
func (s *FileSet) WithNeed(device protocol.DeviceID, fn Iterator) {
|
||||
@@ -230,9 +249,13 @@ func (s *FileSet) Availability(file string) []protocol.DeviceID {
|
||||
}
|
||||
|
||||
func (s *FileSet) LocalVersion(device protocol.DeviceID) int64 {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
return s.localVersion[device]
|
||||
if device == protocol.LocalDeviceID {
|
||||
return atomic.LoadInt64(&s.localVersion)
|
||||
}
|
||||
|
||||
s.updateMutex.Lock()
|
||||
defer s.updateMutex.Unlock()
|
||||
return s.remoteLocalVersion[device]
|
||||
}
|
||||
|
||||
func (s *FileSet) LocalSize() (files, deleted int, bytes int64) {
|
||||
@@ -243,6 +266,37 @@ func (s *FileSet) GlobalSize() (files, deleted int, bytes int64) {
|
||||
return s.globalSize.Size()
|
||||
}
|
||||
|
||||
func (s *FileSet) IndexID(device protocol.DeviceID) protocol.IndexID {
|
||||
id := s.db.getIndexID(device[:], []byte(s.folder))
|
||||
if id == 0 && device == protocol.LocalDeviceID {
|
||||
// No index ID set yet. We create one now.
|
||||
id = protocol.NewIndexID()
|
||||
s.db.setIndexID(device[:], []byte(s.folder), id)
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
func (s *FileSet) SetIndexID(device protocol.DeviceID, id protocol.IndexID) {
|
||||
if device == protocol.LocalDeviceID {
|
||||
panic("do not explicitly set index ID for local device")
|
||||
}
|
||||
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
|
||||
// the last item, but Syncthing v0.14.0 and other implementations may not
|
||||
// implement update sorting....
|
||||
func maxLocalVersion(fs []protocol.FileInfo) int64 {
|
||||
var max int64
|
||||
for _, f := range fs {
|
||||
if f.LocalVersion > max {
|
||||
max = f.LocalVersion
|
||||
}
|
||||
}
|
||||
return max
|
||||
}
|
||||
|
||||
// DropFolder clears out all information related to the given folder from the
|
||||
// database.
|
||||
func DropFolder(db *Instance, folder string) {
|
||||
|
||||
+37
-5
@@ -100,11 +100,11 @@ func TestGlobalSet(t *testing.T) {
|
||||
m := db.NewFileSet("test", ldb)
|
||||
|
||||
local0 := fileList{
|
||||
protocol.FileInfo{Name: "a", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(1)},
|
||||
protocol.FileInfo{Name: "b", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(2)},
|
||||
protocol.FileInfo{Name: "c", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(3)},
|
||||
protocol.FileInfo{Name: "d", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(4)},
|
||||
protocol.FileInfo{Name: "z", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(8)},
|
||||
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)},
|
||||
}
|
||||
local1 := fileList{
|
||||
protocol.FileInfo{Name: "a", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(1)},
|
||||
@@ -687,3 +687,35 @@ func BenchmarkUpdateOneFile(b *testing.B) {
|
||||
|
||||
b.ReportAllocs()
|
||||
}
|
||||
|
||||
func TestIndexID(t *testing.T) {
|
||||
ldb := db.OpenMemory()
|
||||
|
||||
s := db.NewFileSet("test", ldb)
|
||||
|
||||
// The Index ID for some random device is zero by default.
|
||||
id := s.IndexID(remoteDevice0)
|
||||
if id != 0 {
|
||||
t.Errorf("index ID for remote device should default to zero, not %d", id)
|
||||
}
|
||||
|
||||
// The Index ID for someone else should be settable
|
||||
s.SetIndexID(remoteDevice0, 42)
|
||||
id = s.IndexID(remoteDevice0)
|
||||
if id != 42 {
|
||||
t.Errorf("index ID for remote device should be remembered; got %d, expected %d", id, 42)
|
||||
}
|
||||
|
||||
// Our own index ID should be generated randomly.
|
||||
id = s.IndexID(protocol.LocalDeviceID)
|
||||
if id == 0 {
|
||||
t.Errorf("index ID for local device should be random, not zero")
|
||||
}
|
||||
t.Logf("random index ID is 0x%016x", id)
|
||||
|
||||
// But of course always the same after that.
|
||||
again := s.IndexID(protocol.LocalDeviceID)
|
||||
if again != id {
|
||||
t.Errorf("index ID changed; %d != %d", again, id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ type CacheEntry struct {
|
||||
when time.Time // When did we get the result
|
||||
found bool // Is it a success (cacheTime applies) or a failure (negCacheTime applies)?
|
||||
validUntil time.Time // Validity time, overrides normal calculation
|
||||
instanceID int64 // for local discovery, the instance ID (random on each restart)
|
||||
}
|
||||
|
||||
// A FinderService is a Finder that has background activity and must be run as
|
||||
|
||||
+11
-6
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/syncthing/syncthing/lib/beacon"
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/rand"
|
||||
"github.com/thejerf/suture"
|
||||
)
|
||||
|
||||
@@ -114,8 +115,9 @@ func (c *localClient) Error() error {
|
||||
|
||||
func (c *localClient) announcementPkt() Announce {
|
||||
return Announce{
|
||||
ID: c.myID[:],
|
||||
Addresses: c.addrList.AllAddresses(),
|
||||
ID: c.myID[:],
|
||||
Addresses: c.addrList.AllAddresses(),
|
||||
InstanceID: rand.Int63(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,9 +196,11 @@ func (c *localClient) registerDevice(src net.Addr, device Announce) bool {
|
||||
copy(id[:], device.ID)
|
||||
|
||||
// Remember whether we already had a valid cache entry for this device.
|
||||
// If the instance ID has changed the remote device has restarted since
|
||||
// we last heard from it, so we should treat it as a new device.
|
||||
|
||||
ce, existsAlready := c.Get(id)
|
||||
isNewDevice := !existsAlready || time.Since(ce.when) > CacheLifeTime
|
||||
isNewDevice := !existsAlready || time.Since(ce.when) > CacheLifeTime || ce.instanceID != device.InstanceID
|
||||
|
||||
// Any empty or unspecified addresses should be set to the source address
|
||||
// of the announcement. We also skip any addresses we can't parse.
|
||||
@@ -245,9 +249,10 @@ func (c *localClient) registerDevice(src net.Addr, device Announce) bool {
|
||||
}
|
||||
|
||||
c.Set(id, CacheEntry{
|
||||
Addresses: validAddresses,
|
||||
when: time.Now(),
|
||||
found: true,
|
||||
Addresses: validAddresses,
|
||||
when: time.Now(),
|
||||
found: true,
|
||||
instanceID: device.InstanceID,
|
||||
})
|
||||
|
||||
if isNewDevice {
|
||||
|
||||
@@ -30,8 +30,9 @@ var _ = math.Inf
|
||||
const _ = proto.GoGoProtoPackageIsVersion1
|
||||
|
||||
type Announce struct {
|
||||
ID []byte `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
Addresses []string `protobuf:"bytes,2,rep,name=addresses" json:"addresses,omitempty"`
|
||||
ID []byte `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
Addresses []string `protobuf:"bytes,2,rep,name=addresses" json:"addresses,omitempty"`
|
||||
InstanceID int64 `protobuf:"varint,3,opt,name=instance_id,json=instanceId,proto3" json:"instance_id,omitempty"`
|
||||
}
|
||||
|
||||
func (m *Announce) Reset() { *m = Announce{} }
|
||||
@@ -78,6 +79,11 @@ func (m *Announce) MarshalTo(data []byte) (int, error) {
|
||||
i += copy(data[i:], s)
|
||||
}
|
||||
}
|
||||
if m.InstanceID != 0 {
|
||||
data[i] = 0x18
|
||||
i++
|
||||
i = encodeVarintLocal(data, i, uint64(m.InstanceID))
|
||||
}
|
||||
return i, nil
|
||||
}
|
||||
|
||||
@@ -121,6 +127,9 @@ func (m *Announce) ProtoSize() (n int) {
|
||||
n += 1 + l + sovLocal(uint64(l))
|
||||
}
|
||||
}
|
||||
if m.InstanceID != 0 {
|
||||
n += 1 + sovLocal(uint64(m.InstanceID))
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
@@ -226,6 +235,25 @@ func (m *Announce) Unmarshal(data []byte) error {
|
||||
}
|
||||
m.Addresses = append(m.Addresses, string(data[iNdEx:postIndex]))
|
||||
iNdEx = postIndex
|
||||
case 3:
|
||||
if wireType != 0 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field InstanceID", wireType)
|
||||
}
|
||||
m.InstanceID = 0
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowLocal
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := data[iNdEx]
|
||||
iNdEx++
|
||||
m.InstanceID |= (int64(b) & 0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
default:
|
||||
iNdEx = preIndex
|
||||
skippy, err := skipLocal(data[iNdEx:])
|
||||
@@ -353,16 +381,18 @@ var (
|
||||
)
|
||||
|
||||
var fileDescriptorLocal = []byte{
|
||||
// 161 bytes of a gzipped FileDescriptorProto
|
||||
// 194 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0xe2, 0xce, 0xc9, 0x4f, 0x4e,
|
||||
0xcc, 0xd1, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x48, 0xc9, 0x2c, 0x4e, 0xce, 0x2f, 0x4b,
|
||||
0x2d, 0x92, 0xd2, 0x4d, 0xcf, 0x2c, 0xc9, 0x28, 0x4d, 0xd2, 0x4b, 0xce, 0xcf, 0xd5, 0x4f, 0xcf,
|
||||
0x4f, 0xcf, 0xd7, 0x07, 0x2b, 0x48, 0x2a, 0x4d, 0x03, 0xf3, 0xc0, 0x1c, 0x30, 0x0b, 0xa2, 0x51,
|
||||
0xc9, 0x81, 0x8b, 0xc3, 0x31, 0x2f, 0x2f, 0xbf, 0x34, 0x2f, 0x39, 0x55, 0x48, 0x8c, 0x8b, 0x29,
|
||||
0xa9, 0x90, 0x8b, 0xc3, 0x31, 0x2f, 0x2f, 0xbf, 0x34, 0x2f, 0x39, 0x55, 0x48, 0x8c, 0x8b, 0x29,
|
||||
0x33, 0x45, 0x82, 0x51, 0x81, 0x51, 0x83, 0xc7, 0x89, 0xed, 0xd1, 0x3d, 0x79, 0x26, 0x4f, 0x97,
|
||||
0x20, 0xa0, 0x88, 0x90, 0x0c, 0x17, 0x67, 0x62, 0x4a, 0x4a, 0x51, 0x6a, 0x71, 0x71, 0x6a, 0xb1,
|
||||
0x04, 0x93, 0x02, 0xb3, 0x06, 0x67, 0x10, 0x42, 0xc0, 0x49, 0xe4, 0xc4, 0x43, 0x39, 0x86, 0x13,
|
||||
0x8f, 0xe4, 0x18, 0x2f, 0x00, 0xf1, 0x83, 0x47, 0x72, 0x0c, 0x0b, 0x1e, 0xcb, 0x31, 0x26, 0xb1,
|
||||
0x81, 0x8d, 0x37, 0x06, 0x04, 0x00, 0x00, 0xff, 0xff, 0xc9, 0xec, 0xea, 0xbc, 0xa6, 0x00, 0x00,
|
||||
0x00,
|
||||
0x04, 0x93, 0x02, 0xb3, 0x06, 0x67, 0x10, 0x42, 0x40, 0x48, 0x9f, 0x8b, 0x3b, 0x33, 0xaf, 0xb8,
|
||||
0x24, 0x11, 0x68, 0x42, 0x3c, 0x50, 0x3b, 0x33, 0x50, 0x3b, 0xb3, 0x13, 0x1f, 0x50, 0x3b, 0x97,
|
||||
0x27, 0x54, 0x18, 0x68, 0x0c, 0x17, 0x4c, 0x89, 0x67, 0x8a, 0x93, 0xc8, 0x89, 0x87, 0x72, 0x0c,
|
||||
0x27, 0x1e, 0xc9, 0x31, 0x5e, 0x00, 0xe2, 0x07, 0x8f, 0xe4, 0x18, 0x16, 0x3c, 0x96, 0x63, 0x4c,
|
||||
0x62, 0x03, 0xbb, 0xc7, 0x18, 0x10, 0x00, 0x00, 0xff, 0xff, 0x91, 0x3f, 0x96, 0x25, 0xd7, 0x00,
|
||||
0x00, 0x00,
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ option (gogoproto.sizer_all) = false;
|
||||
option (gogoproto.protosizer_all) = true;
|
||||
|
||||
message Announce {
|
||||
bytes id = 1 [(gogoproto.customname) = "ID"];
|
||||
repeated string addresses = 2;
|
||||
bytes id = 1 [(gogoproto.customname) = "ID"];
|
||||
repeated string addresses = 2;
|
||||
int64 instance_id = 3 [(gogoproto.customname) = "InstanceID"];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
// 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 discover
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
)
|
||||
|
||||
func TestRandomLocalInstanceID(t *testing.T) {
|
||||
c, err := NewLocal(protocol.LocalDeviceID, ":0", &fakeAddressLister{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
go c.Serve()
|
||||
defer c.Stop()
|
||||
|
||||
lc := c.(*localClient)
|
||||
|
||||
p0 := lc.announcementPkt()
|
||||
p1 := lc.announcementPkt()
|
||||
if p0.InstanceID == p1.InstanceID {
|
||||
t.Error("each generated packet should have a new instance id")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalInstanceIDShouldTriggerNew(t *testing.T) {
|
||||
c, err := NewLocal(protocol.LocalDeviceID, ":0", &fakeAddressLister{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
lc := c.(*localClient)
|
||||
src := &net.UDPAddr{IP: []byte{10, 20, 30, 40}, Port: 50}
|
||||
|
||||
new := lc.registerDevice(src, Announce{
|
||||
ID: []byte{10, 20, 30, 40, 50, 60, 70, 80, 90},
|
||||
Addresses: []string{"tcp://0.0.0.0:22000"},
|
||||
InstanceID: 1234567890,
|
||||
})
|
||||
|
||||
if !new {
|
||||
t.Fatal("first register should be new")
|
||||
}
|
||||
|
||||
new = lc.registerDevice(src, Announce{
|
||||
ID: []byte{10, 20, 30, 40, 50, 60, 70, 80, 90},
|
||||
Addresses: []string{"tcp://0.0.0.0:22000"},
|
||||
InstanceID: 1234567890,
|
||||
})
|
||||
|
||||
if new {
|
||||
t.Fatal("second register should not be new")
|
||||
}
|
||||
|
||||
new = lc.registerDevice(src, Announce{
|
||||
ID: []byte{42, 10, 20, 30, 40, 50, 60, 70, 80, 90},
|
||||
Addresses: []string{"tcp://0.0.0.0:22000"},
|
||||
InstanceID: 1234567890,
|
||||
})
|
||||
|
||||
if !new {
|
||||
t.Fatal("new device ID should be new")
|
||||
}
|
||||
|
||||
new = lc.registerDevice(src, Announce{
|
||||
ID: []byte{10, 20, 30, 40, 50, 60, 70, 80, 90},
|
||||
Addresses: []string{"tcp://0.0.0.0:22000"},
|
||||
InstanceID: 91234567890,
|
||||
})
|
||||
|
||||
if !new {
|
||||
t.Fatal("new instance ID should be new")
|
||||
}
|
||||
}
|
||||
+110
-32
@@ -8,6 +8,7 @@ package model
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -647,6 +648,10 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
|
||||
|
||||
tempIndexFolders := make([]string, 0, len(cm.Folders))
|
||||
|
||||
m.pmut.RLock()
|
||||
conn := m.conn[deviceID]
|
||||
m.pmut.RUnlock()
|
||||
|
||||
m.fmut.Lock()
|
||||
for _, folder := range cm.Folders {
|
||||
if !m.folderSharedWithUnlocked(folder.ID, deviceID) {
|
||||
@@ -661,6 +666,71 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
|
||||
if !folder.DisableTempIndexes {
|
||||
tempIndexFolders = append(tempIndexFolders, folder.ID)
|
||||
}
|
||||
|
||||
fs := m.folderFiles[folder.ID]
|
||||
myIndexID := fs.IndexID(protocol.LocalDeviceID)
|
||||
myLocalVersion := fs.LocalVersion(protocol.LocalDeviceID)
|
||||
var startLocalVersion int64
|
||||
|
||||
for _, dev := range folder.Devices {
|
||||
if bytes.Equal(dev.ID, m.id[:]) {
|
||||
// This is the other side's description of what it knows
|
||||
// about us. Lets check to see if we can start sending index
|
||||
// updates directly or need to send the index from start...
|
||||
|
||||
if dev.IndexID == myIndexID {
|
||||
// They say they've seen our index ID before, so we can
|
||||
// send a delta update only.
|
||||
|
||||
if dev.MaxLocalVersion > myLocalVersion {
|
||||
// Safety check. They claim to have more or newer
|
||||
// index data than we have - either we have lost
|
||||
// index data, or reset the index without resetting
|
||||
// the IndexID, or something else weird has
|
||||
// happened. We send a full index to reset the
|
||||
// situation.
|
||||
l.Infof("Device %v folder %q is delta index compatible, but seems out of sync with reality", deviceID, folder.ID)
|
||||
startLocalVersion = 0
|
||||
continue
|
||||
}
|
||||
|
||||
l.Infof("Device %v folder %q is delta index compatible (mlv=%d)", deviceID, folder.ID, dev.MaxLocalVersion)
|
||||
startLocalVersion = dev.MaxLocalVersion
|
||||
} else if dev.IndexID != 0 {
|
||||
// They say they've seen an index ID from us, but it's
|
||||
// not the right one. Either they are confused or we
|
||||
// must have reset our database since last talking to
|
||||
// them. We'll start with a full index transfer.
|
||||
l.Infof("Device %v folder %q has mismatching index ID for us (%v != %v)", deviceID, folder.ID, dev.IndexID, myIndexID)
|
||||
startLocalVersion = 0
|
||||
}
|
||||
} else if bytes.Equal(dev.ID, deviceID[:]) && dev.IndexID != 0 {
|
||||
// This is the other side's description of themselves. We
|
||||
// check to see that it matches the IndexID we have on file,
|
||||
// otherwise we drop our old index data and expect to get a
|
||||
// completely new set.
|
||||
|
||||
theirIndexID := fs.IndexID(deviceID)
|
||||
if dev.IndexID == 0 {
|
||||
// They're not announcing an index ID. This means they
|
||||
// do not support delta indexes and we should clear any
|
||||
// information we have from them before accepting their
|
||||
// index, which will presumably be a full index.
|
||||
fs.Replace(deviceID, nil)
|
||||
} else if dev.IndexID != theirIndexID {
|
||||
// The index ID we have on file is not what they're
|
||||
// announcing. They must have reset their database and
|
||||
// will probably send us a full index. We drop any
|
||||
// information we have and remember this new index ID
|
||||
// instead.
|
||||
l.Infof("Device %v folder %q has a new index ID (%v)", deviceID, folder.ID, dev.IndexID)
|
||||
fs.Replace(deviceID, nil)
|
||||
fs.SetIndexID(deviceID, dev.IndexID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
go sendIndexes(conn, folder.ID, fs, m.folderIgnores[folder.ID], startLocalVersion)
|
||||
}
|
||||
m.fmut.Unlock()
|
||||
|
||||
@@ -763,12 +833,6 @@ func (m *Model) Close(device protocol.DeviceID, err error) {
|
||||
})
|
||||
|
||||
m.pmut.Lock()
|
||||
m.fmut.RLock()
|
||||
for _, folder := range m.deviceFolders[device] {
|
||||
m.folderFiles[folder].Replace(device, nil)
|
||||
}
|
||||
m.fmut.RUnlock()
|
||||
|
||||
conn, ok := m.conn[device]
|
||||
if ok {
|
||||
m.progressEmitter.temporaryIndexUnsubscribe(conn)
|
||||
@@ -1044,13 +1108,6 @@ func (m *Model) AddConnection(conn connections.Connection, hello protocol.HelloR
|
||||
|
||||
cm := m.generateClusterConfig(deviceID)
|
||||
conn.ClusterConfig(cm)
|
||||
|
||||
m.fmut.RLock()
|
||||
for _, folder := range m.deviceFolders[deviceID] {
|
||||
fs := m.folderFiles[folder]
|
||||
go sendIndexes(conn, folder, fs, m.folderIgnores[folder])
|
||||
}
|
||||
m.fmut.RUnlock()
|
||||
m.pmut.Unlock()
|
||||
|
||||
device, ok := m.cfg.Devices()[deviceID]
|
||||
@@ -1146,15 +1203,15 @@ func (m *Model) receivedFile(folder string, file protocol.FileInfo) {
|
||||
m.folderStatRef(folder).ReceivedFile(file.Name, file.IsDeleted())
|
||||
}
|
||||
|
||||
func sendIndexes(conn protocol.Connection, folder string, fs *db.FileSet, ignores *ignore.Matcher) {
|
||||
func sendIndexes(conn protocol.Connection, folder string, fs *db.FileSet, ignores *ignore.Matcher, startLocalVersion int64) {
|
||||
deviceID := conn.ID()
|
||||
name := conn.Name()
|
||||
var err error
|
||||
|
||||
l.Debugf("sendIndexes for %s-%s/%q starting", deviceID, name, folder)
|
||||
l.Debugf("sendIndexes for %s-%s/%q starting (slv=%d)", deviceID, name, folder, startLocalVersion)
|
||||
defer l.Debugf("sendIndexes for %s-%s/%q exiting: %v", deviceID, name, folder, err)
|
||||
|
||||
minLocalVer, err := sendIndexTo(true, 0, conn, folder, fs, ignores)
|
||||
minLocalVer, err := sendIndexTo(startLocalVersion, conn, folder, fs, ignores)
|
||||
|
||||
// Subscribe to LocalIndexUpdated (we have new information to send) and
|
||||
// DeviceDisconnected (it might be us who disconnected, so we should
|
||||
@@ -1177,7 +1234,7 @@ func sendIndexes(conn protocol.Connection, folder string, fs *db.FileSet, ignore
|
||||
continue
|
||||
}
|
||||
|
||||
minLocalVer, err = sendIndexTo(false, minLocalVer, conn, folder, fs, ignores)
|
||||
minLocalVer, err = sendIndexTo(minLocalVer, conn, folder, fs, ignores)
|
||||
|
||||
// Wait a short amount of time before entering the next loop. If there
|
||||
// are continuous changes happening to the local index, this gives us
|
||||
@@ -1186,14 +1243,18 @@ func sendIndexes(conn protocol.Connection, folder string, fs *db.FileSet, ignore
|
||||
}
|
||||
}
|
||||
|
||||
func sendIndexTo(initial bool, minLocalVer int64, conn protocol.Connection, folder string, fs *db.FileSet, ignores *ignore.Matcher) (int64, error) {
|
||||
func sendIndexTo(minLocalVer int64, conn protocol.Connection, folder string, fs *db.FileSet, ignores *ignore.Matcher) (int64, error) {
|
||||
deviceID := conn.ID()
|
||||
name := conn.Name()
|
||||
batch := make([]protocol.FileInfo, 0, indexBatchSize)
|
||||
currentBatchSize := 0
|
||||
maxLocalVer := int64(0)
|
||||
initial := minLocalVer == 0
|
||||
maxLocalVer := minLocalVer
|
||||
var err error
|
||||
|
||||
sorter := NewIndexSorter()
|
||||
defer sorter.Close()
|
||||
|
||||
fs.WithHave(protocol.LocalDeviceID, func(fi db.FileIntf) bool {
|
||||
f := fi.(protocol.FileInfo)
|
||||
if f.LocalVersion <= minLocalVer {
|
||||
@@ -1209,6 +1270,11 @@ func sendIndexTo(initial bool, minLocalVer int64, conn protocol.Connection, fold
|
||||
return true
|
||||
}
|
||||
|
||||
sorter.Append(f)
|
||||
return true
|
||||
})
|
||||
|
||||
sorter.Sorted(func(f protocol.FileInfo) bool {
|
||||
if len(batch) == indexBatchSize || currentBatchSize > indexTargetSize {
|
||||
if initial {
|
||||
if err = conn.Index(folder, batch); err != nil {
|
||||
@@ -1293,7 +1359,7 @@ func (m *Model) localChangeDetected(folder, path string, files []protocol.FileIn
|
||||
objType := "file"
|
||||
action := "modified"
|
||||
|
||||
// If our local vector is verison 1 AND it is the only version
|
||||
// If our local vector is version 1 AND it is the only version
|
||||
// vector so far seen for this file then it is a new file. Else if
|
||||
// it is > 1 it's not new, and if it is 1 but another shortId
|
||||
// version vector exists then it is new for us but created elsewhere
|
||||
@@ -1311,11 +1377,8 @@ func (m *Model) localChangeDetected(folder, path string, files []protocol.FileIn
|
||||
action = "deleted"
|
||||
}
|
||||
|
||||
// If the file is a level or more deep then the forward slash seperator is embedded
|
||||
// in the filename and makes the path look wierd on windows, so lets fix it
|
||||
filename := filepath.FromSlash(file.Name)
|
||||
// And append it to the filepath
|
||||
path := filepath.Join(path, filename)
|
||||
// The full file path, adjusted to the local path separator character.
|
||||
path := filepath.Join(path, filepath.FromSlash(file.Name))
|
||||
|
||||
events.Default.Log(events.LocalChangeDetected, map[string]string{
|
||||
"folder": folder,
|
||||
@@ -1335,7 +1398,7 @@ func (m *Model) requestGlobal(deviceID protocol.DeviceID, folder, name string, o
|
||||
return nil, fmt.Errorf("requestGlobal: no such device: %s", deviceID)
|
||||
}
|
||||
|
||||
l.Debugf("%v REQ(out): %s: %q / %q o=%d s=%d h=%x ft=%t op=%s", m, deviceID, folder, name, offset, size, hash, fromTemporary)
|
||||
l.Debugf("%v REQ(out): %s: %q / %q o=%d s=%d h=%x ft=%t", m, deviceID, folder, name, offset, size, hash, fromTemporary)
|
||||
|
||||
return nc.Request(folder, name, offset, size, hash, fromTemporary)
|
||||
}
|
||||
@@ -1655,6 +1718,8 @@ func (m *Model) generateClusterConfig(device protocol.DeviceID) protocol.Cluster
|
||||
m.fmut.RLock()
|
||||
for _, folder := range m.deviceFolders[device] {
|
||||
folderCfg := m.cfg.Folders()[folder]
|
||||
fs := m.folderFiles[folder]
|
||||
|
||||
protocolFolder := protocol.Folder{
|
||||
ID: folder,
|
||||
Label: folderCfg.Label,
|
||||
@@ -1671,13 +1736,26 @@ func (m *Model) generateClusterConfig(device protocol.DeviceID) protocol.Cluster
|
||||
// TODO: Set read only bit when relevant, and when we have per device
|
||||
// access controls.
|
||||
deviceCfg := m.cfg.Devices()[device]
|
||||
|
||||
var indexID protocol.IndexID
|
||||
var maxLocalVersion int64
|
||||
if device == m.id {
|
||||
indexID = fs.IndexID(protocol.LocalDeviceID)
|
||||
maxLocalVersion = fs.LocalVersion(protocol.LocalDeviceID)
|
||||
} else {
|
||||
indexID = fs.IndexID(device)
|
||||
maxLocalVersion = fs.LocalVersion(device)
|
||||
}
|
||||
|
||||
protocolDevice := protocol.Device{
|
||||
ID: device[:],
|
||||
Name: deviceCfg.Name,
|
||||
Addresses: deviceCfg.Addresses,
|
||||
Compression: deviceCfg.Compression,
|
||||
CertName: deviceCfg.CertName,
|
||||
Introducer: deviceCfg.Introducer,
|
||||
ID: device[:],
|
||||
Name: deviceCfg.Name,
|
||||
Addresses: deviceCfg.Addresses,
|
||||
Compression: deviceCfg.Compression,
|
||||
CertName: deviceCfg.CertName,
|
||||
Introducer: deviceCfg.Introducer,
|
||||
IndexID: indexID,
|
||||
MaxLocalVersion: maxLocalVersion,
|
||||
}
|
||||
|
||||
protocolFolder.Devices = append(protocolFolder.Devices, protocolDevice)
|
||||
|
||||
@@ -563,7 +563,7 @@ func TestIgnores(t *testing.T) {
|
||||
|
||||
// Invalid path, marker should be missing, hence returns an error.
|
||||
m.AddFolder(config.FolderConfiguration{ID: "fresh", RawPath: "XXX"})
|
||||
ignores, _, err = m.GetIgnores("fresh")
|
||||
_, _, err = m.GetIgnores("fresh")
|
||||
if err == nil {
|
||||
t.Error("No error")
|
||||
}
|
||||
|
||||
+14
-5
@@ -433,12 +433,21 @@ func (f *rwFolder) pullerIteration(ignores *ignore.Matcher) int {
|
||||
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
|
||||
f.queue.Push(file.Name, file.Size, file.Modified)
|
||||
// A new or changed file or symlink. This is the only case where
|
||||
// we do stuff concurrently in the background. We only queue
|
||||
// files where we are connected to at least one device that has
|
||||
// the file.
|
||||
|
||||
devices := folderFiles.Availability(file.Name)
|
||||
for _, dev := range devices {
|
||||
if f.model.ConnectedTo(dev) {
|
||||
f.queue.Push(file.Name, file.Size, file.Modified)
|
||||
changed++
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
changed++
|
||||
return true
|
||||
})
|
||||
|
||||
@@ -932,7 +941,7 @@ func (f *rwFolder) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocks
|
||||
|
||||
// Check for an old temporary file which might have some blocks we could
|
||||
// reuse.
|
||||
tempBlocks, err := scanner.HashFile(tempName, protocol.BlockSize, 0, nil)
|
||||
tempBlocks, err := scanner.HashFile(tempName, protocol.BlockSize, nil)
|
||||
if err == nil {
|
||||
// Check for any reusable blocks in the temp file
|
||||
tempCopyBlocks, _ := scanner.BlockDiff(tempBlocks, file.Blocks)
|
||||
|
||||
@@ -223,7 +223,7 @@ func TestCopierFinder(t *testing.T) {
|
||||
}
|
||||
|
||||
// Verify that the fetched blocks have actually been written to the temp file
|
||||
blks, err := scanner.HashFile(tempFile, protocol.BlockSize, 0, nil)
|
||||
blks, err := scanner.HashFile(tempFile, protocol.BlockSize, nil)
|
||||
if err != nil {
|
||||
t.Log(err)
|
||||
}
|
||||
|
||||
@@ -121,24 +121,40 @@ func (s *sharedPullerState) tempFile() (io.WriterAt, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// The permissions to use for the temporary file should be those of the
|
||||
// final file, except we need user read & write at minimum. The
|
||||
// permissions will be set to the final value later, but in the meantime
|
||||
// we don't want to have a temporary file with looser permissions than
|
||||
// the final outcome.
|
||||
mode := os.FileMode(s.file.Permissions) | 0600
|
||||
if s.ignorePerms {
|
||||
// When ignorePerms is set we use a very permissive mode and let the
|
||||
// system umask filter it.
|
||||
mode = 0666
|
||||
}
|
||||
|
||||
// Attempt to create the temp file
|
||||
// RDWR because of issue #2994.
|
||||
flags := os.O_RDWR
|
||||
if s.reused == 0 {
|
||||
flags |= os.O_CREATE | os.O_EXCL
|
||||
} else {
|
||||
} else if !s.ignorePerms {
|
||||
// With sufficiently bad luck when exiting or crashing, we may have
|
||||
// had time to chmod the temp file to read only state but not yet
|
||||
// moved it to it's final name. This leaves us with a read only temp
|
||||
// file that we're going to try to reuse. To handle that, we need to
|
||||
// make sure we have write permissions on the file before opening it.
|
||||
err := os.Chmod(s.tempName, 0644)
|
||||
if !s.ignorePerms && err != nil {
|
||||
//
|
||||
// When ignorePerms is set we trust that the permissions are fine
|
||||
// already and make no modification, as we would otherwise override
|
||||
// what the umask dictates.
|
||||
|
||||
if err := os.Chmod(s.tempName, mode); err != nil {
|
||||
s.failLocked("dst create chmod", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
fd, err := os.OpenFile(s.tempName, flags, 0666)
|
||||
fd, err := os.OpenFile(s.tempName, flags, mode)
|
||||
if err != nil {
|
||||
s.failLocked("dst create", err)
|
||||
return nil, err
|
||||
|
||||
@@ -29,6 +29,9 @@ func TestSourceFileOK(t *testing.T) {
|
||||
|
||||
bs := make([]byte, 6)
|
||||
n, err := fd.Read(bs)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if n != len(bs) {
|
||||
t.Fatalf("Wrong read length %d != %d", n, len(bs))
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
// 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 model
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io/ioutil"
|
||||
"sort"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/opt"
|
||||
)
|
||||
|
||||
const (
|
||||
maxBytesInMemory = 512 << 10
|
||||
)
|
||||
|
||||
// The IndexSorter sorts FileInfos based on their LocalVersion. You use it
|
||||
// by first Append()ing all entries to be sorted, then calling Sorted()
|
||||
// which will iterate over all the items in correctly sorted order.
|
||||
type IndexSorter interface {
|
||||
Append(f protocol.FileInfo)
|
||||
Sorted(fn func(f protocol.FileInfo) bool)
|
||||
Close()
|
||||
}
|
||||
|
||||
type internalIndexSorter interface {
|
||||
IndexSorter
|
||||
full() bool
|
||||
copyTo(to IndexSorter)
|
||||
}
|
||||
|
||||
// NewIndexSorter returns a new IndexSorter that will start out in memory
|
||||
// for efficiency but switch to on disk storage once the amount of data
|
||||
// becomes large.
|
||||
func NewIndexSorter() IndexSorter {
|
||||
return &autoSwitchingIndexSorter{
|
||||
internalIndexSorter: newInMemoryIndexSorter(),
|
||||
}
|
||||
}
|
||||
|
||||
// An autoSwitchingSorter starts out as an inMemorySorter but becomes an
|
||||
// onDiskSorter when the in memory sorter is full().
|
||||
type autoSwitchingIndexSorter struct {
|
||||
internalIndexSorter
|
||||
}
|
||||
|
||||
func (s *autoSwitchingIndexSorter) Append(f protocol.FileInfo) {
|
||||
if s.internalIndexSorter.full() {
|
||||
// We spill before adding a file instead of after, to handle the
|
||||
// case where we're over max size but won't add any more files, in
|
||||
// which case we *don't* need to spill. An example of this would be
|
||||
// an index containing just a single large file.
|
||||
l.Debugf("sorter %p spills to disk", s)
|
||||
next := newOnDiskIndexSorter()
|
||||
s.internalIndexSorter.copyTo(next)
|
||||
s.internalIndexSorter = next
|
||||
}
|
||||
s.internalIndexSorter.Append(f)
|
||||
}
|
||||
|
||||
// An inMemoryIndexSorter is simply a slice of FileInfos. The full() method
|
||||
// returns true when the number of files exceeds maxFiles or the total
|
||||
// number of blocks exceeds maxBlocks.
|
||||
type inMemoryIndexSorter struct {
|
||||
files []protocol.FileInfo
|
||||
bytes int
|
||||
maxBytes int
|
||||
}
|
||||
|
||||
func newInMemoryIndexSorter() *inMemoryIndexSorter {
|
||||
return &inMemoryIndexSorter{
|
||||
maxBytes: maxBytesInMemory,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *inMemoryIndexSorter) Append(f protocol.FileInfo) {
|
||||
s.files = append(s.files, f)
|
||||
s.bytes += f.ProtoSize()
|
||||
}
|
||||
|
||||
func (s *inMemoryIndexSorter) Sorted(fn func(protocol.FileInfo) bool) {
|
||||
sort.Sort(byLocalVersion(s.files))
|
||||
for _, f := range s.files {
|
||||
if !fn(f) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *inMemoryIndexSorter) Close() {
|
||||
}
|
||||
|
||||
func (s *inMemoryIndexSorter) full() bool {
|
||||
return s.bytes >= s.maxBytes
|
||||
}
|
||||
|
||||
func (s *inMemoryIndexSorter) copyTo(dst IndexSorter) {
|
||||
for _, f := range s.files {
|
||||
dst.Append(f)
|
||||
}
|
||||
}
|
||||
|
||||
// byLocalVersion sorts FileInfos by LocalVersion
|
||||
type byLocalVersion []protocol.FileInfo
|
||||
|
||||
func (l byLocalVersion) Len() int {
|
||||
return len(l)
|
||||
}
|
||||
func (l byLocalVersion) 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
|
||||
}
|
||||
|
||||
// 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
|
||||
// onDiskIndexSorter you must call Close() to remove the temporary database.
|
||||
type onDiskIndexSorter struct {
|
||||
db *leveldb.DB
|
||||
dir string
|
||||
}
|
||||
|
||||
func newOnDiskIndexSorter() *onDiskIndexSorter {
|
||||
// Set options to minimize resource usage.
|
||||
opts := &opt.Options{
|
||||
OpenFilesCacheCapacity: 10,
|
||||
WriteBuffer: 512 << 10,
|
||||
}
|
||||
|
||||
// Use a temporary database directory.
|
||||
tmp, err := ioutil.TempDir("", "syncthing-db.")
|
||||
if err != nil {
|
||||
panic("creating temporary directory: " + err.Error())
|
||||
}
|
||||
db, err := leveldb.OpenFile(tmp, opts)
|
||||
if err != nil {
|
||||
panic("creating temporary database: " + err.Error())
|
||||
}
|
||||
|
||||
s := &onDiskIndexSorter{
|
||||
db: db,
|
||||
dir: tmp,
|
||||
}
|
||||
l.Debugf("onDiskIndexSorter %p created at %s", s, tmp)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *onDiskIndexSorter) Append(f protocol.FileInfo) {
|
||||
key := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(key[:], uint64(f.LocalVersion))
|
||||
data, err := f.Marshal()
|
||||
if err != nil {
|
||||
panic("bug: marshalling FileInfo should never fail: " + err.Error())
|
||||
}
|
||||
err = s.db.Put(key, data, nil)
|
||||
if err != nil {
|
||||
panic("writing to temporary database: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (s *onDiskIndexSorter) Sorted(fn func(protocol.FileInfo) bool) {
|
||||
it := s.db.NewIterator(nil, nil)
|
||||
defer it.Release()
|
||||
for it.Next() {
|
||||
var f protocol.FileInfo
|
||||
if err := f.Unmarshal(it.Value()); err != nil {
|
||||
panic("unmarshal failed: " + err.Error())
|
||||
}
|
||||
if !fn(f) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *onDiskIndexSorter) Close() {
|
||||
l.Debugf("onDiskIndexSorter %p closes", s)
|
||||
s.db.Close()
|
||||
osutil.RemoveAll(s.dir)
|
||||
}
|
||||
|
||||
func (s *onDiskIndexSorter) full() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *onDiskIndexSorter) copyTo(dst IndexSorter) {
|
||||
// Just wrap Sorted() if we need to support this in the future.
|
||||
panic("unsupported")
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
// 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 model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/rand"
|
||||
)
|
||||
|
||||
func TestInMemoryIndexSorter(t *testing.T) {
|
||||
// An inMemorySorter should be able to absorb a few files in unsorted
|
||||
// order, and return them sorted.
|
||||
|
||||
s := newInMemoryIndexSorter()
|
||||
addFiles(50, s)
|
||||
verifySorted(t, s, 50)
|
||||
verifyBreak(t, s, 50)
|
||||
s.Close()
|
||||
}
|
||||
|
||||
func TestOnDiskIndexSorter(t *testing.T) {
|
||||
// An onDiskSorter should be able to absorb a few files in unsorted
|
||||
// order, and return them sorted.
|
||||
|
||||
s := newOnDiskIndexSorter()
|
||||
addFiles(50, s)
|
||||
verifySorted(t, s, 50)
|
||||
verifyBreak(t, s, 50)
|
||||
|
||||
// The temporary database should exist on disk. When Close()d, it should
|
||||
// be removed.
|
||||
|
||||
info, err := os.Stat(s.dir)
|
||||
if err != nil {
|
||||
t.Fatal("temp database should exist on disk:", err)
|
||||
}
|
||||
if !info.IsDir() {
|
||||
t.Fatal("temp database should be a directory")
|
||||
}
|
||||
|
||||
s.Close()
|
||||
|
||||
_, err = os.Stat(s.dir)
|
||||
if !os.IsNotExist(err) {
|
||||
t.Fatal("temp database should have been removed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndexSorter(t *testing.T) {
|
||||
// An default IndexSorter should be able to absorb files, have them in
|
||||
// memory, and at some point switch to an on disk database.
|
||||
|
||||
s := NewIndexSorter()
|
||||
defer s.Close()
|
||||
|
||||
// We should start out as an in memory store.
|
||||
|
||||
nFiles := 1
|
||||
addFiles(1, s)
|
||||
verifySorted(t, s, nFiles)
|
||||
|
||||
as := s.(*autoSwitchingIndexSorter)
|
||||
if _, ok := as.internalIndexSorter.(*inMemoryIndexSorter); !ok {
|
||||
t.Fatalf("the sorter should be in memory after only one file")
|
||||
}
|
||||
|
||||
// At some point, for sure with less than maxBytesInMemory files, we
|
||||
// should switch over to an on disk sorter.
|
||||
for i := 0; i < maxBytesInMemory; i++ {
|
||||
addFiles(1, s)
|
||||
nFiles++
|
||||
if _, ok := as.internalIndexSorter.(*onDiskIndexSorter); ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := as.internalIndexSorter.(*onDiskIndexSorter); !ok {
|
||||
t.Fatalf("the sorter should be on disk after %d files", nFiles)
|
||||
}
|
||||
|
||||
verifySorted(t, s, nFiles)
|
||||
|
||||
// For test coverage, as some methods are called on the onDiskSorter
|
||||
// only after switching to it.
|
||||
|
||||
addFiles(1, s)
|
||||
verifySorted(t, s, nFiles+1)
|
||||
}
|
||||
|
||||
// addFiles adds files with random LocalVersion 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())}}},
|
||||
Blocks: []protocol.BlockInfo{{
|
||||
Size: int32(rand.Intn(128 << 10)),
|
||||
Hash: []byte(rand.String(32)),
|
||||
}},
|
||||
}
|
||||
s.Append(f)
|
||||
}
|
||||
}
|
||||
|
||||
// verifySorted checks that the files are returned sorted by LocalVersion.
|
||||
func verifySorted(t *testing.T, s IndexSorter, expected int) {
|
||||
prevLocalVer := int64(-1)
|
||||
seen := 0
|
||||
s.Sorted(func(f protocol.FileInfo) bool {
|
||||
if f.LocalVersion <= prevLocalVer {
|
||||
t.Fatalf("Unsorted LocalVer, %d <= %d", f.LocalVersion, prevLocalVer)
|
||||
}
|
||||
prevLocalVer = f.LocalVersion
|
||||
seen++
|
||||
return true
|
||||
})
|
||||
if seen != expected {
|
||||
t.Fatalf("expected %d files returned, got %d", expected, seen)
|
||||
}
|
||||
}
|
||||
|
||||
// verifyBreak checks that the Sorter stops iteration once we return false.
|
||||
func verifyBreak(t *testing.T, s IndexSorter, expected int) {
|
||||
prevLocalVer := 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 len(f.Blocks) != 1 {
|
||||
t.Fatalf("incorrect number of blocks %d != 1", len(f.Blocks))
|
||||
}
|
||||
if len(f.Version.Counters) != 1 {
|
||||
t.Fatalf("incorrect number of version counters %d != 1", len(f.Version.Counters))
|
||||
}
|
||||
prevLocalVer = f.LocalVersion
|
||||
seen++
|
||||
return seen < expected/2
|
||||
})
|
||||
if seen != expected/2 {
|
||||
t.Fatalf("expected %d files iterated over, got %d", expected, seen)
|
||||
}
|
||||
}
|
||||
+128
-98
@@ -262,6 +262,7 @@ type Device struct {
|
||||
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"`
|
||||
}
|
||||
|
||||
func (m *Device) Reset() { *m = Device{} }
|
||||
@@ -672,6 +673,11 @@ func (m *Device) MarshalTo(data []byte) (int, error) {
|
||||
}
|
||||
i++
|
||||
}
|
||||
if m.IndexID != 0 {
|
||||
data[i] = 0x40
|
||||
i++
|
||||
i = encodeVarintBep(data, i, uint64(m.IndexID))
|
||||
}
|
||||
return i, nil
|
||||
}
|
||||
|
||||
@@ -1288,6 +1294,9 @@ func (m *Device) ProtoSize() (n int) {
|
||||
if m.Introducer {
|
||||
n += 2
|
||||
}
|
||||
if m.IndexID != 0 {
|
||||
n += 1 + sovBep(uint64(m.IndexID))
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
@@ -2246,6 +2255,25 @@ func (m *Device) Unmarshal(data []byte) error {
|
||||
}
|
||||
}
|
||||
m.Introducer = bool(v != 0)
|
||||
case 8:
|
||||
if wireType != 0 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field IndexID", wireType)
|
||||
}
|
||||
m.IndexID = 0
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowBep
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := data[iNdEx]
|
||||
iNdEx++
|
||||
m.IndexID |= (IndexID(b) & 0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
default:
|
||||
iNdEx = preIndex
|
||||
skippy, err := skipBep(data[iNdEx:])
|
||||
@@ -3898,102 +3926,104 @@ var (
|
||||
)
|
||||
|
||||
var fileDescriptorBep = []byte{
|
||||
// 1543 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xac, 0x56, 0x41, 0x6f, 0x1a, 0xc7,
|
||||
0x17, 0x37, 0xb0, 0x2c, 0x30, 0xc6, 0x0e, 0x9e, 0x38, 0x0e, 0xff, 0x8d, 0xff, 0xb6, 0xbb, 0x49,
|
||||
0x54, 0x17, 0x35, 0x4e, 0x9b, 0x54, 0x8d, 0x54, 0xa9, 0x95, 0x30, 0xac, 0x1d, 0x14, 0xbc, 0x90,
|
||||
0x05, 0x9c, 0xa6, 0x87, 0xa2, 0x85, 0x1d, 0xf0, 0x2a, 0xcb, 0x0e, 0xdd, 0x5d, 0x92, 0xb8, 0x5f,
|
||||
0xa0, 0x52, 0xfb, 0x05, 0x7a, 0xa9, 0x94, 0x6b, 0xef, 0xfd, 0x10, 0x39, 0x46, 0x39, 0xf6, 0x10,
|
||||
0xb5, 0xe9, 0xa5, 0x1f, 0xa0, 0xbd, 0xf7, 0xcd, 0xcc, 0x2e, 0x2c, 0xc6, 0xae, 0x72, 0xe8, 0x01,
|
||||
0x31, 0xf3, 0xde, 0x6f, 0xde, 0xcc, 0xfc, 0xde, 0xef, 0xbd, 0x59, 0x94, 0xeb, 0x91, 0xf1, 0xde,
|
||||
0xd8, 0xa3, 0x01, 0xc5, 0x59, 0xfe, 0xd7, 0xa7, 0x8e, 0x72, 0x6b, 0x68, 0x07, 0x27, 0x93, 0xde,
|
||||
0x5e, 0x9f, 0x8e, 0x6e, 0x0f, 0xe9, 0x90, 0xde, 0xe6, 0x9e, 0xde, 0x64, 0xc0, 0x67, 0x7c, 0xc2,
|
||||
0x47, 0x62, 0xa1, 0x3a, 0x46, 0xe9, 0xfb, 0xc4, 0x71, 0x28, 0xde, 0x46, 0xcb, 0x16, 0x79, 0x6a,
|
||||
0xf7, 0x49, 0xd7, 0x35, 0x47, 0xa4, 0x98, 0xd8, 0x49, 0xec, 0xe6, 0x0c, 0x24, 0x4c, 0x3a, 0x58,
|
||||
0x18, 0xa0, 0xef, 0xd8, 0xc4, 0x0d, 0x04, 0x20, 0x29, 0x00, 0xc2, 0xc4, 0x01, 0x37, 0xd1, 0x6a,
|
||||
0x08, 0x78, 0x4a, 0x3c, 0xdf, 0xa6, 0x6e, 0x31, 0xc5, 0x31, 0x2b, 0xc2, 0x7a, 0x2c, 0x8c, 0xaa,
|
||||
0x8f, 0xe4, 0xfb, 0xc4, 0xb4, 0x88, 0x87, 0x3f, 0x40, 0x52, 0x70, 0x3a, 0x16, 0x7b, 0xad, 0xde,
|
||||
0xb9, 0xb2, 0x17, 0xdd, 0x61, 0xef, 0x88, 0xf8, 0xbe, 0x39, 0x24, 0x6d, 0x70, 0x1a, 0x1c, 0x82,
|
||||
0xbf, 0x80, 0xcd, 0xe9, 0x68, 0xec, 0x81, 0x83, 0x05, 0x4e, 0xf2, 0x15, 0x9b, 0x0b, 0x2b, 0x2a,
|
||||
0x33, 0x8c, 0x11, 0x5f, 0xa0, 0x96, 0xd1, 0x4a, 0xc5, 0x99, 0xf8, 0x01, 0xf1, 0x2a, 0xd4, 0x1d,
|
||||
0xd8, 0x43, 0xfc, 0x11, 0xca, 0x0c, 0xa8, 0x03, 0xa7, 0xf0, 0x61, 0xfb, 0xd4, 0xee, 0xf2, 0x9d,
|
||||
0xc2, 0x2c, 0xd8, 0x01, 0x77, 0xec, 0x4b, 0x2f, 0xdf, 0x6c, 0x2f, 0x19, 0x11, 0x4c, 0xfd, 0x21,
|
||||
0x89, 0x64, 0xe1, 0xc1, 0x1b, 0x28, 0x69, 0x5b, 0x82, 0xa2, 0x7d, 0xf9, 0xed, 0x9b, 0xed, 0x64,
|
||||
0xad, 0x6a, 0x80, 0x05, 0xaf, 0xa3, 0xb4, 0x63, 0xf6, 0x88, 0x13, 0x92, 0x23, 0x26, 0xf8, 0x1a,
|
||||
0xca, 0x79, 0x70, 0xe1, 0x2e, 0x75, 0x9d, 0x53, 0x4e, 0x49, 0xd6, 0xc8, 0x32, 0x43, 0x03, 0xe6,
|
||||
0xf8, 0x16, 0xc2, 0xf6, 0xd0, 0xa5, 0x1e, 0xe9, 0x8e, 0x89, 0x37, 0xb2, 0xf9, 0x69, 0xfd, 0xa2,
|
||||
0xc4, 0x51, 0x6b, 0xc2, 0xd3, 0x9c, 0x39, 0xf0, 0x75, 0xb4, 0x12, 0xc2, 0x2d, 0xe2, 0x90, 0x80,
|
||||
0x14, 0xd3, 0x1c, 0x99, 0x17, 0xc6, 0x2a, 0xb7, 0xc1, 0xdd, 0xd6, 0x2d, 0xdb, 0x37, 0x7b, 0x0e,
|
||||
0xe9, 0x06, 0x64, 0x34, 0xee, 0xda, 0xae, 0x45, 0x9e, 0x13, 0xbf, 0x28, 0x73, 0x2c, 0x0e, 0x7d,
|
||||
0x6d, 0x70, 0xd5, 0x84, 0x87, 0xb1, 0x21, 0x32, 0xed, 0x17, 0x0b, 0x67, 0xd9, 0xa8, 0x72, 0x47,
|
||||
0xc4, 0x46, 0x08, 0x53, 0xff, 0x4a, 0x20, 0x59, 0x78, 0x62, 0x6c, 0xe4, 0xe7, 0xd8, 0xc0, 0x48,
|
||||
0x8a, 0x29, 0x85, 0x8f, 0xf1, 0x26, 0xca, 0x99, 0x96, 0xc5, 0xb2, 0x02, 0x5b, 0xa5, 0x60, 0xab,
|
||||
0x9c, 0x31, 0x33, 0xe0, 0x7b, 0xf3, 0x59, 0x96, 0xce, 0xea, 0xe2, 0xa2, 0xf4, 0x32, 0x8a, 0xfb,
|
||||
0xc4, 0x0b, 0x95, 0x99, 0xe6, 0xfb, 0x65, 0x99, 0x81, 0xeb, 0xb2, 0x84, 0xd6, 0x46, 0xe6, 0xf3,
|
||||
0xae, 0x43, 0xfb, 0xa6, 0x33, 0x95, 0x26, 0xe3, 0x22, 0x65, 0x5c, 0x02, 0x47, 0x9d, 0xd9, 0x43,
|
||||
0x71, 0xe2, 0x2d, 0x84, 0x6c, 0x37, 0xf0, 0xa8, 0x35, 0x81, 0xe5, 0xc5, 0x0c, 0x27, 0x2c, 0x66,
|
||||
0x51, 0x1b, 0x28, 0xcd, 0x39, 0x83, 0x4b, 0xcb, 0x42, 0x18, 0x61, 0xa5, 0x84, 0x33, 0xbc, 0x87,
|
||||
0xd2, 0x03, 0xdb, 0x81, 0xcb, 0x25, 0x39, 0x8f, 0x38, 0xa6, 0x2a, 0x30, 0xd7, 0xdc, 0x01, 0x0d,
|
||||
0x99, 0x14, 0x30, 0xb5, 0x83, 0x96, 0x79, 0xc0, 0xce, 0xd8, 0x32, 0x03, 0xf2, 0x9f, 0x85, 0xfd,
|
||||
0x2e, 0x85, 0xb2, 0x91, 0x67, 0x9a, 0x88, 0x44, 0x2c, 0x11, 0xa5, 0xb0, 0xf6, 0x44, 0x25, 0x6d,
|
||||
0x2c, 0xc6, 0x8b, 0x15, 0x1f, 0xac, 0xf7, 0xed, 0x6f, 0x09, 0xd7, 0x6e, 0xca, 0xe0, 0x63, 0xbc,
|
||||
0x83, 0x96, 0xcf, 0x0a, 0x76, 0xc5, 0x88, 0x9b, 0xb0, 0x82, 0xb2, 0x23, 0x6a, 0xd9, 0x03, 0x9b,
|
||||
0x58, 0x3c, 0x25, 0x29, 0x63, 0x3a, 0xc7, 0x45, 0xa6, 0x37, 0xa6, 0x55, 0x2b, 0x14, 0x65, 0x34,
|
||||
0x65, 0x1e, 0xdb, 0x7d, 0x6a, 0x3a, 0xa0, 0x28, 0xc1, 0x7e, 0x34, 0x65, 0xed, 0xc5, 0xa5, 0x73,
|
||||
0x55, 0x92, 0xe5, 0x80, 0x15, 0x97, 0xc6, 0x2b, 0x04, 0xa4, 0x1c, 0xe5, 0x38, 0x07, 0xfe, 0x39,
|
||||
0x29, 0x1f, 0x93, 0x7e, 0x40, 0xa7, 0x85, 0x1d, 0xc2, 0x58, 0x4d, 0xcd, 0x6b, 0x03, 0xf1, 0xd3,
|
||||
0xe6, 0x9d, 0xb8, 0x30, 0x3e, 0x46, 0xf2, 0x3e, 0x18, 0x9e, 0x44, 0x05, 0x72, 0x79, 0x16, 0x95,
|
||||
0xdb, 0x63, 0x29, 0x90, 0x7b, 0x1c, 0xf8, 0x99, 0xf4, 0xe3, 0x8b, 0xed, 0x25, 0xf5, 0x21, 0xca,
|
||||
0x4d, 0x01, 0x2c, 0xbd, 0x74, 0x30, 0xf0, 0x49, 0xc0, 0x73, 0x91, 0x32, 0xc2, 0xd9, 0x94, 0x61,
|
||||
0x96, 0x8d, 0x74, 0xc8, 0x30, 0xd8, 0x4e, 0x4c, 0xff, 0x84, 0xb3, 0x9e, 0x37, 0xf8, 0x38, 0x0c,
|
||||
0xf9, 0x39, 0x92, 0xc5, 0x4d, 0xf0, 0x5d, 0x94, 0xed, 0xd3, 0x89, 0x1b, 0xcc, 0xda, 0xd8, 0x5a,
|
||||
0xbc, 0x5a, 0xb8, 0x27, 0x3c, 0xd5, 0x14, 0xa8, 0x1e, 0xa0, 0x4c, 0xe8, 0x02, 0x4e, 0xa3, 0xd2,
|
||||
0x95, 0xf6, 0xaf, 0x30, 0xd8, 0xaf, 0x6f, 0xb6, 0x33, 0xad, 0x13, 0xea, 0x05, 0xb5, 0xea, 0x7c,
|
||||
0x5f, 0x83, 0x1c, 0x4c, 0xc4, 0xf9, 0x24, 0x43, 0x4c, 0xd4, 0x5f, 0x12, 0x28, 0x63, 0x90, 0x6f,
|
||||
0x26, 0xc4, 0x0f, 0x62, 0x3d, 0x20, 0x3d, 0xd7, 0x03, 0x66, 0x7a, 0x4e, 0xce, 0xe9, 0x39, 0x92,
|
||||
0x64, 0x2a, 0x26, 0xc9, 0x19, 0x39, 0xd2, 0xb9, 0xe4, 0xa4, 0xcf, 0x21, 0x47, 0x9e, 0x91, 0xc3,
|
||||
0x04, 0x32, 0xf0, 0xe8, 0x88, 0xf7, 0x3c, 0xea, 0x99, 0xde, 0x69, 0xa8, 0xa0, 0x15, 0x66, 0x6d,
|
||||
0x47, 0x46, 0xb5, 0x8b, 0xb2, 0x06, 0xf1, 0xc7, 0xa0, 0x15, 0x72, 0xe1, 0xb1, 0x21, 0x3c, 0x94,
|
||||
0xa3, 0xc9, 0x0f, 0x0d, 0xe1, 0xd9, 0x18, 0xbf, 0x8f, 0xa4, 0x3e, 0xb5, 0xc4, 0x91, 0x57, 0xe3,
|
||||
0xf9, 0xd7, 0x3c, 0x8f, 0xc2, 0xb3, 0x62, 0x41, 0xb9, 0x30, 0x00, 0x3c, 0xa9, 0x85, 0x2a, 0x7d,
|
||||
0xe6, 0x3a, 0xd4, 0xb4, 0x9a, 0x1e, 0x1d, 0xb2, 0x1e, 0x75, 0x61, 0x5d, 0x57, 0x51, 0x66, 0xc2,
|
||||
0x2b, 0x3f, 0xaa, 0xec, 0x1b, 0xf3, 0x95, 0x78, 0x36, 0x90, 0x68, 0x13, 0x91, 0x82, 0xc3, 0xa5,
|
||||
0xea, 0xeb, 0x04, 0x52, 0x2e, 0x46, 0xe3, 0x1a, 0x5a, 0x16, 0xc8, 0x6e, 0xec, 0xb9, 0xdd, 0x7d,
|
||||
0x97, 0x8d, 0x78, 0x13, 0x40, 0x93, 0xe9, 0xf8, 0xdc, 0x9e, 0x1e, 0xab, 0xb8, 0xd4, 0x3b, 0x57,
|
||||
0x1c, 0xaf, 0x91, 0xe9, 0xcb, 0x24, 0xc1, 0xdd, 0xd3, 0x46, 0xbe, 0x27, 0x0a, 0x85, 0xdb, 0x54,
|
||||
0x19, 0x49, 0x4d, 0xdb, 0x1d, 0xaa, 0xdb, 0x28, 0x5d, 0x71, 0x28, 0x4f, 0x96, 0x0c, 0xcf, 0xa6,
|
||||
0x0f, 0xdb, 0x84, 0x1c, 0x8a, 0x59, 0xe9, 0x75, 0x12, 0x2d, 0xc7, 0xbe, 0x18, 0xe0, 0x3c, 0xab,
|
||||
0x95, 0x7a, 0xa7, 0xd5, 0xd6, 0x8c, 0x6e, 0xa5, 0xa1, 0x1f, 0xd4, 0x0e, 0x0b, 0x4b, 0xca, 0xe6,
|
||||
0xf7, 0x3f, 0xed, 0x14, 0x47, 0x33, 0xd0, 0xfc, 0xc7, 0x00, 0x6c, 0x51, 0xd3, 0xab, 0xda, 0x97,
|
||||
0x85, 0x84, 0xb2, 0x0e, 0xc0, 0x42, 0x0c, 0x28, 0xba, 0xfd, 0x87, 0x28, 0xcf, 0x01, 0xdd, 0x4e,
|
||||
0xb3, 0x5a, 0x6e, 0x6b, 0x85, 0xa4, 0xa2, 0x00, 0x6e, 0xe3, 0x2c, 0x2e, 0xe4, 0xfb, 0x3a, 0xd4,
|
||||
0x85, 0xf6, 0xb0, 0xa3, 0xb5, 0xda, 0x85, 0x94, 0xb2, 0x01, 0x40, 0x1c, 0x03, 0x46, 0x15, 0x73,
|
||||
0x13, 0x64, 0xa8, 0xb5, 0x9a, 0x0d, 0xbd, 0xa5, 0x15, 0x24, 0xe5, 0x2a, 0xa0, 0x2e, 0xcf, 0xa1,
|
||||
0x42, 0x85, 0x7e, 0x8a, 0xd6, 0xaa, 0x8d, 0x47, 0x7a, 0xbd, 0x51, 0xae, 0x76, 0x9b, 0x46, 0xe3,
|
||||
0x10, 0xd6, 0xb4, 0x0a, 0x69, 0x65, 0x1b, 0xf0, 0xd7, 0x62, 0xf8, 0x05, 0xc1, 0xfd, 0x1f, 0xd8,
|
||||
0xab, 0xe9, 0x87, 0x05, 0x59, 0xb9, 0x0c, 0xd0, 0x4b, 0x31, 0x28, 0x23, 0x95, 0xdd, 0xb8, 0x52,
|
||||
0x6f, 0xc0, 0xd6, 0x99, 0x85, 0x1b, 0x73, 0xb2, 0x4b, 0x5f, 0x23, 0xbc, 0xf8, 0x4d, 0x85, 0x6f,
|
||||
0x20, 0x49, 0x6f, 0xe8, 0x1a, 0x10, 0xca, 0xef, 0xbf, 0x88, 0xd0, 0xa9, 0x4b, 0xb0, 0x8a, 0x52,
|
||||
0xf5, 0xaf, 0x3e, 0x01, 0x32, 0xff, 0x07, 0xa0, 0x2b, 0x8b, 0x20, 0x70, 0x96, 0x28, 0x5a, 0x8e,
|
||||
0x07, 0x56, 0x51, 0xf6, 0x48, 0x6b, 0x97, 0x81, 0xdc, 0x32, 0x04, 0xe7, 0x47, 0x8a, 0xdc, 0x47,
|
||||
0x24, 0x30, 0x79, 0x01, 0x6e, 0xa2, 0xb4, 0xae, 0x1d, 0x6b, 0x06, 0x04, 0x5e, 0x03, 0xc0, 0x4a,
|
||||
0x04, 0xd0, 0x09, 0xe8, 0x0a, 0x5e, 0x6e, 0xb9, 0x5c, 0x7f, 0x54, 0x7e, 0xdc, 0x82, 0xe4, 0x60,
|
||||
0x70, 0xaf, 0x46, 0xee, 0xb2, 0xf3, 0xcc, 0x3c, 0xf5, 0x4b, 0x7f, 0x27, 0x50, 0x3e, 0xfe, 0xb6,
|
||||
0xc1, 0x02, 0xe9, 0xa0, 0x56, 0xd7, 0xa2, 0xed, 0xe2, 0x3e, 0x36, 0xc6, 0xbb, 0x28, 0x57, 0xad,
|
||||
0x19, 0x5a, 0xa5, 0xdd, 0x30, 0x1e, 0x47, 0x77, 0x89, 0x83, 0xaa, 0xb6, 0xc7, 0xc5, 0xcd, 0xbe,
|
||||
0xe1, 0xf2, 0xad, 0xc7, 0x47, 0xf5, 0x9a, 0xfe, 0xa0, 0xcb, 0x23, 0x26, 0x95, 0x6b, 0x00, 0xbe,
|
||||
0x1a, 0x07, 0xb7, 0x4e, 0x47, 0x8e, 0xed, 0x3e, 0xe1, 0x81, 0xef, 0xa1, 0xb5, 0x08, 0x3e, 0xdb,
|
||||
0x20, 0xa5, 0xec, 0xc0, 0x9a, 0xcd, 0x73, 0xd6, 0xcc, 0xf6, 0xb9, 0x8b, 0x2e, 0x45, 0x0b, 0x3b,
|
||||
0xfa, 0x03, 0x1d, 0x64, 0x01, 0xca, 0xd9, 0x82, 0x65, 0xca, 0x39, 0xcb, 0x3a, 0xee, 0x13, 0x17,
|
||||
0x44, 0x51, 0xfa, 0x39, 0x81, 0x72, 0xd3, 0x0e, 0xc5, 0x78, 0xd6, 0x1b, 0x5d, 0xcd, 0x30, 0x1a,
|
||||
0x46, 0x74, 0xf1, 0xa9, 0x53, 0xa7, 0x7c, 0x88, 0xdf, 0x43, 0x99, 0x43, 0x4d, 0xd7, 0x8c, 0x5a,
|
||||
0x25, 0xaa, 0x87, 0x29, 0xe4, 0x90, 0xb8, 0xc4, 0xb3, 0xfb, 0xf0, 0xe5, 0x9e, 0x87, 0x30, 0xad,
|
||||
0x4e, 0xe5, 0x7e, 0x74, 0x63, 0x2e, 0xe0, 0x58, 0xa8, 0xd6, 0xa4, 0x7f, 0xc2, 0x6f, 0x5b, 0x62,
|
||||
0xa5, 0x73, 0x5c, 0xae, 0xd7, 0xaa, 0x02, 0x9a, 0x52, 0x8a, 0x00, 0x5d, 0x9f, 0x42, 0x6b, 0xe2,
|
||||
0x79, 0x67, 0xd8, 0x92, 0x85, 0xb6, 0xfe, 0xbd, 0x17, 0xc1, 0x67, 0x87, 0x5c, 0x6e, 0x36, 0x35,
|
||||
0xbd, 0x1a, 0x9d, 0x7e, 0xe6, 0x2b, 0x8f, 0xc7, 0xc4, 0xb5, 0x18, 0xe2, 0xa0, 0x61, 0x1c, 0x6a,
|
||||
0xed, 0xe8, 0xf0, 0x33, 0xc4, 0x01, 0xf5, 0x86, 0x24, 0xd8, 0xdf, 0x7c, 0xf9, 0xfb, 0xd6, 0xd2,
|
||||
0x2b, 0xf8, 0xbd, 0x7c, 0xbb, 0x95, 0x78, 0x05, 0xbf, 0xdf, 0xde, 0x6e, 0x2d, 0xfd, 0x09, 0xff,
|
||||
0x2f, 0xfe, 0xd8, 0x4a, 0xf4, 0x64, 0xde, 0xbb, 0xee, 0xfe, 0x13, 0x00, 0x00, 0xff, 0xff, 0x88,
|
||||
0x06, 0x3b, 0x0f, 0x5d, 0x0d, 0x00, 0x00,
|
||||
// 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,
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@ message Device {
|
||||
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];
|
||||
}
|
||||
|
||||
enum Compression {
|
||||
|
||||
@@ -8,7 +8,11 @@ package protocol
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/rand"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -21,8 +25,8 @@ func (m Hello) Magic() uint32 {
|
||||
}
|
||||
|
||||
func (f FileInfo) String() string {
|
||||
return fmt.Sprintf("File{Name:%q, Permissions:0%o, Modified:%d, Version:%v, Length:%d, Deleted:%v, Invalid:%v, NoPermissions:%v, Blocks:%v}",
|
||||
f.Name, f.Permissions, f.Modified, f.Version, f.Size, f.Deleted, f.Invalid, f.NoPermissions, f.Blocks)
|
||||
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)
|
||||
}
|
||||
|
||||
func (f FileInfo) IsDeleted() bool {
|
||||
@@ -94,3 +98,27 @@ func (b BlockInfo) String() string {
|
||||
func (b BlockInfo) IsEmpty() bool {
|
||||
return b.Size == BlockSize && bytes.Equal(b.Hash, sha256OfEmptyBlock[:])
|
||||
}
|
||||
|
||||
type IndexID uint64
|
||||
|
||||
func (i IndexID) String() string {
|
||||
return fmt.Sprintf("0x%16X", uint64(i))
|
||||
}
|
||||
|
||||
func (i IndexID) Marshal() ([]byte, error) {
|
||||
bs := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(bs, uint64(i))
|
||||
return bs, nil
|
||||
}
|
||||
|
||||
func (i *IndexID) Unmarshal(bs []byte) error {
|
||||
if len(bs) != 8 {
|
||||
return errors.New("incorrect IndexID length")
|
||||
}
|
||||
*i = IndexID(binary.BigEndian.Uint64(bs))
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewIndexID() IndexID {
|
||||
return IndexID(rand.Int64())
|
||||
}
|
||||
|
||||
+17
-7
@@ -56,16 +56,19 @@ func IsVersionMismatch(err error) bool {
|
||||
}
|
||||
|
||||
func readHello(c io.Reader) (HelloResult, error) {
|
||||
header := make([]byte, 8)
|
||||
header := make([]byte, 4)
|
||||
if _, err := io.ReadFull(c, header); err != nil {
|
||||
return HelloResult{}, err
|
||||
}
|
||||
|
||||
switch binary.BigEndian.Uint32(header[:4]) {
|
||||
switch binary.BigEndian.Uint32(header) {
|
||||
case HelloMessageMagic:
|
||||
// This is a v0.14 Hello message in proto format
|
||||
msgSize := binary.BigEndian.Uint32(header[4:])
|
||||
if msgSize > 1024 {
|
||||
if _, err := io.ReadFull(c, header[:2]); err != nil {
|
||||
return HelloResult{}, err
|
||||
}
|
||||
msgSize := binary.BigEndian.Uint16(header[:2])
|
||||
if msgSize > 32767 {
|
||||
return HelloResult{}, fmt.Errorf("hello message too big")
|
||||
}
|
||||
buf := make([]byte, msgSize)
|
||||
@@ -86,7 +89,10 @@ func readHello(c io.Reader) (HelloResult, error) {
|
||||
|
||||
case Version13HelloMagic:
|
||||
// This is a v0.13 Hello message in XDR format
|
||||
msgSize := binary.BigEndian.Uint32(header[4:])
|
||||
if _, err := io.ReadFull(c, header[:4]); err != nil {
|
||||
return HelloResult{}, err
|
||||
}
|
||||
msgSize := binary.BigEndian.Uint32(header[:4])
|
||||
if msgSize > 1024 {
|
||||
return HelloResult{}, fmt.Errorf("hello message too big")
|
||||
}
|
||||
@@ -120,10 +126,14 @@ func writeHello(c io.Writer, h HelloIntf) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(msg) > 32767 {
|
||||
// The header length must be a positive signed int16
|
||||
panic("bug: attempting to serialize too large hello message")
|
||||
}
|
||||
|
||||
header := make([]byte, 8)
|
||||
header := make([]byte, 6)
|
||||
binary.BigEndian.PutUint32(header[:4], h.Magic())
|
||||
binary.BigEndian.PutUint32(header[4:], uint32(len(msg)))
|
||||
binary.BigEndian.PutUint16(header[4:], uint16(len(msg)))
|
||||
|
||||
_, err = c.Write(append(header, msg...))
|
||||
return err
|
||||
|
||||
@@ -26,9 +26,9 @@ func TestVersion14Hello(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
hdrBuf := make([]byte, 8)
|
||||
hdrBuf := make([]byte, 6)
|
||||
binary.BigEndian.PutUint32(hdrBuf, HelloMessageMagic)
|
||||
binary.BigEndian.PutUint32(hdrBuf[4:], uint32(len(msgBuf)))
|
||||
binary.BigEndian.PutUint16(hdrBuf[4:], uint16(len(msgBuf)))
|
||||
|
||||
outBuf := new(bytes.Buffer)
|
||||
outBuf.Write(hdrBuf)
|
||||
|
||||
@@ -41,7 +41,7 @@ func (m nativeModel) Request(deviceID DeviceID, folder string, name string, offs
|
||||
|
||||
func fixupFiles(folder string, files []FileInfo) {
|
||||
for i, f := range files {
|
||||
if strings.ContainsAny(f.Name, disallowedCharacters) {
|
||||
if strings.ContainsAny(f.Name, disallowedCharacters) || strings.HasSuffix(f.Name, " ") {
|
||||
if f.IsDeleted() {
|
||||
// Don't complain if the file is marked as deleted, since it
|
||||
// can't possibly exist here anyway.
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright (C) 2016 The Protocol Authors.
|
||||
|
||||
// +build windows
|
||||
|
||||
package protocol
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestFixupFiles(t *testing.T) {
|
||||
fs := []FileInfo{
|
||||
{Name: "ok"}, // This file is OK
|
||||
{Name: "b<d"}, // The rest should be marked as invalid
|
||||
{Name: "b?d"},
|
||||
{Name: "bad "},
|
||||
}
|
||||
|
||||
fixupFiles("default", fs)
|
||||
|
||||
if fs[0].IsInvalid() {
|
||||
t.Error("fs[0] should not be invalid")
|
||||
}
|
||||
for i := 1; i < len(fs); i++ {
|
||||
if !fs[i].IsInvalid() {
|
||||
t.Errorf("fs[%d] should be invalid", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -288,6 +288,10 @@ func (c *rawConnection) readerLoop() (err error) {
|
||||
}
|
||||
|
||||
msg, err := c.readMessage()
|
||||
if err == errUnknownMessage {
|
||||
// Unknown message types are skipped, for future extensibility.
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+43
-11
@@ -7,6 +7,7 @@
|
||||
package scanner
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
@@ -39,24 +40,45 @@ func newParallelHasher(dir string, blockSize, workers int, outbox, inbox chan pr
|
||||
}()
|
||||
}
|
||||
|
||||
func HashFile(path string, blockSize int, sizeHint int64, counter Counter) ([]protocol.BlockInfo, error) {
|
||||
func HashFile(path string, blockSize int, counter Counter) ([]protocol.BlockInfo, error) {
|
||||
fd, err := os.Open(path)
|
||||
if err != nil {
|
||||
l.Debugln("open:", err)
|
||||
return []protocol.BlockInfo{}, err
|
||||
return nil, err
|
||||
}
|
||||
defer fd.Close()
|
||||
|
||||
if sizeHint == 0 {
|
||||
fi, err := fd.Stat()
|
||||
if err != nil {
|
||||
l.Debugln("stat:", err)
|
||||
return []protocol.BlockInfo{}, err
|
||||
}
|
||||
sizeHint = fi.Size()
|
||||
// Get the size and modtime of the file before we start hashing it.
|
||||
|
||||
fi, err := fd.Stat()
|
||||
if err != nil {
|
||||
l.Debugln("stat before:", err)
|
||||
return nil, err
|
||||
}
|
||||
size := fi.Size()
|
||||
modTime := fi.ModTime()
|
||||
|
||||
// Hash the file. This may take a while for large files.
|
||||
|
||||
blocks, err := Blocks(fd, blockSize, size, counter)
|
||||
if err != nil {
|
||||
l.Debugln("blocks:", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return Blocks(fd, blockSize, sizeHint, counter)
|
||||
// Recheck the size and modtime again. If they differ, the file changed
|
||||
// while we were reading it and our hash results are invalid.
|
||||
|
||||
fi, err = fd.Stat()
|
||||
if err != nil {
|
||||
l.Debugln("stat after:", err)
|
||||
return nil, err
|
||||
}
|
||||
if size != fi.Size() || !modTime.Equal(fi.ModTime()) {
|
||||
return nil, errors.New("file changed during hashing")
|
||||
}
|
||||
|
||||
return blocks, nil
|
||||
}
|
||||
|
||||
func hashFiles(dir string, blockSize int, outbox, inbox chan protocol.FileInfo, counter Counter, cancel chan struct{}) {
|
||||
@@ -71,13 +93,23 @@ func hashFiles(dir string, blockSize int, outbox, inbox chan protocol.FileInfo,
|
||||
panic("Bug. Asked to hash a directory or a deleted file.")
|
||||
}
|
||||
|
||||
blocks, err := HashFile(filepath.Join(dir, f.Name), blockSize, f.Size, counter)
|
||||
blocks, err := HashFile(filepath.Join(dir, f.Name), blockSize, counter)
|
||||
if err != nil {
|
||||
l.Debugln("hash error:", f.Name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
f.Blocks = blocks
|
||||
|
||||
// The size we saw when initially deciding to hash the file
|
||||
// might not have been the size it actually had when we hashed
|
||||
// it. Update the size from the block list.
|
||||
|
||||
f.Size = 0
|
||||
for _, b := range blocks {
|
||||
f.Size += int64(b.Size)
|
||||
}
|
||||
|
||||
select {
|
||||
case outbox <- f:
|
||||
case <-cancel:
|
||||
|
||||
@@ -29,9 +29,10 @@ func Blocks(r io.Reader, blocksize int, sizehint int64, counter Counter) ([]prot
|
||||
var blocks []protocol.BlockInfo
|
||||
var hashes, thisHash []byte
|
||||
|
||||
if sizehint > 0 {
|
||||
if sizehint >= 0 {
|
||||
// Allocate contiguous blocks for the BlockInfo structures and their
|
||||
// hashes once and for all.
|
||||
// hashes once and for all, and stick to the specified size.
|
||||
r = io.LimitReader(r, sizehint)
|
||||
numBlocks := int(sizehint / int64(blocksize))
|
||||
blocks = make([]protocol.BlockInfo, 0, numBlocks)
|
||||
hashes = make([]byte, 0, hashLength*numBlocks)
|
||||
|
||||
@@ -51,7 +51,7 @@ var blocksTestData = []struct {
|
||||
func TestBlocks(t *testing.T) {
|
||||
for _, test := range blocksTestData {
|
||||
buf := bytes.NewBuffer(test.data)
|
||||
blocks, err := Blocks(buf, test.blocksize, 0, nil)
|
||||
blocks, err := Blocks(buf, test.blocksize, -1, nil)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -105,8 +105,8 @@ var diffTestData = []struct {
|
||||
|
||||
func TestDiff(t *testing.T) {
|
||||
for i, test := range diffTestData {
|
||||
a, _ := Blocks(bytes.NewBufferString(test.a), test.s, 0, nil)
|
||||
b, _ := Blocks(bytes.NewBufferString(test.b), test.s, 0, nil)
|
||||
a, _ := Blocks(bytes.NewBufferString(test.a), test.s, -1, nil)
|
||||
b, _ := Blocks(bytes.NewBufferString(test.b), test.s, -1, nil)
|
||||
_, d := BlockDiff(a, b)
|
||||
if len(d) != len(test.d) {
|
||||
t.Fatalf("Incorrect length for diff %d; %d != %d", i, len(d), len(test.d))
|
||||
|
||||
+1
-3
@@ -394,7 +394,7 @@ func (w *walker) walkSymlink(absPath, relPath string, dchan chan protocol.FileIn
|
||||
return true, nil
|
||||
}
|
||||
|
||||
blocks, err := Blocks(strings.NewReader(target), w.BlockSize, 0, nil)
|
||||
blocks, err := Blocks(strings.NewReader(target), w.BlockSize, -1, nil)
|
||||
if err != nil {
|
||||
l.Debugln("hash link error:", absPath, err)
|
||||
return true, nil
|
||||
@@ -470,8 +470,6 @@ func (w *walker) normalizePath(absPath, relPath string) (normPath string, skip b
|
||||
l.Infof(`File "%s" has UTF8 encoding conflict with another file; ignoring.`, relPath)
|
||||
return "", true
|
||||
}
|
||||
|
||||
relPath = normPath
|
||||
}
|
||||
|
||||
return normPath, false
|
||||
|
||||
@@ -148,7 +148,7 @@ func TestVerify(t *testing.T) {
|
||||
progress := newByteCounter()
|
||||
defer progress.Close()
|
||||
|
||||
blocks, err := Blocks(buf, blocksize, 0, progress)
|
||||
blocks, err := Blocks(buf, blocksize, -1, progress)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -281,6 +281,56 @@ func TestIssue1507(t *testing.T) {
|
||||
fn("", nil, protocol.ErrClosed)
|
||||
}
|
||||
|
||||
func TestWalkSymlink(t *testing.T) {
|
||||
if !symlinks.Supported {
|
||||
t.Skip("skipping unsupported symlink test")
|
||||
return
|
||||
}
|
||||
|
||||
// Create a folder with a symlink in it
|
||||
|
||||
osutil.RemoveAll("_symlinks")
|
||||
defer osutil.RemoveAll("_symlinks")
|
||||
|
||||
os.Mkdir("_symlinks", 0755)
|
||||
symlinks.Create("_symlinks/link", "destination", symlinks.TargetUnknown)
|
||||
|
||||
// Scan it
|
||||
|
||||
fchan, err := Walk(Config{
|
||||
Dir: "_symlinks",
|
||||
BlockSize: 128 * 1024,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var files []protocol.FileInfo
|
||||
for f := range fchan {
|
||||
files = append(files, f)
|
||||
}
|
||||
|
||||
// Verify that we got one symlink and with the correct block contents
|
||||
|
||||
if len(files) != 1 {
|
||||
t.Errorf("expected 1 symlink, not %d", len(files))
|
||||
}
|
||||
if len(files[0].Blocks) != 1 {
|
||||
t.Errorf("expected 1 block, not %d", len(files[0].Blocks))
|
||||
}
|
||||
|
||||
if files[0].Blocks[0].Size != int32(len("destination")) {
|
||||
t.Errorf("expected block length %d, not %d", len("destination"), files[0].Blocks[0].Size)
|
||||
}
|
||||
|
||||
// echo -n "destination" | openssl dgst -sha256
|
||||
hash := "b5c755aaab1038b3d5627bbde7f47ca80c5f5c0481c6d33f04139d07aa1530e7"
|
||||
if fmt.Sprintf("%x", files[0].Blocks[0].Hash) != hash {
|
||||
t.Errorf("incorrect hash")
|
||||
}
|
||||
}
|
||||
|
||||
func walkDir(dir string) ([]protocol.FileInfo, error) {
|
||||
fchan, err := Walk(Config{
|
||||
Dir: dir,
|
||||
@@ -380,7 +430,7 @@ func BenchmarkHashFile(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
if _, err := HashFile(testdataName, protocol.BlockSize, testdataSize, nil); err != nil {
|
||||
if _, err := HashFile(testdataName, protocol.BlockSize, nil); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ func (s *FolderStatisticsReference) GetLastFile() LastFile {
|
||||
if !ok {
|
||||
return LastFile{}
|
||||
}
|
||||
deleted, ok := s.ns.Bool("lastFileDeleted")
|
||||
deleted, _ := s.ns.Bool("lastFileDeleted")
|
||||
return LastFile{
|
||||
At: at,
|
||||
Filename: file,
|
||||
|
||||
+479
-1171
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-CONFIG" "5" "July 02, 2016" "v0.13" "Syncthing"
|
||||
.TH "SYNCTHING-CONFIG" "5" "July 24, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-config \- Syncthing Configuration
|
||||
.
|
||||
@@ -455,7 +455,7 @@ These are equivalent and will result in Syncthing listening on all
|
||||
interfaces via both IPv4 and IPv6.
|
||||
.UNINDENT
|
||||
.TP
|
||||
.B username
|
||||
.B user
|
||||
Set to require authentication.
|
||||
.TP
|
||||
.B password
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-DEVICE-IDS" "7" "July 02, 2016" "v0.13" "Syncthing"
|
||||
.TH "SYNCTHING-DEVICE-IDS" "7" "July 24, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-device-ids \- Understanding Device IDs
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-EVENT-API" "7" "July 02, 2016" "v0.13" "Syncthing"
|
||||
.TH "SYNCTHING-EVENT-API" "7" "July 24, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-event-api \- Event API
|
||||
.
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-FAQ" "7" "July 02, 2016" "v0.13" "Syncthing"
|
||||
.TH "SYNCTHING-FAQ" "7" "July 24, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-faq \- Frequently Asked Questions
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-GLOBALDISCO" "7" "July 02, 2016" "v0.13" "Syncthing"
|
||||
.TH "SYNCTHING-GLOBALDISCO" "7" "July 24, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-globaldisco \- Global Discovery Protocol v3
|
||||
.
|
||||
|
||||
+33
-71
@@ -1,8 +1,8 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-LOCALDISCO" "7" "July 02, 2016" "v0.13" "Syncthing"
|
||||
.TH "SYNCTHING-LOCALDISCO" "7" "July 24, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-localdisco \- Local Discovery Protocol v3
|
||||
syncthing-localdisco \- Local Discovery Protocol v4
|
||||
.
|
||||
.nr rst2man-indent-level 0
|
||||
.
|
||||
@@ -39,16 +39,19 @@ reply; the only message type is Announcement.
|
||||
On multihomed hosts the announcement packets should be sent on each interface
|
||||
on which Syncthing will accept connections.
|
||||
.sp
|
||||
The announcement packet is sent over UDP.
|
||||
.sp
|
||||
For IPv4, the Announcement packet is broadcast either to the link\-specific
|
||||
broadcast address, or to the generic link\-local broadcast address
|
||||
\fB255.255.255.255\fP, with destination port 21027.
|
||||
.sp
|
||||
For IPv6, the Announcement packet is multicast to the transient link\-local
|
||||
multicast address \fB[ff12::8384]\fP, with destination port 21027.
|
||||
multicast address \fBff12::8384\fP, with destination port 21027.
|
||||
.sp
|
||||
It is recommended that local discovery Announcement packets be sent on a 30 to
|
||||
60 second interval, possibly with immediate transmissions when a previously
|
||||
unknown device is discovered.
|
||||
It is recommended that local discovery Announcement packets be sent on a 30
|
||||
to 60 second interval, possibly with immediate transmissions when a
|
||||
previously unknown device is discovered or a device has restarted (see the
|
||||
\fBinstance_id\fP field).
|
||||
.SH DEVICE ID
|
||||
.sp
|
||||
The device ID is the SHA\-256 (32 bytes) of the device X.509 certificate. See
|
||||
@@ -61,51 +64,13 @@ The Announcement packet has the following structure:
|
||||
.sp
|
||||
.nf
|
||||
.ft C
|
||||
Announce Structure:
|
||||
|
||||
0 1 2 3
|
||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
| Magic |
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
/ /
|
||||
\e Device Structure \e
|
||||
/ /
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
| Number of Extra Devices |
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
/ /
|
||||
\e Zero or more Device Structures \e
|
||||
/ /
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
|
||||
Device Structure:
|
||||
|
||||
0 1 2 3
|
||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
| Length of ID |
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
/ /
|
||||
\e ID (variable length) \e
|
||||
/ /
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
| Number of Addresses |
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
/ /
|
||||
\e Zero or more Address Structures \e
|
||||
/ /
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
|
||||
Address Structure:
|
||||
|
||||
0 1 2 3
|
||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
| Length of URL |
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
/ /
|
||||
\e URL (variable length) \e
|
||||
\e Announce Message \e
|
||||
/ /
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
.ft P
|
||||
@@ -113,46 +78,43 @@ Address Structure:
|
||||
.UNINDENT
|
||||
.UNINDENT
|
||||
.sp
|
||||
The corresponding XDR representation is as follows (see
|
||||
\fI\%RFC4506\fP <\fBhttp://tools.ietf.org/html/rfc4506\fP> for the XDR format):
|
||||
There is no explicit length field as the length is given by the length of
|
||||
the discovery announcement packet itself.
|
||||
.sp
|
||||
The Magic field is a 32 bit word representing 0x2EA7D90B in network (big
|
||||
endian) byte order. It identifies the packet as being a Syncthing discovery
|
||||
protocol packet.
|
||||
.sp
|
||||
The Announce Message contents are in protocol buffer format using the
|
||||
following schema:
|
||||
.INDENT 0.0
|
||||
.INDENT 3.5
|
||||
.sp
|
||||
.nf
|
||||
.ft C
|
||||
struct Announcement {
|
||||
unsigned int Magic;
|
||||
Device This;
|
||||
Device Extra<>;
|
||||
}
|
||||
|
||||
struct Device {
|
||||
opaque ID<32>;
|
||||
Address Addresses<16>;
|
||||
}
|
||||
|
||||
struct Address {
|
||||
string URL<2083>;
|
||||
message Announce {
|
||||
bytes id = 1;
|
||||
repeated string addresses = 2;
|
||||
int64 instance_id = 3;
|
||||
}
|
||||
.ft P
|
||||
.fi
|
||||
.UNINDENT
|
||||
.UNINDENT
|
||||
.sp
|
||||
In the \fBAnnounce\fP structure field \fBMagic\fP is used to ensure
|
||||
a correct datagram was received and MUST be equal to \fB0x7D79BC40\fP\&.
|
||||
The \fBid\fP field contains the Device ID of the sending device.
|
||||
.sp
|
||||
The first Device structure contains information about the sending
|
||||
device. The following zero or more Extra devices contain information
|
||||
about other devices known to the sending device.
|
||||
The \fBaddresses\fP field contains a list of addresses where the device can be
|
||||
contacted. Direct connections will typically have the \fBtcp://\fP scheme.
|
||||
Relay connections will typically use the \fBrelay://\fP scheme.
|
||||
.sp
|
||||
In the \fBDevice\fP structure, field \fBDeviceID\fP is the SHA\-256 (32
|
||||
bytes) of the device X.509 certificate, as explained in section \fIDevice
|
||||
ID\fP\&.
|
||||
When interpreting addresses with an unspecified address, e.g.,
|
||||
\fBtcp://0.0.0.0:22000\fP or \fBtcp://:42424\fP, the source address of the
|
||||
discovery announcement is to be used.
|
||||
.sp
|
||||
For each \fBAddress\fP the \fBURL\fP field contains the actual target address.
|
||||
Direct connections will typically have the \fBtcp://\fP scheme. Relay connections
|
||||
will typically use the \fBrelay://\fP scheme.
|
||||
The \fBinstance_id\fP field is set to a randomly generated ID at client
|
||||
startup. Other devices on the network can detect a change in instance ID
|
||||
between two announces and conclude that the announcing device has restarted.
|
||||
.SH AUTHOR
|
||||
The Syncthing Authors
|
||||
.SH COPYRIGHT
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-NETWORKING" "7" "July 02, 2016" "v0.13" "Syncthing"
|
||||
.TH "SYNCTHING-NETWORKING" "7" "July 24, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-networking \- Firewall Setup
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-RELAY" "7" "July 02, 2016" "v0.13" "Syncthing"
|
||||
.TH "SYNCTHING-RELAY" "7" "July 24, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-relay \- Relay Protocol v1
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-REST-API" "7" "July 02, 2016" "v0.13" "Syncthing"
|
||||
.TH "SYNCTHING-REST-API" "7" "July 24, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-rest-api \- REST API
|
||||
.
|
||||
@@ -401,6 +401,11 @@ Returns the list of recent log entries.
|
||||
.fi
|
||||
.UNINDENT
|
||||
.UNINDENT
|
||||
.SS POST /rest/system/pause
|
||||
.sp
|
||||
Pause the given device.
|
||||
.sp
|
||||
Takes the mandatory \fBdevice\fP parameter and returns status 200 and no content upon success, or status 500 and a plain text error on failure.
|
||||
.SS GET /rest/system/ping
|
||||
.sp
|
||||
Returns a \fB{"ping": "pong"}\fP object.
|
||||
@@ -438,6 +443,11 @@ $ curl \-X POST \-H "X\-API\-Key: abc123" http://localhost:8384/rest/system/rese
|
||||
.SS POST /rest/system/restart
|
||||
.sp
|
||||
Post with empty body to immediately restart Syncthing.
|
||||
.SS POST /rest/system/resume
|
||||
.sp
|
||||
Resume the given device.
|
||||
.sp
|
||||
Takes the mandatory \fBdevice\fP parameter and returns status 200 and no content upon success, or status 500 and a plain text error on failure.
|
||||
.SS POST /rest/system/shutdown
|
||||
.sp
|
||||
Post with empty body to cause Syncthing to exit and not restart.
|
||||
@@ -690,7 +700,7 @@ provides a list of strings which represent globbing patterns described by gobwas
|
||||
.SS POST /rest/db/ignores
|
||||
.sp
|
||||
Expects a format similar to the output of \fBGET\fP call, but only
|
||||
containing the \fBignore\fP field (\fBpatterns\fP field should be omitted).
|
||||
containing the \fBignore\fP field (\fBexpanded\fP field should be omitted).
|
||||
It takes one parameter, \fBfolder\fP, and either updates the content of
|
||||
the \fB\&.stignore\fP echoing it back as a response, or returns an error.
|
||||
.SS GET /rest/db/need
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-SECURITY" "7" "July 02, 2016" "v0.13" "Syncthing"
|
||||
.TH "SYNCTHING-SECURITY" "7" "July 24, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-security \- Security Principles
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-STIGNORE" "5" "July 02, 2016" "v0.13" "Syncthing"
|
||||
.TH "SYNCTHING-STIGNORE" "5" "July 24, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-stignore \- Prevent files from being synchronized to other nodes
|
||||
.
|
||||
@@ -150,6 +150,7 @@ and an \fB\&.stignore\fP file with the contents:
|
||||
.sp
|
||||
.nf
|
||||
.ft C
|
||||
(?d).DS_Store
|
||||
!frobble
|
||||
!quuz
|
||||
foo
|
||||
@@ -168,7 +169,7 @@ all files and directories called "foo", ending in a "2" or starting with
|
||||
.sp
|
||||
.nf
|
||||
.ft C
|
||||
(?d).DS_Store # ignored, will be deleted if gets in the way of parent directory removal
|
||||
\&.DS_Store # ignored, will be deleted if gets in the way of parent directory removal
|
||||
foo # ignored, matches "foo"
|
||||
foofoo # synced, does not match "foo" but would match "foo*" or "*foo"
|
||||
bar/ # synced
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "TODO" "7" "July 02, 2016" "v0.13" "Syncthing"
|
||||
.TH "TODO" "7" "July 24, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
Todo \- Keep automatic backups of deleted files by other nodes
|
||||
.
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING" "1" "July 02, 2016" "v0.13" "Syncthing"
|
||||
.TH "SYNCTHING" "1" "July 24, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing \- Syncthing
|
||||
.
|
||||
|
||||
@@ -34,6 +34,7 @@ var excludeCommits = stringSetFromStrings([]string{
|
||||
"3626003f680bad3e63677982576d3a05421e88e9",
|
||||
"342036408e65bd25bb6afbcc705e2e2c013bb01f",
|
||||
"e37cefdbee1c1cd95ad095b5da6d1252723f103b",
|
||||
"bcc5d7c00f52552303b463d43a636f27b7f7e19b",
|
||||
})
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -29,6 +29,7 @@ var copyrightRegexps = []string{
|
||||
`Copyright`,
|
||||
`package auto`,
|
||||
`automatically generated by genxdr`,
|
||||
`generated by protoc`,
|
||||
}
|
||||
|
||||
var copyrightRe = regexp.MustCompile(strings.Join(copyrightRegexps, "|"))
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
// 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/.
|
||||
|
||||
// +build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -50,7 +50,8 @@ func benchmarkTransfer(t *testing.T, files, sizeExp int) {
|
||||
log.Println("Generating files...")
|
||||
if files == 1 {
|
||||
// Special case. Generate one file with the specified size exactly.
|
||||
fd, err := os.Open("../LICENSE")
|
||||
var fd *os.File
|
||||
fd, err = os.Open("../LICENSE")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
ISC License
|
||||
|
||||
Copyright (c) 2015, Gregory J. Oschwald <oschwald@gmail.com>
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
PERFORMANCE OF THIS SOFTWARE.
|
||||
+288
@@ -0,0 +1,288 @@
|
||||
// Package geoip2 provides a wrapper around the maxminddb package for
|
||||
// easy use with the MaxMind GeoIP2 and GeoLite2 databases. The records for
|
||||
// the IP address is returned from this package as well-formed structures
|
||||
// that match the internal layout of data from MaxMind.
|
||||
package geoip2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/oschwald/maxminddb-golang"
|
||||
)
|
||||
|
||||
// The City structure corresponds to the data in the GeoIP2/GeoLite2 City
|
||||
// databases.
|
||||
type City struct {
|
||||
City struct {
|
||||
GeoNameID uint `maxminddb:"geoname_id"`
|
||||
Names map[string]string `maxminddb:"names"`
|
||||
} `maxminddb:"city"`
|
||||
Continent struct {
|
||||
Code string `maxminddb:"code"`
|
||||
GeoNameID uint `maxminddb:"geoname_id"`
|
||||
Names map[string]string `maxminddb:"names"`
|
||||
} `maxminddb:"continent"`
|
||||
Country struct {
|
||||
GeoNameID uint `maxminddb:"geoname_id"`
|
||||
IsoCode string `maxminddb:"iso_code"`
|
||||
Names map[string]string `maxminddb:"names"`
|
||||
} `maxminddb:"country"`
|
||||
Location struct {
|
||||
AccuracyRadius uint16 `maxminddb:"accuracy_radius"`
|
||||
Latitude float64 `maxminddb:"latitude"`
|
||||
Longitude float64 `maxminddb:"longitude"`
|
||||
MetroCode uint `maxminddb:"metro_code"`
|
||||
TimeZone string `maxminddb:"time_zone"`
|
||||
} `maxminddb:"location"`
|
||||
Postal struct {
|
||||
Code string `maxminddb:"code"`
|
||||
} `maxminddb:"postal"`
|
||||
RegisteredCountry struct {
|
||||
GeoNameID uint `maxminddb:"geoname_id"`
|
||||
IsoCode string `maxminddb:"iso_code"`
|
||||
Names map[string]string `maxminddb:"names"`
|
||||
} `maxminddb:"registered_country"`
|
||||
RepresentedCountry struct {
|
||||
GeoNameID uint `maxminddb:"geoname_id"`
|
||||
IsoCode string `maxminddb:"iso_code"`
|
||||
Names map[string]string `maxminddb:"names"`
|
||||
Type string `maxminddb:"type"`
|
||||
} `maxminddb:"represented_country"`
|
||||
Subdivisions []struct {
|
||||
GeoNameID uint `maxminddb:"geoname_id"`
|
||||
IsoCode string `maxminddb:"iso_code"`
|
||||
Names map[string]string `maxminddb:"names"`
|
||||
} `maxminddb:"subdivisions"`
|
||||
Traits struct {
|
||||
IsAnonymousProxy bool `maxminddb:"is_anonymous_proxy"`
|
||||
IsSatelliteProvider bool `maxminddb:"is_satellite_provider"`
|
||||
} `maxminddb:"traits"`
|
||||
}
|
||||
|
||||
// The Country structure corresponds to the data in the GeoIP2/GeoLite2
|
||||
// Country databases.
|
||||
type Country struct {
|
||||
Continent struct {
|
||||
Code string `maxminddb:"code"`
|
||||
GeoNameID uint `maxminddb:"geoname_id"`
|
||||
Names map[string]string `maxminddb:"names"`
|
||||
} `maxminddb:"continent"`
|
||||
Country struct {
|
||||
GeoNameID uint `maxminddb:"geoname_id"`
|
||||
IsoCode string `maxminddb:"iso_code"`
|
||||
Names map[string]string `maxminddb:"names"`
|
||||
} `maxminddb:"country"`
|
||||
RegisteredCountry struct {
|
||||
GeoNameID uint `maxminddb:"geoname_id"`
|
||||
IsoCode string `maxminddb:"iso_code"`
|
||||
Names map[string]string `maxminddb:"names"`
|
||||
} `maxminddb:"registered_country"`
|
||||
RepresentedCountry struct {
|
||||
GeoNameID uint `maxminddb:"geoname_id"`
|
||||
IsoCode string `maxminddb:"iso_code"`
|
||||
Names map[string]string `maxminddb:"names"`
|
||||
Type string `maxminddb:"type"`
|
||||
} `maxminddb:"represented_country"`
|
||||
Traits struct {
|
||||
IsAnonymousProxy bool `maxminddb:"is_anonymous_proxy"`
|
||||
IsSatelliteProvider bool `maxminddb:"is_satellite_provider"`
|
||||
} `maxminddb:"traits"`
|
||||
}
|
||||
|
||||
// The AnonymousIP structure corresponds to the data in the GeoIP2
|
||||
// Anonymous IP database.
|
||||
type AnonymousIP struct {
|
||||
IsAnonymous bool `maxminddb:"is_anonymous"`
|
||||
IsAnonymousVPN bool `maxminddb:"is_anonymous_vpn"`
|
||||
IsHostingProvider bool `maxminddb:"is_hosting_provider"`
|
||||
IsPublicProxy bool `maxminddb:"is_public_proxy"`
|
||||
IsTorExitNode bool `maxminddb:"is_tor_exit_node"`
|
||||
}
|
||||
|
||||
// The ConnectionType structure corresponds to the data in the GeoIP2
|
||||
// Connection-Type database.
|
||||
type ConnectionType struct {
|
||||
ConnectionType string `maxminddb:"connection_type"`
|
||||
}
|
||||
|
||||
// The Domain structure corresponds to the data in the GeoIP2 Domain database.
|
||||
type Domain struct {
|
||||
Domain string `maxminddb:"domain"`
|
||||
}
|
||||
|
||||
// The ISP structure corresponds to the data in the GeoIP2 ISP database.
|
||||
type ISP struct {
|
||||
AutonomousSystemNumber uint `maxminddb:"autonomous_system_number"`
|
||||
AutonomousSystemOrganization string `maxminddb:"autonomous_system_organization"`
|
||||
ISP string `maxminddb:"isp"`
|
||||
Organization string `maxminddb:"organization"`
|
||||
}
|
||||
|
||||
type databaseType int
|
||||
|
||||
const (
|
||||
isAnonymousIP = 1 << iota
|
||||
isCity
|
||||
isConnectionType
|
||||
isCountry
|
||||
isDomain
|
||||
isEnterprise
|
||||
isISP
|
||||
)
|
||||
|
||||
// Reader holds the maxminddb.Reader structure. It should be created
|
||||
// using the Open function.
|
||||
type Reader struct {
|
||||
mmdbReader *maxminddb.Reader
|
||||
databaseType databaseType
|
||||
}
|
||||
|
||||
// InvalidMethodError is returned when a lookup method is called on a
|
||||
// database that it does not support. For instance, calling the ISP method
|
||||
// on a City database.
|
||||
type InvalidMethodError struct {
|
||||
Method string
|
||||
DatabaseType string
|
||||
}
|
||||
|
||||
func (e InvalidMethodError) Error() string {
|
||||
return fmt.Sprintf(`geoip2: the %s method does not support the %s database`,
|
||||
e.Method, e.DatabaseType)
|
||||
}
|
||||
|
||||
// UnknownDatabaseTypeError is returned when an unknown database type is
|
||||
// opened.
|
||||
type UnknownDatabaseTypeError struct {
|
||||
DatabaseType string
|
||||
}
|
||||
|
||||
func (e UnknownDatabaseTypeError) Error() string {
|
||||
return fmt.Sprintf(`geoip2: reader does not support the "%s" database type`,
|
||||
e.DatabaseType)
|
||||
}
|
||||
|
||||
// Open takes a string path to a file and returns a Reader structure or an
|
||||
// error. The database file is opened using a memory map. Use the Close method
|
||||
// on the Reader object to return the resources to the system.
|
||||
func Open(file string) (*Reader, error) {
|
||||
reader, err := maxminddb.Open(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dbType, err := getDBType(reader)
|
||||
return &Reader{reader, dbType}, err
|
||||
}
|
||||
|
||||
// FromBytes takes a byte slice corresponding to a GeoIP2/GeoLite2 database
|
||||
// file and returns a Reader structure or an error.
|
||||
func FromBytes(bytes []byte) (*Reader, error) {
|
||||
reader, err := maxminddb.FromBytes(bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dbType, err := getDBType(reader)
|
||||
return &Reader{reader, dbType}, err
|
||||
}
|
||||
|
||||
func getDBType(reader *maxminddb.Reader) (databaseType, error) {
|
||||
switch reader.Metadata.DatabaseType {
|
||||
case "GeoIP2-Anonymous-IP":
|
||||
return isAnonymousIP, nil
|
||||
// We allow City lookups on Country for back compat
|
||||
case "GeoLite2-City", "GeoIP2-City", "GeoIP2-Precision-City", "GeoLite2-Country",
|
||||
"GeoIP2-Country":
|
||||
return isCity | isCountry, nil
|
||||
case "GeoIP2-Connection-Type":
|
||||
return isConnectionType, nil
|
||||
case "GeoIP2-Domain":
|
||||
return isDomain, nil
|
||||
case "GeoIP2-Enterprise":
|
||||
return isEnterprise | isCity | isCountry, nil
|
||||
case "GeoIP2-ISP", "GeoIP2-Precision-ISP":
|
||||
return isISP, nil
|
||||
default:
|
||||
return 0, UnknownDatabaseTypeError{reader.Metadata.DatabaseType}
|
||||
}
|
||||
}
|
||||
|
||||
// City takes an IP address as a net.IP struct and returns a City struct
|
||||
// and/or an error. Although this can be used with other databases, this
|
||||
// method generally should be used with the GeoIP2 or GeoLite2 City databases.
|
||||
func (r *Reader) City(ipAddress net.IP) (*City, error) {
|
||||
if isCity&r.databaseType == 0 {
|
||||
return nil, InvalidMethodError{"City", r.Metadata().DatabaseType}
|
||||
}
|
||||
var city City
|
||||
err := r.mmdbReader.Lookup(ipAddress, &city)
|
||||
return &city, err
|
||||
}
|
||||
|
||||
// Country takes an IP address as a net.IP struct and returns a Country struct
|
||||
// and/or an error. Although this can be used with other databases, this
|
||||
// method generally should be used with the GeoIP2 or GeoLite2 Country
|
||||
// databases.
|
||||
func (r *Reader) Country(ipAddress net.IP) (*Country, error) {
|
||||
if isCountry&r.databaseType == 0 {
|
||||
return nil, InvalidMethodError{"Country", r.Metadata().DatabaseType}
|
||||
}
|
||||
var country Country
|
||||
err := r.mmdbReader.Lookup(ipAddress, &country)
|
||||
return &country, err
|
||||
}
|
||||
|
||||
// AnonymousIP takes an IP address as a net.IP struct and returns a
|
||||
// AnonymousIP struct and/or an error.
|
||||
func (r *Reader) AnonymousIP(ipAddress net.IP) (*AnonymousIP, error) {
|
||||
if isAnonymousIP&r.databaseType == 0 {
|
||||
return nil, InvalidMethodError{"AnonymousIP", r.Metadata().DatabaseType}
|
||||
}
|
||||
var anonIP AnonymousIP
|
||||
err := r.mmdbReader.Lookup(ipAddress, &anonIP)
|
||||
return &anonIP, err
|
||||
}
|
||||
|
||||
// ConnectionType takes an IP address as a net.IP struct and returns a
|
||||
// ConnectionType struct and/or an error
|
||||
func (r *Reader) ConnectionType(ipAddress net.IP) (*ConnectionType, error) {
|
||||
if isConnectionType&r.databaseType == 0 {
|
||||
return nil, InvalidMethodError{"ConnectionType", r.Metadata().DatabaseType}
|
||||
}
|
||||
var val ConnectionType
|
||||
err := r.mmdbReader.Lookup(ipAddress, &val)
|
||||
return &val, err
|
||||
}
|
||||
|
||||
// Domain takes an IP address as a net.IP struct and returns a
|
||||
// Domain struct and/or an error
|
||||
func (r *Reader) Domain(ipAddress net.IP) (*Domain, error) {
|
||||
if isDomain&r.databaseType == 0 {
|
||||
return nil, InvalidMethodError{"Domain", r.Metadata().DatabaseType}
|
||||
}
|
||||
var val Domain
|
||||
err := r.mmdbReader.Lookup(ipAddress, &val)
|
||||
return &val, err
|
||||
}
|
||||
|
||||
// ISP takes an IP address as a net.IP struct and returns a ISP struct and/or
|
||||
// an error
|
||||
func (r *Reader) ISP(ipAddress net.IP) (*ISP, error) {
|
||||
if isISP&r.databaseType == 0 {
|
||||
return nil, InvalidMethodError{"ISP", r.Metadata().DatabaseType}
|
||||
}
|
||||
var val ISP
|
||||
err := r.mmdbReader.Lookup(ipAddress, &val)
|
||||
return &val, err
|
||||
}
|
||||
|
||||
// Metadata takes no arguments and returns a struct containing metadata about
|
||||
// the MaxMind database in use by the Reader.
|
||||
func (r *Reader) Metadata() maxminddb.Metadata {
|
||||
return r.mmdbReader.Metadata
|
||||
}
|
||||
|
||||
// Close unmaps the database file from virtual memory and returns the
|
||||
// resources to the system.
|
||||
func (r *Reader) Close() error {
|
||||
return r.mmdbReader.Close()
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
Copyright (c) 2015, Gregory J. Oschwald <oschwald@gmail.com>
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
PERFORMANCE OF THIS SOFTWARE.
|
||||
+559
@@ -0,0 +1,559 @@
|
||||
package maxminddb
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"math"
|
||||
"math/big"
|
||||
"reflect"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type decoder struct {
|
||||
buffer []byte
|
||||
}
|
||||
|
||||
type dataType int
|
||||
|
||||
const (
|
||||
_Extended dataType = iota
|
||||
_Pointer
|
||||
_String
|
||||
_Float64
|
||||
_Bytes
|
||||
_Uint16
|
||||
_Uint32
|
||||
_Map
|
||||
_Int32
|
||||
_Uint64
|
||||
_Uint128
|
||||
_Slice
|
||||
_Container
|
||||
_Marker
|
||||
_Bool
|
||||
_Float32
|
||||
)
|
||||
|
||||
func (d *decoder) decode(offset uint, result reflect.Value) (uint, error) {
|
||||
typeNum, size, newOffset := d.decodeCtrlData(offset)
|
||||
|
||||
if typeNum != _Pointer && result.Kind() == reflect.Uintptr {
|
||||
result.Set(reflect.ValueOf(uintptr(offset)))
|
||||
return d.nextValueOffset(offset, 1), nil
|
||||
}
|
||||
return d.decodeFromType(typeNum, size, newOffset, result)
|
||||
}
|
||||
|
||||
func (d *decoder) decodeCtrlData(offset uint) (dataType, uint, uint) {
|
||||
newOffset := offset + 1
|
||||
ctrlByte := d.buffer[offset]
|
||||
|
||||
typeNum := dataType(ctrlByte >> 5)
|
||||
if typeNum == _Extended {
|
||||
typeNum = dataType(d.buffer[newOffset] + 7)
|
||||
newOffset++
|
||||
}
|
||||
|
||||
var size uint
|
||||
size, newOffset = d.sizeFromCtrlByte(ctrlByte, newOffset, typeNum)
|
||||
return typeNum, size, newOffset
|
||||
}
|
||||
|
||||
func (d *decoder) sizeFromCtrlByte(ctrlByte byte, offset uint, typeNum dataType) (uint, uint) {
|
||||
size := uint(ctrlByte & 0x1f)
|
||||
if typeNum == _Extended {
|
||||
return size, offset
|
||||
}
|
||||
|
||||
var bytesToRead uint
|
||||
if size > 28 {
|
||||
bytesToRead = size - 28
|
||||
}
|
||||
|
||||
newOffset := offset + bytesToRead
|
||||
sizeBytes := d.buffer[offset:newOffset]
|
||||
|
||||
switch {
|
||||
case size == 29:
|
||||
size = 29 + uint(sizeBytes[0])
|
||||
case size == 30:
|
||||
size = 285 + uint(uintFromBytes(0, sizeBytes))
|
||||
case size > 30:
|
||||
size = uint(uintFromBytes(0, sizeBytes)) + 65821
|
||||
}
|
||||
return size, newOffset
|
||||
}
|
||||
|
||||
func (d *decoder) decodeFromType(dtype dataType, size uint, offset uint, result reflect.Value) (uint, error) {
|
||||
for result.Kind() == reflect.Ptr {
|
||||
if result.IsNil() {
|
||||
result.Set(reflect.New(result.Type().Elem()))
|
||||
}
|
||||
result = result.Elem()
|
||||
}
|
||||
|
||||
switch dtype {
|
||||
case _Bool:
|
||||
return d.unmarshalBool(size, offset, result)
|
||||
case _Bytes:
|
||||
return d.unmarshalBytes(size, offset, result)
|
||||
case _Float32:
|
||||
return d.unmarshalFloat32(size, offset, result)
|
||||
case _Float64:
|
||||
return d.unmarshalFloat64(size, offset, result)
|
||||
case _Int32:
|
||||
return d.unmarshalInt32(size, offset, result)
|
||||
case _Map:
|
||||
return d.unmarshalMap(size, offset, result)
|
||||
case _Pointer:
|
||||
return d.unmarshalPointer(size, offset, result)
|
||||
case _Slice:
|
||||
return d.unmarshalSlice(size, offset, result)
|
||||
case _String:
|
||||
return d.unmarshalString(size, offset, result)
|
||||
case _Uint16:
|
||||
return d.unmarshalUint(size, offset, result, 16)
|
||||
case _Uint32:
|
||||
return d.unmarshalUint(size, offset, result, 32)
|
||||
case _Uint64:
|
||||
return d.unmarshalUint(size, offset, result, 64)
|
||||
case _Uint128:
|
||||
return d.unmarshalUint128(size, offset, result)
|
||||
default:
|
||||
return 0, newInvalidDatabaseError("unknown type: %d", dtype)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *decoder) unmarshalBool(size uint, offset uint, result reflect.Value) (uint, error) {
|
||||
if size > 1 {
|
||||
return 0, newInvalidDatabaseError("the MaxMind DB file's data section contains bad data (bool size of %v)", size)
|
||||
}
|
||||
value, newOffset, err := d.decodeBool(size, offset)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
switch result.Kind() {
|
||||
default:
|
||||
return newOffset, newUnmarshalTypeError(value, result.Type())
|
||||
case reflect.Bool:
|
||||
result.SetBool(value)
|
||||
return newOffset, nil
|
||||
case reflect.Interface:
|
||||
result.Set(reflect.ValueOf(value))
|
||||
return newOffset, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (d *decoder) unmarshalBytes(size uint, offset uint, result reflect.Value) (uint, error) {
|
||||
value, newOffset, err := d.decodeBytes(size, offset)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
switch result.Kind() {
|
||||
default:
|
||||
return newOffset, newUnmarshalTypeError(value, result.Type())
|
||||
case reflect.Slice:
|
||||
result.SetBytes(value)
|
||||
return newOffset, nil
|
||||
case reflect.Interface:
|
||||
result.Set(reflect.ValueOf(value))
|
||||
return newOffset, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (d *decoder) unmarshalFloat32(size uint, offset uint, result reflect.Value) (uint, error) {
|
||||
if size != 4 {
|
||||
return 0, newInvalidDatabaseError("the MaxMind DB file's data section contains bad data (float32 size of %v)", size)
|
||||
}
|
||||
value, newOffset, err := d.decodeFloat32(size, offset)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
switch result.Kind() {
|
||||
default:
|
||||
return newOffset, newUnmarshalTypeError(value, result.Type())
|
||||
case reflect.Float32, reflect.Float64:
|
||||
result.SetFloat(float64(value))
|
||||
return newOffset, nil
|
||||
case reflect.Interface:
|
||||
result.Set(reflect.ValueOf(value))
|
||||
return newOffset, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (d *decoder) unmarshalFloat64(size uint, offset uint, result reflect.Value) (uint, error) {
|
||||
|
||||
if size != 8 {
|
||||
return 0, newInvalidDatabaseError("the MaxMind DB file's data section contains bad data (float 64 size of %v)", size)
|
||||
}
|
||||
value, newOffset, err := d.decodeFloat64(size, offset)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
switch result.Kind() {
|
||||
default:
|
||||
return newOffset, newUnmarshalTypeError(value, result.Type())
|
||||
case reflect.Float32, reflect.Float64:
|
||||
if result.OverflowFloat(value) {
|
||||
return 0, newUnmarshalTypeError(value, result.Type())
|
||||
}
|
||||
result.SetFloat(value)
|
||||
return newOffset, nil
|
||||
case reflect.Interface:
|
||||
result.Set(reflect.ValueOf(value))
|
||||
return newOffset, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (d *decoder) unmarshalInt32(size uint, offset uint, result reflect.Value) (uint, error) {
|
||||
if size > 4 {
|
||||
return 0, newInvalidDatabaseError("the MaxMind DB file's data section contains bad data (int32 size of %v)", size)
|
||||
}
|
||||
value, newOffset, err := d.decodeInt(size, offset)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
switch result.Kind() {
|
||||
default:
|
||||
return newOffset, newUnmarshalTypeError(value, result.Type())
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
n := int64(value)
|
||||
if result.OverflowInt(n) {
|
||||
return 0, newUnmarshalTypeError(value, result.Type())
|
||||
}
|
||||
result.SetInt(n)
|
||||
return newOffset, nil
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
n := uint64(value)
|
||||
if result.OverflowUint(n) {
|
||||
return 0, newUnmarshalTypeError(value, result.Type())
|
||||
}
|
||||
result.SetUint(n)
|
||||
return newOffset, nil
|
||||
case reflect.Interface:
|
||||
result.Set(reflect.ValueOf(value))
|
||||
return newOffset, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (d *decoder) unmarshalMap(size uint, offset uint, result reflect.Value) (uint, error) {
|
||||
switch result.Kind() {
|
||||
default:
|
||||
return 0, newUnmarshalTypeError("map", result.Type())
|
||||
case reflect.Struct:
|
||||
return d.decodeStruct(size, offset, result)
|
||||
case reflect.Map:
|
||||
return d.decodeMap(size, offset, result)
|
||||
case reflect.Interface:
|
||||
rv := reflect.ValueOf(make(map[string]interface{}, size))
|
||||
newOffset, err := d.decodeMap(size, offset, rv)
|
||||
result.Set(rv)
|
||||
return newOffset, err
|
||||
}
|
||||
}
|
||||
|
||||
func (d *decoder) unmarshalPointer(size uint, offset uint, result reflect.Value) (uint, error) {
|
||||
pointer, newOffset := d.decodePointer(size, offset)
|
||||
_, err := d.decode(pointer, result)
|
||||
return newOffset, err
|
||||
}
|
||||
|
||||
func (d *decoder) unmarshalSlice(size uint, offset uint, result reflect.Value) (uint, error) {
|
||||
|
||||
switch result.Kind() {
|
||||
default:
|
||||
return 0, newUnmarshalTypeError("array", result.Type())
|
||||
case reflect.Slice:
|
||||
return d.decodeSlice(size, offset, result)
|
||||
case reflect.Interface:
|
||||
a := []interface{}{}
|
||||
rv := reflect.ValueOf(&a).Elem()
|
||||
newOffset, err := d.decodeSlice(size, offset, rv)
|
||||
result.Set(rv)
|
||||
return newOffset, err
|
||||
}
|
||||
}
|
||||
|
||||
func (d *decoder) unmarshalString(size uint, offset uint, result reflect.Value) (uint, error) {
|
||||
value, newOffset, err := d.decodeString(size, offset)
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
switch result.Kind() {
|
||||
default:
|
||||
return newOffset, newUnmarshalTypeError(value, result.Type())
|
||||
case reflect.String:
|
||||
result.SetString(value)
|
||||
return newOffset, nil
|
||||
case reflect.Interface:
|
||||
result.Set(reflect.ValueOf(value))
|
||||
return newOffset, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (d *decoder) unmarshalUint(size uint, offset uint, result reflect.Value, uintType uint) (uint, error) {
|
||||
if size > uintType/8 {
|
||||
return 0, newInvalidDatabaseError("the MaxMind DB file's data section contains bad data (uint%v size of %v)", uintType, size)
|
||||
}
|
||||
|
||||
value, newOffset, err := d.decodeUint(size, offset)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
switch result.Kind() {
|
||||
default:
|
||||
return newOffset, newUnmarshalTypeError(value, result.Type())
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
n := int64(value)
|
||||
if result.OverflowInt(n) {
|
||||
return 0, newUnmarshalTypeError(value, result.Type())
|
||||
}
|
||||
result.SetInt(n)
|
||||
return newOffset, nil
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
if result.OverflowUint(value) {
|
||||
return 0, newUnmarshalTypeError(value, result.Type())
|
||||
}
|
||||
result.SetUint(value)
|
||||
return newOffset, nil
|
||||
case reflect.Interface:
|
||||
result.Set(reflect.ValueOf(value))
|
||||
return newOffset, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (d *decoder) unmarshalUint128(size uint, offset uint, result reflect.Value) (uint, error) {
|
||||
if size > 16 {
|
||||
return 0, newInvalidDatabaseError("the MaxMind DB file's data section contains bad data (uint128 size of %v)", size)
|
||||
}
|
||||
value, newOffset, err := d.decodeUint128(size, offset)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// XXX - this should allow *big.Int rather than just bigInt
|
||||
// Currently this is reported as invalid
|
||||
switch result.Kind() {
|
||||
default:
|
||||
return newOffset, newUnmarshalTypeError(value, result.Type())
|
||||
case reflect.Struct:
|
||||
result.Set(reflect.ValueOf(*value))
|
||||
return newOffset, nil
|
||||
case reflect.Interface, reflect.Ptr:
|
||||
result.Set(reflect.ValueOf(value))
|
||||
return newOffset, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (d *decoder) decodeBool(size uint, offset uint) (bool, uint, error) {
|
||||
return size != 0, offset, nil
|
||||
}
|
||||
|
||||
func (d *decoder) decodeBytes(size uint, offset uint) ([]byte, uint, error) {
|
||||
newOffset := offset + size
|
||||
bytes := make([]byte, size)
|
||||
copy(bytes, d.buffer[offset:newOffset])
|
||||
return bytes, newOffset, nil
|
||||
}
|
||||
|
||||
func (d *decoder) decodeFloat64(size uint, offset uint) (float64, uint, error) {
|
||||
newOffset := offset + size
|
||||
bits := binary.BigEndian.Uint64(d.buffer[offset:newOffset])
|
||||
return math.Float64frombits(bits), newOffset, nil
|
||||
}
|
||||
|
||||
func (d *decoder) decodeFloat32(size uint, offset uint) (float32, uint, error) {
|
||||
newOffset := offset + size
|
||||
bits := binary.BigEndian.Uint32(d.buffer[offset:newOffset])
|
||||
return math.Float32frombits(bits), newOffset, nil
|
||||
}
|
||||
|
||||
func (d *decoder) decodeInt(size uint, offset uint) (int, uint, error) {
|
||||
newOffset := offset + size
|
||||
var val int32
|
||||
for _, b := range d.buffer[offset:newOffset] {
|
||||
val = (val << 8) | int32(b)
|
||||
}
|
||||
return int(val), newOffset, nil
|
||||
}
|
||||
|
||||
func (d *decoder) decodeMap(size uint, offset uint, result reflect.Value) (uint, error) {
|
||||
if result.IsNil() {
|
||||
result.Set(reflect.MakeMap(result.Type()))
|
||||
}
|
||||
|
||||
for i := uint(0); i < size; i++ {
|
||||
var key string
|
||||
var err error
|
||||
key, offset, err = d.decodeKeyString(offset)
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
value := reflect.New(result.Type().Elem())
|
||||
offset, err = d.decode(offset, value)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
result.SetMapIndex(reflect.ValueOf(key), value.Elem())
|
||||
}
|
||||
return offset, nil
|
||||
}
|
||||
|
||||
func (d *decoder) decodePointer(size uint, offset uint) (uint, uint) {
|
||||
pointerSize := ((size >> 3) & 0x3) + 1
|
||||
newOffset := offset + pointerSize
|
||||
pointerBytes := d.buffer[offset:newOffset]
|
||||
var prefix uint64
|
||||
if pointerSize == 4 {
|
||||
prefix = 0
|
||||
} else {
|
||||
prefix = uint64(size & 0x7)
|
||||
}
|
||||
unpacked := uint(uintFromBytes(prefix, pointerBytes))
|
||||
|
||||
var pointerValueOffset uint
|
||||
switch pointerSize {
|
||||
case 1:
|
||||
pointerValueOffset = 0
|
||||
case 2:
|
||||
pointerValueOffset = 2048
|
||||
case 3:
|
||||
pointerValueOffset = 526336
|
||||
case 4:
|
||||
pointerValueOffset = 0
|
||||
}
|
||||
|
||||
pointer := unpacked + pointerValueOffset
|
||||
|
||||
return pointer, newOffset
|
||||
}
|
||||
|
||||
func (d *decoder) decodeSlice(size uint, offset uint, result reflect.Value) (uint, error) {
|
||||
result.Set(reflect.MakeSlice(result.Type(), int(size), int(size)))
|
||||
for i := 0; i < int(size); i++ {
|
||||
var err error
|
||||
offset, err = d.decode(offset, result.Index(i))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
return offset, nil
|
||||
}
|
||||
|
||||
func (d *decoder) decodeString(size uint, offset uint) (string, uint, error) {
|
||||
newOffset := offset + size
|
||||
return string(d.buffer[offset:newOffset]), newOffset, nil
|
||||
}
|
||||
|
||||
var (
|
||||
fieldMap = map[reflect.Type]map[string]int{}
|
||||
fieldMapMu sync.RWMutex
|
||||
)
|
||||
|
||||
func (d *decoder) decodeStruct(size uint, offset uint, result reflect.Value) (uint, error) {
|
||||
resultType := result.Type()
|
||||
|
||||
fieldMapMu.RLock()
|
||||
fields, ok := fieldMap[resultType]
|
||||
fieldMapMu.RUnlock()
|
||||
if !ok {
|
||||
numFields := resultType.NumField()
|
||||
fields = make(map[string]int, numFields)
|
||||
for i := 0; i < numFields; i++ {
|
||||
fieldType := resultType.Field(i)
|
||||
|
||||
fieldName := fieldType.Name
|
||||
if tag := fieldType.Tag.Get("maxminddb"); tag != "" {
|
||||
fieldName = tag
|
||||
}
|
||||
fields[fieldName] = i
|
||||
}
|
||||
fieldMapMu.Lock()
|
||||
fieldMap[resultType] = fields
|
||||
fieldMapMu.Unlock()
|
||||
}
|
||||
|
||||
for i := uint(0); i < size; i++ {
|
||||
var (
|
||||
err error
|
||||
key string
|
||||
)
|
||||
key, offset, err = d.decodeStructKey(offset)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
i, ok := fields[key]
|
||||
if !ok {
|
||||
offset = d.nextValueOffset(offset, 1)
|
||||
continue
|
||||
}
|
||||
offset, err = d.decode(offset, result.Field(i))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
return offset, nil
|
||||
}
|
||||
|
||||
func (d *decoder) decodeUint(size uint, offset uint) (uint64, uint, error) {
|
||||
newOffset := offset + size
|
||||
val := uintFromBytes(0, d.buffer[offset:newOffset])
|
||||
|
||||
return val, newOffset, nil
|
||||
}
|
||||
|
||||
func (d *decoder) decodeUint128(size uint, offset uint) (*big.Int, uint, error) {
|
||||
newOffset := offset + size
|
||||
val := new(big.Int)
|
||||
val.SetBytes(d.buffer[offset:newOffset])
|
||||
|
||||
return val, newOffset, nil
|
||||
}
|
||||
|
||||
func uintFromBytes(prefix uint64, uintBytes []byte) uint64 {
|
||||
val := prefix
|
||||
for _, b := range uintBytes {
|
||||
val = (val << 8) | uint64(b)
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
func (d *decoder) decodeKeyString(offset uint) (string, uint, error) {
|
||||
typeNum, size, newOffset := d.decodeCtrlData(offset)
|
||||
if typeNum == _Pointer {
|
||||
pointer, ptrOffset := d.decodePointer(size, newOffset)
|
||||
key, _, err := d.decodeKeyString(pointer)
|
||||
return key, ptrOffset, err
|
||||
}
|
||||
if typeNum != _String {
|
||||
return "", 0, newInvalidDatabaseError("unexpected type when decoding string: %v", typeNum)
|
||||
}
|
||||
return d.decodeString(size, newOffset)
|
||||
}
|
||||
|
||||
// This function is used to skip ahead to the next value without decoding
|
||||
// the one at the offset passed in. The size bits have different meanings for
|
||||
// different data types
|
||||
func (d *decoder) nextValueOffset(offset uint, numberToSkip uint) uint {
|
||||
if numberToSkip == 0 {
|
||||
return offset
|
||||
}
|
||||
typeNum, size, offset := d.decodeCtrlData(offset)
|
||||
switch typeNum {
|
||||
case _Pointer:
|
||||
_, offset = d.decodePointer(size, offset)
|
||||
case _Map:
|
||||
numberToSkip += 2 * size
|
||||
case _Slice:
|
||||
numberToSkip += size
|
||||
case _Bool:
|
||||
default:
|
||||
offset += size
|
||||
}
|
||||
return d.nextValueOffset(offset, numberToSkip-1)
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package maxminddb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// InvalidDatabaseError is returned when the database contains invalid data
|
||||
// and cannot be parsed.
|
||||
type InvalidDatabaseError struct {
|
||||
message string
|
||||
}
|
||||
|
||||
func newInvalidDatabaseError(format string, args ...interface{}) InvalidDatabaseError {
|
||||
return InvalidDatabaseError{fmt.Sprintf(format, args...)}
|
||||
}
|
||||
|
||||
func (e InvalidDatabaseError) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
// UnmarshalTypeError is returned when the value in the database cannot be
|
||||
// assigned to the specified data type.
|
||||
type UnmarshalTypeError struct {
|
||||
Value string // stringified copy of the database value that caused the error
|
||||
Type reflect.Type // type of the value that could not be assign to
|
||||
}
|
||||
|
||||
func newUnmarshalTypeError(value interface{}, rType reflect.Type) UnmarshalTypeError {
|
||||
return UnmarshalTypeError{
|
||||
Value: fmt.Sprintf("%v", value),
|
||||
Type: rType,
|
||||
}
|
||||
}
|
||||
|
||||
func (e UnmarshalTypeError) Error() string {
|
||||
return fmt.Sprintf("maxminddb: cannot unmarshal %s into type %s", e.Value, e.Type.String())
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// +build appengine
|
||||
|
||||
package maxminddb
|
||||
|
||||
func (d *decoder) decodeStructKey(offset uint) (string, uint, error) {
|
||||
return d.decodeKeyString(offset)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user