Compare commits

...
84 Commits
Author SHA1 Message Date
Jakob Borg c87faace6b Merge remote-tracking branch 'syncthing/pr/1995'
* syncthing/pr/1995:
  Add switch to disable relays
  Do not start relay service unless explicitly asked for, or global announcement server is running
  Add dynamic relay lookup (DDoS relays.syncthing.net!)
  Discovery clients now take an announcer, global discovery is delayed
  Expose connection type and relay status in the UI
  Add dependencies (fixes #1364)
  Check relays for available devices
  Add incoming connection relay service
  Add unsubscribe to config
  Connections have types
  Large refactoring/feature commit
2015-08-20 09:13:37 +02:00
Zillode e2be051558 Merge pull request #2169 from calmh/restartmon
Retain standard streams over restart (fixes #2155)
2015-08-19 23:16:32 +02:00
Audrius Butkevicius 1e8b185377 Add switch to disable relays 2015-08-19 21:13:40 +01:00
Audrius Butkevicius 031804827f Do not start relay service unless explicitly asked for, or global announcement server is running 2015-08-19 21:13:10 +01:00
Audrius Butkevicius 6cccd9b6fc Add dynamic relay lookup (DDoS relays.syncthing.net!) 2015-08-19 21:12:34 +01:00
Audrius Butkevicius 687fbb0a7e Discovery clients now take an announcer, global discovery is delayed 2015-08-19 21:12:00 +01:00
Audrius Butkevicius 8f2db99c86 Expose connection type and relay status in the UI 2015-08-19 21:11:55 +01:00
Audrius Butkevicius 2c0f8dc546 Add dependencies (fixes #1364) 2015-08-19 21:06:46 +01:00
Audrius Butkevicius a388fb0bb7 Check relays for available devices 2015-08-19 20:57:37 +01:00
Audrius Butkevicius 27465353c1 Add incoming connection relay service 2015-08-19 20:57:33 +01:00
Audrius Butkevicius c2ccab4361 Add unsubscribe to config 2015-08-19 20:55:33 +01:00
Audrius Butkevicius bb876eac82 Connections have types 2015-08-19 20:55:29 +01:00
Audrius Butkevicius 34c04babbe Large refactoring/feature commit
1. Change listen addresses to URIs
2. Break out connectionSvc to support listeners and dialers based on schema
3. Add relay announcement and lookups part of discovery service
2015-08-19 20:53:01 +01:00
Audrius Butkevicius 50702eda94 Merge pull request #2171 from Zillode/staggered-test
Add unit test for staggered versioning (fixes #2165)
2015-08-19 20:11:19 +01:00
Jakob Borg 59eeafbdfa Merge pull request #2174 from alex2108/master
Fix time zone error in staggered versioning (fixes #2165)
2015-08-19 20:25:24 +02:00
Alexander Graf abc606608c Fix time zone error in staggered versioning (fixes #2165) 2015-08-19 17:23:50 +02:00
Jakob Borg 1487552b48 s/in/at/ (fixes #2158) 2015-08-19 10:42:48 +02:00
Jakob Borg c7dbe18df6 Newest first should be different from oldest first (fixes #2161) 2015-08-19 09:42:52 +02:00
Jakob Borg c2bc3358cc Merge pull request #2168 from calmh/codename
Add release code name
2015-08-19 08:31:22 +02:00
Lode Hoste 47a1494d68 Add unit test for staggered versioning (fixes #2165) 2015-08-18 19:52:58 +02:00
Jakob Borg dbb388719e Retain standard streams over restart (fixes #2155) 2015-08-18 17:24:50 +02:00
Jakob Borg 283c91548a Add release code name
I figured we're missing out on being cool and awesome by not having an
alphabetically based release code name like the big guys. This commit
fixes that. I've unilaterally decided on a theme of "$metal $bug"
because metals are kind of cool, and bugs, well, ...
2015-08-18 13:33:36 +02:00
Audrius Butkevicius 38b93bd310 Merge pull request #2167 from uok/fixicon
Fix missing folder master icon
2015-08-18 09:52:22 +01:00
Ben Schulz 8dcc30ac83 Fix missing folder master icon 2015-08-18 10:40:18 +02:00
Jakob Borg 0ee123375d Merge remote-tracking branch 'syncthing/pr/2117'
* syncthing/pr/2117:
  Disable testing upgrade endpoint because it fails when disconnected
2015-08-18 09:15:00 +02:00
Jakob Borg be18cbef8b Update dependencies 2015-08-18 08:56:07 +02:00
Lode Hoste 8eb494c13e Disable testing upgrade endpoint because it fails when disconnected 2015-08-17 22:08:35 +02:00
Audrius Butkevicius b6b6375f70 Merge pull request #2163 from calmh/dbrecover
Recover from 'corrupted or incomplete CURRENT file' etc
2015-08-16 15:58:51 +01:00
Jakob Borg 8783688391 Recover from 'corrupted or incomplete CURRENT file' etc (fixes #2017) 2015-08-16 16:36:06 +02:00
Jakob Borg 98afc3e99c Docs & translation update 2015-08-16 15:29:48 +02:00
Audrius Butkevicius 50a1858367 Merge pull request #2136 from calmh/noarchivedir
Clarify and correct handling of existing files/directories when pulling
2015-08-15 14:31:38 +01:00
Audrius Butkevicius f3f586773b Merge pull request #2160 from calmh/rlimit
Increase open file (fd) limit if possible
2015-08-15 14:31:20 +01:00
Jakob Borg 61a182077f Clarify and correct handling of existing files/directories when pulling
This fixes a corner case I discovered in the symlink branch, where we
unexpectedly succeed in "replacing" an entire non-empty directory tree
with a file or symlink. This happens when archiving is in use, as we
then just move the entire tree away into the archive. This is wrong as
we should just archive files and fail on non-empty dirs in all cases.

New handling first checks what the (old) thing is, and if it's a
directory or symlink just does the delete, otherwise does conflict
handling or archiving as appropriate.
2015-08-15 15:29:59 +02:00
Jakob Borg 1c9513e770 Increase open file (fd) limit if possible
This will decrease the risk of running out of file descriptors for the
database and other bad things, which could otherwise potentially happen
if we're serving lots of requests and scanning in parallel, etc.

Windows doesn't have a per process open file limit like Unix so we don't
need to worry about it there.
2015-08-15 15:28:53 +02:00
Jakob Borg 5e5eb9bf8e Update test configs to v11 2015-08-14 14:19:43 +02:00
Audrius Butkevicius 7a9bb65e03 Merge pull request #2156 from calmh/stuckatzero
Don't get stuck at "Syncing 0%" when adding a new folder
2015-08-14 10:42:01 +01:00
Jakob Borg a5345ac71e Don't get stuck at "Syncing 0%" when adding a new folder
The number of copiers and pullers is set to default at config loading
time, but the new folder configuration doesn't pass through config
loading so we start up with 0 copiers and 0 pullers and hence get stuck.
I moved the default handling to the puller itself instead. I think this
way is also cleaner as we get to keep the 0 in the config and the puller
gets to decide the defaults on it's own.
2015-08-14 10:35:51 +02:00
Jakob Borg ae5079f7b4 Update lang-en.json 2015-08-14 09:13:09 +02:00
Jakob Borg ea1ecfbc38 Merge pull request #2147 from uok/dontbesonegative
Prevent negative values for number inputs
2015-08-14 09:12:10 +02:00
Jakob Borg a84b6b4bcc Merge pull request #2145 from uok/awesome
Change to Font Awesome icon font (fixes #2138)
2015-08-14 09:11:14 +02:00
Ben Schulz 93023128fd Prevent negative values for number inputs
- settings: incoming/outgoing rate limit - min: 0
- folder: maximum age (staggered file versioning) - min: 0
- help texts for validation
2015-08-13 15:56:10 +02:00
Ben Schulz 77157f16a1 Change to Font Awesome icon font (fixes #2138)
- remove Glyphicon assets and customize bootstrap CSS
- add Font Awesome v4.4.0 assets
- replace Glyphicons with Font Awesome icons in HTML
- add icons to modal headers
- add attribution for Font Awesome
- format HTML source code for buttons
2015-08-13 15:41:51 +02:00
Jakob Borg 99736e5066 Ensure dir before files ordering when scanning 2015-08-13 13:01:50 +02:00
Jakob Borg 681306b7a1 Clean up the scripts a bit (...)
- Move the Go files into script/ instead of random places
- Rewrite check-contrib.sh into check-authors.go and check-copyright.go
- Clean up build.sh a little bit
2015-08-13 12:35:26 +02:00
Jakob Borg 5f36c9d4de Merge pull request #2152 from Zillode/fix-gui-reorg
Revert small changes made during reorg GUI
2015-08-12 21:47:25 +02:00
Lode Hoste 6cbc8791b1 Revert small changes made during reorg GUI 2015-08-12 21:31:34 +02:00
Jakob Borg c08de67b0d Remove erroneous file 2015-08-11 17:05:59 +02:00
Audrius Butkevicius b21e18dfad Merge pull request #2149 from kamadak/fix-delete-folder
Fix deleting a folder.
2015-08-11 00:14:26 +01:00
KAMADA Ken'ichi 1e497915be Fix deleting a folder.
Removing a folder does not work in the "Edit Folder" dialog.
This bug was introduced in 26d52be.
2015-08-11 07:48:48 +09:00
Audrius Butkevicius 3704c41dda Merge pull request #2146 from uok/double
Remove double slashes in directives (fixes #2143)
2015-08-10 11:45:03 +01:00
Ben Schulz 6ff31ac666 Remove double slashes in directives (fixes #2143) 2015-08-10 11:48:39 +02:00
Jakob Borg a2f73a7d35 Allow specifying Docker image to use for building 2015-08-09 14:40:18 +02:00
Jakob Borg 1492e57676 Minor typo in UPnP service description list 2015-08-09 14:14:13 +02:00
Jakob Borg 7504fc53b6 Merge branch 'v0.11'
* v0.11:
  Translations and docs update
  Enable browser caching of static resources
  Handle multiple case insensitivity prefixes in ignores (fixes #2134)
  Make rescan available for unshared folders
  Add timeout for peek (fixes #1035)
  Fix TestReset when Syncthing shuts down too fast
  Clarify password in integration tests
  Properly rename config files during integration tests (fixes #1769)
2015-08-09 11:58:18 +02:00
Jakob Borg 49aa9399be Repair config tests 2015-08-09 11:46:28 +02:00
Jakob Borg 2bcb57c994 Merge branch 'pr-2066'
* pr-2066:
  Configurable home disk percentage, translations
  Add minimum disk free percentage to GUI
  Stop folder when running out of disk space (fixes #2057)
2015-08-09 10:38:33 +02:00
Jakob Borg a2df691c7d Configurable home disk percentage, translations 2015-08-09 10:37:23 +02:00
Lode HosteandJakob Borg 58f1191f2d Add minimum disk free percentage to GUI 2015-08-09 10:37:23 +02:00
Lode HosteandJakob Borg dfaa999291 Stop folder when running out of disk space (fixes #2057)
& tweaks by calmh
2015-08-09 10:37:23 +02:00
Jakob Borg 6a58033f2b Merge pull request #2124 from calmh/go15
Updates for Go 1.5
2015-08-09 09:37:09 +02:00
Jakob Borg 7705a6c1f1 mv internal lib 2015-08-09 09:35:26 +02:00
Jakob Borg 0a803891a4 Updates for Go 1.5 2015-08-09 09:35:25 +02:00
Jakob Borg 7d620a93b9 Merge pull request #2141 from kamadak/fix-setting-addrlist
Fix editing address lists in the Settings dialog.
2015-08-09 09:11:30 +02:00
KAMADA Ken'ichi 3f3b2f4c99 Fix editing address lists in the Settings dialog.
Setting "Sync Protocol Listen Addresses" and "Global Discovery
Server" in the Settings dialog does not work.  This bug seems to
have been introduced in 26d52be.
2015-08-09 15:52:22 +09:00
Audrius Butkevicius 9eb4089710 Merge pull request #2137 from calmh/caching
Enable browser caching of static resources
2015-08-08 13:20:58 +01:00
Jakob Borg 257d1afdf8 Enable browser caching of static resources
This sends the Cache-Control header to allow caching of static resources,
and checks the If-Modified-Since header to allow browser to use the
cached resource on refresh. Also fixes some paths that caused redirects
(core//foo -> core/foo)
2015-08-08 13:50:18 +02:00
Audrius Butkevicius dad1fb7805 Merge pull request #2135 from calmh/caseinsign
Handle multiple case insensitivity prefixes in ignores (fixes #2134)
2015-08-08 12:04:46 +01:00
Jakob Borg e312fdd4f8 Handle multiple case insensitivity prefixes in ignores (fixes #2134) 2015-08-08 11:58:20 +02:00
Jakob Borg 9b6681d543 Use grid instead of three-column (fixes #2130) 2015-08-08 11:37:19 +02:00
Jakob Borg bcc04623c1 Fix editing device address (fixes #2129) 2015-08-08 11:29:13 +02:00
Jakob Borg d75d162058 Merge pull request #2126 from AudriusButkevicius/peek
Add timeout for peek (fixes #1035)
2015-08-07 17:30:54 +02:00
Audrius Butkevicius 8f2294bbd4 Merge pull request #2128 from Zillode/fix-rescan-btn-unshared
Make rescan available for unshared folders
2015-08-06 20:54:41 +01:00
Lode Hoste d1b91c7619 Make rescan available for unshared folders 2015-08-06 21:52:29 +02:00
Audrius Butkevicius 1b6b481fcc Add timeout for peek (fixes #1035) 2015-08-06 12:07:34 +01:00
Jakob Borg dd64ba1910 Merge pull request #2116 from Zillode/fix-test-reset
Fix TestReset when Syncthing shuts down too fast
2015-08-05 09:47:23 +02:00
Jakob Borg 82a5d1cd79 Merge pull request #2108 from Zillode/fix-delete-button
Rename 'delete' to 'remove' (fixes #1007)
2015-08-05 09:34:27 +02:00
Jakob Borg c26c172d01 Merge pull request #2103 from Zillode/fix-integration-rename-windows
Fix integration rename windows
2015-08-05 08:59:53 +02:00
Lode Hoste a7b7aaa7cb Fix TestReset when Syncthing shuts down too fast 2015-08-04 09:20:46 +02:00
Lode Hoste 4e5c02c05c Clarify password in integration tests 2015-08-03 20:03:50 +02:00
Lode Hoste 2baf61fda3 Properly rename config files during integration tests (fixes #1769) 2015-08-03 20:03:50 +02:00
Lode Hoste 219a25fe80 Rename 'delete' to 'remove' (fixes #1007) 2015-08-03 20:01:26 +02:00
Jakob Borg b1dd704819 Rebuild assets 2015-08-02 09:41:22 +02:00
Jakob Borg 9513e91d66 Flatten GUI tree somewhat
The very deep tree structure didn't really aggree with me, sorry. This
makes the core module rather large, but on the other hand that just
highlights that it is rather large.
2015-08-02 09:24:01 +02:00
Dennis WilsonandJakob Borg 26d52bedb3 Squashed commit of pull request #1954 2015-08-02 09:21:46 +02:00
372 changed files with 5979 additions and 3245 deletions
+20 -8
View File
@@ -1,13 +1,17 @@
{
"ImportPath": "github.com/syncthing/syncthing",
"GoVersion": "devel",
"GoVersion": "go1.5rc1",
"Packages": [
"./cmd/..."
],
"Deps": [
{
"ImportPath": "github.com/bkaradzic/go-lz4",
"Rev": "4f7c2045dbd17b802370e2e6022200468abf02ba"
"Rev": "d47913b1412890a261b9fefae99d72d2bf5aebd8"
},
{
"ImportPath": "github.com/calmh/du",
"Rev": "3c0690cca16228b97741327b1b6781397afbdb24"
},
{
"ImportPath": "github.com/calmh/logger",
@@ -23,7 +27,7 @@
},
{
"ImportPath": "github.com/golang/snappy",
"Rev": "0c7f8a7704bfec561913f4df52c832f094ef56f0"
"Rev": "723cc1e459b8eea2dea4583200fd60757d40097a"
},
{
"ImportPath": "github.com/juju/ratelimit",
@@ -37,9 +41,17 @@
"ImportPath": "github.com/syncthing/protocol",
"Rev": "ebcdea63c07327a342f65415bbadc497462b8f1f"
},
{
"ImportPath": "github.com/syncthing/relaysrv/client",
"Rev": "7c6a31017968e7c1a69148db1ca3dea71eba8236"
},
{
"ImportPath": "github.com/syncthing/relaysrv/protocol",
"Rev": "7c6a31017968e7c1a69148db1ca3dea71eba8236"
},
{
"ImportPath": "github.com/syndtr/goleveldb/leveldb",
"Rev": "183614d6b32571e867df4cf086f5480ceefbdfac"
"Rev": "b743d92d3215f11c9b5ce8830fafe1f16786adf4"
},
{
"ImportPath": "github.com/thejerf/suture",
@@ -59,19 +71,19 @@
},
{
"ImportPath": "golang.org/x/crypto/bcrypt",
"Rev": "7d5b0be716b9d6d4269afdaae10032bb296d3cdf"
"Rev": "c16968172724c0b5e8bdc6ad33f5a79443a44cd7"
},
{
"ImportPath": "golang.org/x/crypto/blowfish",
"Rev": "7d5b0be716b9d6d4269afdaae10032bb296d3cdf"
"Rev": "c16968172724c0b5e8bdc6ad33f5a79443a44cd7"
},
{
"ImportPath": "golang.org/x/text/transform",
"Rev": "3eb7007b740b66a77f3c85f2660a0240b284115a"
"Rev": "723492b65e225eafcba054e76ba18bb9c5ac1ea2"
},
{
"ImportPath": "golang.org/x/text/unicode/norm",
"Rev": "3eb7007b740b66a77f3c85f2660a0240b284115a"
"Rev": "723492b65e225eafcba054e76ba18bb9c5ac1ea2"
}
]
}
+1
View File
@@ -4,4 +4,5 @@ go:
- 1.1
- 1.2
- 1.3
- 1.4
- tip
+1 -1
View File
@@ -4,7 +4,7 @@ go-lz4
go-lz4 is port of LZ4 lossless compression algorithm to Go. The original C code
is located at:
https://code.google.com/p/lz4/
https://github.com/Cyan4973/lz4
Status
------
+24
View File
@@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
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 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.
For more information, please refer to <http://unlicense.org>
+14
View File
@@ -0,0 +1,14 @@
du
==
Get total and available disk space on a given volume.
Documentation
-------------
http://godoc.org/github.com/calmh/du
License
-------
Public Domain
+21
View File
@@ -0,0 +1,21 @@
package main
import (
"fmt"
"log"
"os"
"github.com/calmh/du"
)
var KB = int64(1024)
func main() {
usage, err := du.Get(os.Args[1])
if err != nil {
log.Fatal(err)
}
fmt.Println("Free:", usage.FreeBytes/(KB*KB), "MiB")
fmt.Println("Available:", usage.AvailBytes/(KB*KB), "MiB")
fmt.Println("Size:", usage.TotalBytes/(KB*KB), "MiB")
}
+8
View File
@@ -0,0 +1,8 @@
package du
// Usage holds information about total and available storage on a volume.
type Usage struct {
TotalBytes int64 // Size of volume
FreeBytes int64 // Unused size
AvailBytes int64 // Available to a non-privileged user
}
+24
View File
@@ -0,0 +1,24 @@
// +build !windows,!netbsd,!openbsd,!solaris
package du
import (
"path/filepath"
"syscall"
)
// Get returns the Usage of a given path, or an error if usage data is
// unavailable.
func Get(path string) (Usage, error) {
var stat syscall.Statfs_t
err := syscall.Statfs(filepath.Clean(path), &stat)
if err != nil {
return Usage{}, err
}
u := Usage{
FreeBytes: int64(stat.Bfree) * int64(stat.Bsize),
TotalBytes: int64(stat.Blocks) * int64(stat.Bsize),
AvailBytes: int64(stat.Bavail) * int64(stat.Bsize),
}
return u, nil
}
+13
View File
@@ -0,0 +1,13 @@
// +build netbsd openbsd solaris
package du
import "errors"
var ErrUnsupported = errors.New("unsupported platform")
// Get returns the Usage of a given path, or an error if usage data is
// unavailable.
func Get(path string) (Usage, error) {
return Usage{}, ErrUnsupported
}
+27
View File
@@ -0,0 +1,27 @@
package du
import (
"syscall"
"unsafe"
)
// Get returns the Usage of a given path, or an error if usage data is
// unavailable.
func Get(path string) (Usage, error) {
h := syscall.MustLoadDLL("kernel32.dll")
c := h.MustFindProc("GetDiskFreeSpaceExW")
var u Usage
ret, _, err := c.Call(
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(path))),
uintptr(unsafe.Pointer(&u.FreeBytes)),
uintptr(unsafe.Pointer(&u.TotalBytes)),
uintptr(unsafe.Pointer(&u.AvailBytes)))
if ret == 0 {
return u, err
}
return u, nil
}
+7 -3
View File
@@ -13,6 +13,8 @@ import (
var (
// ErrCorrupt reports that the input is invalid.
ErrCorrupt = errors.New("snappy: corrupt input")
// ErrTooLarge reports that the uncompressed length is too large.
ErrTooLarge = errors.New("snappy: decoded block is too large")
// ErrUnsupported reports that the input isn't supported.
ErrUnsupported = errors.New("snappy: unsupported input")
)
@@ -27,11 +29,13 @@ func DecodedLen(src []byte) (int, error) {
// that the length header occupied.
func decodedLen(src []byte) (blockLen, headerLen int, err error) {
v, n := binary.Uvarint(src)
if n <= 0 {
if n <= 0 || v > 0xffffffff {
return 0, 0, ErrCorrupt
}
if uint64(int(v)) != v {
return 0, 0, errors.New("snappy: decoded block is too large")
const wordSize = 32 << (^uint(0) >> 32 & 1)
if wordSize == 32 && v > 0x7fffffff {
return 0, 0, ErrTooLarge
}
return int(v), n, nil
}
+10
View File
@@ -86,6 +86,16 @@ func TestInvalidVarint(t *testing.T) {
if _, err := Decode(nil, data); err != ErrCorrupt {
t.Errorf("Decode: got %v, want ErrCorrupt", err)
}
// The encoded varint overflows 32 bits
data = []byte("\xff\xff\xff\xff\xff\x00")
if _, err := DecodedLen(data); err != ErrCorrupt {
t.Errorf("DecodedLen: got %v, want ErrCorrupt", err)
}
if _, err := Decode(nil, data); err != ErrCorrupt {
t.Errorf("Decode: got %v, want ErrCorrupt", err)
}
}
func cmp(a, b []byte) error {
+280
View File
@@ -0,0 +1,280 @@
// Copyright (C) 2015 Audrius Butkevicius and Contributors (see the CONTRIBUTORS file).
package client
import (
"crypto/tls"
"fmt"
"log"
"net"
"net/url"
"time"
syncthingprotocol "github.com/syncthing/protocol"
"github.com/syncthing/relaysrv/protocol"
"github.com/syncthing/syncthing/lib/sync"
)
type ProtocolClient struct {
URI *url.URL
Invitations chan protocol.SessionInvitation
closeInvitationsOnFinish bool
config *tls.Config
timeout time.Duration
stop chan struct{}
stopped chan struct{}
conn *tls.Conn
mut sync.RWMutex
connected bool
}
func NewProtocolClient(uri *url.URL, certs []tls.Certificate, invitations chan protocol.SessionInvitation) *ProtocolClient {
closeInvitationsOnFinish := false
if invitations == nil {
closeInvitationsOnFinish = true
invitations = make(chan protocol.SessionInvitation)
}
return &ProtocolClient{
URI: uri,
Invitations: invitations,
closeInvitationsOnFinish: closeInvitationsOnFinish,
config: configForCerts(certs),
timeout: time.Minute * 2,
stop: make(chan struct{}),
stopped: make(chan struct{}),
mut: sync.NewRWMutex(),
connected: false,
}
}
func (c *ProtocolClient) Serve() {
c.stop = make(chan struct{})
c.stopped = make(chan struct{})
defer close(c.stopped)
if err := c.connect(); err != nil {
if debug {
l.Debugln("Relay connect:", err)
}
return
}
if debug {
l.Debugln(c, "connected", c.conn.RemoteAddr())
}
if err := c.join(); err != nil {
c.conn.Close()
l.Infoln("Relay join:", err)
return
}
if err := c.conn.SetDeadline(time.Time{}); err != nil {
l.Infoln("Relay set deadline:", err)
return
}
if debug {
l.Debugln(c, "joined", c.conn.RemoteAddr(), "via", c.conn.LocalAddr())
}
defer c.cleanup()
c.mut.Lock()
c.connected = true
c.mut.Unlock()
messages := make(chan interface{})
errors := make(chan error, 1)
go messageReader(c.conn, messages, errors)
timeout := time.NewTimer(c.timeout)
for {
select {
case message := <-messages:
timeout.Reset(c.timeout)
if debug {
log.Printf("%s received message %T", c, message)
}
switch msg := message.(type) {
case protocol.Ping:
if err := protocol.WriteMessage(c.conn, protocol.Pong{}); err != nil {
l.Infoln("Relay write:", err)
return
}
if debug {
l.Debugln(c, "sent pong")
}
case protocol.SessionInvitation:
ip := net.IP(msg.Address)
if len(ip) == 0 || ip.IsUnspecified() {
msg.Address = c.conn.RemoteAddr().(*net.TCPAddr).IP[:]
}
c.Invitations <- msg
default:
l.Infoln("Relay: protocol error: unexpected message %v", msg)
return
}
case <-c.stop:
if debug {
l.Debugln(c, "stopping")
}
return
case err := <-errors:
l.Infoln("Relay received:", err)
return
case <-timeout.C:
if debug {
l.Debugln(c, "timed out")
}
return
}
}
}
func (c *ProtocolClient) Stop() {
if c.stop == nil {
return
}
close(c.stop)
<-c.stopped
}
func (c *ProtocolClient) StatusOK() bool {
c.mut.RLock()
con := c.connected
c.mut.RUnlock()
return con
}
func (c *ProtocolClient) String() string {
return fmt.Sprintf("ProtocolClient@%p", c)
}
func (c *ProtocolClient) connect() error {
if c.URI.Scheme != "relay" {
return fmt.Errorf("Unsupported relay schema:", c.URI.Scheme)
}
conn, err := tls.Dial("tcp", c.URI.Host, c.config)
if err != nil {
return err
}
if err := conn.SetDeadline(time.Now().Add(10 * time.Second)); err != nil {
conn.Close()
return err
}
if err := performHandshakeAndValidation(conn, c.URI); err != nil {
conn.Close()
return err
}
c.conn = conn
return nil
}
func (c *ProtocolClient) cleanup() {
if c.closeInvitationsOnFinish {
close(c.Invitations)
c.Invitations = make(chan protocol.SessionInvitation)
}
if debug {
l.Debugln(c, "cleaning up")
}
c.mut.Lock()
c.connected = false
c.mut.Unlock()
c.conn.Close()
}
func (c *ProtocolClient) join() error {
if err := protocol.WriteMessage(c.conn, protocol.JoinRelayRequest{}); err != nil {
return err
}
message, err := protocol.ReadMessage(c.conn)
if err != nil {
return err
}
switch msg := message.(type) {
case protocol.Response:
if msg.Code != 0 {
return fmt.Errorf("Incorrect response code %d: %s", msg.Code, msg.Message)
}
default:
return fmt.Errorf("protocol error: expecting response got %v", msg)
}
return nil
}
func performHandshakeAndValidation(conn *tls.Conn, uri *url.URL) error {
if err := conn.Handshake(); err != nil {
return err
}
cs := conn.ConnectionState()
if !cs.NegotiatedProtocolIsMutual || cs.NegotiatedProtocol != protocol.ProtocolName {
return fmt.Errorf("protocol negotiation error")
}
q := uri.Query()
relayIDs := q.Get("id")
if relayIDs != "" {
relayID, err := syncthingprotocol.DeviceIDFromString(relayIDs)
if err != nil {
return fmt.Errorf("relay address contains invalid verification id: %s", err)
}
certs := cs.PeerCertificates
if cl := len(certs); cl != 1 {
return fmt.Errorf("unexpected certificate count: %d", cl)
}
remoteID := syncthingprotocol.NewDeviceID(certs[0].Raw)
if remoteID != relayID {
return fmt.Errorf("relay id does not match. Expected %v got %v", relayID, remoteID)
}
}
return nil
}
func messageReader(conn net.Conn, messages chan<- interface{}, errors chan<- error) {
for {
msg, err := protocol.ReadMessage(conn)
if err != nil {
errors <- err
return
}
messages <- msg
}
}
+15
View File
@@ -0,0 +1,15 @@
// Copyright (C) 2015 Audrius Butkevicius and Contributors (see the CONTRIBUTORS file).
package client
import (
"os"
"strings"
"github.com/calmh/logger"
)
var (
debug = strings.Contains(os.Getenv("STTRACE"), "relay") || os.Getenv("STTRACE") == "all"
l = logger.DefaultLogger
)
+117
View File
@@ -0,0 +1,117 @@
// Copyright (C) 2015 Audrius Butkevicius and Contributors (see the CONTRIBUTORS file).
package client
import (
"crypto/tls"
"fmt"
"net"
"net/url"
"strconv"
"time"
syncthingprotocol "github.com/syncthing/protocol"
"github.com/syncthing/relaysrv/protocol"
)
func GetInvitationFromRelay(uri *url.URL, id syncthingprotocol.DeviceID, certs []tls.Certificate) (protocol.SessionInvitation, error) {
if uri.Scheme != "relay" {
return protocol.SessionInvitation{}, fmt.Errorf("Unsupported relay scheme:", uri.Scheme)
}
conn, err := tls.Dial("tcp", uri.Host, configForCerts(certs))
conn.SetDeadline(time.Now().Add(10 * time.Second))
if err != nil {
return protocol.SessionInvitation{}, err
}
if err := performHandshakeAndValidation(conn, uri); err != nil {
return protocol.SessionInvitation{}, err
}
defer conn.Close()
request := protocol.ConnectRequest{
ID: id[:],
}
if err := protocol.WriteMessage(conn, request); err != nil {
return protocol.SessionInvitation{}, err
}
message, err := protocol.ReadMessage(conn)
if err != nil {
return protocol.SessionInvitation{}, err
}
switch msg := message.(type) {
case protocol.Response:
return protocol.SessionInvitation{}, fmt.Errorf("Incorrect response code %d: %s", msg.Code, msg.Message)
case protocol.SessionInvitation:
if debug {
l.Debugln("Received invitation", msg, "via", conn.LocalAddr())
}
ip := net.IP(msg.Address)
if len(ip) == 0 || ip.IsUnspecified() {
msg.Address = conn.RemoteAddr().(*net.TCPAddr).IP[:]
}
return msg, nil
default:
return protocol.SessionInvitation{}, fmt.Errorf("protocol error: unexpected message %v", msg)
}
}
func JoinSession(invitation protocol.SessionInvitation) (net.Conn, error) {
addr := net.JoinHostPort(net.IP(invitation.Address).String(), strconv.Itoa(int(invitation.Port)))
conn, err := net.Dial("tcp", addr)
if err != nil {
return nil, err
}
request := protocol.JoinSessionRequest{
Key: invitation.Key,
}
conn.SetDeadline(time.Now().Add(10 * time.Second))
err = protocol.WriteMessage(conn, request)
if err != nil {
return nil, err
}
message, err := protocol.ReadMessage(conn)
if err != nil {
return nil, err
}
conn.SetDeadline(time.Time{})
switch msg := message.(type) {
case protocol.Response:
if msg.Code != 0 {
return nil, fmt.Errorf("Incorrect response code %d: %s", msg.Code, msg.Message)
}
return conn, nil
default:
return nil, fmt.Errorf("protocol error: expecting response got %v", msg)
}
}
func configForCerts(certs []tls.Certificate) *tls.Config {
return &tls.Config{
Certificates: certs,
NextProtos: []string{protocol.ProtocolName},
ClientAuth: tls.RequestClientCert,
SessionTicketsDisabled: true,
InsecureSkipVerify: true,
MinVersion: tls.VersionTLS12,
CipherSuites: []uint16{
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,
},
}
}
@@ -0,0 +1,65 @@
// Copyright (C) 2015 Audrius Butkevicius and Contributors (see the CONTRIBUTORS file).
//go:generate -command genxdr go run ../../syncthing/Godeps/_workspace/src/github.com/calmh/xdr/cmd/genxdr/main.go
//go:generate genxdr -o packets_xdr.go packets.go
package protocol
import (
"fmt"
syncthingprotocol "github.com/syncthing/protocol"
"net"
)
const (
messageTypePing int32 = iota
messageTypePong
messageTypeJoinRelayRequest
messageTypeJoinSessionRequest
messageTypeResponse
messageTypeConnectRequest
messageTypeSessionInvitation
)
type header struct {
magic uint32
messageType int32
messageLength int32
}
type Ping struct{}
type Pong struct{}
type JoinRelayRequest struct{}
type JoinSessionRequest struct {
Key []byte // max:32
}
type Response struct {
Code int32
Message string
}
type ConnectRequest struct {
ID []byte // max:32
}
type SessionInvitation struct {
From []byte // max:32
Key []byte // max:32
Address []byte // max:32
Port uint16
ServerSocket bool
}
func (i SessionInvitation) String() string {
return fmt.Sprintf("%s@%s", syncthingprotocol.DeviceIDFromBytes(i.From), i.AddressString())
}
func (i SessionInvitation) GoString() string {
return i.String()
}
func (i SessionInvitation) AddressString() string {
return fmt.Sprintf("%s:%d", net.IP(i.Address), i.Port)
}
@@ -0,0 +1,567 @@
// ************************************************************
// This file is automatically generated by genxdr. Do not edit.
// ************************************************************
package protocol
import (
"bytes"
"io"
"github.com/calmh/xdr"
)
/*
header 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 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| message Type |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| message Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
struct header {
unsigned int magic;
int messageType;
int messageLength;
}
*/
func (o header) EncodeXDR(w io.Writer) (int, error) {
var xw = xdr.NewWriter(w)
return o.EncodeXDRInto(xw)
}
func (o header) MarshalXDR() ([]byte, error) {
return o.AppendXDR(make([]byte, 0, 128))
}
func (o header) MustMarshalXDR() []byte {
bs, err := o.MarshalXDR()
if err != nil {
panic(err)
}
return bs
}
func (o header) AppendXDR(bs []byte) ([]byte, error) {
var aw = xdr.AppendWriter(bs)
var xw = xdr.NewWriter(&aw)
_, err := o.EncodeXDRInto(xw)
return []byte(aw), err
}
func (o header) EncodeXDRInto(xw *xdr.Writer) (int, error) {
xw.WriteUint32(o.magic)
xw.WriteUint32(uint32(o.messageType))
xw.WriteUint32(uint32(o.messageLength))
return xw.Tot(), xw.Error()
}
func (o *header) DecodeXDR(r io.Reader) error {
xr := xdr.NewReader(r)
return o.DecodeXDRFrom(xr)
}
func (o *header) UnmarshalXDR(bs []byte) error {
var br = bytes.NewReader(bs)
var xr = xdr.NewReader(br)
return o.DecodeXDRFrom(xr)
}
func (o *header) DecodeXDRFrom(xr *xdr.Reader) error {
o.magic = xr.ReadUint32()
o.messageType = int32(xr.ReadUint32())
o.messageLength = int32(xr.ReadUint32())
return xr.Error()
}
/*
Ping 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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
struct Ping {
}
*/
func (o Ping) EncodeXDR(w io.Writer) (int, error) {
var xw = xdr.NewWriter(w)
return o.EncodeXDRInto(xw)
}
func (o Ping) MarshalXDR() ([]byte, error) {
return o.AppendXDR(make([]byte, 0, 128))
}
func (o Ping) MustMarshalXDR() []byte {
bs, err := o.MarshalXDR()
if err != nil {
panic(err)
}
return bs
}
func (o Ping) AppendXDR(bs []byte) ([]byte, error) {
var aw = xdr.AppendWriter(bs)
var xw = xdr.NewWriter(&aw)
_, err := o.EncodeXDRInto(xw)
return []byte(aw), err
}
func (o Ping) EncodeXDRInto(xw *xdr.Writer) (int, error) {
return xw.Tot(), xw.Error()
}
func (o *Ping) DecodeXDR(r io.Reader) error {
xr := xdr.NewReader(r)
return o.DecodeXDRFrom(xr)
}
func (o *Ping) UnmarshalXDR(bs []byte) error {
var br = bytes.NewReader(bs)
var xr = xdr.NewReader(br)
return o.DecodeXDRFrom(xr)
}
func (o *Ping) DecodeXDRFrom(xr *xdr.Reader) error {
return xr.Error()
}
/*
Pong 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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
struct Pong {
}
*/
func (o Pong) EncodeXDR(w io.Writer) (int, error) {
var xw = xdr.NewWriter(w)
return o.EncodeXDRInto(xw)
}
func (o Pong) MarshalXDR() ([]byte, error) {
return o.AppendXDR(make([]byte, 0, 128))
}
func (o Pong) MustMarshalXDR() []byte {
bs, err := o.MarshalXDR()
if err != nil {
panic(err)
}
return bs
}
func (o Pong) AppendXDR(bs []byte) ([]byte, error) {
var aw = xdr.AppendWriter(bs)
var xw = xdr.NewWriter(&aw)
_, err := o.EncodeXDRInto(xw)
return []byte(aw), err
}
func (o Pong) EncodeXDRInto(xw *xdr.Writer) (int, error) {
return xw.Tot(), xw.Error()
}
func (o *Pong) DecodeXDR(r io.Reader) error {
xr := xdr.NewReader(r)
return o.DecodeXDRFrom(xr)
}
func (o *Pong) UnmarshalXDR(bs []byte) error {
var br = bytes.NewReader(bs)
var xr = xdr.NewReader(br)
return o.DecodeXDRFrom(xr)
}
func (o *Pong) DecodeXDRFrom(xr *xdr.Reader) error {
return xr.Error()
}
/*
JoinRelayRequest 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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
struct JoinRelayRequest {
}
*/
func (o JoinRelayRequest) EncodeXDR(w io.Writer) (int, error) {
var xw = xdr.NewWriter(w)
return o.EncodeXDRInto(xw)
}
func (o JoinRelayRequest) MarshalXDR() ([]byte, error) {
return o.AppendXDR(make([]byte, 0, 128))
}
func (o JoinRelayRequest) MustMarshalXDR() []byte {
bs, err := o.MarshalXDR()
if err != nil {
panic(err)
}
return bs
}
func (o JoinRelayRequest) AppendXDR(bs []byte) ([]byte, error) {
var aw = xdr.AppendWriter(bs)
var xw = xdr.NewWriter(&aw)
_, err := o.EncodeXDRInto(xw)
return []byte(aw), err
}
func (o JoinRelayRequest) EncodeXDRInto(xw *xdr.Writer) (int, error) {
return xw.Tot(), xw.Error()
}
func (o *JoinRelayRequest) DecodeXDR(r io.Reader) error {
xr := xdr.NewReader(r)
return o.DecodeXDRFrom(xr)
}
func (o *JoinRelayRequest) UnmarshalXDR(bs []byte) error {
var br = bytes.NewReader(bs)
var xr = xdr.NewReader(br)
return o.DecodeXDRFrom(xr)
}
func (o *JoinRelayRequest) DecodeXDRFrom(xr *xdr.Reader) error {
return xr.Error()
}
/*
JoinSessionRequest 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 Key |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/ /
\ Key (variable length) \
/ /
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
struct JoinSessionRequest {
opaque Key<32>;
}
*/
func (o JoinSessionRequest) EncodeXDR(w io.Writer) (int, error) {
var xw = xdr.NewWriter(w)
return o.EncodeXDRInto(xw)
}
func (o JoinSessionRequest) MarshalXDR() ([]byte, error) {
return o.AppendXDR(make([]byte, 0, 128))
}
func (o JoinSessionRequest) MustMarshalXDR() []byte {
bs, err := o.MarshalXDR()
if err != nil {
panic(err)
}
return bs
}
func (o JoinSessionRequest) AppendXDR(bs []byte) ([]byte, error) {
var aw = xdr.AppendWriter(bs)
var xw = xdr.NewWriter(&aw)
_, err := o.EncodeXDRInto(xw)
return []byte(aw), err
}
func (o JoinSessionRequest) EncodeXDRInto(xw *xdr.Writer) (int, error) {
if l := len(o.Key); l > 32 {
return xw.Tot(), xdr.ElementSizeExceeded("Key", l, 32)
}
xw.WriteBytes(o.Key)
return xw.Tot(), xw.Error()
}
func (o *JoinSessionRequest) DecodeXDR(r io.Reader) error {
xr := xdr.NewReader(r)
return o.DecodeXDRFrom(xr)
}
func (o *JoinSessionRequest) UnmarshalXDR(bs []byte) error {
var br = bytes.NewReader(bs)
var xr = xdr.NewReader(br)
return o.DecodeXDRFrom(xr)
}
func (o *JoinSessionRequest) DecodeXDRFrom(xr *xdr.Reader) error {
o.Key = xr.ReadBytesMax(32)
return xr.Error()
}
/*
Response 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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Code |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Length of Message |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/ /
\ Message (variable length) \
/ /
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
struct Response {
int Code;
string Message<>;
}
*/
func (o Response) EncodeXDR(w io.Writer) (int, error) {
var xw = xdr.NewWriter(w)
return o.EncodeXDRInto(xw)
}
func (o Response) MarshalXDR() ([]byte, error) {
return o.AppendXDR(make([]byte, 0, 128))
}
func (o Response) MustMarshalXDR() []byte {
bs, err := o.MarshalXDR()
if err != nil {
panic(err)
}
return bs
}
func (o Response) AppendXDR(bs []byte) ([]byte, error) {
var aw = xdr.AppendWriter(bs)
var xw = xdr.NewWriter(&aw)
_, err := o.EncodeXDRInto(xw)
return []byte(aw), err
}
func (o Response) EncodeXDRInto(xw *xdr.Writer) (int, error) {
xw.WriteUint32(uint32(o.Code))
xw.WriteString(o.Message)
return xw.Tot(), xw.Error()
}
func (o *Response) DecodeXDR(r io.Reader) error {
xr := xdr.NewReader(r)
return o.DecodeXDRFrom(xr)
}
func (o *Response) UnmarshalXDR(bs []byte) error {
var br = bytes.NewReader(bs)
var xr = xdr.NewReader(br)
return o.DecodeXDRFrom(xr)
}
func (o *Response) DecodeXDRFrom(xr *xdr.Reader) error {
o.Code = int32(xr.ReadUint32())
o.Message = xr.ReadString()
return xr.Error()
}
/*
ConnectRequest 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 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/ /
\ ID (variable length) \
/ /
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
struct ConnectRequest {
opaque ID<32>;
}
*/
func (o ConnectRequest) EncodeXDR(w io.Writer) (int, error) {
var xw = xdr.NewWriter(w)
return o.EncodeXDRInto(xw)
}
func (o ConnectRequest) MarshalXDR() ([]byte, error) {
return o.AppendXDR(make([]byte, 0, 128))
}
func (o ConnectRequest) MustMarshalXDR() []byte {
bs, err := o.MarshalXDR()
if err != nil {
panic(err)
}
return bs
}
func (o ConnectRequest) AppendXDR(bs []byte) ([]byte, error) {
var aw = xdr.AppendWriter(bs)
var xw = xdr.NewWriter(&aw)
_, err := o.EncodeXDRInto(xw)
return []byte(aw), err
}
func (o ConnectRequest) EncodeXDRInto(xw *xdr.Writer) (int, error) {
if l := len(o.ID); l > 32 {
return xw.Tot(), xdr.ElementSizeExceeded("ID", l, 32)
}
xw.WriteBytes(o.ID)
return xw.Tot(), xw.Error()
}
func (o *ConnectRequest) DecodeXDR(r io.Reader) error {
xr := xdr.NewReader(r)
return o.DecodeXDRFrom(xr)
}
func (o *ConnectRequest) UnmarshalXDR(bs []byte) error {
var br = bytes.NewReader(bs)
var xr = xdr.NewReader(br)
return o.DecodeXDRFrom(xr)
}
func (o *ConnectRequest) DecodeXDRFrom(xr *xdr.Reader) error {
o.ID = xr.ReadBytesMax(32)
return xr.Error()
}
/*
SessionInvitation 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 From |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/ /
\ From (variable length) \
/ /
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Length of Key |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/ /
\ Key (variable length) \
/ /
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Length of Address |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/ /
\ Address (variable length) \
/ /
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 0x0000 | Port |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Server Socket (V=0 or 1) |V|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
struct SessionInvitation {
opaque From<32>;
opaque Key<32>;
opaque Address<32>;
unsigned int Port;
bool ServerSocket;
}
*/
func (o SessionInvitation) EncodeXDR(w io.Writer) (int, error) {
var xw = xdr.NewWriter(w)
return o.EncodeXDRInto(xw)
}
func (o SessionInvitation) MarshalXDR() ([]byte, error) {
return o.AppendXDR(make([]byte, 0, 128))
}
func (o SessionInvitation) MustMarshalXDR() []byte {
bs, err := o.MarshalXDR()
if err != nil {
panic(err)
}
return bs
}
func (o SessionInvitation) AppendXDR(bs []byte) ([]byte, error) {
var aw = xdr.AppendWriter(bs)
var xw = xdr.NewWriter(&aw)
_, err := o.EncodeXDRInto(xw)
return []byte(aw), err
}
func (o SessionInvitation) EncodeXDRInto(xw *xdr.Writer) (int, error) {
if l := len(o.From); l > 32 {
return xw.Tot(), xdr.ElementSizeExceeded("From", l, 32)
}
xw.WriteBytes(o.From)
if l := len(o.Key); l > 32 {
return xw.Tot(), xdr.ElementSizeExceeded("Key", l, 32)
}
xw.WriteBytes(o.Key)
if l := len(o.Address); l > 32 {
return xw.Tot(), xdr.ElementSizeExceeded("Address", l, 32)
}
xw.WriteBytes(o.Address)
xw.WriteUint16(o.Port)
xw.WriteBool(o.ServerSocket)
return xw.Tot(), xw.Error()
}
func (o *SessionInvitation) DecodeXDR(r io.Reader) error {
xr := xdr.NewReader(r)
return o.DecodeXDRFrom(xr)
}
func (o *SessionInvitation) UnmarshalXDR(bs []byte) error {
var br = bytes.NewReader(bs)
var xr = xdr.NewReader(br)
return o.DecodeXDRFrom(xr)
}
func (o *SessionInvitation) DecodeXDRFrom(xr *xdr.Reader) error {
o.From = xr.ReadBytesMax(32)
o.Key = xr.ReadBytesMax(32)
o.Address = xr.ReadBytesMax(32)
o.Port = xr.ReadUint16()
o.ServerSocket = xr.ReadBool()
return xr.Error()
}
@@ -0,0 +1,114 @@
// Copyright (C) 2015 Audrius Butkevicius and Contributors (see the CONTRIBUTORS file).
package protocol
import (
"fmt"
"io"
)
const (
magic = 0x9E79BC40
ProtocolName = "bep-relay"
)
var (
ResponseSuccess = Response{0, "success"}
ResponseNotFound = Response{1, "not found"}
ResponseAlreadyConnected = Response{2, "already connected"}
ResponseInternalError = Response{99, "internal error"}
ResponseUnexpectedMessage = Response{100, "unexpected message"}
)
func WriteMessage(w io.Writer, message interface{}) error {
header := header{
magic: magic,
}
var payload []byte
var err error
switch msg := message.(type) {
case Ping:
payload, err = msg.MarshalXDR()
header.messageType = messageTypePing
case Pong:
payload, err = msg.MarshalXDR()
header.messageType = messageTypePong
case JoinRelayRequest:
payload, err = msg.MarshalXDR()
header.messageType = messageTypeJoinRelayRequest
case JoinSessionRequest:
payload, err = msg.MarshalXDR()
header.messageType = messageTypeJoinSessionRequest
case Response:
payload, err = msg.MarshalXDR()
header.messageType = messageTypeResponse
case ConnectRequest:
payload, err = msg.MarshalXDR()
header.messageType = messageTypeConnectRequest
case SessionInvitation:
payload, err = msg.MarshalXDR()
header.messageType = messageTypeSessionInvitation
default:
err = fmt.Errorf("Unknown message type")
}
if err != nil {
return err
}
header.messageLength = int32(len(payload))
headerpayload, err := header.MarshalXDR()
if err != nil {
return err
}
_, err = w.Write(append(headerpayload, payload...))
return err
}
func ReadMessage(r io.Reader) (interface{}, error) {
var header header
if err := header.DecodeXDR(r); err != nil {
return nil, err
}
if header.magic != magic {
return nil, fmt.Errorf("magic mismatch")
}
switch header.messageType {
case messageTypePing:
var msg Ping
err := msg.DecodeXDR(r)
return msg, err
case messageTypePong:
var msg Pong
err := msg.DecodeXDR(r)
return msg, err
case messageTypeJoinRelayRequest:
var msg JoinRelayRequest
err := msg.DecodeXDR(r)
return msg, err
case messageTypeJoinSessionRequest:
var msg JoinSessionRequest
err := msg.DecodeXDR(r)
return msg, err
case messageTypeResponse:
var msg Response
err := msg.DecodeXDR(r)
return msg, err
case messageTypeConnectRequest:
var msg ConnectRequest
err := msg.DecodeXDR(r)
return msg, err
case messageTypeSessionInvitation:
var msg SessionInvitation
err := msg.DecodeXDR(r)
return msg, err
}
return nil, fmt.Errorf("Unknown message type")
}
+6 -3
View File
@@ -291,6 +291,7 @@ func recoverTable(s *session, o *opt.Options) error {
// We will drop corrupted table.
strict = o.GetStrict(opt.StrictRecovery)
noSync = o.GetNoSync()
rec = &sessionRecord{}
bpool = util.NewBufferPool(o.GetBlockSize() + 5)
@@ -328,9 +329,11 @@ func recoverTable(s *session, o *opt.Options) error {
if err != nil {
return
}
err = writer.Sync()
if err != nil {
return
if !noSync {
err = writer.Sync()
if err != nil {
return
}
}
size = int64(tw.BytesLen())
return
+1 -1
View File
@@ -129,7 +129,7 @@ func (db *DB) Write(b *Batch, wo *opt.WriteOptions) (err error) {
return
}
b.init(wo.GetSync())
b.init(wo.GetSync() && !db.s.o.GetNoSync())
// The write happen synchronously.
select {
@@ -52,12 +52,14 @@ func IsCorrupted(err error) bool {
switch err.(type) {
case *ErrCorrupted:
return true
case *storage.ErrCorrupted:
return true
}
return false
}
// ErrMissingFiles is the type that indicating a corruption due to missing
// files.
// files. ErrMissingFiles always wrapped with ErrCorrupted.
type ErrMissingFiles struct {
Files []*storage.FileInfo
}
@@ -308,6 +308,11 @@ type Options struct {
// The default is 2.
MaxMemCompationLevel int
// NoSync allows completely disable fsync.
//
// The default is false.
NoSync bool
// NumLevel defines number of database level. The level shouldn't changed
// between opens, or the database will panic.
//
@@ -546,6 +551,13 @@ func (o *Options) GetMaxMemCompationLevel() int {
return level
}
func (o *Options) GetNoSync() bool {
if o == nil {
return false
}
return o.NoSync
}
func (o *Options) GetNumLevel() int {
if o == nil || o.NumLevel <= 0 {
return DefaultNumLevel
+5 -3
View File
@@ -240,9 +240,11 @@ func (s *session) flushManifest(rec *sessionRecord) (err error) {
if err != nil {
return
}
err = s.manifestWriter.Sync()
if err != nil {
return
if !s.o.GetNoSync() {
err = s.manifestWriter.Sync()
if err != nil {
return
}
}
s.recordCommited(rec)
return
@@ -243,7 +243,10 @@ func (fs *fileStorage) GetManifest() (f File, err error) {
rem = append(rem, fn)
}
if !pend1 || cerr == nil {
cerr = fmt.Errorf("leveldb/storage: corrupted or incomplete %s file", fn)
cerr = &ErrCorrupted{
File: fsParseName(filepath.Base(fn)),
Err: errors.New("leveldb/storage: corrupted or incomplete manifest file"),
}
}
} else if f != nil && f1.Num() < f.Num() {
fs.log(fmt.Sprintf("skipping %s: obsolete", fn))
@@ -326,8 +329,7 @@ func (fs *fileStorage) Close() error {
runtime.SetFinalizer(fs, nil)
if fs.open > 0 {
fs.log(fmt.Sprintf("refuse to close, %d files still open", fs.open))
return fmt.Errorf("leveldb/storage: cannot close, %d files still open", fs.open)
fs.log(fmt.Sprintf("close: warning, %d files still open", fs.open))
}
fs.open = -1
e1 := fs.logw.Close()
@@ -505,30 +507,37 @@ func (f *file) path() string {
return filepath.Join(f.fs.path, f.name())
}
func (f *file) parse(name string) bool {
var num uint64
func fsParseName(name string) *FileInfo {
fi := &FileInfo{}
var tail string
_, err := fmt.Sscanf(name, "%d.%s", &num, &tail)
_, err := fmt.Sscanf(name, "%d.%s", &fi.Num, &tail)
if err == nil {
switch tail {
case "log":
f.t = TypeJournal
fi.Type = TypeJournal
case "ldb", "sst":
f.t = TypeTable
fi.Type = TypeTable
case "tmp":
f.t = TypeTemp
fi.Type = TypeTemp
default:
return false
return nil
}
f.num = num
return true
return fi
}
n, _ := fmt.Sscanf(name, "MANIFEST-%d%s", &num, &tail)
n, _ := fmt.Sscanf(name, "MANIFEST-%d%s", &fi.Num, &tail)
if n == 1 {
f.t = TypeManifest
f.num = num
return true
fi.Type = TypeManifest
return fi
}
return nil
}
return false
func (f *file) parse(name string) bool {
fi := fsParseName(name)
if fi == nil {
return false
}
f.t = fi.Type
f.num = fi.Num
return true
}
@@ -50,13 +50,23 @@ func rename(oldpath, newpath string) error {
return os.Rename(oldpath, newpath)
}
func isErrInvalid(err error) bool {
if err == os.ErrInvalid {
return true
}
if syserr, ok := err.(*os.SyscallError); ok && syserr.Err == syscall.EINVAL {
return true
}
return false
}
func syncDir(name string) error {
f, err := os.Open(name)
if err != nil {
return err
}
defer f.Close()
if err := f.Sync(); err != nil {
if err := f.Sync(); err != nil && !isErrInvalid(err) {
return err
}
return nil
@@ -46,6 +46,22 @@ var (
ErrClosed = errors.New("leveldb/storage: closed")
)
// ErrCorrupted is the type that wraps errors that indicate corruption of
// a file. Package storage has its own type instead of using
// errors.ErrCorrupted to prevent circular import.
type ErrCorrupted struct {
File *FileInfo
Err error
}
func (e *ErrCorrupted) Error() string {
if e.File != nil {
return fmt.Sprintf("%v [file=%v]", e.Err, e.File)
} else {
return e.Err.Error()
}
}
// Syncer is the interface that wraps basic Sync method.
type Syncer interface {
// Sync commits the current contents of the file to stable storage.
+7 -3
View File
@@ -287,6 +287,7 @@ func (x *tFilesSortByNum) Less(i, j int) bool {
// Table operations.
type tOps struct {
s *session
noSync bool
cache *cache.Cache
bcache *cache.Cache
bpool *util.BufferPool
@@ -458,6 +459,7 @@ func newTableOps(s *session) *tOps {
}
return &tOps{
s: s,
noSync: s.o.GetNoSync(),
cache: cache.NewCache(cacher),
bcache: bcache,
bpool: bpool,
@@ -505,9 +507,11 @@ func (w *tWriter) finish() (f *tFile, err error) {
if err != nil {
return
}
err = w.w.Sync()
if err != nil {
return
if !w.t.noSync {
err = w.w.Sync()
if err != nil {
return
}
}
f = newTableFile(w.file, uint64(w.tw.BytesLen()), iKey(w.first), iKey(w.last))
return
+35 -21
View File
@@ -36,6 +36,7 @@ var (
goos string
noupgrade bool
version string
goVersion float64
race bool
)
@@ -70,7 +71,7 @@ func main() {
log.Printf("Unknown goarch %q; proceed with caution!", goarch)
}
checkRequiredGoVersion()
goVersion, _ = checkRequiredGoVersion()
if flag.NArg() == 0 {
var tags []string
@@ -80,9 +81,9 @@ func main() {
install("./cmd/...", tags)
vet("./cmd/syncthing")
vet("./internal/...")
vet("./lib/...")
lint("./cmd/syncthing")
lint("./internal/...")
lint("./lib/...")
return
}
@@ -142,11 +143,11 @@ func main() {
case "vet":
vet("./cmd/syncthing")
vet("./internal/...")
vet("./lib/...")
case "lint":
lint("./cmd/syncthing")
lint("./internal/...")
lint("./lib/...")
default:
log.Fatalf("Unknown command %q", cmd)
@@ -154,7 +155,7 @@ func main() {
}
}
func checkRequiredGoVersion() {
func checkRequiredGoVersion() (float64, bool) {
ver := run("go", "version")
re := regexp.MustCompile(`go version go(\d+\.\d+)`)
if m := re.FindSubmatch(ver); len(m) == 2 {
@@ -163,14 +164,16 @@ func checkRequiredGoVersion() {
f, err := strconv.ParseFloat(vs, 64)
if err != nil {
log.Printf("*** Couldn't parse Go version out of %q.\n*** This isn't known to work, proceed on your own risk.", vs)
return
return 0, false
}
if f < minGoVersion {
log.Fatalf("*** Go version %.01f is less than required %.01f.\n*** This is known not to work, not proceeding.", f, minGoVersion)
}
} else {
log.Printf("*** Unknown Go version %q.\n*** This isn't known to work, proceed on your own risk.", ver)
return f, true
}
log.Printf("*** Unknown Go version %q.\n*** This isn't known to work, proceed on your own risk.", ver)
return 0, false
}
func setup() {
@@ -178,6 +181,8 @@ func setup() {
runPrint("go", "get", "-v", "golang.org/x/tools/cmd/vet")
runPrint("go", "get", "-v", "golang.org/x/net/html")
runPrint("go", "get", "-v", "github.com/tools/godep")
runPrint("go", "get", "-v", "github.com/axw/gocov/gocov")
runPrint("go", "get", "-v", "github.com/AlekSi/gocov-xml")
}
func test(pkg string) {
@@ -402,16 +407,16 @@ func setBuildEnv() {
func assets() {
setBuildEnv()
runPipe("internal/auto/gui.files.go", "go", "run", "cmd/genassets/main.go", "gui")
runPipe("lib/auto/gui.files.go", "go", "run", "script/genassets.go", "gui")
}
func xdr() {
runPrint("go", "generate", "./internal/discover", "./internal/db")
runPrint("go", "generate", "./lib/discover", "./lib/db")
}
func translate() {
os.Chdir("gui/assets/lang")
runPipe("lang-en-new.json", "go", "run", "../../../cmd/translate/main.go", "lang-en.json", "../../index.html")
runPipe("lang-en-new.json", "go", "run", "../../../script/translate.go", "lang-en.json", "../../")
os.Remove("lang-en.json")
err := os.Rename("lang-en-new.json", "lang-en.json")
if err != nil {
@@ -422,7 +427,7 @@ func translate() {
func transifex() {
os.Chdir("gui/assets/lang")
runPrint("go", "run", "../../../cmd/transifexdl/main.go")
runPrint("go", "run", "../../../script/transifexdl.go")
os.Chdir("../../..")
assets()
}
@@ -438,13 +443,18 @@ func clean() {
}
func ldflags() string {
var b bytes.Buffer
sep := ' '
if goVersion > 1.4 {
sep = '='
}
b := new(bytes.Buffer)
b.WriteString("-w")
b.WriteString(fmt.Sprintf(" -X main.Version %s", version))
b.WriteString(fmt.Sprintf(" -X main.BuildStamp %d", buildStamp()))
b.WriteString(fmt.Sprintf(" -X main.BuildUser %s", buildUser()))
b.WriteString(fmt.Sprintf(" -X main.BuildHost %s", buildHost()))
b.WriteString(fmt.Sprintf(" -X main.BuildEnv %s", buildEnvironment()))
fmt.Fprintf(b, " -X main.Version%c%s", sep, version)
fmt.Fprintf(b, " -X main.BuildStamp%c%d", sep, buildStamp())
fmt.Fprintf(b, " -X main.BuildUser%c%s", sep, buildUser())
fmt.Fprintf(b, " -X main.BuildHost%c%s", sep, buildHost())
fmt.Fprintf(b, " -X main.BuildEnv%c%s", sep, buildEnvironment())
return b.String()
}
@@ -742,7 +752,9 @@ func vet(pkg string) {
if falseAlarmComposites.Match(line) || exitStatus.Match(line) {
continue
}
log.Printf("%s", line)
if len(line) > 0 {
log.Printf("%s", line)
}
}
}
@@ -758,6 +770,8 @@ func lint(pkg string) {
if analCommentPolicy.Match(line) {
continue
}
log.Printf("%s", line)
if len(line) > 0 {
log.Printf("%s", line)
}
}
}
+73 -67
View File
@@ -4,92 +4,97 @@ IFS=$'\n\t'
STTRACE=${STTRACE:-}
script() {
name="$1"
shift
go run "script/$name.go" "$@"
}
build() {
go run build.go "$@"
}
case "${1:-default}" in
default)
go run build.go
build
;;
clean)
go run build.go "$1"
;;
test)
ulimit -t 60 &>/dev/null || true
ulimit -d 512000 &>/dev/null || true
ulimit -m 512000 &>/dev/null || true
go run build.go test
;;
bench)
LOGGER_DISCARD=1 go run build.go bench | go run benchfilter.go
build "$@"
;;
tar)
go run build.go "$1"
build "$@"
;;
deps)
go run build.go "$1"
build "$@"
;;
assets)
go run build.go "$1"
build "$@"
;;
xdr)
go run build.go "$1"
build "$@"
;;
translate)
go run build.go "$1"
;;
prerelease)
go run build.go transifex
git add -A gui/assets/ internal/auto/
pushd man ; ./refresh.sh ; popd
git add -A man
echo
echo Changelog:
go run changelog.go
build "$@"
;;
deb)
go run build.go "$1"
;;
noupgrade)
go run build.go -no-upgrade tar
;;
all)
go run build.go -goos darwin -goarch amd64 tar
go run build.go -goos darwin -goarch 386 tar
go run build.go -goos dragonfly -goarch 386 tar
go run build.go -goos dragonfly -goarch amd64 tar
go run build.go -goos freebsd -goarch 386 tar
go run build.go -goos freebsd -goarch amd64 tar
go run build.go -goos linux -goarch 386 tar
go run build.go -goos linux -goarch amd64 tar
go run build.go -goos linux -goarch arm tar
go run build.go -goos netbsd -goarch 386 tar
go run build.go -goos netbsd -goarch amd64 tar
go run build.go -goos openbsd -goarch 386 tar
go run build.go -goos openbsd -goarch amd64 tar
go run build.go -goos solaris -goarch amd64 tar
go run build.go -goos windows -goarch 386 zip
go run build.go -goos windows -goarch amd64 zip
build "$@"
;;
setup)
echo "Don't worry, just build."
build "$@"
;;
test)
ulimit -t 600 &>/dev/null || true
ulimit -d 512000 &>/dev/null || true
ulimit -m 512000 &>/dev/null || true
LOGGER_DISCARD=1 build test
;;
bench)
LOGGER_DISCARD=1 build bench | script benchfilter
;;
prerelease)
build transifex
git add -A gui/assets/ lib/auto/
pushd man ; ./refresh.sh ; popd
git add -A man
;;
noupgrade)
build -no-upgrade tar
;;
all)
build -goos darwin -goarch amd64 tar
build -goos dragonfly -goarch amd64 tar
build -goos freebsd -goarch 386 tar
build -goos freebsd -goarch amd64 tar
build -goos linux -goarch 386 tar
build -goos linux -goarch amd64 tar
build -goos linux -goarch arm tar
build -goos netbsd -goarch 386 tar
build -goos netbsd -goarch amd64 tar
build -goos openbsd -goarch 386 tar
build -goos openbsd -goarch amd64 tar
build -goos solaris -goarch amd64 tar
build -goos windows -goarch 386 zip
build -goos windows -goarch amd64 zip
;;
test-cov)
@@ -97,9 +102,6 @@ case "${1:-default}" in
ulimit -d 512000 &>/dev/null || true
ulimit -m 512000 &>/dev/null || true
go get github.com/axw/gocov/gocov
go get github.com/AlekSi/gocov-xml
echo "mode: set" > coverage.out
fail=0
@@ -125,11 +127,12 @@ case "${1:-default}" in
;;
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" \
syncthing/build:latest \
"$img" \
sh -c './build.sh clean \
&& ./build.sh test-cov \
&& ./build.sh bench \
@@ -137,11 +140,12 @@ case "${1:-default}" in
;;
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" \
syncthing/build:latest \
"$img" \
sh -euxc './build.sh clean \
&& go run build.go -race \
&& export GOPATH=$(pwd)/Godeps/_workspace:$GOPATH \
@@ -151,21 +155,23 @@ case "${1:-default}" in
;;
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" \
syncthing/build:latest \
"$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" \
syncthing/build:latest \
"$img" \
sh -euxc 'go run build.go vet'
;;
-53
View File
@@ -1,53 +0,0 @@
#!/bin/bash
missing-authors() {
for email in $(git log --format=%ae HEAD | sort | uniq) ; do
grep -q "$email" AUTHORS || echo $email
done
}
no-docs-typos() {
# Commits that are known to not change code
grep -v 63bd0136fb40a91efaa279cb4b4159d82e8e6904 |\
grep -v 4e2feb6fbc791bb8a2daf0ab8efb10775d66343e |\
grep -v f2459ef3319b2f060dbcdacd0c35a1788a94b8bd |\
grep -v b61f418bf2d1f7d5a9d7088a20a2a448e5e66801 |\
grep -v f0621207e3953711f9ab86d99724f1d0faac45b1 |\
grep -v f1120d7aa936c0658429edef0037792520b46334 |\
grep -v a9339d0627fff439879d157c75077f02c9fac61b |\
grep -v 254c63763a3ad42fd82259f1767db526cff94a14 |\
grep -v 4b76ec40c07078beaa2c5e250ed7d9bd6276a718 |\
grep -v ffc39dfbcb34eacc3ea12327a02b6e7741a2c207 |\
grep -v 32a76901a91ff0f663db6f0830e0aedec946e4d0 |\
grep -v af3288043a49bcc28f8ae3060852a09de552fe5f |\
grep -v 3626003f680bad3e63677982576d3a05421e88e9
}
print-missing-authors() {
for email in $(missing-authors) ; do
git log --author="$email" --format="%H %ae %s" | no-docs-typos
done
}
print-missing-copyright() {
find . -name \*.go | xargs egrep -L 'Copyright|automatically generated' | grep -v Godeps | grep -v internal/auto/
}
authors=$(print-missing-authors)
if [[ ! -z $authors ]] ; then
echo '***'
echo Author emails not in AUTHORS:
echo $authors
echo '***'
exit 1
fi
copy=$(print-missing-copyright)
if [[ ! -z $copy ]] ; then
echo ***
echo Files missing copyright notice:
echo $copy
echo ***
exit 1
fi
+1 -1
View File
@@ -16,7 +16,7 @@ import (
"os"
"path/filepath"
"github.com/syncthing/syncthing/internal/symlinks"
"github.com/syncthing/syncthing/lib/symlinks"
)
func main() {
+1 -1
View File
@@ -13,7 +13,7 @@ import (
"path/filepath"
"github.com/syncthing/protocol"
"github.com/syncthing/syncthing/internal/scanner"
"github.com/syncthing/syncthing/lib/scanner"
)
func main() {
+10 -6
View File
@@ -12,7 +12,7 @@ import (
"os"
"github.com/syncthing/protocol"
"github.com/syncthing/syncthing/internal/discover"
"github.com/syncthing/syncthing/lib/discover"
)
func main() {
@@ -21,11 +21,11 @@ func main() {
var server string
flag.StringVar(&server, "server", "udp4://announce.syncthing.net:22026", "Announce server")
flag.StringVar(&server, "server", "udp4://announce.syncthing.net:22027", "Announce server")
flag.Parse()
if len(flag.Args()) != 1 || server == "" {
log.Printf("Usage: %s [-server=\"udp4://announce.syncthing.net:22026\"] <device>", os.Args[0])
log.Printf("Usage: %s [-server=\"udp4://announce.syncthing.net:22027\"] <device>", os.Args[0])
os.Exit(64)
}
@@ -35,9 +35,13 @@ func main() {
os.Exit(1)
}
discoverer := discover.NewDiscoverer(protocol.LocalDeviceID, nil)
discoverer := discover.NewDiscoverer(protocol.LocalDeviceID, nil, nil)
discoverer.StartGlobal([]string{server}, 1)
for _, addr := range discoverer.Lookup(id) {
log.Println(addr)
addresses, relays := discoverer.Lookup(id)
for _, addr := range addresses {
log.Println("address:", addr)
}
for _, addr := range relays {
log.Println("relay:", addr)
}
}
+1 -1
View File
@@ -14,7 +14,7 @@ import (
"os"
"github.com/syncthing/protocol"
"github.com/syncthing/syncthing/internal/db"
"github.com/syncthing/syncthing/lib/db"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/opt"
)
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"encoding/json"
"io"
"github.com/syncthing/syncthing/internal/events"
"github.com/syncthing/syncthing/lib/events"
)
// The auditSvc subscribes to events and writes these in JSON format, one
+1 -1
View File
@@ -12,7 +12,7 @@ import (
"testing"
"time"
"github.com/syncthing/syncthing/internal/events"
"github.com/syncthing/syncthing/lib/events"
)
func TestAuditService(t *testing.T) {
+196 -149
View File
@@ -11,16 +11,28 @@ import (
"fmt"
"io"
"net"
"strings"
"net/url"
"sync"
"time"
"github.com/syncthing/protocol"
"github.com/syncthing/syncthing/internal/config"
"github.com/syncthing/syncthing/internal/events"
"github.com/syncthing/syncthing/internal/model"
"github.com/syncthing/relaysrv/client"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/model"
"github.com/syncthing/syncthing/lib/osutil"
"github.com/thejerf/suture"
)
type DialerFactory func(*url.URL, *tls.Config) (*tls.Conn, error)
type ListenerFactory func(*url.URL, *tls.Config, chan<- model.IntermediateConnection)
var (
dialers = make(map[string]DialerFactory, 0)
listeners = make(map[string]ListenerFactory, 0)
)
// The connection service listens on TLS and dials configured unconnected
// devices. Successful connections are handed to the model.
type connectionSvc struct {
@@ -29,48 +41,36 @@ type connectionSvc struct {
myID protocol.DeviceID
model *model.Model
tlsCfg *tls.Config
conns chan *tls.Conn
conns chan model.IntermediateConnection
lastRelayCheck map[protocol.DeviceID]time.Time
mut sync.RWMutex
connType map[protocol.DeviceID]model.ConnectionType
relaysEnabled bool
}
func newConnectionSvc(cfg *config.Wrapper, myID protocol.DeviceID, model *model.Model, tlsCfg *tls.Config) *connectionSvc {
func newConnectionSvc(cfg *config.Wrapper, myID protocol.DeviceID, mdl *model.Model, tlsCfg *tls.Config) *connectionSvc {
svc := &connectionSvc{
Supervisor: suture.NewSimple("connectionSvc"),
cfg: cfg,
myID: myID,
model: model,
model: mdl,
tlsCfg: tlsCfg,
conns: make(chan *tls.Conn),
conns: make(chan model.IntermediateConnection),
connType: make(map[protocol.DeviceID]model.ConnectionType),
relaysEnabled: cfg.Options().RelaysEnabled,
lastRelayCheck: make(map[protocol.DeviceID]time.Time),
}
cfg.Subscribe(svc)
// There are several moving parts here; one routine per listening address
// to handle incoming connections, one routine to periodically attempt
// outgoing connections, and lastly one routine to the the common handling
// regardless of whether the connection was incoming or outgoing. It ends
// up as in the diagram below. We embed a Supervisor to manage the
// routines (i.e. log and restart if they crash or exit, etc).
//
// +-----------------+
// Incoming | +---------------+-+ +-----------------+
// Connections | | | | | Outgoing
// -------------->| | svc.listen | | | Connections
// | | (1 per listen | | svc.connect |-------------->
// | | address) | | |
// +-+ | | |
// +-----------------+ +-----------------+
// v v
// | |
// | |
// +------------+-----------+
// |
// | svc.conns
// v
// +-----------------+
// | |
// | |
// | svc.handle |------> model.AddConnection()
// | |
// | |
// +-----------------+
// outgoing connections, one routine to the the common handling
// regardless of whether the connection was incoming or outgoing.
// Furthermore, a relay service which handles incoming requests to connect
// via the relays.
//
// TODO: Clean shutdown, and/or handling config changes on the fly. We
// partly do this now - new devices and addresses will be picked up, but
@@ -79,11 +79,25 @@ func newConnectionSvc(cfg *config.Wrapper, myID protocol.DeviceID, model *model.
svc.Add(serviceFunc(svc.connect))
for _, addr := range svc.cfg.Options().ListenAddress {
addr := addr
listener := serviceFunc(func() {
svc.listen(addr)
})
svc.Add(listener)
uri, err := url.Parse(addr)
if err != nil {
l.Infoln("Failed to parse listen address:", addr, err)
continue
}
listener, ok := listeners[uri.Scheme]
if !ok {
l.Infoln("Unknown listen address scheme:", uri.String())
continue
}
if debugNet {
l.Debugln("listening on", uri.String())
}
svc.Add(serviceFunc(func() {
listener(uri, svc.tlsCfg, svc.conns)
}))
}
svc.Add(serviceFunc(svc.handle))
@@ -92,15 +106,15 @@ func newConnectionSvc(cfg *config.Wrapper, myID protocol.DeviceID, model *model.
func (s *connectionSvc) handle() {
next:
for conn := range s.conns {
cs := conn.ConnectionState()
for c := range s.conns {
cs := c.Conn.ConnectionState()
// We should have negotiated the next level protocol "bep/1.0" as part
// of the TLS handshake. Unfortunately this can't be a hard error,
// because there are implementations out there that don't support
// protocol negotiation (iOS for one...).
if !cs.NegotiatedProtocolIsMutual || cs.NegotiatedProtocol != bepProtocolName {
l.Infof("Peer %s did not negotiate bep/1.0", conn.RemoteAddr())
l.Infof("Peer %s did not negotiate bep/1.0", c.Conn.RemoteAddr())
}
// We should have received exactly one certificate from the other
@@ -108,8 +122,8 @@ next:
// connection.
certs := cs.PeerCertificates
if cl := len(certs); cl != 1 {
l.Infof("Got peer certificate list of length %d != 1 from %s; protocol error", cl, conn.RemoteAddr())
conn.Close()
l.Infof("Got peer certificate list of length %d != 1 from %s; protocol error", cl, c.Conn.RemoteAddr())
c.Conn.Close()
continue
}
remoteCert := certs[0]
@@ -120,19 +134,29 @@ next:
// clients between the same NAT gateway, and global discovery.
if remoteID == myID {
l.Infof("Connected to myself (%s) - should not happen", remoteID)
conn.Close()
c.Conn.Close()
continue
}
// We should not already be connected to the other party. TODO: This
// could use some better handling. If the old connection is dead but
// hasn't timed out yet we may want to drop *that* connection and keep
// this one. But in case we are two devices connecting to each other
// in parallel we don't want to do that or we end up with no
// connections still established...
if s.model.ConnectedTo(remoteID) {
// If we have a relay connection, and the new incoming connection is
// not a relay connection, we should drop that, and prefer the this one.
s.mut.RLock()
ct, ok := s.connType[remoteID]
s.mut.RUnlock()
if ok && !ct.IsDirect() && c.ConnType.IsDirect() {
if debugNet {
l.Debugln("Switching connections", remoteID)
}
s.model.Close(remoteID, fmt.Errorf("switching connections"))
} else if s.model.ConnectedTo(remoteID) {
// We should not already be connected to the other party. TODO: This
// could use some better handling. If the old connection is dead but
// hasn't timed out yet we may want to drop *that* connection and keep
// this one. But in case we are two devices connecting to each other
// in parallel we don't want to do that or we end up with no
// connections still established...
l.Infof("Connected to already connected device (%s)", remoteID)
conn.Close()
c.Conn.Close()
continue
}
@@ -150,35 +174,42 @@ next:
// Incorrect certificate name is something the user most
// likely wants to know about, since it's an advanced
// config. Warn instead of Info.
l.Warnf("Bad certificate from %s (%v): %v", remoteID, conn.RemoteAddr(), err)
conn.Close()
l.Warnf("Bad certificate from %s (%v): %v", remoteID, c.Conn.RemoteAddr(), err)
c.Conn.Close()
continue next
}
// If rate limiting is set, and based on the address we should
// limit the connection, then we wrap it in a limiter.
limit := s.shouldLimit(conn.RemoteAddr())
limit := s.shouldLimit(c.Conn.RemoteAddr())
wr := io.Writer(conn)
wr := io.Writer(c.Conn)
if limit && writeRateLimit != nil {
wr = &limitedWriter{conn, writeRateLimit}
wr = &limitedWriter{c.Conn, writeRateLimit}
}
rd := io.Reader(conn)
rd := io.Reader(c.Conn)
if limit && readRateLimit != nil {
rd = &limitedReader{conn, readRateLimit}
rd = &limitedReader{c.Conn, readRateLimit}
}
name := fmt.Sprintf("%s-%s", conn.LocalAddr(), conn.RemoteAddr())
name := fmt.Sprintf("%s-%s (%s)", c.Conn.LocalAddr(), c.Conn.RemoteAddr(), c.ConnType)
protoConn := protocol.NewConnection(remoteID, rd, wr, s.model, name, deviceCfg.Compression)
l.Infof("Established secure connection to %s at %s", remoteID, name)
if debugNet {
l.Debugf("cipher suite: %04X in lan: %t", conn.ConnectionState().CipherSuite, !limit)
l.Debugf("cipher suite: %04X in lan: %t", c.Conn.ConnectionState().CipherSuite, !limit)
}
s.model.AddConnection(conn, protoConn)
s.model.AddConnection(model.Connection{
c.Conn,
protoConn,
c.ConnType,
})
s.mut.Lock()
s.connType[remoteID] = c.ConnType
s.mut.Unlock()
continue next
}
}
@@ -186,54 +217,14 @@ next:
if !s.cfg.IgnoredDevice(remoteID) {
events.Default.Log(events.DeviceRejected, map[string]string{
"device": remoteID.String(),
"address": conn.RemoteAddr().String(),
"address": c.Conn.RemoteAddr().String(),
})
l.Infof("Connection from %s with unknown device ID %s", conn.RemoteAddr(), remoteID)
l.Infof("Connection from %s (%s) with unknown device ID %s", c.Conn.RemoteAddr(), c.ConnType, remoteID)
} else {
l.Infof("Connection from %s with ignored device ID %s", conn.RemoteAddr(), remoteID)
l.Infof("Connection from %s (%s) with ignored device ID %s", c.Conn.RemoteAddr(), c.ConnType, remoteID)
}
conn.Close()
}
}
func (s *connectionSvc) listen(addr string) {
if debugNet {
l.Debugln("listening on", addr)
}
tcaddr, err := net.ResolveTCPAddr("tcp", addr)
if err != nil {
l.Fatalln("listen (BEP):", err)
}
listener, err := net.ListenTCP("tcp", tcaddr)
if err != nil {
l.Fatalln("listen (BEP):", err)
}
for {
conn, err := listener.Accept()
if err != nil {
l.Warnln("Accepting connection:", err)
continue
}
if debugNet {
l.Debugln("connect from", conn.RemoteAddr())
}
tcpConn := conn.(*net.TCPConn)
s.setTCPOptions(tcpConn)
tc := tls.Server(conn, s.tlsCfg)
err = tc.Handshake()
if err != nil {
l.Infoln("TLS handshake:", err)
tc.Close()
continue
}
s.conns <- tc
c.Conn.Close()
}
}
@@ -246,19 +237,24 @@ func (s *connectionSvc) connect() {
continue
}
if s.model.ConnectedTo(deviceID) {
connected := s.model.ConnectedTo(deviceID)
s.mut.RLock()
ct, ok := s.connType[deviceID]
relaysEnabled := s.relaysEnabled
s.mut.RUnlock()
if connected && ok && ct.IsDirect() {
continue
}
var addrs []string
var relays []string
for _, addr := range deviceCfg.Addresses {
if addr == "dynamic" {
if discoverer != nil {
t := discoverer.Lookup(deviceID)
if len(t) == 0 {
continue
}
t, r := discoverer.Lookup(deviceID)
addrs = append(addrs, t...)
relays = append(relays, r...)
}
} else {
addrs = append(addrs, addr)
@@ -266,45 +262,108 @@ func (s *connectionSvc) connect() {
}
for _, addr := range addrs {
host, port, err := net.SplitHostPort(addr)
if err != nil && strings.HasPrefix(err.Error(), "missing port") {
// addr is on the form "1.2.3.4"
addr = net.JoinHostPort(addr, "22000")
} else if err == nil && port == "" {
// addr is on the form "1.2.3.4:"
addr = net.JoinHostPort(host, "22000")
uri, err := url.Parse(addr)
if err != nil {
l.Infoln("Failed to parse connection url:", addr, err)
continue
}
dialer, ok := dialers[uri.Scheme]
if !ok {
l.Infoln("Unknown address schema", uri.String())
continue
}
if debugNet {
l.Debugln("dial", deviceCfg.DeviceID, addr)
l.Debugln("dial", deviceCfg.DeviceID, uri.String())
}
raddr, err := net.ResolveTCPAddr("tcp", addr)
conn, err := dialer(uri, s.tlsCfg)
if err != nil {
if debugNet {
l.Debugln(err)
l.Debugln("dial failed", deviceCfg.DeviceID, uri.String(), err)
}
continue
}
conn, err := net.DialTCP("tcp", nil, raddr)
if connected {
s.model.Close(deviceID, fmt.Errorf("switching connections"))
}
s.conns <- model.IntermediateConnection{
conn, model.ConnectionTypeBasicDial,
}
continue nextDevice
}
// Only connect via relays if not already connected
// Also, do not set lastRelayCheck time if we have no relays,
// as otherwise when we do discover relays, we might have to
// wait up to RelayReconnectIntervalM to connect again.
// Also, do not try relays if we are explicitly told not to.
if connected || len(relays) == 0 || !relaysEnabled {
continue nextDevice
}
reconIntv := time.Duration(s.cfg.Options().RelayReconnectIntervalM) * time.Minute
if last, ok := s.lastRelayCheck[deviceID]; ok && time.Since(last) < reconIntv {
if debugNet {
l.Debugln("Skipping connecting via relay to", deviceID, "last checked at", last)
}
continue nextDevice
} else if debugNet {
l.Debugln("Trying relay connections to", deviceID, relays)
}
s.lastRelayCheck[deviceID] = time.Now()
for _, addr := range relays {
uri, err := url.Parse(addr)
if err != nil {
if debugNet {
l.Debugln(err)
}
l.Infoln("Failed to parse relay connection url:", addr, err)
continue
}
s.setTCPOptions(conn)
inv, err := client.GetInvitationFromRelay(uri, deviceID, s.tlsCfg.Certificates)
if err != nil {
if debugNet {
l.Debugf("Failed to get invitation for %s from %s: %v", deviceID, uri, err)
}
continue
} else if debugNet {
l.Debugln("Succesfully retrieved relay invitation", inv, "from", uri)
}
tc := tls.Client(conn, s.tlsCfg)
conn, err := client.JoinSession(inv)
if err != nil {
if debugNet {
l.Debugf("Failed to join relay session %s: %v", inv, err)
}
continue
} else if debugNet {
l.Debugln("Sucessfully joined relay session", inv)
}
err = osutil.SetTCPOptions(conn.(*net.TCPConn))
if err != nil {
l.Infoln(err)
}
var tc *tls.Conn
if inv.ServerSocket {
tc = tls.Server(conn, s.tlsCfg)
} else {
tc = tls.Client(conn, s.tlsCfg)
}
err = tc.Handshake()
if err != nil {
l.Infoln("TLS handshake:", err)
l.Infof("TLS handshake (BEP/relay %s): %v", inv, err)
tc.Close()
continue
}
s.conns <- tc
s.conns <- model.IntermediateConnection{
tc, model.ConnectionTypeRelayDial,
}
continue nextDevice
}
}
@@ -317,22 +376,6 @@ func (s *connectionSvc) connect() {
}
}
func (*connectionSvc) setTCPOptions(conn *net.TCPConn) {
var err error
if err = conn.SetLinger(0); err != nil {
l.Infoln(err)
}
if err = conn.SetNoDelay(false); err != nil {
l.Infoln(err)
}
if err = conn.SetKeepAlivePeriod(60 * time.Second); err != nil {
l.Infoln(err)
}
if err = conn.SetKeepAlive(true); err != nil {
l.Infoln(err)
}
}
func (s *connectionSvc) shouldLimit(addr net.Addr) bool {
if s.cfg.Options().LimitBandwidthInLan {
return true
@@ -355,6 +398,10 @@ func (s *connectionSvc) VerifyConfiguration(from, to config.Configuration) error
}
func (s *connectionSvc) CommitConfiguration(from, to config.Configuration) bool {
s.mut.Lock()
s.relaysEnabled = to.Options.RelaysEnabled
s.mut.Unlock()
// We require a restart if a device as been removed.
newDevices := make(map[protocol.DeviceID]bool, len(to.Devices))
+105
View File
@@ -0,0 +1,105 @@
// Copyright (C) 2015 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package main
import (
"crypto/tls"
"net"
"net/url"
"strings"
"github.com/syncthing/syncthing/lib/model"
"github.com/syncthing/syncthing/lib/osutil"
)
func init() {
dialers["tcp"] = tcpDialer
listeners["tcp"] = tcpListener
}
func tcpDialer(uri *url.URL, tlsCfg *tls.Config) (*tls.Conn, error) {
host, port, err := net.SplitHostPort(uri.Host)
if err != nil && strings.HasPrefix(err.Error(), "missing port") {
// addr is on the form "1.2.3.4"
uri.Host = net.JoinHostPort(uri.Host, "22000")
} else if err == nil && port == "" {
// addr is on the form "1.2.3.4:"
uri.Host = net.JoinHostPort(host, "22000")
}
raddr, err := net.ResolveTCPAddr("tcp", uri.Host)
if err != nil {
if debugNet {
l.Debugln(err)
}
return nil, err
}
conn, err := net.DialTCP("tcp", nil, raddr)
if err != nil {
if debugNet {
l.Debugln(err)
}
return nil, err
}
err = osutil.SetTCPOptions(conn)
if err != nil {
l.Infoln(err)
}
tc := tls.Client(conn, tlsCfg)
err = tc.Handshake()
if err != nil {
tc.Close()
return nil, err
}
return tc, nil
}
func tcpListener(uri *url.URL, tlsCfg *tls.Config, conns chan<- model.IntermediateConnection) {
tcaddr, err := net.ResolveTCPAddr("tcp", uri.Host)
if err != nil {
l.Fatalln("listen (BEP/tcp):", err)
return
}
listener, err := net.ListenTCP("tcp", tcaddr)
if err != nil {
l.Fatalln("listen (BEP/tcp):", err)
return
}
for {
conn, err := listener.Accept()
if err != nil {
l.Warnln("Accepting connection (BEP/tcp):", err)
continue
}
if debugNet {
l.Debugln("connect from", conn.RemoteAddr())
}
err = osutil.SetTCPOptions(conn.(*net.TCPConn))
if err != nil {
l.Infoln(err)
}
tc := tls.Server(conn, tlsCfg)
err = tc.Handshake()
if err != nil {
l.Infoln("TLS handshake (BEP/tcp):", err)
tc.Close()
continue
}
conns <- model.IntermediateConnection{
tc, model.ConnectionTypeBasicAccept,
}
}
}
+13 -9
View File
@@ -26,15 +26,15 @@ import (
"github.com/calmh/logger"
"github.com/syncthing/protocol"
"github.com/syncthing/syncthing/internal/auto"
"github.com/syncthing/syncthing/internal/config"
"github.com/syncthing/syncthing/internal/db"
"github.com/syncthing/syncthing/internal/discover"
"github.com/syncthing/syncthing/internal/events"
"github.com/syncthing/syncthing/internal/model"
"github.com/syncthing/syncthing/internal/osutil"
"github.com/syncthing/syncthing/internal/sync"
"github.com/syncthing/syncthing/internal/upgrade"
"github.com/syncthing/syncthing/lib/auto"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/db"
"github.com/syncthing/syncthing/lib/discover"
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/model"
"github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/sync"
"github.com/syncthing/syncthing/lib/upgrade"
"github.com/vitrun/qart/qr"
"golang.org/x/crypto/bcrypt"
)
@@ -355,6 +355,7 @@ func (s *apiSvc) getSystemVersion(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(map[string]string{
"version": Version,
"codename": Codename,
"longVersion": LongVersion,
"os": runtime.GOOS,
"arch": runtime.GOARCH,
@@ -627,6 +628,9 @@ func (s *apiSvc) getSystemStatus(w http.ResponseWriter, r *http.Request) {
if cfg.Options().GlobalAnnEnabled && discoverer != nil {
res["extAnnounceOK"] = discoverer.ExtAnnounceOK()
}
if relaySvc != nil {
res["relayClientStatus"] = relaySvc.ClientStatus()
}
cpuUsageLock.RLock()
var cpusum float64
for _, p := range cpuUsagePercent {
+2 -2
View File
@@ -14,8 +14,8 @@ import (
"strings"
"time"
"github.com/syncthing/syncthing/internal/config"
"github.com/syncthing/syncthing/internal/sync"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/sync"
"golang.org/x/crypto/bcrypt"
)
+2 -2
View File
@@ -13,8 +13,8 @@ import (
"os"
"strings"
"github.com/syncthing/syncthing/internal/osutil"
"github.com/syncthing/syncthing/internal/sync"
"github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/sync"
)
var csrfTokens []string
+1 -1
View File
@@ -13,7 +13,7 @@ import (
"strings"
"time"
"github.com/syncthing/syncthing/internal/osutil"
"github.com/syncthing/syncthing/lib/osutil"
)
type locationEnum string
+74 -22
View File
@@ -28,14 +28,16 @@ import (
"github.com/calmh/logger"
"github.com/juju/ratelimit"
"github.com/syncthing/protocol"
"github.com/syncthing/syncthing/internal/config"
"github.com/syncthing/syncthing/internal/db"
"github.com/syncthing/syncthing/internal/discover"
"github.com/syncthing/syncthing/internal/events"
"github.com/syncthing/syncthing/internal/model"
"github.com/syncthing/syncthing/internal/osutil"
"github.com/syncthing/syncthing/internal/symlinks"
"github.com/syncthing/syncthing/internal/upgrade"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/db"
"github.com/syncthing/syncthing/lib/discover"
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/model"
"github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/relay"
"github.com/syncthing/syncthing/lib/symlinks"
"github.com/syncthing/syncthing/lib/upgrade"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/errors"
"github.com/syndtr/goleveldb/leveldb/opt"
@@ -45,6 +47,7 @@ import (
var (
Version = "unknown-dev"
Codename = "Aluminium Ant"
BuildEnv = "default"
BuildStamp = "0"
BuildDate time.Time
@@ -93,7 +96,7 @@ func init() {
BuildDate = time.Unix(int64(stamp), 0)
date := BuildDate.UTC().Format("2006-01-02 15:04:05 MST")
LongVersion = fmt.Sprintf("syncthing %s (%s %s-%s %s) %s@%s %s", Version, runtime.Version(), runtime.GOOS, runtime.GOARCH, BuildEnv, BuildUser, BuildHost, date)
LongVersion = fmt.Sprintf(`syncthing %s "%s" (%s %s-%s %s) %s@%s %s`, Version, Codename, runtime.Version(), runtime.GOOS, runtime.GOARCH, BuildEnv, BuildUser, BuildHost, date)
if os.Getenv("STTRACE") != "" {
logFlags = log.Ltime | log.Ldate | log.Lmicroseconds | log.Lshortfile
@@ -109,6 +112,7 @@ var (
readRateLimit *ratelimit.Bucket
stop = make(chan int)
discoverer *discover.Discoverer
relaySvc *relay.Svc
cert tls.Certificate
lans []*net.IPNet
)
@@ -454,6 +458,11 @@ func syncthingMain() {
runtime.GOMAXPROCS(runtime.NumCPU())
}
// Attempt to increase the limit on number of open files to the maximum
// allowed, in case we have many peers. We don't really care enough to
// report the error if there is one.
osutil.MaximizeOpenFileLimit()
// Ensure that that we have a certificate and key.
cert, err := tls.LoadX509KeyPair(locations[locCertFile], locations[locKeyFile])
if err != nil {
@@ -586,9 +595,19 @@ func syncthingMain() {
dbFile := locations[locDatabase]
ldb, err := leveldb.OpenFile(dbFile, dbOpts())
if err != nil && errors.IsCorrupted(err) {
if leveldbIsCorrupted(err) {
ldb, err = leveldb.RecoverFile(dbFile, dbOpts())
}
if leveldbIsCorrupted(err) {
// The database is corrupted, and we've tried to recover it but it
// didn't work. At this point there isn't much to do beyond dropping
// the database and reindexing...
l.Infoln("Database corruption detected, unable to recover. Reinitializing...")
if err := resetDB(); err != nil {
l.Fatalln("Remove database:", err)
}
ldb, err = leveldb.OpenFile(dbFile, dbOpts())
}
if err != nil {
l.Fatalln("Cannot open database:", err, "- Is another copy of Syncthing already running?")
}
@@ -611,7 +630,7 @@ func syncthingMain() {
m.StartDeadlockDetector(time.Duration(it) * time.Second)
}
} else if !IsRelease || IsBeta {
m.StartDeadlockDetector(20 * 60 * time.Second)
m.StartDeadlockDetector(20 * time.Minute)
}
// Clear out old indexes for other devices. Otherwise we'll start up and
@@ -642,15 +661,30 @@ func syncthingMain() {
// The default port we announce, possibly modified by setupUPnP next.
addr, err := net.ResolveTCPAddr("tcp", opts.ListenAddress[0])
uri, err := url.Parse(opts.ListenAddress[0])
if err != nil {
l.Fatalf("Failed to parse listen address %s: %v", opts.ListenAddress[0], err)
}
addr, err := net.ResolveTCPAddr("tcp", uri.Host)
if err != nil {
l.Fatalln("Bad listen address:", err)
}
// Start the relevant services
connectionSvc := newConnectionSvc(cfg, myID, m, tlsCfg)
mainSvc.Add(connectionSvc)
if opts.RelaysEnabled && (opts.GlobalAnnEnabled || opts.RelayWithoutGlobalAnn) {
relaySvc = relay.NewSvc(cfg, tlsCfg, connectionSvc.conns)
connectionSvc.Add(relaySvc)
}
// Start discovery
localPort := addr.Port
discoverer = discovery(localPort)
discoverer = discovery(localPort, relaySvc)
// Start UPnP. The UPnP service will restart global discovery if the
// external port changes.
@@ -660,10 +694,6 @@ func syncthingMain() {
mainSvc.Add(upnpSvc)
}
connectionSvc := newConnectionSvc(cfg, myID, m, tlsCfg)
cfg.Subscribe(connectionSvc)
mainSvc.Add(connectionSvc)
if cpuProfile {
f, err := os.Create(fmt.Sprintf("cpu-%d.pprof", os.Getpid()))
if err != nil {
@@ -837,6 +867,7 @@ func defaultConfig(myName string) config.Configuration {
ID: "default",
RawPath: locations[locDefFolder],
RescanIntervalS: 60,
MinDiskFreePct: 1,
Devices: []config.FolderDeviceConfiguration{{DeviceID: myID}},
},
}
@@ -883,18 +914,23 @@ func shutdown() {
stop <- exitSuccess
}
func discovery(extPort int) *discover.Discoverer {
func discovery(extPort int, relaySvc *relay.Svc) *discover.Discoverer {
opts := cfg.Options()
disc := discover.NewDiscoverer(myID, opts.ListenAddress)
disc := discover.NewDiscoverer(myID, opts.ListenAddress, relaySvc)
if opts.LocalAnnEnabled {
l.Infoln("Starting local discovery announcements")
disc.StartLocal(opts.LocalAnnPort, opts.LocalAnnMCAddr)
}
if opts.GlobalAnnEnabled {
l.Infoln("Starting global discovery announcements")
disc.StartGlobal(opts.GlobalAnnServers, uint16(extPort))
go func() {
// Defer starting global announce server, giving time to connect
// to relay servers.
time.Sleep(5 * time.Second)
l.Infoln("Starting global discovery announcements")
disc.StartGlobal(opts.GlobalAnnServers, uint16(extPort))
}()
}
return disc
@@ -1099,3 +1135,19 @@ func checkShortIDs(cfg *config.Wrapper) error {
}
return nil
}
// A "better" version of leveldb's errors.IsCorrupted.
func leveldbIsCorrupted(err error) bool {
switch {
case err == nil:
return false
case errors.IsCorrupted(err):
return true
case strings.Contains(err.Error(), "corrupted"):
return true
}
return false
}
+3 -3
View File
@@ -11,9 +11,9 @@ import (
"testing"
"github.com/syncthing/protocol"
"github.com/syncthing/syncthing/internal/config"
"github.com/syncthing/syncthing/internal/db"
"github.com/syncthing/syncthing/internal/model"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/db"
"github.com/syncthing/syncthing/lib/model"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/storage"
+40 -5
View File
@@ -17,8 +17,8 @@ import (
"syscall"
"time"
"github.com/syncthing/syncthing/internal/osutil"
"github.com/syncthing/syncthing/internal/sync"
"github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/sync"
)
var (
@@ -146,9 +146,8 @@ func monitorMain() {
// binary as part of the upgrade process.
l.Infoln("Restarting monitor...")
os.Setenv("STNORESTART", "")
err := exec.Command(args[0], args[1:]...).Start()
if err != nil {
l.Warnln("restart:", err)
if err = restartMonitor(args); err != nil {
l.Warnln("Restart:", err)
}
return
}
@@ -227,3 +226,39 @@ func copyStdout(stdout io.Reader, dst io.Writer) {
dst.Write([]byte(line))
}
}
func restartMonitor(args []string) error {
if runtime.GOOS != "windows" {
// syscall.Exec is the cleanest way to restart on Unixes as it
// replaces the current process with the new one, keeping the pid and
// controlling terminal and so on
return restartMonitorUnix(args)
}
// but it isn't supported on Windows, so there we start a normal
// exec.Command and return.
return restartMonitorWindows(args)
}
func restartMonitorUnix(args []string) error {
if !strings.ContainsRune(args[0], os.PathSeparator) {
// The path to the binary doesn't contain a slash, so it should be
// found in $PATH.
binary, err := exec.LookPath(args[0])
if err != nil {
return err
}
args[0] = binary
}
return syscall.Exec(args[0], args, os.Environ())
}
func restartMonitorWindows(args []string) error {
cmd := exec.Command(args[0], args[1:]...)
// Retain the standard streams
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
cmd.Stdin = os.Stdin
return cmd.Start()
}
+3 -3
View File
@@ -9,9 +9,9 @@ package main
import (
"time"
"github.com/syncthing/syncthing/internal/events"
"github.com/syncthing/syncthing/internal/model"
"github.com/syncthing/syncthing/internal/sync"
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/model"
"github.com/syncthing/syncthing/lib/sync"
"github.com/thejerf/suture"
)
+2 -2
View File
@@ -10,8 +10,8 @@ import (
"fmt"
"time"
"github.com/syncthing/syncthing/internal/config"
"github.com/syncthing/syncthing/internal/upnp"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/upnp"
)
// The UPnP service runs a loop for discovery of IGDs (Internet Gateway
+2 -2
View File
@@ -17,8 +17,8 @@ import (
"runtime"
"time"
"github.com/syncthing/syncthing/internal/config"
"github.com/syncthing/syncthing/internal/model"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/model"
"github.com/thejerf/suture"
)
+2 -2
View File
@@ -9,7 +9,7 @@ package main
import (
"fmt"
"github.com/syncthing/syncthing/internal/events"
"github.com/syncthing/syncthing/lib/events"
)
// The verbose logging service subscribes to events and prints these in
@@ -74,7 +74,7 @@ func (s *verboseSvc) formatEvent(ev events.Event) string {
return fmt.Sprintf("Discovered device %v at %v", data["device"], data["addrs"])
case events.DeviceConnected:
data := ev.Data.(map[string]string)
return fmt.Sprintf("Connected to device %v at %v", data["id"], data["addr"])
return fmt.Sprintf("Connected to device %v at %v (type %s)", data["id"], data["addr"], data["type"])
case events.DeviceDisconnected:
data := ev.Data.(map[string]string)
return fmt.Sprintf("Disconnected from device %v", data["id"])
+1 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<!--
Make sure the "syncthing" executable is located in ~/bin/syncthing.
Make sure the "syncthing" executable is located at ~/bin/syncthing.
Replace the string "USERNAME" in this file with your username, such as "jb".
Copy this file to ~/Library/LaunchAgents/syncthing.plist.
Execute "launchctl load ~/Library/LaunchAgents/syncthing.plist".
+1 -1
View File
@@ -67,7 +67,7 @@ identicon {
top: 1px;
}
.panel-heading .glyphicon {
.panel-heading .fa, .modal-header .fa {
margin-right: 10px;
}
+1
View File
@@ -184,6 +184,7 @@
"The number of old versions to keep, per file.": "Броят стари версии, които да бъдат пазени за всеки файл.",
"The number of versions must be a number and cannot be blank.": "Броят версии трябва да бъде число и не може да бъде празно.",
"The path cannot be blank.": "Пътят неможе да бъде празен.",
"The rate limit must be a non-negative number (0: no limit)": "Ограничението на скорста трябва да бъде положително число (0: неограничено)",
"The rescan interval must be a non-negative number of seconds.": "Интервала на сканиране трябва да бъде не отрицателно число в секунди.",
"They are retried automatically and will be synced when the error is resolved.": "Ще бъдат спрени и автоматично синхронизирани, когато грешката бъде оправена.",
"This is a major version upgrade.": "Това е нова основна версия.",
+1
View File
@@ -184,6 +184,7 @@
"The number of old versions to keep, per file.": "El nombre de versions antigues que es mantenen per fitxer.",
"The number of versions must be a number and cannot be blank.": "El nombre de versions ha de ser un número i no es pot deixar en blanc.",
"The path cannot be blank.": "El camí no pot estar en blanc.",
"The rate limit must be a non-negative number (0: no limit)": "The rate limit must be a non-negative number (0: no limit)",
"The rescan interval must be a non-negative number of seconds.": "El interval de re-escaneig ha der ser un nombre positiu de segons.",
"They are retried automatically and will be synced when the error is resolved.": "They are retried automatically and will be synced when the error is resolved.",
"This is a major version upgrade.": "Aquesta és una actualització de versió major.",
+1
View File
@@ -184,6 +184,7 @@
"The number of old versions to keep, per file.": "El nombre de versions antigues per a guardar, per cada fitxer.",
"The number of versions must be a number and cannot be blank.": "El nombre de versions deu ser un nombre i no pot estar buit.",
"The path cannot be blank.": "La ruta no pot estar buida.",
"The rate limit must be a non-negative number (0: no limit)": "The rate limit must be a non-negative number (0: no limit)",
"The rescan interval must be a non-negative number of seconds.": "L'interval de reescaneig deu ser un nombre positiu de segons.",
"They are retried automatically and will be synced when the error is resolved.": "They are retried automatically and will be synced when the error is resolved.",
"This is a major version upgrade.": "Aquesta és una actualització important de la versió.",
+4 -3
View File
@@ -93,7 +93,7 @@
"Major Upgrade": "Důležitá aktualizace",
"Maximum Age": "Maximální časový limit",
"Metadata Only": "Pouze metadata",
"Minimum Free Disk Space": "Minimum Free Disk Space",
"Minimum Free Disk Space": "Minimální velikost volného místa na disku",
"Move to top of queue": "Přesunout na začátek fronty",
"Multi level wildcard (matches multiple directory levels)": "Víceúrovňový zástupný znak (shoda skrz více úrovní adresářů)",
"Never": "Nikdy",
@@ -121,7 +121,7 @@
"RAM Utilization": "Využití RAM",
"Random": "Náhodně",
"Release Notes": "Poznámky k vydání",
"Remove": "Remove",
"Remove": "Odstranit",
"Rescan": "Opakovat skenování",
"Rescan All": "Opakovat skenování všech",
"Rescan Interval": "Interval opakování skenování",
@@ -178,12 +178,13 @@
"The following items could not be synchronized.": "Následující položky nemohly být synchronizovány.",
"The maximum age must be a number and cannot be blank.": "Nejvyšší stáří je třeba zadat v podobě čísla a nemůže být prázdné.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Maximální doba pro zachování verze (dny, zapsáním hodnoty 0 bude ponecháno navždy).",
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).",
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "Procentuální údaj minimální velikosti volného místa na disku musí být číslo mezi 0 až 100 (včetně).",
"The number of days must be a number and cannot be blank.": "Počet dní musí být číslo a nesmí být prázdný.",
"The number of days to keep files in the trash can. Zero means forever.": "Počet dní, po který budou soubory uchovány v koši. Nula znamená navždy.",
"The number of old versions to keep, per file.": "Počet starších verzí k zachování pro každý soubor.",
"The number of versions must be a number and cannot be blank.": "Počet verzí musí být číslo a nemůže být prázdné.",
"The path cannot be blank.": "Cesta nesmí být prázdná.",
"The rate limit must be a non-negative number (0: no limit)": "Limit rychlosti musí být nezáporné číslo (0: bez limitu)",
"The rescan interval must be a non-negative number of seconds.": "Interval opakování skenování musí být pozitivní číslo.",
"They are retried automatically and will be synced when the error is resolved.": "Nové pokusy o synchronizaci budou probíhat automaticky a položky budou synchronizovány jakmile bude chyba odstraněna.",
"This is a major version upgrade.": "Toto je důležitá aktualizace.",
+4 -3
View File
@@ -93,7 +93,7 @@
"Major Upgrade": "Hauptversionsupgrade",
"Maximum Age": "Höchstalter",
"Metadata Only": "Nur Metadaten",
"Minimum Free Disk Space": "Minimum Free Disk Space",
"Minimum Free Disk Space": "Minimal freier Fetsplattenspeicher",
"Move to top of queue": "An den Anfang der Warteschlange setzen",
"Multi level wildcard (matches multiple directory levels)": "Verschachteltes Maskenzeichen (wird für verschachtelte Verzeichnisse verwendet)",
"Never": "Nie",
@@ -121,7 +121,7 @@
"RAM Utilization": "RAM Auslastung",
"Random": "Zufall",
"Release Notes": "Veröffentlichungsnotizen",
"Remove": "Remove",
"Remove": "Entfernen",
"Rescan": "Neu scannen",
"Rescan All": "Alle neu scannen",
"Rescan Interval": "Scanintervall",
@@ -178,12 +178,13 @@
"The following items could not be synchronized.": "Die folgenden Dateien konnten nicht synchronisiert werden.",
"The maximum age must be a number and cannot be blank.": "Das Höchstalter muss angegeben werden und eine Zahl sein.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Die längste Zeit, die alte Versionen vorgehalten werden (in Tagen, 0 bedeutet, alte Versionen für immer zu behalten).",
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).",
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "Der Prozentsatz des minimal freien Festplattenspeichers muss eine nichtnegative Zahl zwischen (inklusive) 0 und 100 sein. ",
"The number of days must be a number and cannot be blank.": "Die Anzahl von Versionen muss eine Zahl und darf nicht leer sein.",
"The number of days to keep files in the trash can. Zero means forever.": "Dauer in Tagen für welche die Dateien aufgehoben werden sollen. 0 bedeutet für immer.",
"The number of old versions to keep, per file.": "Anzahl der alten Versionen, die von jeder Datei gespeichert werden sollen.",
"The number of versions must be a number and cannot be blank.": "Die Anzahl von Versionen muss eine Zahl und darf nicht leer sein.",
"The path cannot be blank.": "Der Pfad darf nicht leer sein.",
"The rate limit must be a non-negative number (0: no limit)": "Das Daterate-Limit muss eine nicht negative Anzahl sein (0: kein Limit)",
"The rescan interval must be a non-negative number of seconds.": "Das Scanintervall muss eine nicht negative Anzahl von Sekunden sein.",
"They are retried automatically and will be synced when the error is resolved.": "Sie werden automatisch heruntergeladen und werden synchronisiert, wenn der Fehler behoben wurde.",
"This is a major version upgrade.": "Dies ist eine neue Hauptversion.",
+1
View File
@@ -184,6 +184,7 @@
"The number of old versions to keep, per file.": "Πόσες παλιότερες εκδόσεις θα διατηρούνται, ανά αρχείο.",
"The number of versions must be a number and cannot be blank.": "Ο αριθμός εκδόσεων πρέπει να είναι αριθμός και σίγουρα όχι κενό.",
"The path cannot be blank.": "Το μονοπάτι δεν μπορεί να είναι κενό.",
"The rate limit must be a non-negative number (0: no limit)": "The rate limit must be a non-negative number (0: no limit)",
"The rescan interval must be a non-negative number of seconds.": "Ο χρόνος επανελέγχου για αλλαγές είναι σε δευτερόλεπτα (δηλ. θετικός αριθμός).",
"They are retried automatically and will be synced when the error is resolved.": "Όταν επιλυθεί το σφάλμα θα κατεβούν και θα συχρονιστούν αυτόματα.",
"This is a major version upgrade.": "Αυτή είναι μιας σημαντική αναβάθμιση.",
+1
View File
@@ -184,6 +184,7 @@
"The number of old versions to keep, per file.": "The number of old versions to keep, per file.",
"The number of versions must be a number and cannot be blank.": "The number of versions must be a number and cannot be blank.",
"The path cannot be blank.": "The path cannot be blank.",
"The rate limit must be a non-negative number (0: no limit)": "The rate limit must be a non-negative number (0: no limit)",
"The rescan interval must be a non-negative number of seconds.": "The rescan interval must be a non-negative number of seconds.",
"They are retried automatically and will be synced when the error is resolved.": "They are retried automatically and will be synced when the error is resolved.",
"This is a major version upgrade.": "This is a major version upgrade.",
+7 -3
View File
@@ -49,8 +49,7 @@
"Edit Folder": "Edit Folder",
"Editing": "Editing",
"Enable UPnP": "Enable UPnP",
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.",
"Enter comma separated addresses (\"ip:port\", \"host:port\") or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated addresses (\"ip:port\", \"host:port\") or \"dynamic\" to perform automatic discovery of the address.",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.",
"Enter ignore patterns, one per line.": "Enter ignore patterns, one per line.",
"Error": "Error",
"External File Versioning": "External File Versioning",
@@ -94,6 +93,7 @@
"Major Upgrade": "Major Upgrade",
"Maximum Age": "Maximum Age",
"Metadata Only": "Metadata Only",
"Minimum Free Disk Space": "Minimum Free Disk Space",
"Move to top of queue": "Move to top of queue",
"Multi level wildcard (matches multiple directory levels)": "Multi level wildcard (matches multiple directory levels)",
"Never": "Never",
@@ -120,7 +120,10 @@
"Quick guide to supported patterns": "Quick guide to supported patterns",
"RAM Utilization": "RAM Utilization",
"Random": "Random",
"Relayed via": "Relayed via",
"Relays": "Relays",
"Release Notes": "Release Notes",
"Remove": "Remove",
"Rescan": "Rescan",
"Rescan All": "Rescan All",
"Rescan Interval": "Rescan Interval",
@@ -171,18 +174,19 @@
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "The first command line parameter is the folder path and the second parameter is the relative path in the folder.",
"The folder ID cannot be blank.": "The folder ID cannot be blank.",
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.",
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscore (_) characters only.": "The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscore (_) characters only.",
"The folder ID must be unique.": "The folder ID must be unique.",
"The folder path cannot be blank.": "The folder path cannot be blank.",
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.",
"The following items could not be synchronized.": "The following items could not be synchronized.",
"The maximum age must be a number and cannot be blank.": "The maximum age must be a number and cannot be blank.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "The maximum time to keep a version (in days, set to 0 to keep versions forever).",
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).",
"The number of days must be a number and cannot be blank.": "The number of days must be a number and cannot be blank.",
"The number of days to keep files in the trash can. Zero means forever.": "The number of days to keep files in the trash can. Zero means forever.",
"The number of old versions to keep, per file.": "The number of old versions to keep, per file.",
"The number of versions must be a number and cannot be blank.": "The number of versions must be a number and cannot be blank.",
"The path cannot be blank.": "The path cannot be blank.",
"The rate limit must be a non-negative number (0: no limit)": "The rate limit must be a non-negative number (0: no limit)",
"The rescan interval must be a non-negative number of seconds.": "The rescan interval must be a non-negative number of seconds.",
"They are retried automatically and will be synced when the error is resolved.": "They are retried automatically and will be synced when the error is resolved.",
"This is a major version upgrade.": "This is a major version upgrade.",
+1
View File
@@ -184,6 +184,7 @@
"The number of old versions to keep, per file.": "El número de versiones a antiguas a mantener para cada fichero.",
"The number of versions must be a number and cannot be blank.": "El número de versiones debe ser un número y no puede estar vacío.",
"The path cannot be blank.": "La ruta no puede estar vacía.",
"The rate limit must be a non-negative number (0: no limit)": "The rate limit must be a non-negative number (0: no limit)",
"The rescan interval must be a non-negative number of seconds.": "El intervalo de actualización debe ser un número positivo de segundos.",
"They are retried automatically and will be synced when the error is resolved.": "They are retried automatically and will be synced when the error is resolved.",
"This is a major version upgrade.": "Hay una actualización importante.",
+1
View File
@@ -184,6 +184,7 @@
"The number of old versions to keep, per file.": "El numero de versiones anteriores a conservar, por archivo.",
"The number of versions must be a number and cannot be blank.": "El número de versiones debe ser un número y no puede estar vacío.",
"The path cannot be blank.": "La ruta no puede estar vacía.",
"The rate limit must be a non-negative number (0: no limit)": "The rate limit must be a non-negative number (0: no limit)",
"The rescan interval must be a non-negative number of seconds.": "El intervalo de reescaneo debe ser un número no negativo de segundos.",
"They are retried automatically and will be synced when the error is resolved.": "They are retried automatically and will be synced when the error is resolved.",
"This is a major version upgrade.": "Esta es una actualización de version mayor.",
+1
View File
@@ -184,6 +184,7 @@
"The number of old versions to keep, per file.": "Säilytettävien vanhojen versioiden määrä tiedostoa kohden.",
"The number of versions must be a number and cannot be blank.": "Versioiden määrän rulee olla numero, eikä se voi olla tyhjä.",
"The path cannot be blank.": "Polku ei voi olla tyhjä.",
"The rate limit must be a non-negative number (0: no limit)": "The rate limit must be a non-negative number (0: no limit)",
"The rescan interval must be a non-negative number of seconds.": "Uudelleenskannauksen aikavälin tulee olla ei-negatiivinen numero sekunteja.",
"They are retried automatically and will be synced when the error is resolved.": "They are retried automatically and will be synced when the error is resolved.",
"This is a major version upgrade.": "This is a major version upgrade.",
+213
View File
@@ -0,0 +1,213 @@
{
"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 new folder?": "Ajouter un nouveau dossier ?",
"Address": "Adresse",
"Addresses": "Adresses",
"Advanced": "Avancé",
"Advanced Configuration": "Configuration avancée",
"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é.",
"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",
"Be careful!": "Faites attention !",
"Bugs": "Bugs",
"CPU Utilization": "Utilisation du CPU",
"Changelog": "Historique des changements",
"Clean out after": "Nettoyer après",
"Close": "Fermer",
"Command": "Commande",
"Comment, when used at the start of a line": "Commentaire lorsque utilisé en début de ligne",
"Compression": "Compression",
"Connection Error": "Erreur de connexion",
"Copied from elsewhere": "Copié d'ailleurs",
"Copied from original": "Copié depuis l'original",
"Copyright © 2015 the following Contributors:": "Copyright © 2015 Les contributeurs suivants:",
"Delete": "Supprimer",
"Deleted": "Supprimé",
"Device ID": "ID du périphérique",
"Device Identification": "Identification de l'appareil",
"Device Name": "Nom du périphérique",
"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é",
"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",
"Editing": "Édition",
"Enable UPnP": "Activer l'UPnP",
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Entrer les adresses \"ip: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.",
"Error": "Erreur",
"External File Versioning": "Gestion externe des versions de fichiers",
"Failed Items": "Éléments en échec",
"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 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 Master": "Répertoire maître",
"Folder Path": "Chemin du répertoire",
"Folders": "Dossiers",
"GUI": "GUI",
"GUI Authentication Password": "Mot de passe d'authentification GUI",
"GUI Authentication User": "Utilisateur autorisé GUI",
"GUI Listen Addresses": "Adresse du GUI",
"Generate": "Générer",
"Global Discovery": "Recherche globale",
"Global Discovery Server": "Serveur global de recherche",
"Global State": "État global",
"Help": "Aide",
"Home page": "Page d'accueil",
"Ignore": "Ignorer",
"Ignore Patterns": "Modèles à éviter",
"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",
"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 seen": "Dernière apparition",
"Later": "Plus tard",
"Local Discovery": "Recherche locale",
"Local State": "État local",
"Local State (Total)": "État local (Total)",
"Major Upgrade": "Mise à jour majeure",
"Maximum Age": "Ancienneté maximum",
"Metadata Only": "Métadonnées uniquement",
"Minimum Free Disk Space": "Espace disque libre minimum",
"Move to top of queue": "Déplacer en haut de la file",
"Multi level wildcard (matches multiple directory levels)": "Astérisque à plusieurs niveaux (correspond aux répertoires et sous-répertoires)",
"Never": "Jamais",
"New Device": "Nouvel appareil",
"New Folder": "Nouveau dossier",
"Newest First": "Les plus récents en premier",
"No": "Non",
"No File Versioning": "Pas de version de fichier",
"Notice": "Notification",
"OK": "OK",
"Off": "Éteint",
"Oldest First": "Les plus anciens en premier",
"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 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)",
"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 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",
"Release Notes": "Notes de version",
"Remove": "Enlever",
"Rescan": "Rescanner",
"Rescan All": "Réanalyser tout",
"Rescan Interval": "Intervalle de scan",
"Restart": "Redémarrer",
"Restart Needed": "Redémarrage nécessaire",
"Restarting": "Redémarrage",
"Reused": "Réutilisé",
"Save": "Sauver",
"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.",
"Settings": "Configuration",
"Share": "Partager",
"Share Folder": "Partager le dossier",
"Share Folders With Device": "Partager des dossiers avec des appareils",
"Share With Devices": "Partage avec des appareils",
"Share this folder?": "Voulez-vous partager ce dossier ?",
"Shared With": "Partagé avec",
"Short identifier for the folder. Must be the same on all cluster devices.": "Identifiant court du dossier. Il doit être le même sur l'ensemble des appareils du groupe.",
"Show ID": "Montrer l'ID",
"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",
"Simple File Versioning": "Suivi simple des versions de fichier",
"Single level wildcard (matches within a directory only)": "Astérisque à un seul niveau (correspond uniquement à lintérieur du dossier)",
"Smallest First": "Les plus petits en premier",
"Source Code": "Code source",
"Staggered File Versioning": "Versions échelonnées de fichier",
"Start Browser": "Démarrer le navigateur web",
"Statistics": "Statistiques",
"Stopped": "Arrêté",
"Support": "Aide",
"Sync Protocol Listen Addresses": "Adresse d'écoute du protocole de synchronisation",
"Syncing": "En cours de synchronisation",
"Syncthing has been shut down.": "Syncthing a été éteint.",
"Syncthing includes the following software or portions thereof:": "Syncthing intègre les logiciels suivants (ou des éléments provenant de ces logiciels) :",
"Syncthing is restarting.": "Syncthing est cours de redémarrage.",
"Syncthing is upgrading.": "Syncthing est cours de mise à jour.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing semble être éteint, ou il y a un problème avec votre connexion Internet. Nouvelle tentative ...",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing semble avoir un problème pour traiter votre demande. S'il vous plait, rafraichissez la page ou redémarrer Syncthing si le problème persiste.",
"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 \"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 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 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.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Le temps maximum de conservation d'une version (en jours, mettre à 0 pour conserver les versions pour toujours)",
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "Le pourcentage d'espace disque libre doit être un nombre positif compris entre 0 et 100 (inclus).",
"The number of days must be a number and cannot be blank.": "Le nombre de jours doit être numérique et ne peut pas être vide.",
"The number of days to keep files in the trash can. Zero means forever.": "Le nombre de jours de conservation des fichiers dans la poubelle. Zéro signifie toujours.",
"The number of old versions to keep, per file.": "Le nombre d'anciennes versions à garder, par fichier.",
"The number of versions must be a number and cannot be blank.": "Le nombre de versions doit être numérique, et ne peut pas être vide.",
"The path cannot be blank.": "Le chemin ne peut pas être vide.",
"The rate limit must be a non-negative number (0: no limit)": "La limite de débit ne doit pas être négative (0: 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 is a major version upgrade.": "Ceci est une mise à jour majeure.",
"Trash Can File Versioning": "Gestion des versions de fichier style poubelle.",
"Unknown": "Inconnu",
"Unshared": "Non partagé",
"Unused": "Non utilisé",
"Up to Date": "Synchronisé",
"Updated": "Mis à jour",
"Upgrade": "Mettre à jour",
"Upgrade To {%version%}": "Mettre à jour vers {{version}}",
"Upgrading": "Mise à jour de Syncthing",
"Upload Rate": "Débit d'envoi",
"Uptime": "Durée de fonctionnement",
"Use HTTPS for GUI": "Utiliser l'HTTPS pour le GUI",
"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.",
"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.",
"Yes": "Oui",
"You must keep at least one version.": "Vous devez garder au minimum une version.",
"full documentation": "documentation complète",
"items": "éléments",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} veut partager le dossier \"{{folder}}\"."
}
+15 -14
View File
@@ -26,7 +26,7 @@
"Clean out after": "Nettoyer après",
"Close": "Fermer",
"Command": "Commande",
"Comment, when used at the start of a line": "Commentaire, lorsque utilisé en début de ligne",
"Comment, when used at the start of a line": "Commentaire lorsque utilisé en début de ligne",
"Compression": "Compression",
"Connection Error": "Erreur de connexion",
"Copied from elsewhere": "Copié d'ailleurs",
@@ -54,12 +54,12 @@
"Error": "Erreur",
"External File Versioning": "Gestion externe des versions de fichiers",
"Failed Items": "Éléments en échec",
"File Pull Order": "Ordre d'envoi de fichier",
"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 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 au reste du groupe.",
"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 Master": "Répertoire maître",
@@ -79,7 +79,7 @@
"Ignore Patterns": "Modèles à éviter",
"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 dossier 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 dossiers et mettre hors-service Syncthing",
"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",
@@ -89,11 +89,11 @@
"Later": "Plus tard",
"Local Discovery": "Recherche locale",
"Local State": "État local",
"Local State (Total)": "Etat local (Total)",
"Local State (Total)": "État local (Total)",
"Major Upgrade": "Mise à jour majeure",
"Maximum Age": "Ancienneté maximum",
"Metadata Only": "Métadonnées uniquement",
"Minimum Free Disk Space": "Minimum Free Disk Space",
"Minimum Free Disk Space": "Espace disque libre minimum",
"Move to top of queue": "Déplacer en haut de la file",
"Multi level wildcard (matches multiple directory levels)": "Astérisque à plusieurs niveaux (correspond aux répertoires et sous-répertoires)",
"Never": "Jamais",
@@ -121,7 +121,7 @@
"RAM Utilization": "Utilisation de la RAM",
"Random": "Aléatoire",
"Release Notes": "Notes de version",
"Remove": "Remove",
"Remove": "Enlever",
"Rescan": "Rescanner",
"Rescan All": "Réanalyser tout",
"Rescan Interval": "Intervalle de scan",
@@ -131,7 +131,7 @@
"Reused": "Réutilisé",
"Save": "Sauver",
"Scanning": "En cours de scan",
"Select the devices to share this folder with.": "Sélectionner les appareils avec qui partager ce répertoire.",
"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.",
"Settings": "Configuration",
"Share": "Partager",
@@ -143,7 +143,7 @@
"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",
"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 distante.",
"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",
"Simple File Versioning": "Suivi simple des versions de fichier",
@@ -162,12 +162,12 @@
"Syncthing is restarting.": "Syncthing est cours de redémarrage.",
"Syncthing is upgrading.": "Syncthing est cours de mise à jour.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing semble être éteint, ou il y a un problème avec votre connexion Internet. Nouvelle tentative ...",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing semble avoir un problème pour traiter votre demande. S'il vous plaît, rafraîchissez la page ou redémarrer Syncthing si le problème persiste.",
"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 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 \"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 répertoires et les versions de l'application. Si les données rapportées sont modifiées cette boite de dialogue vous redemandera votre confirmation.",
"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 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.",
@@ -178,22 +178,23 @@
"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.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Le temps maximum de conservation d'une version (en jours, mettre à 0 pour conserver les versions pour toujours)",
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).",
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "Le pourcentage d'espace disque libre doit être un nombre positif compris entre 0 et 100 (inclus).",
"The number of days must be a number and cannot be blank.": "Le nombre de jours doit être numérique et ne peut pas être vide.",
"The number of days to keep files in the trash can. Zero means forever.": "Le nombre de jours de conservation des fichiers dans la poubelle. Zéro signifie toujours.",
"The number of old versions to keep, per file.": "Le nombre d'anciennes versions à garder, par fichier.",
"The number of versions must be a number and cannot be blank.": "Le nombre de versions doit être numérique, et ne peut pas être vide.",
"The path cannot be blank.": "Le chemin ne peut pas être vide.",
"The rate limit must be a non-negative number (0: no limit)": "La limite de débit ne doit pas être négative (0: 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 is a major version upgrade.": "Ceci est une mise à jour majeure",
"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",
"Unshared": "Non partagé",
"Unused": "Non utilisé",
"Up to Date": "Synchronisé",
"Updated": "Mis à jour",
"Upgrade": "Mise à jour",
"Upgrade": "Mettre à jour",
"Upgrade To {%version%}": "Mettre à jour vers {{version}}",
"Upgrading": "Mise à jour de Syncthing",
"Upload Rate": "Débit d'envoi",
+1
View File
@@ -184,6 +184,7 @@
"The number of old versions to keep, per file.": "A megtartott régi verziók száma, fájlonként.",
"The number of versions must be a number and cannot be blank.": "A megtartott verziók száma nem lehet üres",
"The path cannot be blank.": "Elérési út nem lehet üres.",
"The rate limit must be a non-negative number (0: no limit)": "The rate limit must be a non-negative number (0: no limit)",
"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.": "They are retried automatically and will be synced when the error is resolved.",
"This is a major version upgrade.": "Ez egy főverzió frissítés.",
+1
View File
@@ -184,6 +184,7 @@
"The number of old versions to keep, per file.": "Il numero di vecchie versioni da mantenere, per file.",
"The number of versions must be a number and cannot be blank.": "Il numero di versioni dev'essere un numero e non può essere vuoto.",
"The path cannot be blank.": "Il percorso non può essere vuoto.",
"The rate limit must be a non-negative number (0: no limit)": "The rate limit must be a non-negative number (0: no limit)",
"The rescan interval must be a non-negative number of seconds.": "L'intervallo di scansione deve essere un numero superiore a zero secondi.",
"They are retried automatically and will be synced when the error is resolved.": "They are retried automatically and will be synced when the error is resolved.",
"This is a major version upgrade.": "Questo è un aggiornamento di versione principale",
+133 -132
View File
@@ -1,212 +1,213 @@
{
"A negative number of days doesn't make sense.": "負の日数は無理です。",
"A new major version may not be compatible with previous versions.": "新しいメジャーバージョンは以前のバージョンと互換性がないかもしれません",
"A negative number of days doesn't make sense.": "負の日数は指定できません。",
"A new major version may not be compatible with previous versions.": "新しいメジャーバージョンは以前のバージョンと互換性がないかもしれません",
"API Key": "APIキー",
"About": "Syncthingについて",
"Actions": "メニュー",
"Add": "追加",
"Add Device": "デバイス追加",
"Add Folder": "フォルダ追加",
"Add new folder?": "フォルダを新規作成しますか",
"Add Device": "デバイス追加",
"Add Folder": "フォルダーを追加",
"Add new folder?": "新しいフォルダーとして追加しますか?",
"Address": "アドレス",
"Addresses": "アドレス",
"Advanced": "高度",
"Advanced": "高度な設定",
"Advanced Configuration": "高度な設定",
"All Data": "全てのデータ",
"Allow Anonymous Usage Reporting?": "匿名で利用状況レポートを許可しますか",
"Alphabetic": "ABC順",
"An external command handles the versioning. It has to remove the file from the synced folder.": "バージョニングを行う外部コマンド同期フォルダからファイルを削除する必要があります。",
"Anonymous Usage Reporting": "匿名での利用状況レポート",
"Any devices configured on an introducer device will be added to this device as well.": "紹介デバイスで設定されたデバイスはここにも追加されます。",
"Automatic upgrades": "自動アップデート",
"Be careful!": "注意してください",
"Allow Anonymous Usage Reporting?": "匿名で利用状況レポートすることを許可しますか?",
"Alphabetic": "アルファベット順",
"An external command handles the versioning. It has to remove the file from the synced folder.": "外部コマンドがバージョン管理を処理します。指定したコマンドが、同期フォルダからファイルを削除する必要があります。",
"Anonymous Usage Reporting": "匿名での利用状況レポート",
"Any devices configured on an introducer device will be added to this device as well.": "紹介デバイスで設定されたデバイスは、このデバイス上でも追加されます。",
"Automatic upgrades": "自動アップグレード",
"Be careful!": "注意してください",
"Bugs": "バグ",
"CPU Utilization": "CPU使用率",
"Changelog": "更新履歴",
"Clean out after": "後で掃除",
"Clean out after": "以下の期間後に完全に削除する",
"Close": "閉じる",
"Command": "コマンド",
"Comment, when used at the start of a line": "行頭で使用されコメント",
"Comment, when used at the start of a line": "行頭で使用された場合、コメント",
"Compression": "圧縮",
"Connection Error": "接続エラー",
"Copied from elsewhere": "他の所からコピーしました",
"Copied from original": "オリジナルからコピーしました",
"Copyright © 2015 the following Contributors:": "Copyright © 2015 以下の協力者たちの皆さん:",
"Copied from elsewhere": "別ファイルからコピー",
"Copied from original": "元ファイルからコピー",
"Copyright © 2015 the following Contributors:": "Copyright © 2015 the following Contributors:",
"Delete": "削除",
"Deleted": "削除した",
"Deleted": "削除",
"Device ID": "デバイスID",
"Device Identification": "デバイスの身分証明書",
"Device Name": "デバイスの名前",
"Device {%device%} ({%address%}) wants to connect. Add new device?": "デバイス{{device}} ({{address}})が接続しますか ",
"Device Identification": "デバイス識別情報",
"Device Name": "デバイス",
"Device {%device%} ({%address%}) wants to connect. Add new device?": "デバイス {{device}} ({{address}}) が接続を求めています。新しいデバイスとして追加しますか?",
"Devices": "デバイス",
"Disconnected": "切断されました",
"Disconnected": "切断",
"Documentation": "マニュアル",
"Download Rate": "ダウンロード",
"Download Rate": "ダウンロード速度",
"Downloaded": "ダウンロード済",
"Downloading": "ダウンロード中",
"Edit": "編集",
"Edit Device": "デバイスの変更",
"Edit Folder": "フォルダーの変更",
"Edit Device": "デバイスの編集",
"Edit Folder": "フォルダーの編集",
"Editing": "編集中",
"Enable UPnP": "UPnPを許可する",
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "自動接続の場合は「dynamic」またはカンマ区切り「IPアドレス:ポート」を入力してください",
"Enter ignore patterns, one per line.": "無視パターンを入力してください。一列一条件。",
"Enable UPnP": "UPnPを有効にする",
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "「IPアドレス:ポート」をコンマで区切って入力してください。自動探索を行う場合は「dynamic」と入力してください",
"Enter ignore patterns, one per line.": "無視するファイル名のパターンを、一行につき一条件で入力してください。",
"Error": "エラー",
"External File Versioning": "外部ファイルバージョニング",
"Failed Items": "失敗したアイテム",
"File Pull Order": "ファイルの引き順番",
"File Versioning": "ファイルバージョニング",
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "更新時、ファイルパーミッションの設定が無視されます。FATファイルシステムでご利用ください。",
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Syncthingによって移動や削除が行われるとファイルは.stversionsフォルダに移されます。",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Syncthingによって移動や削除が行われるとファイルは.stversionsフォルダ内のタイムスタンプバージョンに移されます。",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "ファイルは他デバイスによる変更から保護されます。しかしこのデバイスでの変更は他のクラスタに送信されます。",
"External File Versioning": "外部バージョン管理",
"Failed Items": "失敗した項目",
"File Pull Order": "ファイルを取得する順番",
"File Versioning": "ファイルバージョン管理",
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "ファイルの変更を探すときにパーミッションを無視します。FATファイルシステムでご利用ください。",
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Syncthingがファイルを置き換えたり削除したりするとき、古い内容を .stversions フォルダに移動します。",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Syncthingがファイルを置き換えたり削除したりするとき、古い内容はファイル名にタイムスタンプを付けて .stversions フォルダーに移動します。",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "ファイルを他のデバイスによる変更から保護ます。一方、このデバイスでの変更は他のデバイスに送信されます。",
"Folder": "フォルダー",
"Folder ID": "フォルダID",
"Folder Master": "フォルダのマスター",
"Folder Path": "フォルダパス",
"Folders": "フォルダ",
"Folder ID": "フォルダID",
"Folder Master": "フォルダのマスター",
"Folder Path": "フォルダパス",
"Folders": "フォルダ",
"GUI": "GUI",
"GUI Authentication Password": "GUI 認証パスワード",
"GUI Authentication User": "GUI 認証ユーザー",
"GUI Listen Addresses": "GUIリスンアドレス",
"GUI Authentication Password": "GUI認証パスワード",
"GUI Authentication User": "GUI認証ユーザー",
"GUI Listen Addresses": "GUI待ち受けアドレスリスト",
"Generate": "生成",
"Global Discovery": "グローバルディスカバリー",
"Global Discovery Server": "グローバルディスカバリーサーバー",
"Global State": "グローバル状態",
"Help": "ヘルプ",
"Home page": "Home page",
"Home page": "ホームページ",
"Ignore": "無視",
"Ignore Patterns": "パターンを無視する",
"Ignore Permissions": "アクセス許可を無視する",
"Incoming Rate Limit (KiB/s)": "着信率制限(KiB/s)",
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "不正な設定はフォルダーコンテンツに損害を与え、Syncthingの表示を操作できなくなる可能性があります。",
"Introducer": "紹介デバイス",
"Inversion of the given condition (i.e. do not exclude)": "条件の裏(と言うのは省かないで)",
"Keep Versions": "バージョン保持",
"Ignore Patterns": "無視するファイル名",
"Ignore Permissions": "パーミッションを無視する",
"Incoming Rate Limit (KiB/s)": "下り帯域制限 (KiB/s)",
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "間違った設定を行うと、フォルダーの内容を壊したり、Syncthingを操作不能にしたりする可能性があります。",
"Introducer": "紹介デバイス",
"Inversion of the given condition (i.e. do not exclude)": "条件の否定 (つまり、無視しないという意味になります)",
"Keep Versions": "保存するバージョンの数",
"Largest First": "大きい順",
"Last File Received": "最後に受けとったファイル",
"Last seen": "最後に見た",
"Later": "後",
"Last File Received": "最後に受信したファイル",
"Last seen": "最終接続日時",
"Later": "後で設定",
"Local Discovery": "ローカルディスカバリー",
"Local State": "ローカル状態",
"Local State (Total)": "ローカル状態(総和)",
"Local State (Total)": "ローカル状態 (合計)",
"Major Upgrade": "メジャーアップグレード",
"Maximum Age": "",
"Metadata Only": "メタデータだけ",
"Minimum Free Disk Space": "Minimum Free Disk Space",
"Maximum Age": "最大寿命",
"Metadata Only": "メタデータのみ",
"Minimum Free Disk Space": "同期を停止する最小空きディスク容量",
"Move to top of queue": "最優先にする",
"Multi level wildcard (matches multiple directory levels)": "広範なワイルドカード(複数のディレクトリに適用されます)",
"Never": "決して",
"Multi level wildcard (matches multiple directory levels)": "多階層ワイルドカード (複数のディレクトリ階層にマッチします)",
"Never": "接続記録なし",
"New Device": "新規デバイス",
"New Folder": "新規フォルダ",
"New Folder": "新規フォルダ",
"Newest First": "新しい順",
"No": "いいえ",
"No File Versioning": "ファイルバージョニング不利用",
"No File Versioning": "バージョン管理をしない",
"Notice": "通知",
"OK": "OK",
"Off": "オフ",
"Oldest First": "古い順",
"Options": "オプション",
"Out of Sync": "同期",
"Out of Sync Items": "シンクアイテム外",
"Outgoing Rate Limit (KiB/s)": "発信率制限(KiB/s)",
"Override Changes": "変更をオーバーライドする",
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "ローカルコンピュータ上のフォルダパス。存在しない場合は作成されます。チルダ(~)をショートカットで利用することができます",
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "バージョンが保持されるパス(空欄の場合、デフォルトで.stversionsになります)",
"Please consult the release notes before performing a major upgrade.": "メジャーアップグレードをする前にリリースノートを参してください。",
"Out of Sync": "同期",
"Out of Sync Items": "同期の必要な項目",
"Outgoing Rate Limit (KiB/s)": "上り帯域制限 (KiB/s)",
"Override Changes": "他のデバイスの変更を上書きする",
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "ローカルコンピュータ上のフォルダパス。存在しない場合は作成されます。チルダ (~) で次のフォルダーを短縮入力できます:",
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "古いバージョンを保存するパス (空欄の場合、デフォルトで .stversions になります)",
"Please consult the release notes before performing a major upgrade.": "メジャーアップグレードを行う前にリリースノートを参してください。",
"Please wait": "お待ちください",
"Preview": "プレビュー",
"Preview Usage Report": "利用状況レポートのプレビュー",
"Quick guide to supported patterns": "サポートされているパターンの簡易ガイド",
"RAM Utilization": "メモリ利用率",
"RAM Utilization": "メモリ使用量",
"Random": "ランダム",
"Release Notes": "リリースノート",
"Remove": "Remove",
"Remove": "除去",
"Rescan": "再スキャン",
"Rescan All": "すべて再スキャン",
"Rescan Interval": "再スキャン間隔",
"Rescan Interval": "再スキャン間隔",
"Restart": "再起動",
"Restart Needed": "再起動が必要です",
"Restarting": "再起動中",
"Reused": "再使用されている",
"Reused": "中断後再利用",
"Save": "保存",
"Scanning": "スキャン中",
"Select the devices to share this folder with.": "このフォルダをシェアするデバイスを選んでください。",
"Select the folders to share with this device.": "このデバイスでシェアしたいフォルダを選んでください",
"Select the devices to share this folder with.": "このフォルダーを共有するデバイスを選択してください。",
"Select the folders to share with this device.": "このデバイスと共有するフォルダを選択してください",
"Settings": "設定",
"Share": "共有",
"Share Folder": "フォルダを共有する",
"Share Folders With Device": "デバイスでフォルダをシェアする",
"Share With Devices": "デバイスでシェアする",
"Share this folder?": "このフォルダを共有しますか",
"Shared With": "シェアされている",
"Short identifier for the folder. Must be the same on all cluster devices.": "このフォルダの短いID。全てのデバイス上で同じである必要があります。",
"Share Folder": "フォルダを共有する",
"Share Folders With Device": "このデバイスと共有するフォルダー",
"Share With Devices": "共有対象のデバイス",
"Share this folder?": "このフォルダを共有しますか?",
"Shared With": "共有相手",
"Short identifier for the folder. Must be the same on all cluster devices.": "このフォルダの短い識別子。このフォルダーを共有する全てのデバイス上で同じである必要があります。",
"Show ID": "IDを表示",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "クラスタステータスでデバイスIDの代わりに表示されます。他のデバイス上でもこれがデフォルトとして表示されます。",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "クラスタステータスでデバイスIDの代わりに表示されます。空欄の場合デバイスが要請する名前に更新されます。",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "クラスタステータスでデバイスIDの代わりに表示されます。他のデバイスに対してもデフォルトの名前として通知されます。",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "クラスタステータスでデバイスIDの代わりに表示されます。空欄の場合デバイスが名乗る名前に更新されます。",
"Shutdown": "シャットダウン",
"Shutdown Complete": "シャットダウン完了",
"Simple File Versioning": "簡易ファイルバージョニング",
"Single level wildcard (matches within a directory only)": "ワイルドカード(一つのディレクトリだけに適用されます)",
"Simple File Versioning": "単純バージョン管理",
"Single level wildcard (matches within a directory only)": "ワイルドカード (単一のディレクトリだけでマッチします)",
"Smallest First": "小さい順",
"Source Code": "ソースコード",
"Staggered File Versioning": "簡易ファイルバージョニング",
"Start Browser": "ブラウザーを起動する",
"Statistics": "Statistics",
"Stopped": "止り",
"Staggered File Versioning": "期間別バージョン管理",
"Start Browser": "起動時にウェブブラウザーで状態を表示する",
"Statistics": "統計情報",
"Stopped": "停止中",
"Support": "サポート",
"Sync Protocol Listen Addresses": "同期プロトコル待ち受けアドレス",
"Sync Protocol Listen Addresses": "Syncプロトコル待ち受けアドレスリスト",
"Syncing": "同期中",
"Syncthing has been shut down.": "Syncthingがシャットダウンしました。",
"Syncthing includes the following software or portions thereof:": "Syncthingは以下のソフトウェアかその一部を内包しています:",
"Syncthing is restarting.": "Syncthingが再起動しています",
"Syncthing is upgrading.": "Syncthingがアップグレード中です",
"Syncthing is restarting.": "Syncthingが再起動しています",
"Syncthing is upgrading.": "Syncthingがアップグレード中です",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthingが落ちているか、インターネット接続に問題があります。リトライ中です…",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "リクエストの処理に問題があるようです。問題が継続する場合、ページを更新するかSyncthingを再起動してください。",
"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.": "デバイスIDは空欄にできません",
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "ここ入力したデバイスIDは他デバイス上の\"編集 > IDを表示\"で見ることができます。スペースとハイフンは無視されます。",
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "暗号化された使用状況レポートが\b日ごとに送られます。こはプラットフォーム、フォルダの大きさ、アプリのバージョンを追跡するために利用されます。レポートのデータが変更された場合、このダイアログがまた表示されます。",
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "入力されたデバイスIDが正しくありません。52から56文字のアルファベットと数字かスペースハイフンの列である必要があります。",
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "第コマンドパラメータはフォルダパス、第二パラメータはフォルダ内の相対パスです。",
"The folder ID cannot be blank.": "フォルダIDは空欄にできません",
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "フォルダID(64文字以内)は数字、ドット(.)、ハイフン(-)、アンダースコア(_)で構成されている必要があります。",
"The folder ID must be unique.": "フォルダIDは固有である必要があります。",
"The folder path cannot be blank.": "フォルダーパスは空欄にできません",
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "以下の間隔が使われます: 最初の一時間はバージョンは30秒ごとに保持、最初の一日は一時間ごとに、最初の30日は一日ごとに、最大寿命までは週間ごとに。",
"The following items could not be synchronized.": "以下のアイテムは同期できませんでした。",
"The maximum age must be a number and cannot be blank.": "最大日数は番号である必要があり、空欄ではいけません。",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "バージョンを保する最大日数(0にすると永的に保します)",
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).",
"The number of days must be a number and cannot be blank.": "日数は番号である必要があり、空欄ではいけません。",
"The number of days to keep files in the trash can. Zero means forever.": "ゴミ箱にファイルを保する日数。0だと永的に保します。",
"The number of old versions to keep, per file.": "ファイルごとの保持する古いバージョンの数",
"The number of versions must be a number and cannot be blank.": "バージョンの数は番号である必要があり、空欄ではいけません。",
"The path cannot be blank.": "パスは空欄にできません",
"The rescan interval must be a non-negative number of seconds.": "リスキャン間隔はマイナス秒ではいけません。",
"They are retried automatically and will be synced when the error is resolved.": "エラーが解決した時それらは自動的にリトライし同期を試みます。",
"This is a major version upgrade.": "メージャーアップグレードです。",
"Trash Can File Versioning": "ゴミ箱のファイルバージョニング",
"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.": "デバイスIDは空欄にできません",
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "ここ入力するデバイスIDは、接続したいデバイスの \"メニュー > IDを表示\" で確認することができます。スペースとハイフンは入力しなくてもかまいません。",
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "利用状況レポートは暗号化されて毎日送信されます。この情報はプラットフォーム、フォルダの大きさ、アプリのバージョンを調査するために使われます。レポートのデータが変更された場合、このダイアログがまた表示されます。",
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "入力されたデバイスIDが正しくありません。デバイスIDは、52文字または56文字のアルファベットと数字からなる文字列です。スペースハイフンは入力してもしなくてもかまいません。",
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "第1コマンドライン引数はフォルダーのパス、第2引数はフォルダ内の相対パスです。",
"The folder ID cannot be blank.": "フォルダIDは空欄にできません",
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "フォルダID64文字以内で、アルファベット、数字、ドット (.)、ハイフン (-)、アンダースコア (_) のみを使うことができます。",
"The folder ID must be unique.": "フォルダIDが重複しています。",
"The folder path cannot be blank.": "フォルダーパスは空欄にできません",
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "保存間隔は次の通りです。最近1時間は30秒ごとに古いバージョンを保存します。同様に、最近1日間は1時間ごとに、最30日間は1日ごとに、その後最大寿命までは1週間ごとに、古いバージョンを保存します。",
"The following items could not be synchronized.": "以下の項目は同期できませんでした。",
"The maximum age must be a number and cannot be blank.": "最大寿命は数値を指定してください。空欄にはできません。",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "古いバージョンを保する最大日数 (0にすると永的に保します)",
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "最小空きディスク容量はパーセントで、0から100の値を入力してください。",
"The number of days must be a number and cannot be blank.": "日数は数値を指定してください。空欄にはできません。",
"The number of days to keep files in the trash can. Zero means forever.": "ゴミ箱にファイルを保する日数。0だと永的に保します。",
"The number of old versions to keep, per file.": "ファイルごと古いバージョンをいくつ保存するかを指定します。",
"The number of versions must be a number and cannot be blank.": "保存するバージョンの数は数値を指定してください。空欄にはできません。",
"The path cannot be blank.": "パスは空欄にできません",
"The rate limit must be a non-negative number (0: no limit)": "帯域制限の値として負の数は指定できません (0にすると制限を行いません)。",
"The rescan interval must be a non-negative number of seconds.": "再スキャン間隔として負の数は指定できません。",
"They are retried automatically and will be synced when the error is resolved.": "エラーが解決すると、自動的に再試行され同期されます。",
"This is a major version upgrade.": "メジャーアップグレードです。",
"Trash Can File Versioning": "ゴミ箱によるバージョン管理",
"Unknown": "不明",
"Unshared": "シェアされていない",
"Unused": "使われていない",
"Unshared": "非共有",
"Unused": "未使用",
"Up to Date": "最新",
"Updated": "更新済み",
"Updated": "更新",
"Upgrade": "アップグレード",
"Upgrade To {%version%}": "{{version}}にアップグレードする",
"Upgrade To {%version%}": "{{version}} にアップグレードする",
"Upgrading": "アップグレード中",
"Upload Rate": "アップロード",
"Upload Rate": "アップロード速度",
"Uptime": "稼働時間",
"Use HTTPS for GUI": "GUIにHTTPSを使",
"Use HTTPS for GUI": "GUIにHTTPSを使用する",
"Version": "バージョン",
"Versions Path": "バージョンパス",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "バージョンは、最大寿命もしくは最大同時数を超えた場合、自動的に削除されます。",
"When adding a new device, keep in mind that this device must be added on the other side too.": "新しいデバイスを加える際、そのデバイスにもこのデバイスを加える必要があること留意してください。",
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "新しいフォルダを追加する際、フォルダIDはケースセンシティブで全てのデバイスで完全に同じである必要があります。",
"Versions Path": "古いバージョンを保存するパス",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "古いバージョンは、最大寿命もしくは期間ごとの最大保存数を超えた場合、自動的に削除されます。",
"When adding a new device, keep in mind that this device must be added on the other side too.": "新しいデバイスを追加する際、そのデバイスにもこのデバイスを追加する必要があること留意してください。",
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "新しいフォルダを追加する際、フォルダIDはデバイス間でフォルダーの対応づけに使われることに注意してください。フォルダーIDは大文字と小文字が区別され、共有するすべてのデバイスの間で完全に一致しなくてはなりません。",
"Yes": "はい",
"You must keep at least one version.": "バージョン一つ少なくとも保持してください",
"full documentation": "完全マニュアル",
"items": "アイテム",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}}がフォルダ\"{{folder}}\"をシェアしがたっています。"
"You must keep at least one version.": "少なくとも一つのバージョンを保存してください",
"full documentation": "詳細なマニュアル",
"items": "項目",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} がフォルダ\"{{folder}}\" を共有するよう求めています。"
}
+1
View File
@@ -184,6 +184,7 @@
"The number of old versions to keep, per file.": "각 파일별로 유지할 이전 버전의 개수를 지정합니다.",
"The number of versions must be a number and cannot be blank.": "버전 개수는 숫자여야 하며 비워 둘 수 없습니다.",
"The path cannot be blank.": "경로는 비워 둘 수 없습니다.",
"The rate limit must be a non-negative number (0: no limit)": "The rate limit must be a non-negative number (0: no limit)",
"The rescan interval must be a non-negative number of seconds.": "재검색 간격은 초단위이며 양수로 입력해야 합니다.",
"They are retried automatically and will be synced when the error is resolved.": "They are retried automatically and will be synced when the error is resolved.",
"This is a major version upgrade.": "이 업데이트는 메이저 버전입니다.",
+7 -6
View File
@@ -19,7 +19,7 @@
"Anonymous Usage Reporting": "Anoniminė vartojimo ataskaita",
"Any devices configured on an introducer device will be added to this device as well.": "Visi supažindintojo įrenginiai bus pridėti prie jūsų įrenginių sąrašo.",
"Automatic upgrades": "Automatiniai atnaujinimai",
"Be careful!": "Be careful!",
"Be careful!": "Būkite atsargūs!",
"Bugs": "Klaidos",
"CPU Utilization": "Procesoriaus panaudojimas",
"Changelog": "Pasikeitimai",
@@ -93,7 +93,7 @@
"Major Upgrade": "Stambus atnaujinimas",
"Maximum Age": "Maksimalus amžius",
"Metadata Only": "Metaduomenims",
"Minimum Free Disk Space": "Minimum Free Disk Space",
"Minimum Free Disk Space": "Minimum laisvos vietos diske",
"Move to top of queue": "Perkelti į eilės priekį",
"Multi level wildcard (matches multiple directory levels)": "Keletos lygių pakaitos (atitinka keletą direktorijų lygių)",
"Never": "Niekada",
@@ -106,8 +106,8 @@
"OK": "Gerai",
"Off": "Netaikoma",
"Oldest First": "Seniausi pirmiau",
"Options": "Options",
"Out of Sync": "Out of Sync",
"Options": "Nustatymai",
"Out of Sync": "Išsisinchronizavę",
"Out of Sync Items": "Nesutikrinta",
"Outgoing Rate Limit (KiB/s)": "Išeinančio srauto maksimalus greitis (KiB/s)",
"Override Changes": "Perrašyti pakeitimus",
@@ -121,7 +121,7 @@
"RAM Utilization": "Atminties naudojimas",
"Random": "Atsitiktinė",
"Release Notes": "Laidos Informacija",
"Remove": "Remove",
"Remove": "Pašalinti",
"Rescan": "Nuskaityti iš naujo",
"Rescan All": "Nuskaityti visus aplankus",
"Rescan Interval": "Pertrauka tarp nuskaitymų",
@@ -152,7 +152,7 @@
"Source Code": "Išeities kodas",
"Staggered File Versioning": "Pakopinis versijų valdymas",
"Start Browser": "Paleisti naršyklę",
"Statistics": "Statistics",
"Statistics": "Statistika",
"Stopped": "Sustabdyta",
"Support": "Pagalba",
"Sync Protocol Listen Addresses": "Sutapatinimo taisyklių adresas",
@@ -184,6 +184,7 @@
"The number of old versions to keep, per file.": "Kiek failo versijų saugoti.",
"The number of versions must be a number and cannot be blank.": "Versijų skaičius turi būti skaitmuo ir negali būti tuščias laukelis.",
"The path cannot be blank.": "Kelias negali būti tuščias.",
"The rate limit must be a non-negative number (0: no limit)": "The rate limit must be a non-negative number (0: no limit)",
"The rescan interval must be a non-negative number of seconds.": "Nuskaitymo dažnis negali būti neigiamas skaičius.",
"They are retried automatically and will be synced when the error is resolved.": "They are retried automatically and will be synced when the error is resolved.",
"This is a major version upgrade.": "Tai yra stambus atnaujinimas.",
+7 -6
View File
@@ -53,7 +53,7 @@
"Enter ignore patterns, one per line.": "Skriv inn mønster som skal utelates, ett per linje.",
"Error": "Feilmelding",
"External File Versioning": "Ekstern versjonskontroll",
"Failed Items": "Failed Items",
"Failed Items": "Elementsynkronisering som har feilet",
"File Pull Order": "Filenes Henterekkefølge",
"File Versioning": "Versjonskontroll",
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Fil bit-rettigheter ignoreres når forandringer oppdages. Bruk FAT filsystem. ",
@@ -65,7 +65,7 @@
"Folder Master": "Styrende Mappe",
"Folder Path": "Mappeplassering",
"Folders": "Mapper",
"GUI": "GUI",
"GUI": "grafisk brukergrensesnitt",
"GUI Authentication Password": "GUI Passord",
"GUI Authentication User": "GUI Bruker",
"GUI Listen Addresses": "GUI Lytteadresse",
@@ -93,7 +93,7 @@
"Major Upgrade": "Hovedoppgradering",
"Maximum Age": "Maksimal Levetid",
"Metadata Only": "Kun metadata",
"Minimum Free Disk Space": "Minimum Free Disk Space",
"Minimum Free Disk Space": "Nødvendig ledig diskplass",
"Move to top of queue": "Flytt til topp av kø",
"Multi level wildcard (matches multiple directory levels)": "Multinivåsøk (søker på flere mappenivå)",
"Never": "Aldri",
@@ -121,7 +121,7 @@
"RAM Utilization": "RAM-utnyttelse",
"Random": "TIlfeldig",
"Release Notes": "Utgivelsesnotat",
"Remove": "Remove",
"Remove": "Fjern",
"Rescan": "Skann på nytt",
"Rescan All": "Skann alt på nytt",
"Rescan Interval": "Skanneintervall",
@@ -178,14 +178,15 @@
"The following items could not be synchronized.": "Følgende filer kunne ikke synkroniseres.",
"The maximum age must be a number and cannot be blank.": "Maksimal levetid må være et tall og kan ikke være tomt.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Maksimal tid å beholde en versjon (i dager, sett til 0 for å beholde versjoner på ubegrenset tid).",
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).",
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "Nødvendig ledig diskplass må være et tall mellom 0 og 100.",
"The number of days must be a number and cannot be blank.": "Antall dager må være et tall og kan ikke være tomt.",
"The number of days to keep files in the trash can. Zero means forever.": "Antall dager man skal bevare filene i papirkurven. Null betyr for alltid.",
"The number of old versions to keep, per file.": "Antall gamle versjoner å beholde, per fil.",
"The number of versions must be a number and cannot be blank.": "Antall versjoner må være et tall og kan ikke være tomt.",
"The path cannot be blank.": "Plasseringen kan ikke være tom.",
"The rate limit must be a non-negative number (0: no limit)": "Hastighetsbegrensningen kan ikke være et negativt tall (0: ingen begrensing)",
"The rescan interval must be a non-negative number of seconds.": "Antall sekund i skanneintervallet kan ikke være negativt.",
"They are retried automatically and will be synced when the error is resolved.": "They are retried automatically and will be synced when the error is resolved.",
"They are retried automatically and will be synced when the error is resolved.": "Disse hentes automatisk og vil synkroniseres når feilen er blitt utbedret.",
"This is a major version upgrade.": "Dette er en hovedoppgradering",
"Trash Can File Versioning": "Fil versjonskontroll i papirkurven",
"Unknown": "Ukjent",
+4 -3
View File
@@ -93,7 +93,7 @@
"Major Upgrade": "Grote update",
"Maximum Age": "Maximum leeftijd",
"Metadata Only": "Alleen metadata",
"Minimum Free Disk Space": "Minimum Free Disk Space",
"Minimum Free Disk Space": "Minimale vrije schijfruimte",
"Move to top of queue": "Verplaats naar het begin van de wachtrij",
"Multi level wildcard (matches multiple directory levels)": "Wildcard op meerdere niveaus (toepasbaar op meerdere mapniveaus)",
"Never": "Nooit",
@@ -121,7 +121,7 @@
"RAM Utilization": "Geheugengebruik",
"Random": "Willekeurig",
"Release Notes": "Release notes",
"Remove": "Remove",
"Remove": "Verwijderen",
"Rescan": "Opnieuw scannen",
"Rescan All": "Scan alles opnieuw",
"Rescan Interval": "Scanfrequentie",
@@ -178,12 +178,13 @@
"The following items could not be synchronized.": "De volgende bestanden konden niet worden gesynchroniseerd.",
"The maximum age must be a number and cannot be blank.": "De maximum leeftijd moet uit cijfers bestaan en mag niet leeggelaten worden.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "De maximale tijdsduur om een versie te bewaren (in dagen, gebruik 0 om versies voor altijd te bewaren).",
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).",
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "Het percentage minimale vrije schijfruimte dient een positief nummer tussen 0 en 100 (inclusief) te zijn.",
"The number of days must be a number and cannot be blank.": "Het aantal dagen moet een getal zijn en kan niet leeg gelaten worden.",
"The number of days to keep files in the trash can. Zero means forever.": "Het aantal dagen dat bestanden in de prullenbak blijven. Type 0 voor oneindig.",
"The number of old versions to keep, per file.": "Het aantal versies dat bewaard moet worden per file.",
"The number of versions must be a number and cannot be blank.": "Het aantal nummers moet een getal zijn en mag niet leeg blijven.",
"The path cannot be blank.": "Het bestandspad mag niet leeg zijn.",
"The rate limit must be a non-negative number (0: no limit)": "De snelheidslimiet moet een positief nummer zijn (0: geen limiet)",
"The rescan interval must be a non-negative number of seconds.": "De scanfrequentie moet een positief getal in seconden zijn.",
"They are retried automatically and will be synced when the error is resolved.": "Het wordt automatisch opnieuw geprobeerd. Bestanden worden gesynchroniseerd als de fout is hersteld.",
"This is a major version upgrade.": "Dit is een grote update.",
+1
View File
@@ -184,6 +184,7 @@
"The number of old versions to keep, per file.": "Tal på gamle versjonar ein skal behalda, per fil.",
"The number of versions must be a number and cannot be blank.": "Tal på versjonar må vera eit tal og kan ikkje vera tomt.",
"The path cannot be blank.": "Bana kan ikkje vera tom.",
"The rate limit must be a non-negative number (0: no limit)": "The rate limit must be a non-negative number (0: no limit)",
"The rescan interval must be a non-negative number of seconds.": "Talet på sekund i skanneintervallet kan ikkje vera negativt.",
"They are retried automatically and will be synced when the error is resolved.": "They are retried automatically and will be synced when the error is resolved.",
"This is a major version upgrade.": "This is a major version upgrade.",
+1
View File
@@ -184,6 +184,7 @@
"The number of old versions to keep, per file.": "Liczba wersji pliku do zachowania.",
"The number of versions must be a number and cannot be blank.": "Liczba wersji musi być liczbą i nie może być pusta.",
"The path cannot be blank.": "Ścieżka nie może być pusta.",
"The rate limit must be a non-negative number (0: no limit)": "The rate limit must be a non-negative number (0: no limit)",
"The rescan interval must be a non-negative number of seconds.": "Interwał skanowania musi być niezerową liczbą sekund.",
"They are retried automatically and will be synced when the error is resolved.": "Ponowne próby zachodzą automatycznie, synchronizacja nastąpi po usunięciu usterki.",
"This is a major version upgrade.": "To jest ważna aktualizacja",
+4 -3
View File
@@ -93,7 +93,7 @@
"Major Upgrade": "Atualização \"major\"",
"Maximum Age": "Idade máxima",
"Metadata Only": "Somente metadados",
"Minimum Free Disk Space": "Minimum Free Disk Space",
"Minimum Free Disk Space": "Espaço livre mínimo no disco",
"Move to top of queue": "Mover para o topo da lista",
"Multi level wildcard (matches multiple directory levels)": "Coringa multi-nível (faz corresponder a vários níveis de pastas)",
"Never": "Nunca",
@@ -121,7 +121,7 @@
"RAM Utilization": "Uso de RAM",
"Random": "Aleatória",
"Release Notes": "Notas de lançamento",
"Remove": "Remove",
"Remove": "Remover",
"Rescan": "Verificar agora",
"Rescan All": "Verificar todas",
"Rescan Interval": "Intervalo entre verificações",
@@ -178,12 +178,13 @@
"The following items could not be synchronized.": "Os itens a seguir não puderam ser sincronizados.",
"The maximum age must be a number and cannot be blank.": "A idade máxima deve ser um valor numérico. O campo não pode ficar vazio.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "O número máximo de dias em que uma versão é guardada. (Use 0 para manter para sempre).",
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).",
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "A porcentagem de espaço livre mínimo no disco deve ser um número positivo entre 0 e 100 (inclusive).",
"The number of days must be a number and cannot be blank.": "O número de dias deve ser um número valido e não pode ficar em branco.",
"The number of days to keep files in the trash can. Zero means forever.": "O número de dias em que são mantidos os arquivos da lixeira. Zero significa para sempre.",
"The number of old versions to keep, per file.": "O número de versões antigas a serem mantidas, por arquivo.",
"The number of versions must be a number and cannot be blank.": "O número de versões deve ser um valor numérico. O campo não pode ficar vazio.",
"The path cannot be blank.": "O caminho não pode ficar vazio.",
"The rate limit must be a non-negative number (0: no limit)": "O limite de velocidade deve ser um número positivo (0: sem limite)",
"The rescan interval must be a non-negative number of seconds.": "O intervalo entre verificações deve ser um número positivo de segundos.",
"They are retried automatically and will be synced when the error is resolved.": "Serão tentadas automaticamente e sincronizadas após o erro ter sido resolvido.",
"This is a major version upgrade.": "Esta é uma atualização para uma versão \"major\".",
+4 -3
View File
@@ -93,7 +93,7 @@
"Major Upgrade": "Actualização importante",
"Maximum Age": "Idade máxima",
"Metadata Only": "Metadados apenas",
"Minimum Free Disk Space": "Minimum Free Disk Space",
"Minimum Free Disk Space": "Espaço livre mínimo no disco",
"Move to top of queue": "Mover para o topo da fila",
"Multi level wildcard (matches multiple directory levels)": "Caractere polivalente multi-nível (faz corresponder a vários níveis de pastas)",
"Never": "Nunca",
@@ -121,7 +121,7 @@
"RAM Utilization": "Utilização da RAM",
"Random": "Aleatória",
"Release Notes": "Notas de lançamento",
"Remove": "Remove",
"Remove": "Remover",
"Rescan": "Verificar agora",
"Rescan All": "Verificar todas agora",
"Rescan Interval": "Intervalo entre verificações",
@@ -178,12 +178,13 @@
"The following items could not be synchronized.": "Não foi possível sincronizar os elementos seguintes.",
"The maximum age must be a number and cannot be blank.": "A idade máxima tem que ser um número e não pode estar vazia.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Tempo máximo, em dias, para manter uma versão (use 0 para manter a versão para sempre).",
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).",
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "A percentagem de espaço livre mínimo no disco tem que ser um número não negativo entre 0 e 100 (inclusive).",
"The number of days must be a number and cannot be blank.": "O número de dias tem que ser um número e não pode estar em branco.",
"The number of days to keep files in the trash can. Zero means forever.": "O número de dias a manter os ficheiros na reciclagem. Zero significa para sempre.",
"The number of old versions to keep, per file.": "O número de versões antigas a manter, por ficheiro.",
"The number of versions must be a number and cannot be blank.": "O número de versões tem que ser um número e não pode estar vazio.",
"The path cannot be blank.": "O caminho não pode estar vazio.",
"The rate limit must be a non-negative number (0: no limit)": "O limite de velocidade tem que ser um número que não seja negativo (0: sem limite)",
"The rescan interval must be a non-negative number of seconds.": "O intervalo entre verificações tem que ser um valor não negativo de segundos.",
"They are retried automatically and will be synced when the error is resolved.": "Será tentado automaticamente e os itens serão sincronizados assim que o erro seja resolvido.",
"This is a major version upgrade.": "Esta é uma actualização para uma versão importante.",
+1
View File
@@ -184,6 +184,7 @@
"The number of old versions to keep, per file.": "Numărul de versiuni vechi de salvat per fişier.",
"The number of versions must be a number and cannot be blank.": "Numărul de versiuni trebuie să fie un număr şi nu poate fi gol.",
"The path cannot be blank.": "Locația nu poate fi goală.",
"The rate limit must be a non-negative number (0: no limit)": "The rate limit must be a non-negative number (0: no limit)",
"The rescan interval must be a non-negative number of seconds.": "Intervalul de rescanare trebuie să nu fie un număr negativ de secunde. ",
"They are retried automatically and will be synced when the error is resolved.": "They are retried automatically and will be synced when the error is resolved.",
"This is a major version upgrade.": "This is a major version upgrade.",
+4 -3
View File
@@ -93,7 +93,7 @@
"Major Upgrade": "Обновление основной версии",
"Maximum Age": "Максимальный срок",
"Metadata Only": "Только метаданные",
"Minimum Free Disk Space": "Minimum Free Disk Space",
"Minimum Free Disk Space": "Минимальное свободное место на диске",
"Move to top of queue": "Поместить в начало очереди",
"Multi level wildcard (matches multiple directory levels)": "Многоуровневая маска (поиск совпадений во всех подпапках)",
"Never": "Никогда",
@@ -121,7 +121,7 @@
"RAM Utilization": "Использование ОЗУ",
"Random": "Случайно",
"Release Notes": "Замечания к версии",
"Remove": "Remove",
"Remove": "Удалить",
"Rescan": "Пересканирование",
"Rescan All": "Пересканировать все",
"Rescan Interval": "Интервал пересканирования",
@@ -178,12 +178,13 @@
"The following items could not be synchronized.": "Невозможно синхронизировать следующие объекты",
"The maximum age must be a number and cannot be blank.": "Максимальный срок должен быть числом и не может быть пустым.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Максимальный срок хранения версии (в днях, 0 значит вечное хранение).",
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).",
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "Минимальное свободное место на диске должно быть в процентах между 0 и 100 включительно.",
"The number of days must be a number and cannot be blank.": "Количество дней должно быть числом и не может быть пустым.",
"The number of days to keep files in the trash can. Zero means forever.": "Количество дней хранения файлов в корзине. Ноль значит навсегда.",
"The number of old versions to keep, per file.": "Количество хранимых версий файла.",
"The number of versions must be a number and cannot be blank.": "Количество версий должно быть числом и не может быть пустым.",
"The path cannot be blank.": "Путь не может быть пустым.",
"The rate limit must be a non-negative number (0: no limit)": "The rate limit must be a non-negative number (0: no limit)",
"The rescan interval must be a non-negative number of seconds.": "Интервал пересканирования должен быть неотрицательным количеством секунд.",
"They are retried automatically and will be synced when the error is resolved.": "Будут синхронизированы автоматически когда ошибка будет исправлена.",
"This is a major version upgrade.": "Это обновление основной версии продукта.",
+1
View File
@@ -184,6 +184,7 @@
"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 rate limit must be a non-negative number (0: no limit)": "The rate limit must be a non-negative number (0: no limit)",
"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.": "They are retried automatically and will be synced when the error is resolved.",
"This is a major version upgrade.": "Det här är en stor uppgradering.",
+1
View File
@@ -184,6 +184,7 @@
"The number of old versions to keep, per file.": "Dosya başına saklanacak eski sürüm.",
"The number of versions must be a number and cannot be blank.": "Sürümlerin sayısı sayı olmalı ve boş bırakılamaz.",
"The path cannot be blank.": "Dizin yolu boş bırakılamaz.",
"The rate limit must be a non-negative number (0: no limit)": "The rate limit must be a non-negative number (0: no limit)",
"The rescan interval must be a non-negative number of seconds.": "Tarama zaman aralığı, saniye cinsinden negatif olmayan bir sayı olmalıdır.",
"They are retried automatically and will be synced when the error is resolved.": "They are retried automatically and will be synced when the error is resolved.",
"This is a major version upgrade.": "This is a major version upgrade.",
+1
View File
@@ -184,6 +184,7 @@
"The number of old versions to keep, per file.": "Кількість старих версій, яку необхідно зберігати для кожного файлу.",
"The number of versions must be a number and cannot be blank.": "Кількість версій повинна бути цифрою та не може бути порожньою.",
"The path cannot be blank.": "Шлях не може бути порожнім.",
"The rate limit must be a non-negative number (0: no limit)": "The rate limit must be a non-negative number (0: no limit)",
"The rescan interval must be a non-negative number of seconds.": "Інтервал повторного сканування повинен бути неід’ємною величиною.",
"They are retried automatically and will be synced when the error is resolved.": "Вони будуть автоматично повторно синхронізовані, коли помилку буде усунено. ",
"This is a major version upgrade.": "Це оновлення мажорної версії",
+4 -3
View File
@@ -93,7 +93,7 @@
"Major Upgrade": "重大更新",
"Maximum Age": "历史版本最长保留时间",
"Metadata Only": "仅元数据",
"Minimum Free Disk Space": "Minimum Free Disk Space",
"Minimum Free Disk Space": "最低空闲磁盘空间",
"Move to top of queue": "移动到队列顶端",
"Multi level wildcard (matches multiple directory levels)": "多级通配符(用以匹配多层文件夹)",
"Never": "从未",
@@ -121,7 +121,7 @@
"RAM Utilization": "内存使用量",
"Random": "随机顺序",
"Release Notes": "发布说明",
"Remove": "Remove",
"Remove": "移除",
"Rescan": "重新扫描",
"Rescan All": "全部重新扫描",
"Rescan Interval": "扫描间隔",
@@ -178,12 +178,13 @@
"The following items could not be synchronized.": "下列项目无法被同步。",
"The maximum age must be a number and cannot be blank.": "最长保留时间必须为数字,且不能为空。",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "历史版本保留的最长天数,0为永久保存",
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).",
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "最低空间磁盘空间的数值必须介于 0-100 的正整数",
"The number of days must be a number and cannot be blank.": "天数必须为数字,且不能为空。",
"The number of days to keep files in the trash can. Zero means forever.": "文件保存在回收站的天数。零表示永久。",
"The number of old versions to keep, per file.": "每个文件保留的版本数量上限。",
"The number of versions must be a number and cannot be blank.": "保留版本数量必须为数字,且不能为空。",
"The path cannot be blank.": "路径不能为空",
"The rate limit must be a non-negative number (0: no limit)": "传输速度限制为非负整数(0 表示不限制)",
"The rescan interval must be a non-negative number of seconds.": "扫描间隔单位为秒,且不能为负数。",
"They are retried automatically and will be synced when the error is resolved.": "系统将会自动重试,当错误被解决时,它们将会被同步。",
"This is a major version upgrade.": "这是一个重大版本更新。",
+1
View File
@@ -184,6 +184,7 @@
"The number of old versions to keep, per file.": "每個檔案要保留的舊版本數量。",
"The number of versions must be a number and cannot be blank.": "每個檔案要保留的舊版本數量必須是數字且不能為空白。",
"The path cannot be blank.": "路徑不能空白。",
"The rate limit must be a non-negative number (0: no limit)": "The rate limit must be a non-negative number (0: no limit)",
"The rescan interval must be a non-negative number of seconds.": "重新掃描間隔必須為一個非負數的秒數。",
"They are retried automatically and will be synced when the error is resolved.": "They are retried automatically and will be synced when the error is resolved.",
"This is a major version upgrade.": "這是一個主要版本更新。",
+1 -1
View File
@@ -1 +1 @@
var langPrettyprint = {"bg":"Bulgarian","ca":"Catalan","ca@valencia":"Catalan (Valencian)","cs":"Czech","de":"German","el":"Greek","en":"English","en-GB":"English (United Kingdom)","es":"Spanish","es-ES":"Spanish (Spain)","fi":"Finnish","fr":"French","hu":"Hungarian","it":"Italian","ja":"Japanese","ko-KR":"Korean (Korea)","lt":"Lithuanian","nb":"Norwegian Bokmål","nl":"Dutch","nn":"Norwegian Nynorsk","pl":"Polish","pt-BR":"Portuguese (Brazil)","pt-PT":"Portuguese (Portugal)","ro-RO":"Romanian (Romania)","ru":"Russian","sv":"Swedish","tr":"Turkish","uk":"Ukrainian","zh-CN":"Chinese (China)","zh-TW":"Chinese (Taiwan)"}
var langPrettyprint = {"bg":"Bulgarian","ca":"Catalan","ca@valencia":"Catalan (Valencian)","cs":"Czech","de":"German","el":"Greek","en":"English","en-GB":"English (United Kingdom)","es":"Spanish","es-ES":"Spanish (Spain)","fi":"Finnish","fr":"French","fr-CA":"French (Canada)","hu":"Hungarian","it":"Italian","ja":"Japanese","ko-KR":"Korean (Korea)","lt":"Lithuanian","nb":"Norwegian Bokmål","nl":"Dutch","nn":"Norwegian Nynorsk","pl":"Polish","pt-BR":"Portuguese (Brazil)","pt-PT":"Portuguese (Portugal)","ro-RO":"Romanian (Romania)","ru":"Russian","sv":"Swedish","tr":"Turkish","uk":"Ukrainian","zh-CN":"Chinese (China)","zh-TW":"Chinese (Taiwan)"}
+1 -1
View File
@@ -1 +1 @@
var validLangs = ["bg","ca","ca@valencia","cs","de","el","en","en-GB","es","es-ES","fi","fr","hu","it","ja","ko-KR","lt","nb","nl","nn","pl","pt-BR","pt-PT","ro-RO","ru","sv","tr","uk","zh-CN","zh-TW"]
var validLangs = ["bg","ca","ca@valencia","cs","de","el","en","en-GB","es","es-ES","fi","fr","fr-CA","hu","it","ja","ko-KR","lt","nb","nl","nn","pl","pt-BR","pt-PT","ro-RO","ru","sv","tr","uk","zh-CN","zh-TW"]
+183 -872
View File
File diff suppressed because it is too large Load Diff
+4 -2
View File
@@ -10,14 +10,16 @@
<div class="modal-content">
<div class="modal-header alert alert-{{status}}">
<h4 class="modal-title">
<span ng-if="icon" class="glyphicon glyphicon-{{icon}}"></span>
<span ng-if="icon" class="fa fa-{{icon}}"></span>
{{title}}
</h4>
</div>
<div class="modal-body" ng-transclude>
</div>
<div ng-if="close" class="modal-footer">
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal"><span class="glyphicon glyphicon-remove"></span>&emsp;<span translate>Close</span></button>
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
<span class="fa fa-times"></span>&nbsp;<span translate>Close</span>
</button>
</div>
</div>
</div>
@@ -1,25 +0,0 @@
/**
* Groups input in chunks of the specified size
*
* E.g. [1, 2, 3, 4, 5] with groupSize = 3 => [[1, 2, 3], [4, 5]]
* Uses pmkr.memoize to avoid infdig, see 'Johnny Hauser's "Filter Stablize" Solution'
* here: http://sobrepere.com/blog/2014/10/14/creating-groupby-filter-angularjs/
*/
angular.module('syncthing.core')
.filter('group', [
'pmkr.filterStabilize',
function (stabilize) {
return stabilize(function(items, groupSize) {
var groups = [];
var inner;
for (var i = 0; i < items.length; i++) {
if (i % groupSize === 0) {
inner = [];
groups.push(inner);
}
inner.push(items[i]);
}
return groups;
});
}
]);
@@ -1,27 +0,0 @@
/**
* m59peacemaker's filterStabilize
*
* See https://github.com/m59peacemaker/angular-pmkr-components/tree/master/src/services/filterStabilize
* Released under the MIT license
*/
angular.module('syncthing.core')
.factory('pmkr.filterStabilize', [
'pmkr.memoize',
function(memoize) {
function service(fn) {
function filter() {
var args = [].slice.call(arguments);
// always pass a copy of the args so that the original input can't be modified
args = angular.copy(args);
// return the `fn` return value or input reference (makes `fn` return optional)
var filtered = fn.apply(this, args) || args[0];
return filtered;
}
var memoized = memoize(filter);
return memoized;
}
return service;
}
]);
@@ -1,28 +0,0 @@
/**
* m59peacemaker's memoize
*
* See https://github.com/m59peacemaker/angular-pmkr-components/tree/master/src/services/memoize
* Released under the MIT license
*/
angular.module('syncthing.core')
.factory('pmkr.memoize', [
function() {
function service() {
return memoizeFactory.apply(this, arguments);
}
function memoizeFactory(fn) {
var cache = {};
function memoized() {
var args = [].slice.call(arguments);
var key = JSON.stringify(args);
if (cache.hasOwnProperty(key)) {
return cache[key];
}
cache[key] = fn.apply(this, arguments);
return cache[key];
}
return memoized;
}
return service;
}
]);
@@ -11,7 +11,13 @@
var syncthing = angular.module('syncthing', [
'angularUtils.directives.dirPagination',
'pascalprecht.translate',
'syncthing.core'
'syncthing.core',
'syncthing.device',
'syncthing.folder',
'syncthing.settings',
'syncthing.transfer',
'syncthing.usagereport'
]);
var urlbase = 'rest';
@@ -20,26 +26,26 @@ syncthing.config(function ($httpProvider, $translateProvider, LocaleServiceProvi
$httpProvider.interceptors.push(function xHeadersResponseInterceptor() {
var guiVersion = null;
var deviceId = null;
return {
response: function onResponse(response) {
var headers = response.headers();
var responseVersion;
var deviceIdShort;
// angular template cache sends no headers
if(Object.keys(headers).length === 0) {
return response;
}
responseVersion = headers['x-syncthing-version'];
if (!guiVersion) {
guiVersion = responseVersion;
} else if (guiVersion != responseVersion) {
document.location.reload(true);
}
if (!deviceId) {
deviceId = headers['x-syncthing-id'];
if (deviceId) {
@@ -48,7 +54,7 @@ syncthing.config(function ($httpProvider, $translateProvider, LocaleServiceProvi
$httpProvider.defaults.xsrfCookieName = 'CSRF-Token-' + deviceIdShort;
}
}
return response;
}
};
@@ -165,4 +171,3 @@ function debounce(func, wait) {
return result;
};
}
@@ -0,0 +1,7 @@
angular.module('syncthing.core')
.directive('aboutModal', function () {
return {
restrict: 'A',
templateUrl: 'syncthing/core/aboutModalView.html'
};
});
+97
View File
@@ -0,0 +1,97 @@
<modal id="about" status="info" icon="heart-o" title="{{'About' | translate}}" large="yes" close="yes">
<h1 class="text-center">
<img alt="Syncthing" title="Syncthing" src="assets/img/logo-horizontal.svg" style="vertical-align: -16px" height="100" width="366"/>
<br/>
<small>{{versionString()}}</small>
<br/>
<small><i>"{{version.codename}}"</i></small>
</h1>
<hr/>
<p translate>Copyright &copy; 2015 the following Contributors:</p>
<div class="row">
<div class="col-md-12">
<ul class="list-unstyled three-columns" id="contributor-list">
<li class="auto-generated">Aaron Bieber</li>
<li class="auto-generated">Alexander Graf</li>
<li class="auto-generated">Andrew Dunham</li>
<li class="auto-generated">Antony Male</li>
<li class="auto-generated">Arthur Axel fREW Schmidt</li>
<li class="auto-generated">Audrius Butkevicius</li>
<li class="auto-generated">Bart De Vries</li>
<li class="auto-generated">Ben Curthoys</li>
<li class="auto-generated">Ben Schulz</li>
<li class="auto-generated">Ben Sidhom</li>
<li class="auto-generated">Brandon Philips</li>
<li class="auto-generated">Brendan Long</li>
<li class="auto-generated">Brian R. Becker</li>
<li class="auto-generated">Caleb Callaway</li>
<li class="auto-generated">Carsten Hagemann</li>
<li class="auto-generated">Cathryne Linenweaver</li>
<li class="auto-generated">Chris Howie</li>
<li class="auto-generated">Chris Joel</li>
<li class="auto-generated">Colin Kennedy</li>
<li class="auto-generated">Daniel Bergmann</li>
<li class="auto-generated">Daniel Martí</li>
<li class="auto-generated">Denis A.</li>
<li class="auto-generated">Dennis Wilson</li>
<li class="auto-generated">Dominik Heidler</li>
<li class="auto-generated">Elias Jarlebring</li>
<li class="auto-generated">Emil Hessman</li>
<li class="auto-generated">Erik Meitner</li>
<li class="auto-generated">Federico Castagnini</li>
<li class="auto-generated">Felix Ableitner</li>
<li class="auto-generated">Felix Unterpaintner</li>
<li class="auto-generated">Francois-Xavier Gsell</li>
<li class="auto-generated">Frank Isemann</li>
<li class="auto-generated">Gilli Sigurdsson</li>
<li class="auto-generated">Jacek Szafarkiewicz</li>
<li class="auto-generated">Jakob Borg</li>
<li class="auto-generated">James Patterson</li>
<li class="auto-generated">Jaroslav Malec</li>
<li class="auto-generated">Jens Diemer</li>
<li class="auto-generated">Jochen Voss</li>
<li class="auto-generated">Johan Vromans</li>
<li class="auto-generated">Karol Różycki</li>
<li class="auto-generated">Ken'ichi Kamada</li>
<li class="auto-generated">Lode Hoste</li>
<li class="auto-generated">Lord Landon Agahnim</li>
<li class="auto-generated">Marc Laporte</li>
<li class="auto-generated">Marc Pujol</li>
<li class="auto-generated">Marcin Dziadus</li>
<li class="auto-generated">Michael Jephcote</li>
<li class="auto-generated">Michael Tilli</li>
<li class="auto-generated">Pascal Jungblut</li>
<li class="auto-generated">Peter Hoeg</li>
<li class="auto-generated">Philippe Schommers</li>
<li class="auto-generated">Phill Luby</li>
<li class="auto-generated">Piotr Bejda</li>
<li class="auto-generated">Ryan Sullivan</li>
<li class="auto-generated">Sergey Mishin</li>
<li class="auto-generated">Stefan Tatschner</li>
<li class="auto-generated">Tim Abell</li>
<li class="auto-generated">Tobias Nygren</li>
<li class="auto-generated">Tomas Cerveny</li>
<li class="auto-generated">Tully Robinson</li>
<li class="auto-generated">Veeti Paananen</li>
<li class="auto-generated">Vil Brekin</li>
</ul>
</div>
</div>
<hr/>
<p translate>Syncthing includes the following software or portions thereof:</p>
<ul class="list-unstyled two-columns">
<li><a href="https://golang.org">The Go Programming Language</a>, Copyright &copy; 2012 The Go Authors.</li>
<li><a href="https://github.com/bkaradzic/go-lz4">bkaradzic/go-lz4</a>, Copyright &copy; 2011-2012 Branimir Karadzic, 2013 Damian Gryski.</li>
<li><a href="https://github.com/kardianos/osext">kardianos/osext</a>, Copyright &copy; 2012 Daniel Theophanes.</li>
<li><a href="https://github.com/golang/snappy">golang/snappy</a>, Copyright &copy; 2011 The Snappy-Go Authors.</li>
<li><a href="https://github.com/juju/ratelimit">juju/ratelimit</a>, Copyright &copy; 2015 Canonical Ltd.</li>
<li><a href="https://github.com/thejerf/suture">thejerf/suture</a>, Copyright &copy; 2014-2015 Barracuda Networks, Inc.</li>
<li><a href="https://github.com/syndtr/goleveldb">syndtr/goleveldb</a>, Copyright &copy; 2012, Suryandaru Triandana</li>
<li><a href="https://github.com/vitrun/qart">vitrun/qart</a>, Copyright &copy; The Go Authors.</li>
<li><a href="https://angularjs.org/">AngularJS</a>, Copyright &copy; 2010-2015 Google, Inc.</li>
<li><a href="http://getbootstrap.com/">Bootstrap</a>, Copyright &copy; 2011-2015 Twitter, Inc.</li>
<li><a href="http://fontawesome.io/">Font Awesome</a>, Copyright &copy; 2015 Dave Gandy</li>
</ul>
</modal>
@@ -21,11 +21,11 @@ angular.module('syncthing.core')
var t = (input/SECONDS_IN[k] | 0); // Math.floor
if (t > 0) {
result += " " + t + k;
result += " " + t + k;
}
if (precision == k) {
return result ? result : "<1" + k;
return result ? result : "<1" + k;
} else {
input %= SECONDS_IN[k];
}
@@ -0,0 +1,7 @@
angular.module('syncthing.core')
.directive('httpErrorDialog', function () {
return {
restrict: 'A',
templateUrl: 'syncthing/core/httpErrorDialogView.html'
};
});
@@ -0,0 +1,5 @@
<modal id="httpError" status="danger" icon="exclamation-circle" title="{{'Connection Error' | translate}}">
<p translate>
Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.
</p>
</modal>

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