Compare commits

...

29 Commits

Author SHA1 Message Date
Audrius Butkevicius
27d5b17096 lib/osutil: Fix priority lowering on Windows
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/4686
2018-01-19 07:21:05 +01:00
Jakob Borg
c2119c9e62 lib/osutil: Don't attempt to reduce our niceness level (fixes #4681)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/4682
2018-01-19 07:20:58 +01:00
Jakob Borg
91210cbb49 lib/osutil: Check PGID before trying to set it (fixes #4679)
Fixes "permission denied" return when are already process group /
session leader.

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/4678
2018-01-19 07:20:47 +01:00
Jakob Borg
582539a1e6 lib/osutil: Disable setting priority on Windows (fixes #4676)
Presumably fixing the crash, leaving us to improve this calmly.

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/4677
2018-01-17 10:14:36 +00:00
Jakob Borg
5cfb9783b3 gui, man: Update docs & translations 2018-01-17 07:45:19 +01:00
Jakob Borg
8de21be274 build: Packaging for stdiscosrv 2018-01-16 20:46:48 +01:00
Jakob Borg
c554ffccc9 cmd/syncthing, lib/config, lib/osutil: Lower process priority (fixes #4628)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/4675
2018-01-15 17:11:14 +00:00
Jakob Borg
838c182b5b cmd/syncthing, lib/sync: Don't do deadlock detection when STDEADLOCKTIMEOUT=0 (fixes #4644)
Allows setting STDEADLOCKTIMEOUT=0 (or any integer <= 0) to disable the
deadlock detection entirely.

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/4673
2018-01-15 13:33:52 +00:00
Jakob Borg
fcc6a677a5 lib/upgrade: Always return latest version, even if older than current (fixes #4654)
The only special check remaining is the one to prefer a minor upgrade
over a major one.

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/4672
2018-01-15 12:13:25 +00:00
Simon Frei
bd63fd73b1 lib/model: Microoptimization of unifySubs and blockDiff
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/4671
LGTM: AudriusButkevicius, calmh
2018-01-14 21:52:41 +00:00
Simon Frei
fecb21cdb1 gui: New rest endpoint to get errors when web UI is opened
Since #4340 pulls aren't happening every 10s anymore and may be delayed up to 1h.
This means that no folder error event reaches the web UI for a long time, thus no
failed items will show up for a long time. Now errors are populated when the
web UI is opened.

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/4650
LGTM: AudriusButkevicius
2018-01-14 17:01:06 +00:00
Lars K.W. Gohlke
89a021609b lib/scanner: Refactoring
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/4642
LGTM: imsodin, AudriusButkevicius
2018-01-14 14:30:11 +00:00
Emil Hessman
bbc178ccc4 lib/fs: harmonize CreateSymlink definitions (fixes #4567)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/4632
LGTM: imsodin, AudriusButkevicius
2018-01-14 14:25:04 +00:00
Simon Frei
f1c73999be gui: Count deleted items for remote out of sync items display (fixes #4668)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/4669
LGTM: calmh, AudriusButkevicius
2018-01-14 12:08:40 +00:00
Jakob Borg
916ec63af6 cmd/stdiscosrv: New discovery server (fixes #4618)
This is a new revision of the discovery server. Relevant changes and
non-changes:

- Protocol towards clients is unchanged.

- Recommended large scale design is still to be deployed nehind nginx (I
  tested, and it's still a lot faster at terminating TLS).

- Database backend is leveldb again, only. It scales enough, is easy to
  setup, and we don't need any backend to take care of.

- Server supports replication. This is a simple TCP channel - protect it
  with a firewall when deploying over the internet. (We deploy this within
  the same datacenter, and with firewall.) Any incoming client announces
  are sent over the replication channel(s) to other peer discosrvs.
  Incoming replication changes are applied to the database as if they came
  from clients, but without the TLS/certificate overhead.

- Metrics are exposed using the prometheus library, when enabled.

- The database values and replication protocol is protobuf, because JSON
  was quite CPU intensive when I tried that and benchmarked it.

- The "Retry-After" value for failed lookups gets slowly increased from
  a default of 120 seconds, by 5 seconds for each failed lookup,
  independently by each discosrv. This lowers the query load over time for
  clients that are never seen. The Retry-After maxes out at 3600 after a
  couple of weeks of this increase. The number of failed lookups is
  stored in the database, now and then (avoiding making each lookup a
  database put).

All in all this means clients can be pointed towards a cluster using
just multiple A / AAAA records to gain both load sharing and redundancy
(if one is down, clients will talk to the remaining ones).

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/4648
2018-01-14 08:52:31 +00:00
Simon Frei
341b9691a7 lib/connections, lib/model: Additional connection info in logs (fixes #4499)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/4665
2018-01-12 11:27:55 +00:00
Jakob Borg
6e0f64017a lib/logger, lib/model: Correct printf format strings in tests
This is a build (test) failure in Go 1.10.
2018-01-12 08:27:00 +01:00
Jakob Borg
14df5211af lib/model: Correctly account handling of sparse blocks (fixes #4657)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/4658
2018-01-11 10:36:35 +00:00
Jakob Borg
5611173366 gui, man: Update docs & translations 2018-01-10 07:45:17 +01:00
Wulf Weich
1afe8ab736 gui: removed faulty char in remoteNeededFiles
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/4660
2018-01-09 19:45:55 +00:00
Vladimir Rusinov
1c8803402e build: Remove ulimit from build.sh
Previous "reasonable resource limits" cause test failures with Go 1.9.
They were added to protect the previous generation of the build server
and no longer needed.

Fixes issue #4653.

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/4656
2018-01-08 17:51:59 +00:00
Antony Male
db03562d43 lib/scanner: Fix UTF-8 normalization on ZFS (fixes #4649)
It turns out that ZFS doesn't do any normalization when storing files,
but does do normalization "as part of any comparison process".

In practice, this seems to mean that if you LStat a normalized filename,
ZFS will return the FileInfo for the un-normalized version of that
filename.

This meant that our test to see whether a separate file with a
normalized version of the filename already exists was failing, as we
were detecting the same file.

The fix is to use os.SameFile, to see whether we're getting the same
FileInfo from the normalized and un-normalized versions of the same
filename.

One complication is that ZFS also seems to apply its magic to os.Rename,
meaning that we can't use it to rename an un-normalized file to its
normalized filename. Instead we have to move via a temporary object. If
the move to the temporary object fails, that's OK, we can skip it and
move on. If the move from the temporary object fails however, I'm not
sure of the best approach: the current one is to leave the temporary
file name as-is, and get Syncthing to syncronize it, so at least we
don't lose the file. I'm not sure if there are any implications of this
however.

As part of reworking normalizePath, I spotted that it appeared to be
returning the wrong thing: the doc and the surrounding code expecting it
to return the normalized filename, but it was returning the
un-normalized one. I fixed this, but it seems suspicious that, if the
previous behaviour was incorrect, noone ever ran afoul of it. Maybe all
filesystems will do some searching and give you a normalized filename if
you request an unnormalized one.

As part of this, I found that TestNormalization was broken: it was
passing, when in fact one of the files it should have verified was
present was missing. Maybe this was related to the above issue with
normalizePath's return value, I'm not sure. Fixed en route.

Kindly tested by @khinsen on the forum, and it appears to work.

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/4646
2018-01-05 18:11:09 +00:00
Simon Frei
d87287c0d0 gui: Prevent error without completion and nicer wrapping (fixes #4636)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/4637
2018-01-05 14:41:40 +00:00
Jakob Borg
992bb0a98c lib/config, lib/discover: Support new discovery cluster (ref #4618)
This adds one new feature, that discovery servers can have ?nolookup to
be used only for announces. The default set of discovery servers is
changed to:

- discovery.s.n used for lookups. This is dual stack load balanced over
  all discovery servers, and returns both IPv4 and IPV6 results when they
  exist.

- discovery-v4.s.n used for announces. This has IPv4 addresses only and
  the discovery servers will update the unspecified address with the IPv4
  source address, as usual.

- discovery-v6.s.n which is exactly the same for IPv6.

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/4647
2018-01-05 14:18:32 +00:00
Lars K.W. Gohlke
e6551c8485 build: Add support for debug-only binary (i.e., dlv)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/4633
LGTM: AudriusButkevicius
2018-01-03 08:07:15 +00:00
Audrius Butkevicius
53f4dfe83c lib/model: Fix panic when auto-accepting existing paused folder (fixes #4636)
This no longer pokes at model internals, and only touches the config.
As a result, model handles this in CommitConfiguration, which restarts
the folders if things change, which repopulate m.folderDevice, m.deviceFolder
and other interal mappings.

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/4639
2018-01-03 07:42:25 +00:00
Jakob Borg
9410634c2b gui, man: Update docs & translations 2018-01-03 07:45:21 +01:00
Audrius Butkevicius
b0e2050cdb cmd/syncthing: UI for version restoration (fixes #2599) (#4602)
cmd/syncthing: Add UI for version restoration (fixes #2599)
2018-01-01 15:39:23 +01:00
Audrius Butkevicius
c7f136c2b8 lib/upnp: Each service is it's own NAT device
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/4625
2017-12-30 19:16:08 +00:00
982 changed files with 238678 additions and 65802 deletions

View File

@@ -46,6 +46,7 @@ var (
extraTags string
installSuffix string
pkgdir string
debugBinary bool
)
type target struct {
@@ -121,7 +122,7 @@ var targets = map[string]target{
archiveFiles: []archiveFile{
{src: "{{binary}}", dst: "{{binary}}", perm: 0755},
{src: "cmd/stdiscosrv/README.md", dst: "README.txt", perm: 0644},
{src: "cmd/stdiscosrv/LICENSE", dst: "LICENSE.txt", perm: 0644},
{src: "LICENSE", dst: "LICENSE.txt", perm: 0644},
{src: "AUTHORS", dst: "AUTHORS.txt", perm: 0644},
},
installationFiles: []archiveFile{
@@ -358,6 +359,7 @@ func parseFlags() {
flag.StringVar(&extraTags, "tags", extraTags, "Extra tags, space separated")
flag.StringVar(&installSuffix, "installsuffix", installSuffix, "Install suffix, optional")
flag.StringVar(&pkgdir, "pkgdir", "", "Set -pkgdir parameter for `go build`")
flag.BoolVar(&debugBinary, "debug-binary", debugBinary, "Create unoptimized binary to use with delve, set -gcflags='-N -l' and omit -ldflags")
flag.Parse()
}
@@ -419,20 +421,9 @@ func install(target target, tags []string) {
log.Fatal(err)
}
os.Setenv("GOBIN", filepath.Join(cwd, "bin"))
args := []string{"install", "-v", "-ldflags", ldflags()}
if pkgdir != "" {
args = append(args, "-pkgdir", pkgdir)
}
if len(tags) > 0 {
args = append(args, "-tags", strings.Join(tags, " "))
}
if installSuffix != "" {
args = append(args, "-installsuffix", installSuffix)
}
if race {
args = append(args, "-race")
}
args = append(args, target.buildPkg)
args := []string{"install", "-v"}
args = appendParameters(args, tags, target)
os.Setenv("GOOS", goos)
os.Setenv("GOARCH", goarch)
@@ -445,7 +436,16 @@ func build(target target, tags []string) {
tags = append(target.tags, tags...)
rmr(target.BinaryName())
args := []string{"build", "-i", "-v", "-ldflags", ldflags()}
args := []string{"build", "-i", "-v"}
args = appendParameters(args, tags, target)
os.Setenv("GOOS", goos)
os.Setenv("GOARCH", goarch)
runPrint("go", args...)
}
func appendParameters(args []string, tags []string, target target) []string {
if pkgdir != "" {
args = append(args, "-pkgdir", pkgdir)
}
@@ -458,11 +458,19 @@ func build(target target, tags []string) {
if race {
args = append(args, "-race")
}
args = append(args, target.buildPkg)
os.Setenv("GOOS", goos)
os.Setenv("GOARCH", goarch)
runPrint("go", args...)
if !debugBinary {
// Regular binaries get version tagged and skip some debug symbols
args = append(args, "-ldflags", ldflags())
} else {
// -gcflags to disable optimizations and inlining. Skip -ldflags
// because `Could not launch program: decoding dwarf section info at
// offset 0x0: too short` on 'dlv exec ...' see
// https://github.com/derekparker/delve/issues/79
args = append(args, "-gcflags", "-N -l")
}
return append(args, target.buildPkg)
}
func buildTar(target target) {
@@ -697,7 +705,7 @@ func shouldRebuildAssets(target, srcdir string) bool {
}
func proto() {
runPrint("go", "generate", "github.com/syncthing/syncthing/lib/...")
runPrint("go", "generate", "github.com/syncthing/syncthing/lib/...", "github.com/syncthing/syncthing/cmd/stdiscosrv")
}
func translate() {

View File

@@ -48,9 +48,6 @@ case "${1:-default}" in
;;
test)
ulimit -t 600 &>/dev/null || true
ulimit -d 512000 &>/dev/null || true
ulimit -m 512000 &>/dev/null || true
LOGGER_DISCARD=1 build test
;;
@@ -94,9 +91,6 @@ case "${1:-default}" in
;;
test-xunit)
ulimit -t 600 &>/dev/null || true
ulimit -d 512000 &>/dev/null || true
ulimit -m 512000 &>/dev/null || true
(GOPATH="$(pwd)/Godeps/_workspace:$GOPATH" go test -v -race ./lib/... ./cmd/... || true) > tests.out
go2xunit -output tests.xml -fail < tests.out

View File

@@ -1,19 +0,0 @@
Copyright (C) 2014-2015 The Discosrv Authors
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -6,33 +6,5 @@ This is the global discovery server for the `syncthing` project.
Usage
-----
The discovery server supports `ql` and `postgres` backends.
Specify the backend via `-db-backend` and the database DSN via `-db-dsn`.
https://docs.syncthing.net/users/stdiscosrv.html
By default it will use in-memory `ql` backend. If you wish to persist the
information on disk between restarts in `ql`, specify a file DSN:
```bash
$ stdiscosrv -db-dsn="file:///var/run/stdiscosrv.db"
```
For `postgres`, you will need to create a database and a user with permissions
to create tables in it, then start the stdiscosrv as follows:
```bash
$ export STDISCOSRV_DB_DSN="postgres://user:password@localhost/databasename"
$ stdiscosrv -db-backend="postgres"
```
You can pass the DSN as command line option, but the value what you pass in will
be visible in most process managers, potentially exposing the database password
to other users.
In all cases, the appropriate tables and indexes will be created at first
startup. If it doesn't exit with an error, you're fine.
See `stdiscosrv -help` for other options.
##### Third-party attribution
[cznic/lldb](https://github.com/cznic/lldb), Copyright (C) 2014 The lldb Authors.

394
cmd/stdiscosrv/apisrv.go Normal file
View File

@@ -0,0 +1,394 @@
// Copyright (C) 2018 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
package main
import (
"bytes"
"crypto/tls"
"encoding/json"
"encoding/pem"
"fmt"
"log"
"math/rand"
"net"
"net/http"
"net/url"
"strconv"
"sync"
"time"
"github.com/syncthing/syncthing/lib/protocol"
"golang.org/x/net/context"
)
// announcement is the format received from and sent to clients
type announcement struct {
Seen time.Time `json:"seen"`
Addresses []string `json:"addresses"`
}
type apiSrv struct {
addr string
cert tls.Certificate
db database
listener net.Listener
repl replicator // optional
useHTTP bool
mapsMut sync.Mutex
misses map[string]int32
}
type requestID int64
func (i requestID) String() string {
return fmt.Sprintf("%016x", int64(i))
}
type contextKey int
const idKey contextKey = iota
func newAPISrv(addr string, cert tls.Certificate, db database, repl replicator, useHTTP bool) *apiSrv {
return &apiSrv{
addr: addr,
cert: cert,
db: db,
repl: repl,
useHTTP: useHTTP,
misses: make(map[string]int32),
}
}
func (s *apiSrv) Serve() {
if s.useHTTP {
listener, err := net.Listen("tcp", s.addr)
if err != nil {
log.Println("Listen:", err)
return
}
s.listener = listener
} else {
tlsCfg := &tls.Config{
Certificates: []tls.Certificate{s.cert},
ClientAuth: tls.RequestClientCert,
SessionTicketsDisabled: 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,
},
}
tlsListener, err := tls.Listen("tcp", s.addr, tlsCfg)
if err != nil {
log.Println("Listen:", err)
return
}
s.listener = tlsListener
}
http.HandleFunc("/", s.handler)
http.HandleFunc("/ping", handlePing)
srv := &http.Server{
ReadTimeout: httpReadTimeout,
WriteTimeout: httpWriteTimeout,
MaxHeaderBytes: httpMaxHeaderBytes,
}
if err := srv.Serve(s.listener); err != nil {
log.Println("Serve:", err)
}
}
var topCtx = context.Background()
func (s *apiSrv) handler(w http.ResponseWriter, req *http.Request) {
t0 := time.Now()
lw := NewLoggingResponseWriter(w)
defer func() {
diff := time.Since(t0)
apiRequestsSeconds.WithLabelValues(req.Method).Observe(diff.Seconds())
apiRequestsTotal.WithLabelValues(req.Method, strconv.Itoa(lw.statusCode)).Inc()
}()
reqID := requestID(rand.Int63())
ctx := context.WithValue(topCtx, idKey, reqID)
if debug {
log.Println(reqID, req.Method, req.URL)
}
var remoteIP net.IP
if s.useHTTP {
remoteIP = net.ParseIP(req.Header.Get("X-Forwarded-For"))
} else {
addr, err := net.ResolveTCPAddr("tcp", req.RemoteAddr)
if err != nil {
log.Println("remoteAddr:", err)
lw.Header().Set("Retry-After", errorRetryAfterString())
http.Error(lw, "Internal Server Error", http.StatusInternalServerError)
apiRequestsTotal.WithLabelValues("no_remote_addr").Inc()
return
}
remoteIP = addr.IP
}
switch req.Method {
case "GET":
s.handleGET(ctx, lw, req)
case "POST":
s.handlePOST(ctx, remoteIP, lw, req)
default:
http.Error(lw, "Method Not Allowed", http.StatusMethodNotAllowed)
}
}
func (s *apiSrv) handleGET(ctx context.Context, w http.ResponseWriter, req *http.Request) {
reqID := ctx.Value(idKey).(requestID)
deviceID, err := protocol.DeviceIDFromString(req.URL.Query().Get("device"))
if err != nil {
if debug {
log.Println(reqID, "bad device param")
}
lookupRequestsTotal.WithLabelValues("bad_request").Inc()
w.Header().Set("Retry-After", errorRetryAfterString())
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
key := deviceID.String()
rec, err := s.db.get(key)
if err != nil {
// some sort of internal error
lookupRequestsTotal.WithLabelValues("internal_error").Inc()
w.Header().Set("Retry-After", errorRetryAfterString())
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
if len(rec.Addresses) == 0 {
lookupRequestsTotal.WithLabelValues("not_found").Inc()
s.mapsMut.Lock()
misses := s.misses[key]
if misses < rec.Misses {
misses = rec.Misses + 1
} else {
misses++
}
s.misses[key] = misses
s.mapsMut.Unlock()
if misses%notFoundMissesWriteInterval == 0 {
rec.Misses = misses
rec.Addresses = nil
// rec.Seen retained from get
s.db.put(key, rec)
}
w.Header().Set("Retry-After", notFoundRetryAfterString(int(misses)))
http.Error(w, "Not Found", http.StatusNotFound)
return
}
lookupRequestsTotal.WithLabelValues("success").Inc()
bs, _ := json.Marshal(announcement{
Seen: time.Unix(0, rec.Seen),
Addresses: addressStrs(rec.Addresses),
})
w.Header().Set("Content-Type", "application/json")
w.Write(bs)
}
func (s *apiSrv) handlePOST(ctx context.Context, remoteIP net.IP, w http.ResponseWriter, req *http.Request) {
reqID := ctx.Value(idKey).(requestID)
rawCert := certificateBytes(req)
if rawCert == nil {
if debug {
log.Println(reqID, "no certificates")
}
announceRequestsTotal.WithLabelValues("no_certificate").Inc()
w.Header().Set("Retry-After", errorRetryAfterString())
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
var ann announcement
if err := json.NewDecoder(req.Body).Decode(&ann); err != nil {
if debug {
log.Println(reqID, "decode:", err)
}
announceRequestsTotal.WithLabelValues("bad_request").Inc()
w.Header().Set("Retry-After", errorRetryAfterString())
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
deviceID := protocol.NewDeviceID(rawCert)
addresses := fixupAddresses(remoteIP, ann.Addresses)
if len(addresses) == 0 {
announceRequestsTotal.WithLabelValues("bad_request").Inc()
w.Header().Set("Retry-After", errorRetryAfterString())
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
if err := s.handleAnnounce(remoteIP, deviceID, addresses); err != nil {
announceRequestsTotal.WithLabelValues("internal_error").Inc()
w.Header().Set("Retry-After", errorRetryAfterString())
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
announceRequestsTotal.WithLabelValues("success").Inc()
w.Header().Set("Reannounce-After", reannounceAfterString())
w.WriteHeader(http.StatusNoContent)
}
func (s *apiSrv) Stop() {
s.listener.Close()
}
func (s *apiSrv) handleAnnounce(remote net.IP, deviceID protocol.DeviceID, addresses []string) error {
key := deviceID.String()
now := time.Now()
expire := now.Add(addressExpiryTime).UnixNano()
dbAddrs := make([]DatabaseAddress, len(addresses))
for i := range addresses {
dbAddrs[i].Address = addresses[i]
dbAddrs[i].Expires = expire
}
seen := now.UnixNano()
if s.repl != nil {
s.repl.send(key, dbAddrs, seen)
}
return s.db.merge(key, dbAddrs, seen)
}
func handlePing(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(204)
}
func certificateBytes(req *http.Request) []byte {
if req.TLS != nil && len(req.TLS.PeerCertificates) > 0 {
return req.TLS.PeerCertificates[0].Raw
}
if hdr := req.Header.Get("X-SSL-Cert"); hdr != "" {
bs := []byte(hdr)
// The certificate is in PEM format but with spaces for newlines. We
// need to reinstate the newlines for the PEM decoder. But we need to
// leave the spaces in the BEGIN and END lines - the first and last
// space - alone.
firstSpace := bytes.Index(bs, []byte(" "))
lastSpace := bytes.LastIndex(bs, []byte(" "))
for i := firstSpace + 1; i < lastSpace; i++ {
if bs[i] == ' ' {
bs[i] = '\n'
}
}
block, _ := pem.Decode(bs)
if block == nil {
// Decoding failed
return nil
}
return block.Bytes
}
return nil
}
// fixupAddresses checks the list of addresses, removing invalid ones and
// replacing unspecified IPs with the given remote IP.
func fixupAddresses(remote net.IP, addresses []string) []string {
fixed := make([]string, 0, len(addresses))
for _, annAddr := range addresses {
uri, err := url.Parse(annAddr)
if err != nil {
continue
}
host, port, err := net.SplitHostPort(uri.Host)
if err != nil {
continue
}
ip := net.ParseIP(host)
if host == "" || ip.IsUnspecified() {
// Do not use IPv6 remote address if requested scheme is tcp4
if uri.Scheme == "tcp4" && remote.To4() == nil {
continue
}
// Do not use IPv4 remote address if requested scheme is tcp6
if uri.Scheme == "tcp6" && remote.To4() != nil {
continue
}
host = remote.String()
}
uri.Host = net.JoinHostPort(host, port)
fixed = append(fixed, uri.String())
}
return fixed
}
type loggingResponseWriter struct {
http.ResponseWriter
statusCode int
}
func NewLoggingResponseWriter(w http.ResponseWriter) *loggingResponseWriter {
return &loggingResponseWriter{w, http.StatusOK}
}
func (lrw *loggingResponseWriter) WriteHeader(code int) {
lrw.statusCode = code
lrw.ResponseWriter.WriteHeader(code)
}
func addressStrs(dbAddrs []DatabaseAddress) []string {
res := make([]string, len(dbAddrs))
for i, a := range dbAddrs {
res[i] = a.Address
}
return res
}
func errorRetryAfterString() string {
return strconv.Itoa(errorRetryAfterSeconds + rand.Intn(errorRetryFuzzSeconds))
}
func notFoundRetryAfterString(misses int) string {
retryAfterS := notFoundRetryMinSeconds + notFoundRetryIncSeconds*misses
if retryAfterS > notFoundRetryMaxSeconds {
retryAfterS = notFoundRetryMaxSeconds
}
retryAfterS += rand.Intn(notFoundRetryFuzzSeconds)
return strconv.Itoa(retryAfterS)
}
func reannounceAfterString() string {
return strconv.Itoa(reannounceAfterSeconds + rand.Intn(reannounzeFuzzSeconds))
}

View File

@@ -1,75 +0,0 @@
// Copyright (C) 2014-2015 Jakob Borg and Contributors (see the CONTRIBUTORS file).
package main
import (
"database/sql"
"log"
"time"
)
type cleansrv struct {
intv time.Duration
db *sql.DB
prep map[string]*sql.Stmt
}
func (s *cleansrv) Serve() {
for {
time.Sleep(next(s.intv))
err := s.cleanOldEntries()
if err != nil {
log.Println("Clean:", err)
}
}
}
func (s *cleansrv) Stop() {
panic("stop unimplemented")
}
func (s *cleansrv) cleanOldEntries() (err error) {
var tx *sql.Tx
tx, err = s.db.Begin()
if err != nil {
return err
}
defer func() {
if err == nil {
err = tx.Commit()
} else {
tx.Rollback()
}
}()
res, err := tx.Stmt(s.prep["cleanAddress"]).Exec()
if err != nil {
return err
}
if rows, _ := res.RowsAffected(); rows > 0 {
log.Printf("Clean: %d old addresses", rows)
}
res, err = tx.Stmt(s.prep["cleanDevice"]).Exec()
if err != nil {
return err
}
if rows, _ := res.RowsAffected(); rows > 0 {
log.Printf("Clean: %d old devices", rows)
}
var devs, addrs int
row := tx.Stmt(s.prep["countDevice"]).QueryRow()
if err = row.Scan(&devs); err != nil {
return err
}
row = tx.Stmt(s.prep["countAddress"]).QueryRow()
if err = row.Scan(&addrs); err != nil {
return err
}
log.Printf("Database: %d devices, %d addresses", devs, addrs)
return nil
}

336
cmd/stdiscosrv/database.go Normal file
View File

@@ -0,0 +1,336 @@
// Copyright (C) 2018 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
//go:generate go run ../../script/protofmt.go database.proto
//go:generate protoc -I ../../../../../ -I ../../vendor/ -I ../../vendor/github.com/gogo/protobuf/protobuf -I . --gogofast_out=. database.proto
package main
import (
"sort"
"time"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/util"
)
type clock interface {
Now() time.Time
}
type defaultClock struct{}
func (defaultClock) Now() time.Time {
return time.Now()
}
type database interface {
put(key string, rec DatabaseRecord) error
merge(key string, addrs []DatabaseAddress, seen int64) error
get(key string) (DatabaseRecord, error)
}
type levelDBStore struct {
db *leveldb.DB
inbox chan func()
stop chan struct{}
clock clock
marshalBuf []byte
}
func newLevelDBStore(dir string) (*levelDBStore, error) {
db, err := leveldb.OpenFile(dir, levelDBOptions)
if err != nil {
return nil, err
}
return &levelDBStore{
db: db,
inbox: make(chan func(), 16),
stop: make(chan struct{}),
clock: defaultClock{},
}, nil
}
func (s *levelDBStore) put(key string, rec DatabaseRecord) error {
t0 := time.Now()
defer func() {
databaseOperationSeconds.WithLabelValues(dbOpPut).Observe(time.Since(t0).Seconds())
}()
rc := make(chan error)
s.inbox <- func() {
size := rec.Size()
if len(s.marshalBuf) < size {
s.marshalBuf = make([]byte, size)
}
n, _ := rec.MarshalTo(s.marshalBuf)
rc <- s.db.Put([]byte(key), s.marshalBuf[:n], nil)
}
err := <-rc
if err != nil {
databaseOperations.WithLabelValues(dbOpPut, dbResError).Inc()
} else {
databaseOperations.WithLabelValues(dbOpPut, dbResSuccess).Inc()
}
return err
}
func (s *levelDBStore) merge(key string, addrs []DatabaseAddress, seen int64) error {
t0 := time.Now()
defer func() {
databaseOperationSeconds.WithLabelValues(dbOpMerge).Observe(time.Since(t0).Seconds())
}()
rc := make(chan error)
newRec := DatabaseRecord{
Addresses: addrs,
Seen: seen,
}
s.inbox <- func() {
// grab the existing record
oldRec, err := s.get(key)
if err != nil {
// "not found" is not an error from get, so this is serious
// stuff only
rc <- err
return
}
newRec = merge(newRec, oldRec)
// We replicate s.put() functionality here ourselves instead of
// calling it because we want to serialize our get above together
// with the put in the same function.
size := newRec.Size()
if len(s.marshalBuf) < size {
s.marshalBuf = make([]byte, size)
}
n, _ := newRec.MarshalTo(s.marshalBuf)
rc <- s.db.Put([]byte(key), s.marshalBuf[:n], nil)
}
err := <-rc
if err != nil {
databaseOperations.WithLabelValues(dbOpMerge, dbResError).Inc()
} else {
databaseOperations.WithLabelValues(dbOpMerge, dbResSuccess).Inc()
}
return err
}
func (s *levelDBStore) get(key string) (DatabaseRecord, error) {
t0 := time.Now()
defer func() {
databaseOperationSeconds.WithLabelValues(dbOpGet).Observe(time.Since(t0).Seconds())
}()
keyBs := []byte(key)
val, err := s.db.Get(keyBs, nil)
if err == leveldb.ErrNotFound {
databaseOperations.WithLabelValues(dbOpGet, dbResNotFound).Inc()
return DatabaseRecord{}, nil
}
if err != nil {
databaseOperations.WithLabelValues(dbOpGet, dbResError).Inc()
return DatabaseRecord{}, err
}
var rec DatabaseRecord
if err := rec.Unmarshal(val); err != nil {
databaseOperations.WithLabelValues(dbOpGet, dbResUnmarshalError).Inc()
return DatabaseRecord{}, nil
}
rec.Addresses = expire(rec.Addresses, s.clock.Now().UnixNano())
databaseOperations.WithLabelValues(dbOpGet, dbResSuccess).Inc()
return rec, nil
}
func (s *levelDBStore) Serve() {
t := time.NewTimer(0)
defer t.Stop()
defer s.db.Close()
// Start the statistics serve routine. It will exit with us when
// statisticsTrigger is closed.
statisticsTrigger := make(chan struct{})
defer close(statisticsTrigger)
statisticsDone := make(chan struct{})
go s.statisticsServe(statisticsTrigger, statisticsDone)
for {
select {
case fn := <-s.inbox:
// Run function in serialized order.
fn()
case <-t.C:
// Trigger the statistics routine to do its thing in the
// background.
statisticsTrigger <- struct{}{}
case <-statisticsDone:
// The statistics routine is done with one iteratation, schedule
// the next.
t.Reset(databaseStatisticsInterval)
case <-s.stop:
// We're done.
return
}
}
}
func (s *levelDBStore) statisticsServe(trigger <-chan struct{}, done chan<- struct{}) {
for range trigger {
t0 := time.Now()
nowNanos := t0.UnixNano()
cutoff24h := t0.Add(-24 * time.Hour).UnixNano()
cutoff1w := t0.Add(-7 * 24 * time.Hour).UnixNano()
current, last24h, last1w, inactive, errors := 0, 0, 0, 0, 0
iter := s.db.NewIterator(&util.Range{}, nil)
for iter.Next() {
// Attempt to unmarshal the record and count the
// failure if there's something wrong with it.
var rec DatabaseRecord
if err := rec.Unmarshal(iter.Value()); err != nil {
errors++
continue
}
// If there are addresses that have not expired it's a current
// record, otherwise account it based on when it was last seen
// (last 24 hours or last week) or finally as inactice.
switch {
case len(expire(rec.Addresses, nowNanos)) > 0:
current++
case rec.Seen > cutoff24h:
last24h++
case rec.Seen > cutoff1w:
last1w++
default:
inactive++
}
}
iter.Release()
databaseKeys.WithLabelValues("current").Set(float64(current))
databaseKeys.WithLabelValues("last24h").Set(float64(last24h))
databaseKeys.WithLabelValues("last1w").Set(float64(last1w))
databaseKeys.WithLabelValues("inactive").Set(float64(inactive))
databaseKeys.WithLabelValues("error").Set(float64(errors))
databaseStatisticsSeconds.Set(time.Since(t0).Seconds())
// Signal that we are done and can be scheduled again.
done <- struct{}{}
}
}
func (s *levelDBStore) Stop() {
close(s.stop)
}
// merge returns the merged result of the two database records a and b. The
// result is the union of the two address sets, with the newer expiry time
// chosen for any duplicates.
func merge(a, b DatabaseRecord) DatabaseRecord {
// Both lists must be sorted for this to work.
sort.Slice(a.Addresses, func(i, j int) bool {
return a.Addresses[i].Address < a.Addresses[j].Address
})
sort.Slice(b.Addresses, func(i, j int) bool {
return b.Addresses[i].Address < b.Addresses[j].Address
})
res := DatabaseRecord{
Addresses: make([]DatabaseAddress, 0, len(a.Addresses)+len(b.Addresses)),
Seen: a.Seen,
}
if b.Seen > a.Seen {
res.Seen = b.Seen
}
aIdx := 0
bIdx := 0
aAddrs := a.Addresses
bAddrs := b.Addresses
loop:
for {
switch {
case aIdx == len(aAddrs) && bIdx == len(bAddrs):
// both lists are exhausted, we are done
break loop
case aIdx == len(aAddrs):
// a is exhausted, pick from b and continue
res.Addresses = append(res.Addresses, bAddrs[bIdx])
bIdx++
continue
case bIdx == len(bAddrs):
// b is exhausted, pick from a and continue
res.Addresses = append(res.Addresses, aAddrs[aIdx])
aIdx++
continue
}
// We have values left on both sides.
aVal := aAddrs[aIdx]
bVal := bAddrs[bIdx]
switch {
case aVal.Address == bVal.Address:
// update for same address, pick newer
if aVal.Expires > bVal.Expires {
res.Addresses = append(res.Addresses, aVal)
} else {
res.Addresses = append(res.Addresses, bVal)
}
aIdx++
bIdx++
case aVal.Address < bVal.Address:
// a is smallest, pick it and continue
res.Addresses = append(res.Addresses, aVal)
aIdx++
default:
// b is smallest, pick it and continue
res.Addresses = append(res.Addresses, bVal)
bIdx++
}
}
return res
}
// expire returns the list of addresses after removing expired entries.
// Expiration happen in place, so the slice given as the parameter is
// destroyed. Internal order is not preserved.
func expire(addrs []DatabaseAddress, now int64) []DatabaseAddress {
i := 0
for i < len(addrs) {
if addrs[i].Expires < now {
// This item is expired. Replace it with the last in the list
// (noop if we are at the last item).
addrs[i] = addrs[len(addrs)-1]
// Wipe the last item of the list to release references to
// strings and stuff.
addrs[len(addrs)-1] = DatabaseAddress{}
// Shorten the slice.
addrs = addrs[:len(addrs)-1]
continue
}
i++
}
return addrs
}

View File

@@ -0,0 +1,743 @@
// Code generated by protoc-gen-gogo. DO NOT EDIT.
// source: database.proto
/*
Package main is a generated protocol buffer package.
It is generated from these files:
database.proto
It has these top-level messages:
DatabaseRecord
ReplicationRecord
DatabaseAddress
*/
package main
import proto "github.com/gogo/protobuf/proto"
import fmt "fmt"
import math "math"
import _ "github.com/gogo/protobuf/gogoproto"
import io "io"
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.GoGoProtoPackageIsVersion2 // please upgrade the proto package
type DatabaseRecord struct {
Addresses []DatabaseAddress `protobuf:"bytes,1,rep,name=addresses" json:"addresses"`
Misses int32 `protobuf:"varint,2,opt,name=misses,proto3" json:"misses,omitempty"`
Seen int64 `protobuf:"varint,3,opt,name=seen,proto3" json:"seen,omitempty"`
}
func (m *DatabaseRecord) Reset() { *m = DatabaseRecord{} }
func (m *DatabaseRecord) String() string { return proto.CompactTextString(m) }
func (*DatabaseRecord) ProtoMessage() {}
func (*DatabaseRecord) Descriptor() ([]byte, []int) { return fileDescriptorDatabase, []int{0} }
type ReplicationRecord struct {
Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
Addresses []DatabaseAddress `protobuf:"bytes,2,rep,name=addresses" json:"addresses"`
Seen int64 `protobuf:"varint,3,opt,name=seen,proto3" json:"seen,omitempty"`
}
func (m *ReplicationRecord) Reset() { *m = ReplicationRecord{} }
func (m *ReplicationRecord) String() string { return proto.CompactTextString(m) }
func (*ReplicationRecord) ProtoMessage() {}
func (*ReplicationRecord) Descriptor() ([]byte, []int) { return fileDescriptorDatabase, []int{1} }
type DatabaseAddress struct {
Address string `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"`
Expires int64 `protobuf:"varint,2,opt,name=expires,proto3" json:"expires,omitempty"`
}
func (m *DatabaseAddress) Reset() { *m = DatabaseAddress{} }
func (m *DatabaseAddress) String() string { return proto.CompactTextString(m) }
func (*DatabaseAddress) ProtoMessage() {}
func (*DatabaseAddress) Descriptor() ([]byte, []int) { return fileDescriptorDatabase, []int{2} }
func init() {
proto.RegisterType((*DatabaseRecord)(nil), "main.DatabaseRecord")
proto.RegisterType((*ReplicationRecord)(nil), "main.ReplicationRecord")
proto.RegisterType((*DatabaseAddress)(nil), "main.DatabaseAddress")
}
func (m *DatabaseRecord) Marshal() (dAtA []byte, err error) {
size := m.Size()
dAtA = make([]byte, size)
n, err := m.MarshalTo(dAtA)
if err != nil {
return nil, err
}
return dAtA[:n], nil
}
func (m *DatabaseRecord) MarshalTo(dAtA []byte) (int, error) {
var i int
_ = i
var l int
_ = l
if len(m.Addresses) > 0 {
for _, msg := range m.Addresses {
dAtA[i] = 0xa
i++
i = encodeVarintDatabase(dAtA, i, uint64(msg.Size()))
n, err := msg.MarshalTo(dAtA[i:])
if err != nil {
return 0, err
}
i += n
}
}
if m.Misses != 0 {
dAtA[i] = 0x10
i++
i = encodeVarintDatabase(dAtA, i, uint64(m.Misses))
}
if m.Seen != 0 {
dAtA[i] = 0x18
i++
i = encodeVarintDatabase(dAtA, i, uint64(m.Seen))
}
return i, nil
}
func (m *ReplicationRecord) Marshal() (dAtA []byte, err error) {
size := m.Size()
dAtA = make([]byte, size)
n, err := m.MarshalTo(dAtA)
if err != nil {
return nil, err
}
return dAtA[:n], nil
}
func (m *ReplicationRecord) MarshalTo(dAtA []byte) (int, error) {
var i int
_ = i
var l int
_ = l
if len(m.Key) > 0 {
dAtA[i] = 0xa
i++
i = encodeVarintDatabase(dAtA, i, uint64(len(m.Key)))
i += copy(dAtA[i:], m.Key)
}
if len(m.Addresses) > 0 {
for _, msg := range m.Addresses {
dAtA[i] = 0x12
i++
i = encodeVarintDatabase(dAtA, i, uint64(msg.Size()))
n, err := msg.MarshalTo(dAtA[i:])
if err != nil {
return 0, err
}
i += n
}
}
if m.Seen != 0 {
dAtA[i] = 0x18
i++
i = encodeVarintDatabase(dAtA, i, uint64(m.Seen))
}
return i, nil
}
func (m *DatabaseAddress) Marshal() (dAtA []byte, err error) {
size := m.Size()
dAtA = make([]byte, size)
n, err := m.MarshalTo(dAtA)
if err != nil {
return nil, err
}
return dAtA[:n], nil
}
func (m *DatabaseAddress) MarshalTo(dAtA []byte) (int, error) {
var i int
_ = i
var l int
_ = l
if len(m.Address) > 0 {
dAtA[i] = 0xa
i++
i = encodeVarintDatabase(dAtA, i, uint64(len(m.Address)))
i += copy(dAtA[i:], m.Address)
}
if m.Expires != 0 {
dAtA[i] = 0x10
i++
i = encodeVarintDatabase(dAtA, i, uint64(m.Expires))
}
return i, nil
}
func encodeFixed64Database(dAtA []byte, offset int, v uint64) int {
dAtA[offset] = uint8(v)
dAtA[offset+1] = uint8(v >> 8)
dAtA[offset+2] = uint8(v >> 16)
dAtA[offset+3] = uint8(v >> 24)
dAtA[offset+4] = uint8(v >> 32)
dAtA[offset+5] = uint8(v >> 40)
dAtA[offset+6] = uint8(v >> 48)
dAtA[offset+7] = uint8(v >> 56)
return offset + 8
}
func encodeFixed32Database(dAtA []byte, offset int, v uint32) int {
dAtA[offset] = uint8(v)
dAtA[offset+1] = uint8(v >> 8)
dAtA[offset+2] = uint8(v >> 16)
dAtA[offset+3] = uint8(v >> 24)
return offset + 4
}
func encodeVarintDatabase(dAtA []byte, offset int, v uint64) int {
for v >= 1<<7 {
dAtA[offset] = uint8(v&0x7f | 0x80)
v >>= 7
offset++
}
dAtA[offset] = uint8(v)
return offset + 1
}
func (m *DatabaseRecord) Size() (n int) {
var l int
_ = l
if len(m.Addresses) > 0 {
for _, e := range m.Addresses {
l = e.Size()
n += 1 + l + sovDatabase(uint64(l))
}
}
if m.Misses != 0 {
n += 1 + sovDatabase(uint64(m.Misses))
}
if m.Seen != 0 {
n += 1 + sovDatabase(uint64(m.Seen))
}
return n
}
func (m *ReplicationRecord) Size() (n int) {
var l int
_ = l
l = len(m.Key)
if l > 0 {
n += 1 + l + sovDatabase(uint64(l))
}
if len(m.Addresses) > 0 {
for _, e := range m.Addresses {
l = e.Size()
n += 1 + l + sovDatabase(uint64(l))
}
}
if m.Seen != 0 {
n += 1 + sovDatabase(uint64(m.Seen))
}
return n
}
func (m *DatabaseAddress) Size() (n int) {
var l int
_ = l
l = len(m.Address)
if l > 0 {
n += 1 + l + sovDatabase(uint64(l))
}
if m.Expires != 0 {
n += 1 + sovDatabase(uint64(m.Expires))
}
return n
}
func sovDatabase(x uint64) (n int) {
for {
n++
x >>= 7
if x == 0 {
break
}
}
return n
}
func sozDatabase(x uint64) (n int) {
return sovDatabase(uint64((x << 1) ^ uint64((int64(x) >> 63))))
}
func (m *DatabaseRecord) Unmarshal(dAtA []byte) error {
l := len(dAtA)
iNdEx := 0
for iNdEx < l {
preIndex := iNdEx
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowDatabase
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
fieldNum := int32(wire >> 3)
wireType := int(wire & 0x7)
if wireType == 4 {
return fmt.Errorf("proto: DatabaseRecord: wiretype end group for non-group")
}
if fieldNum <= 0 {
return fmt.Errorf("proto: DatabaseRecord: illegal tag %d (wire type %d)", fieldNum, wire)
}
switch fieldNum {
case 1:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Addresses", wireType)
}
var msglen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowDatabase
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
msglen |= (int(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
if msglen < 0 {
return ErrInvalidLengthDatabase
}
postIndex := iNdEx + msglen
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.Addresses = append(m.Addresses, DatabaseAddress{})
if err := m.Addresses[len(m.Addresses)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
return err
}
iNdEx = postIndex
case 2:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field Misses", wireType)
}
m.Misses = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowDatabase
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
m.Misses |= (int32(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
case 3:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field Seen", wireType)
}
m.Seen = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowDatabase
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
m.Seen |= (int64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
default:
iNdEx = preIndex
skippy, err := skipDatabase(dAtA[iNdEx:])
if err != nil {
return err
}
if skippy < 0 {
return ErrInvalidLengthDatabase
}
if (iNdEx + skippy) > l {
return io.ErrUnexpectedEOF
}
iNdEx += skippy
}
}
if iNdEx > l {
return io.ErrUnexpectedEOF
}
return nil
}
func (m *ReplicationRecord) Unmarshal(dAtA []byte) error {
l := len(dAtA)
iNdEx := 0
for iNdEx < l {
preIndex := iNdEx
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowDatabase
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
fieldNum := int32(wire >> 3)
wireType := int(wire & 0x7)
if wireType == 4 {
return fmt.Errorf("proto: ReplicationRecord: wiretype end group for non-group")
}
if fieldNum <= 0 {
return fmt.Errorf("proto: ReplicationRecord: illegal tag %d (wire type %d)", fieldNum, wire)
}
switch fieldNum {
case 1:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Key", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowDatabase
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
return ErrInvalidLengthDatabase
}
postIndex := iNdEx + intStringLen
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.Key = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
case 2:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Addresses", wireType)
}
var msglen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowDatabase
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
msglen |= (int(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
if msglen < 0 {
return ErrInvalidLengthDatabase
}
postIndex := iNdEx + msglen
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.Addresses = append(m.Addresses, DatabaseAddress{})
if err := m.Addresses[len(m.Addresses)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
return err
}
iNdEx = postIndex
case 3:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field Seen", wireType)
}
m.Seen = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowDatabase
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
m.Seen |= (int64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
default:
iNdEx = preIndex
skippy, err := skipDatabase(dAtA[iNdEx:])
if err != nil {
return err
}
if skippy < 0 {
return ErrInvalidLengthDatabase
}
if (iNdEx + skippy) > l {
return io.ErrUnexpectedEOF
}
iNdEx += skippy
}
}
if iNdEx > l {
return io.ErrUnexpectedEOF
}
return nil
}
func (m *DatabaseAddress) Unmarshal(dAtA []byte) error {
l := len(dAtA)
iNdEx := 0
for iNdEx < l {
preIndex := iNdEx
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowDatabase
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
fieldNum := int32(wire >> 3)
wireType := int(wire & 0x7)
if wireType == 4 {
return fmt.Errorf("proto: DatabaseAddress: wiretype end group for non-group")
}
if fieldNum <= 0 {
return fmt.Errorf("proto: DatabaseAddress: illegal tag %d (wire type %d)", fieldNum, wire)
}
switch fieldNum {
case 1:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Address", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowDatabase
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
return ErrInvalidLengthDatabase
}
postIndex := iNdEx + intStringLen
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.Address = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
case 2:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field Expires", wireType)
}
m.Expires = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowDatabase
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
m.Expires |= (int64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
default:
iNdEx = preIndex
skippy, err := skipDatabase(dAtA[iNdEx:])
if err != nil {
return err
}
if skippy < 0 {
return ErrInvalidLengthDatabase
}
if (iNdEx + skippy) > l {
return io.ErrUnexpectedEOF
}
iNdEx += skippy
}
}
if iNdEx > l {
return io.ErrUnexpectedEOF
}
return nil
}
func skipDatabase(dAtA []byte) (n int, err error) {
l := len(dAtA)
iNdEx := 0
for iNdEx < l {
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowDatabase
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
wireType := int(wire & 0x7)
switch wireType {
case 0:
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowDatabase
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
iNdEx++
if dAtA[iNdEx-1] < 0x80 {
break
}
}
return iNdEx, nil
case 1:
iNdEx += 8
return iNdEx, nil
case 2:
var length int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowDatabase
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
length |= (int(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
iNdEx += length
if length < 0 {
return 0, ErrInvalidLengthDatabase
}
return iNdEx, nil
case 3:
for {
var innerWire uint64
var start int = iNdEx
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowDatabase
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
innerWire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
innerWireType := int(innerWire & 0x7)
if innerWireType == 4 {
break
}
next, err := skipDatabase(dAtA[start:])
if err != nil {
return 0, err
}
iNdEx = start + next
}
return iNdEx, nil
case 4:
return iNdEx, nil
case 5:
iNdEx += 4
return iNdEx, nil
default:
return 0, fmt.Errorf("proto: illegal wireType %d", wireType)
}
}
panic("unreachable")
}
var (
ErrInvalidLengthDatabase = fmt.Errorf("proto: negative length found during unmarshaling")
ErrIntOverflowDatabase = fmt.Errorf("proto: integer overflow")
)
func init() { proto.RegisterFile("database.proto", fileDescriptorDatabase) }
var fileDescriptorDatabase = []byte{
// 254 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x4b, 0x49, 0x2c, 0x49,
0x4c, 0x4a, 0x2c, 0x4e, 0xd5, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0xc9, 0x4d, 0xcc, 0xcc,
0x93, 0xd2, 0x4d, 0xcf, 0x2c, 0xc9, 0x28, 0x4d, 0xd2, 0x4b, 0xce, 0xcf, 0xd5, 0x4f, 0xcf, 0x4f,
0xcf, 0xd7, 0x07, 0x4b, 0x26, 0x95, 0xa6, 0x81, 0x79, 0x60, 0x0e, 0x98, 0x05, 0xd1, 0xa4, 0x54,
0xce, 0xc5, 0xe7, 0x02, 0x35, 0x26, 0x28, 0x35, 0x39, 0xbf, 0x28, 0x45, 0xc8, 0x92, 0x8b, 0x33,
0x31, 0x25, 0xa5, 0x28, 0xb5, 0xb8, 0x38, 0xb5, 0x58, 0x82, 0x51, 0x81, 0x59, 0x83, 0xdb, 0x48,
0x54, 0x0f, 0x64, 0xb4, 0x1e, 0x4c, 0xa1, 0x23, 0x44, 0xda, 0x89, 0xe5, 0xc4, 0x3d, 0x79, 0x86,
0x20, 0x84, 0x6a, 0x21, 0x31, 0x2e, 0xb6, 0xdc, 0x4c, 0xb0, 0x3e, 0x26, 0x05, 0x46, 0x0d, 0xd6,
0x20, 0x28, 0x4f, 0x48, 0x88, 0x8b, 0xa5, 0x38, 0x35, 0x35, 0x4f, 0x82, 0x59, 0x81, 0x51, 0x83,
0x39, 0x08, 0xcc, 0x56, 0x2a, 0xe1, 0x12, 0x0c, 0x4a, 0x2d, 0xc8, 0xc9, 0x4c, 0x4e, 0x2c, 0xc9,
0xcc, 0xcf, 0x83, 0xda, 0x2d, 0xc0, 0xc5, 0x9c, 0x9d, 0x5a, 0x29, 0xc1, 0xa8, 0xc0, 0xa8, 0xc1,
0x19, 0x04, 0x62, 0xa2, 0xba, 0x86, 0x89, 0x24, 0xd7, 0x60, 0xb3, 0xd5, 0x95, 0x8b, 0x1f, 0x4d,
0x9f, 0x90, 0x04, 0x17, 0x3b, 0x54, 0x0f, 0xd4, 0x5e, 0x18, 0x17, 0x24, 0x93, 0x5a, 0x51, 0x90,
0x59, 0x04, 0xf5, 0x0f, 0x73, 0x10, 0x8c, 0xeb, 0x24, 0x70, 0xe2, 0xa1, 0x1c, 0xc3, 0x89, 0x47,
0x72, 0x8c, 0x17, 0x1e, 0xc9, 0x31, 0x3e, 0x78, 0x24, 0xc7, 0x98, 0xc4, 0x06, 0x0e, 0x4e, 0x63,
0x40, 0x00, 0x00, 0x00, 0xff, 0xff, 0x9e, 0x45, 0x60, 0x7e, 0x95, 0x01, 0x00, 0x00,
}

View File

@@ -0,0 +1,30 @@
// Copyright (C) 2018 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
syntax = "proto3";
package main;
import "github.com/gogo/protobuf/gogoproto/gogo.proto";
option (gogoproto.goproto_getters_all) = false;
message DatabaseRecord {
repeated DatabaseAddress addresses = 1 [(gogoproto.nullable) = false];
int32 misses = 2; // Number of lookups without hits
int64 seen = 3; // Unix nanos, last device announce
}
message ReplicationRecord {
string key = 1;
repeated DatabaseAddress addresses = 2 [(gogoproto.nullable) = false];
int64 seen = 3; // Unix nanos, last device announce
}
message DatabaseAddress {
string address = 1;
int64 expires = 2; // Unix nanos
}

View File

@@ -0,0 +1,211 @@
// Copyright (C) 2018 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
package main
import (
"fmt"
"os"
"testing"
"time"
)
func TestDatabaseGetSet(t *testing.T) {
os.RemoveAll("_database")
defer os.RemoveAll("_database")
db, err := newLevelDBStore("_database")
if err != nil {
t.Fatal(err)
}
go db.Serve()
defer db.Stop()
// Check missing record
rec, err := db.get("abcd")
if err != nil {
t.Error("not found should not be an error")
}
if len(rec.Addresses) != 0 {
t.Error("addresses should be empty")
}
if rec.Misses != 0 {
t.Error("missing should be zero")
}
// Set up a clock
now := time.Now()
tc := &testClock{now}
db.clock = tc
// Put a record
rec.Addresses = []DatabaseAddress{
{Address: "tcp://1.2.3.4:5", Expires: tc.Now().Add(time.Minute).UnixNano()},
}
if err := db.put("abcd", rec); err != nil {
t.Fatal(err)
}
// Verify it
rec, err = db.get("abcd")
if err != nil {
t.Fatal(err)
}
if len(rec.Addresses) != 1 {
t.Log(rec.Addresses)
t.Fatal("should have one address")
}
if rec.Addresses[0].Address != "tcp://1.2.3.4:5" {
t.Log(rec.Addresses)
t.Error("incorrect address")
}
// Wind the clock one half expiry, and merge in a new address
tc.wind(30 * time.Second)
addrs := []DatabaseAddress{
{Address: "tcp://6.7.8.9:0", Expires: tc.Now().Add(time.Minute).UnixNano()},
}
if err := db.merge("abcd", addrs, tc.Now().UnixNano()); err != nil {
t.Fatal(err)
}
// Verify it
rec, err = db.get("abcd")
if err != nil {
t.Fatal(err)
}
if len(rec.Addresses) != 2 {
t.Log(rec.Addresses)
t.Fatal("should have two addresses")
}
if rec.Addresses[0].Address != "tcp://1.2.3.4:5" {
t.Log(rec.Addresses)
t.Error("incorrect address[0]")
}
if rec.Addresses[1].Address != "tcp://6.7.8.9:0" {
t.Log(rec.Addresses)
t.Error("incorrect address[1]")
}
// Pass the first expiry time
tc.wind(45 * time.Second)
// Verify it
rec, err = db.get("abcd")
if err != nil {
t.Fatal(err)
}
if len(rec.Addresses) != 1 {
t.Log(rec.Addresses)
t.Fatal("should have one address")
}
if rec.Addresses[0].Address != "tcp://6.7.8.9:0" {
t.Log(rec.Addresses)
t.Error("incorrect address")
}
// Put a record with misses
rec = DatabaseRecord{Misses: 42}
if err := db.put("efgh", rec); err != nil {
t.Fatal(err)
}
// Verify it
rec, err = db.get("efgh")
if err != nil {
t.Fatal(err)
}
if len(rec.Addresses) != 0 {
t.Log(rec.Addresses)
t.Fatal("should have no addresses")
}
if rec.Misses != 42 {
t.Log(rec.Misses)
t.Error("incorrect misses")
}
// Set an address
addrs = []DatabaseAddress{
{Address: "tcp://6.7.8.9:0", Expires: tc.Now().Add(time.Minute).UnixNano()},
}
if err := db.merge("efgh", addrs, tc.Now().UnixNano()); err != nil {
t.Fatal(err)
}
// Verify it
rec, err = db.get("efgh")
if err != nil {
t.Fatal(err)
}
if len(rec.Addresses) != 1 {
t.Log(rec.Addresses)
t.Fatal("should have one addres")
}
if rec.Misses != 0 {
t.Log(rec.Misses)
t.Error("should have no misses")
}
}
func TestFilter(t *testing.T) {
// all cases are expired with t=10
cases := []struct {
a []DatabaseAddress
b []DatabaseAddress
}{
{
a: nil,
b: nil,
},
{
a: []DatabaseAddress{{Address: "a", Expires: 9}, {Address: "b", Expires: 9}, {Address: "c", Expires: 9}},
b: []DatabaseAddress{},
},
{
a: []DatabaseAddress{{Address: "a", Expires: 10}},
b: []DatabaseAddress{{Address: "a", Expires: 10}},
},
{
a: []DatabaseAddress{{Address: "a", Expires: 10}, {Address: "b", Expires: 10}, {Address: "c", Expires: 10}},
b: []DatabaseAddress{{Address: "a", Expires: 10}, {Address: "b", Expires: 10}, {Address: "c", Expires: 10}},
},
{
a: []DatabaseAddress{{Address: "a", Expires: 5}, {Address: "b", Expires: 15}, {Address: "c", Expires: 5}, {Address: "d", Expires: 15}, {Address: "e", Expires: 5}},
b: []DatabaseAddress{{Address: "d", Expires: 15}, {Address: "b", Expires: 15}}, // gets reordered
},
}
for _, tc := range cases {
res := expire(tc.a, 10)
if fmt.Sprint(res) != fmt.Sprint(tc.b) {
t.Errorf("Incorrect result %v, expected %v", res, tc.b)
}
}
}
type testClock struct {
now time.Time
}
func (t *testClock) wind(d time.Duration) {
t.now = t.now.Add(d)
}
func (t *testClock) Now() time.Time {
return t.now
}

View File

@@ -1,32 +0,0 @@
// Copyright (C) 2014-2015 Jakob Borg and Contributors (see the CONTRIBUTORS file).
package main
import (
"database/sql"
"fmt"
)
type setupFunc func(db *sql.DB) error
type compileFunc func(db *sql.DB) (map[string]*sql.Stmt, error)
var (
setupFuncs = make(map[string]setupFunc)
compileFuncs = make(map[string]compileFunc)
)
func register(name string, setup setupFunc, compile compileFunc) {
setupFuncs[name] = setup
compileFuncs[name] = compile
}
func setup(backend string, db *sql.DB) (map[string]*sql.Stmt, error) {
setup, ok := setupFuncs[backend]
if !ok {
return nil, fmt.Errorf("Unsupported backend")
}
if err := setup(db); err != nil {
return nil, err
}
return compileFuncs[backend](db)
}

View File

@@ -1,29 +1,70 @@
// Copyright (C) 2014-2015 Jakob Borg and Contributors (see the CONTRIBUTORS file).
// Copyright (C) 2018 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
package main
import (
"crypto/tls"
"database/sql"
"flag"
"fmt"
"log"
"net"
"net/http"
"os"
"runtime"
"strconv"
"strings"
"time"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/tlsutil"
"github.com/syndtr/goleveldb/leveldb/opt"
"github.com/thejerf/suture"
)
const (
minNegCache = 60 // seconds
maxNegCache = 3600 // seconds
maxDeviceAge = 7 * 86400 // one week, in seconds
addressExpiryTime = 2 * time.Hour
databaseStatisticsInterval = 5 * time.Minute
// Reannounce-After is set to reannounceAfterSeconds +
// random(reannounzeFuzzSeconds), similar for Retry-After
reannounceAfterSeconds = 3300
reannounzeFuzzSeconds = 300
errorRetryAfterSeconds = 1500
errorRetryFuzzSeconds = 300
// Retry for not found is minSeconds + failures * incSeconds +
// random(fuzz), where failures is the number of consecutive lookups
// with no answer, up to maxSeconds. The fuzz is applied after capping
// to maxSeconds.
notFoundRetryMinSeconds = 60
notFoundRetryMaxSeconds = 3540
notFoundRetryIncSeconds = 10
notFoundRetryFuzzSeconds = 60
// How often (in requests) we serialize the missed counter to database.
notFoundMissesWriteInterval = 10
httpReadTimeout = 5 * time.Second
httpWriteTimeout = 5 * time.Second
httpMaxHeaderBytes = 1 << 10
// Size of the replication outbox channel
replicationOutboxSize = 10000
)
// These options make the database a little more optimized for writes, at
// the expense of some memory usage and risk of losing writes in a (system)
// crash.
var levelDBOptions = &opt.Options{
NoSync: true,
WriteBuffer: 32 << 20, // default 4<<20
}
var (
Version string
BuildStamp string
@@ -43,17 +84,7 @@ func init() {
}
var (
lruSize = 10240
limitAvg = 5
limitBurst = 20
globalStats stats
statsFile string
backend = "ql"
dsn = getEnvDefault("STDISCOSRV_DB_DSN", "memory://stdiscosrv")
certFile = "cert.pem"
keyFile = "key.pem"
debug = false
useHTTP = false
debug = false
)
func main() {
@@ -63,84 +94,112 @@ func main() {
)
var listen string
var dir string
var metricsListen string
var replicationListen string
var replicationPeers string
var certFile string
var keyFile string
var useHTTP bool
log.SetOutput(os.Stdout)
log.SetFlags(0)
flag.StringVar(&certFile, "cert", "./cert.pem", "Certificate file")
flag.StringVar(&dir, "db-dir", "./discovery.db", "Database directory")
flag.BoolVar(&debug, "debug", false, "Print debug output")
flag.BoolVar(&useHTTP, "http", false, "Listen on HTTP (behind an HTTPS proxy)")
flag.StringVar(&listen, "listen", ":8443", "Listen address")
flag.IntVar(&lruSize, "limit-cache", lruSize, "Limiter cache entries")
flag.IntVar(&limitAvg, "limit-avg", limitAvg, "Allowed average package rate, per 10 s")
flag.IntVar(&limitBurst, "limit-burst", limitBurst, "Allowed burst size, packets")
flag.StringVar(&statsFile, "stats-file", statsFile, "File to write periodic operation stats to")
flag.StringVar(&backend, "db-backend", backend, "Database backend to use")
flag.StringVar(&dsn, "db-dsn", dsn, "Database DSN")
flag.StringVar(&certFile, "cert", certFile, "Certificate file")
flag.StringVar(&keyFile, "key", keyFile, "Key file")
flag.BoolVar(&debug, "debug", debug, "Debug")
flag.BoolVar(&useHTTP, "http", useHTTP, "Listen on HTTP (behind an HTTPS proxy)")
flag.StringVar(&keyFile, "key", "./key.pem", "Key file")
flag.StringVar(&metricsListen, "metrics-listen", "", "Metrics listen address")
flag.StringVar(&replicationPeers, "replicate", "", "Replication peers, id@address, comma separated")
flag.StringVar(&replicationListen, "replication-listen", ":19200", "Replication listen address")
flag.Parse()
log.Println(LongVersion)
var cert tls.Certificate
var err error
if !useHTTP {
cert, err = tls.LoadX509KeyPair(certFile, keyFile)
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
log.Println("Failed to load keypair. Generating one, this might take a while...")
cert, err = tlsutil.NewCertificate(certFile, keyFile, "stdiscosrv", 0)
if err != nil {
log.Println("Failed to load keypair. Generating one, this might take a while...")
cert, err = tlsutil.NewCertificate(certFile, keyFile, "stdiscosrv", 3072)
if err != nil {
log.Fatalln("Failed to generate X509 key pair:", err)
}
log.Fatalln("Failed to generate X509 key pair:", err)
}
devID := protocol.NewDeviceID(cert.Certificate[0])
log.Println("Server device ID is", devID)
}
db, err := sql.Open(backend, dsn)
if err != nil {
log.Fatalln("sql.Open:", err)
}
prep, err := setup(backend, db)
if err != nil {
log.Fatalln("Setup:", err)
devID := protocol.NewDeviceID(cert.Certificate[0])
log.Println("Server device ID is", devID)
// Parse the replication specs, if any.
var allowedReplicationPeers []protocol.DeviceID
var replicationDestinations []string
parts := strings.Split(replicationPeers, ",")
for _, part := range parts {
fields := strings.Split(part, "@")
switch len(fields) {
case 2:
// This is an id@address specification. Grab the address for the
// destination list. Try to resolve it once to catch obvious
// syntax errors here rather than having the sender service fail
// repeatedly later.
_, err := net.ResolveTCPAddr("tcp", fields[1])
if err != nil {
log.Fatalln("Resolving address:", err)
}
replicationDestinations = append(replicationDestinations, fields[1])
fallthrough // N.B.
case 1:
// The first part is always a device ID.
id, err := protocol.DeviceIDFromString(fields[0])
if err != nil {
log.Fatalln("Parsing device ID:", err)
}
allowedReplicationPeers = append(allowedReplicationPeers, id)
default:
log.Fatalln("Unrecognized replication spec:", part)
}
}
// Root of the service tree.
main := suture.NewSimple("main")
main.Add(&querysrv{
addr: listen,
cert: cert,
db: db,
prep: prep,
})
// Start the database.
db, err := newLevelDBStore(dir)
if err != nil {
log.Fatalln("Open database:", err)
}
main.Add(db)
main.Add(&cleansrv{
intv: cleanIntv,
db: db,
prep: prep,
})
// Start any replication senders.
var repl replicationMultiplexer
for _, dst := range replicationDestinations {
rs := newReplicationSender(dst, cert, allowedReplicationPeers)
main.Add(rs)
repl = append(repl, rs)
}
main.Add(&statssrv{
intv: statsIntv,
file: statsFile,
db: db,
})
// If we have replication configured, start the replication listener.
if len(allowedReplicationPeers) > 0 {
rl := newReplicationListener(replicationListen, cert, allowedReplicationPeers, db)
main.Add(rl)
}
globalStats.Reset()
// Start the main API server.
qs := newAPISrv(listen, cert, db, repl, useHTTP)
main.Add(qs)
// If we have a metrics port configured, start a metrics handler.
if metricsListen != "" {
go func() {
mux := http.NewServeMux()
mux.Handle("/metrics", promhttp.Handler())
log.Fatal(http.ListenAndServe(metricsListen, mux))
}()
}
// Engage!
main.Serve()
}
func getEnvDefault(key, def string) string {
if val := os.Getenv(key); val != "" {
return val
}
return def
}
func next(intv time.Duration) time.Duration {
t0 := time.Now()
t1 := t0.Add(intv).Truncate(intv)
return t1.Sub(t0)
}

View File

@@ -1,98 +0,0 @@
// Copyright (C) 2014-2015 Jakob Borg and Contributors (see the CONTRIBUTORS file).
package main
import (
"database/sql"
"fmt"
_ "github.com/lib/pq"
)
func init() {
register("postgres", postgresSetup, postgresCompile)
}
func postgresSetup(db *sql.DB) error {
var err error
db.SetMaxIdleConns(4)
db.SetMaxOpenConns(8)
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS Devices (
DeviceID CHAR(63) NOT NULL PRIMARY KEY,
Seen TIMESTAMP NOT NULL
)`)
if err != nil {
return err
}
var tmp string
row := db.QueryRow(`SELECT 'DevicesDeviceIDIndex'::regclass`)
if err = row.Scan(&tmp); err != nil {
_, err = db.Exec(`CREATE INDEX DevicesDeviceIDIndex ON Devices (DeviceID)`)
}
if err != nil {
return err
}
row = db.QueryRow(`SELECT 'DevicesSeenIndex'::regclass`)
if err = row.Scan(&tmp); err != nil {
_, err = db.Exec(`CREATE INDEX DevicesSeenIndex ON Devices (Seen)`)
}
if err != nil {
return err
}
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS Addresses (
DeviceID CHAR(63) NOT NULL,
Seen TIMESTAMP NOT NULL,
Address VARCHAR(2048) NOT NULL
)`)
if err != nil {
return err
}
row = db.QueryRow(`SELECT 'AddressesDeviceIDSeenIndex'::regclass`)
if err = row.Scan(&tmp); err != nil {
_, err = db.Exec(`CREATE INDEX AddressesDeviceIDSeenIndex ON Addresses (DeviceID, Seen)`)
}
if err != nil {
return err
}
row = db.QueryRow(`SELECT 'AddressesDeviceIDAddressIndex'::regclass`)
if err = row.Scan(&tmp); err != nil {
_, err = db.Exec(`CREATE INDEX AddressesDeviceIDAddressIndex ON Addresses (DeviceID, Address)`)
}
if err != nil {
return err
}
return nil
}
func postgresCompile(db *sql.DB) (map[string]*sql.Stmt, error) {
stmts := map[string]string{
"cleanAddress": "DELETE FROM Addresses WHERE Seen < now() - '2 hour'::INTERVAL",
"cleanDevice": fmt.Sprintf("DELETE FROM Devices WHERE Seen < now() - '%d hour'::INTERVAL", maxDeviceAge/3600),
"countAddress": "SELECT count(*) FROM Addresses",
"countDevice": "SELECT count(*) FROM Devices",
"insertAddress": "INSERT INTO Addresses (DeviceID, Seen, Address) VALUES ($1, now(), $2)",
"insertDevice": "INSERT INTO Devices (DeviceID, Seen) VALUES ($1, now())",
"selectAddress": "SELECT Address FROM Addresses WHERE DeviceID=$1 AND Seen > now() - '1 hour'::INTERVAL ORDER BY random() LIMIT 16",
"selectDevice": "SELECT Seen FROM Devices WHERE DeviceID=$1",
"updateAddress": "UPDATE Addresses SET Seen=now() WHERE DeviceID=$1 AND Address=$2",
"updateDevice": "UPDATE Devices SET Seen=now() WHERE DeviceID=$1",
}
res := make(map[string]*sql.Stmt, len(stmts))
for key, stmt := range stmts {
prep, err := db.Prepare(stmt)
if err != nil {
return nil, err
}
res[key] = prep
}
return res, nil
}

View File

@@ -1,81 +0,0 @@
// Copyright (C) 2015 Audrius Butkevicius and Contributors (see the CONTRIBUTORS file).
package main
import (
"database/sql"
"fmt"
"log"
"github.com/cznic/ql"
)
func init() {
ql.RegisterDriver()
register("ql", qlSetup, qlCompile)
}
func qlSetup(db *sql.DB) (err error) {
tx, err := db.Begin()
if err != nil {
return
}
defer func() {
if err == nil {
err = tx.Commit()
} else {
tx.Rollback()
}
}()
_, err = tx.Exec(`CREATE TABLE IF NOT EXISTS Devices (
DeviceID STRING NOT NULL,
Seen TIME NOT NULL
)`)
if err != nil {
return
}
if _, err = tx.Exec(`CREATE INDEX IF NOT EXISTS DevicesDeviceIDIndex ON Devices (DeviceID)`); err != nil {
return
}
_, err = tx.Exec(`CREATE TABLE IF NOT EXISTS Addresses (
DeviceID STRING NOT NULL,
Seen TIME NOT NULL,
Address STRING NOT NULL,
)`)
if err != nil {
return
}
_, err = tx.Exec(`CREATE INDEX IF NOT EXISTS AddressesDeviceIDAddressIndex ON Addresses (DeviceID, Address)`)
return
}
func qlCompile(db *sql.DB) (map[string]*sql.Stmt, error) {
stmts := map[string]string{
"cleanAddress": `DELETE FROM Addresses WHERE Seen < now() - duration("2h")`,
"cleanDevice": fmt.Sprintf(`DELETE FROM Devices WHERE Seen < now() - duration("%dh")`, maxDeviceAge/3600),
"countAddress": "SELECT count(*) FROM Addresses",
"countDevice": "SELECT count(*) FROM Devices",
"insertAddress": "INSERT INTO Addresses (DeviceID, Seen, Address) VALUES ($1, now(), $2)",
"insertDevice": "INSERT INTO Devices (DeviceID, Seen) VALUES ($1, now())",
"selectAddress": `SELECT Address from Addresses WHERE DeviceID==$1 AND Seen > now() - duration("1h") LIMIT 16`,
"selectDevice": "SELECT Seen FROM Devices WHERE DeviceID==$1",
"updateAddress": "UPDATE Addresses Seen=now() WHERE DeviceID==$1 AND Address==$2",
"updateDevice": "UPDATE Devices Seen=now() WHERE DeviceID==$1",
}
res := make(map[string]*sql.Stmt, len(stmts))
for key, stmt := range stmts {
prep, err := db.Prepare(stmt)
if err != nil {
log.Println("Failed to compile", stmt)
return nil, err
}
res[key] = prep
}
return res, nil
}

View File

@@ -1,492 +0,0 @@
// Copyright (C) 2014-2015 Jakob Borg and Contributors (see the CONTRIBUTORS file).
package main
import (
"bytes"
"crypto/tls"
"database/sql"
"encoding/json"
"encoding/pem"
"fmt"
"log"
"math/rand"
"net"
"net/http"
"net/url"
"strconv"
"sync"
"time"
"github.com/golang/groupcache/lru"
"github.com/syncthing/syncthing/lib/protocol"
"golang.org/x/net/context"
"golang.org/x/time/rate"
)
type querysrv struct {
addr string
db *sql.DB
prep map[string]*sql.Stmt
limiter *safeCache
cert tls.Certificate
listener net.Listener
}
type announcement struct {
Seen time.Time `json:"seen"`
Addresses []string `json:"addresses"`
}
type safeCache struct {
*lru.Cache
mut sync.Mutex
}
func (s *safeCache) Get(key string) (val interface{}, ok bool) {
s.mut.Lock()
val, ok = s.Cache.Get(key)
s.mut.Unlock()
return
}
func (s *safeCache) Add(key string, val interface{}) {
s.mut.Lock()
s.Cache.Add(key, val)
s.mut.Unlock()
}
type requestID int64
func (i requestID) String() string {
return fmt.Sprintf("%016x", int64(i))
}
type contextKey int
const idKey contextKey = iota
func negCacheFor(lastSeen time.Time) int {
since := time.Since(lastSeen).Seconds()
if since >= maxDeviceAge {
return maxNegCache
}
if since < 0 {
// That's weird
return minNegCache
}
// Return a value linearly scaled from minNegCache (at zero seconds ago)
// to maxNegCache (at maxDeviceAge seconds ago).
r := since / maxDeviceAge
return int(minNegCache + r*(maxNegCache-minNegCache))
}
func (s *querysrv) Serve() {
s.limiter = &safeCache{
Cache: lru.New(lruSize),
}
if useHTTP {
listener, err := net.Listen("tcp", s.addr)
if err != nil {
log.Println("Listen:", err)
return
}
s.listener = listener
} else {
tlsCfg := &tls.Config{
Certificates: []tls.Certificate{s.cert},
ClientAuth: tls.RequestClientCert,
SessionTicketsDisabled: 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,
},
}
tlsListener, err := tls.Listen("tcp", s.addr, tlsCfg)
if err != nil {
log.Println("Listen:", err)
return
}
s.listener = tlsListener
}
http.HandleFunc("/v2/", s.handler)
http.HandleFunc("/ping", handlePing)
srv := &http.Server{
ReadTimeout: 5 * time.Second,
WriteTimeout: 5 * time.Second,
MaxHeaderBytes: 1 << 10,
}
if err := srv.Serve(s.listener); err != nil {
log.Println("Serve:", err)
}
}
var topCtx = context.Background()
func (s *querysrv) handler(w http.ResponseWriter, req *http.Request) {
reqID := requestID(rand.Int63())
ctx := context.WithValue(topCtx, idKey, reqID)
if debug {
log.Println(reqID, req.Method, req.URL)
}
t0 := time.Now()
defer func() {
diff := time.Since(t0)
var comment string
if diff > time.Second {
comment = "(very slow request)"
} else if diff > 100*time.Millisecond {
comment = "(slow request)"
}
if comment != "" || debug {
log.Println(reqID, req.Method, req.URL, "completed in", diff, comment)
}
}()
var remoteIP net.IP
if useHTTP {
remoteIP = net.ParseIP(req.Header.Get("X-Forwarded-For"))
} else {
addr, err := net.ResolveTCPAddr("tcp", req.RemoteAddr)
if err != nil {
log.Println("remoteAddr:", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
remoteIP = addr.IP
}
if s.limit(remoteIP) {
if debug {
log.Println(remoteIP, "is limited")
}
w.Header().Set("Retry-After", "60")
http.Error(w, "Too Many Requests", 429)
return
}
switch req.Method {
case "GET":
s.handleGET(ctx, w, req)
case "POST":
s.handlePOST(ctx, remoteIP, w, req)
default:
globalStats.Error()
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
}
}
func (s *querysrv) handleGET(ctx context.Context, w http.ResponseWriter, req *http.Request) {
reqID := ctx.Value(idKey).(requestID)
deviceID, err := protocol.DeviceIDFromString(req.URL.Query().Get("device"))
if err != nil {
if debug {
log.Println(reqID, "bad device param")
}
globalStats.Error()
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
var ann announcement
ann.Seen, err = s.getDeviceSeen(deviceID)
negCache := strconv.Itoa(negCacheFor(ann.Seen))
w.Header().Set("Retry-After", negCache)
w.Header().Set("Cache-Control", "public, max-age="+negCache)
if err != nil {
// The device is not in the database.
globalStats.Query()
http.Error(w, "Not Found", http.StatusNotFound)
return
}
t0 := time.Now()
ann.Addresses, err = s.getAddresses(ctx, deviceID)
if err != nil {
log.Println(reqID, "getAddresses:", err)
globalStats.Error()
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
if debug {
log.Println(reqID, "getAddresses in", time.Since(t0))
}
globalStats.Query()
if len(ann.Addresses) == 0 {
http.Error(w, "Not Found", http.StatusNotFound)
return
}
globalStats.Answer()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(ann)
}
func (s *querysrv) handlePOST(ctx context.Context, remoteIP net.IP, w http.ResponseWriter, req *http.Request) {
reqID := ctx.Value(idKey).(requestID)
rawCert := certificateBytes(req)
if rawCert == nil {
if debug {
log.Println(reqID, "no certificates")
}
globalStats.Error()
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
var ann announcement
if err := json.NewDecoder(req.Body).Decode(&ann); err != nil {
if debug {
log.Println(reqID, "decode:", err)
}
globalStats.Error()
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
deviceID := protocol.NewDeviceID(rawCert)
// handleAnnounce returns *two* errors. The first indicates a problem with
// something the client posted to us. We should return a 400 Bad Request
// and not worry about it. The second indicates that the request was fine,
// but something internal messed up. We should log it and respond with a
// more apologetic 500 Internal Server Error.
userErr, internalErr := s.handleAnnounce(ctx, remoteIP, deviceID, ann.Addresses)
if userErr != nil {
if debug {
log.Println(reqID, "handleAnnounce:", userErr)
}
globalStats.Error()
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
if internalErr != nil {
log.Println(reqID, "handleAnnounce:", internalErr)
globalStats.Error()
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
globalStats.Announce()
// TODO: Slowly increase this for stable clients
w.Header().Set("Reannounce-After", "1800")
// We could return the lookup result here, but it's kind of unnecessarily
// expensive to go query the database again so we let the client decide to
// do a lookup if they really care.
w.WriteHeader(http.StatusNoContent)
}
func (s *querysrv) Stop() {
s.listener.Close()
}
func (s *querysrv) handleAnnounce(ctx context.Context, remote net.IP, deviceID protocol.DeviceID, addresses []string) (userErr, internalErr error) {
reqID := ctx.Value(idKey).(requestID)
tx, err := s.db.Begin()
if err != nil {
internalErr = err
return
}
defer func() {
// Since we return from a bunch of different places, we handle
// rollback in the defer.
if internalErr != nil || userErr != nil {
tx.Rollback()
}
}()
for _, annAddr := range addresses {
uri, err := url.Parse(annAddr)
if err != nil {
userErr = err
return
}
host, port, err := net.SplitHostPort(uri.Host)
if err != nil {
userErr = err
return
}
ip := net.ParseIP(host)
if host == "" || ip.IsUnspecified() {
// Do not use IPv6 remote address if requested scheme is tcp4
if uri.Scheme == "tcp4" && remote.To4() == nil {
continue
}
// Do not use IPv4 remote address if requested scheme is tcp6
if uri.Scheme == "tcp6" && remote.To4() != nil {
continue
}
host = remote.String()
}
uri.Host = net.JoinHostPort(host, port)
if err := s.updateAddress(ctx, tx, deviceID, uri.String()); err != nil {
internalErr = err
return
}
}
if err := s.updateDevice(ctx, tx, deviceID); err != nil {
internalErr = err
return
}
t0 := time.Now()
internalErr = tx.Commit()
if debug {
log.Println(reqID, "commit in", time.Since(t0))
}
return
}
func (s *querysrv) limit(remote net.IP) bool {
key := remote.String()
bkt, ok := s.limiter.Get(key)
if ok {
bkt := bkt.(*rate.Limiter)
if !bkt.Allow() {
// Rate limit exceeded; ignore packet
return true
}
} else {
// limitAvg is in packets per ten seconds.
s.limiter.Add(key, rate.NewLimiter(rate.Limit(limitAvg)/10, limitBurst))
}
return false
}
func (s *querysrv) updateDevice(ctx context.Context, tx *sql.Tx, device protocol.DeviceID) error {
reqID := ctx.Value(idKey).(requestID)
t0 := time.Now()
res, err := tx.Stmt(s.prep["updateDevice"]).Exec(device.String())
if err != nil {
return err
}
if debug {
log.Println(reqID, "updateDevice in", time.Since(t0))
}
if rows, _ := res.RowsAffected(); rows == 0 {
t0 = time.Now()
_, err := tx.Stmt(s.prep["insertDevice"]).Exec(device.String())
if err != nil {
return err
}
if debug {
log.Println(reqID, "insertDevice in", time.Since(t0))
}
}
return nil
}
func (s *querysrv) updateAddress(ctx context.Context, tx *sql.Tx, device protocol.DeviceID, uri string) error {
res, err := tx.Stmt(s.prep["updateAddress"]).Exec(device.String(), uri)
if err != nil {
return err
}
if rows, _ := res.RowsAffected(); rows == 0 {
_, err := tx.Stmt(s.prep["insertAddress"]).Exec(device.String(), uri)
if err != nil {
return err
}
}
return nil
}
func (s *querysrv) getAddresses(ctx context.Context, device protocol.DeviceID) ([]string, error) {
rows, err := s.prep["selectAddress"].Query(device.String())
if err != nil {
return nil, err
}
defer rows.Close()
var res []string
for rows.Next() {
var addr string
err := rows.Scan(&addr)
if err != nil {
log.Println("Scan:", err)
continue
}
res = append(res, addr)
}
return res, nil
}
func (s *querysrv) getDeviceSeen(device protocol.DeviceID) (time.Time, error) {
row := s.prep["selectDevice"].QueryRow(device.String())
var seen time.Time
if err := row.Scan(&seen); err != nil {
return time.Time{}, err
}
return seen.In(time.UTC), nil
}
func handlePing(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(204)
}
func certificateBytes(req *http.Request) []byte {
if req.TLS != nil && len(req.TLS.PeerCertificates) > 0 {
return req.TLS.PeerCertificates[0].Raw
}
if hdr := req.Header.Get("X-SSL-Cert"); hdr != "" {
bs := []byte(hdr)
// The certificate is in PEM format but with spaces for newlines. We
// need to reinstate the newlines for the PEM decoder. But we need to
// leave the spaces in the BEGIN and END lines - the first and last
// space - alone.
firstSpace := bytes.Index(bs, []byte(" "))
lastSpace := bytes.LastIndex(bs, []byte(" "))
for i := firstSpace + 1; i < lastSpace; i++ {
if bs[i] == ' ' {
bs[i] = '\n'
}
}
block, _ := pem.Decode(bs)
if block == nil {
// Decoding failed
return nil
}
return block.Bytes
}
return nil
}

View File

@@ -0,0 +1,304 @@
// Copyright (C) 2018 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
package main
import (
"crypto/tls"
"encoding/binary"
"fmt"
io "io"
"log"
"net"
"time"
"github.com/syncthing/syncthing/lib/protocol"
)
type replicator interface {
send(key string, addrs []DatabaseAddress, seen int64)
}
// a replicationSender tries to connect to the remote address and provide
// them with a feed of replication updates.
type replicationSender struct {
dst string
cert tls.Certificate // our certificate
allowedIDs []protocol.DeviceID
outbox chan ReplicationRecord
stop chan struct{}
}
func newReplicationSender(dst string, cert tls.Certificate, allowedIDs []protocol.DeviceID) *replicationSender {
return &replicationSender{
dst: dst,
cert: cert,
allowedIDs: allowedIDs,
outbox: make(chan ReplicationRecord, replicationOutboxSize),
stop: make(chan struct{}),
}
}
func (s *replicationSender) Serve() {
// Sleep a little at startup. Peers often restart at the same time, and
// this avoid the service failing and entering backoff state
// unnecessarily, while also reducing the reconnect rate to something
// reasonable by default.
time.Sleep(2 * time.Second)
tlsCfg := &tls.Config{
Certificates: []tls.Certificate{s.cert},
MinVersion: tls.VersionTLS12,
InsecureSkipVerify: true,
}
// Dial the TLS connection.
conn, err := tls.Dial("tcp", s.dst, tlsCfg)
if err != nil {
log.Println("Replication connect:", err)
return
}
defer func() {
conn.SetWriteDeadline(time.Now().Add(time.Second))
conn.Close()
}()
// Get the other side device ID.
remoteID, err := deviceID(conn)
if err != nil {
log.Println("Replication connect:", err)
return
}
// Verify it's in the set of allowed device IDs.
if !deviceIDIn(remoteID, s.allowedIDs) {
log.Println("Replication connect: unexpected device ID:", remoteID)
return
}
// Send records.
buf := make([]byte, 1024)
for {
select {
case rec := <-s.outbox:
// Buffer must hold record plus four bytes for size
size := rec.Size()
if len(buf) < size+4 {
buf = make([]byte, size+4)
}
// Record comes after the four bytes size
n, err := rec.MarshalTo(buf[4:])
if err != nil {
// odd to get an error here, but we haven't sent anything
// yet so it's not fatal
replicationSendsTotal.WithLabelValues("error").Inc()
log.Println("Replication marshal:", err)
continue
}
binary.BigEndian.PutUint32(buf, uint32(n))
// Send
conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
if _, err := conn.Write(buf[:4+n]); err != nil {
replicationSendsTotal.WithLabelValues("error").Inc()
log.Println("Replication write:", err)
return
}
replicationSendsTotal.WithLabelValues("success").Inc()
case <-s.stop:
return
}
}
}
func (s *replicationSender) Stop() {
close(s.stop)
}
func (s *replicationSender) String() string {
return fmt.Sprintf("replicationSender(%q)", s.dst)
}
func (s *replicationSender) send(key string, ps []DatabaseAddress, seen int64) {
item := ReplicationRecord{
Key: key,
Addresses: ps,
}
// The send should never block. The inbox is suitably buffered for at
// least a few seconds of stalls, which shouldn't happen in practice.
select {
case s.outbox <- item:
default:
replicationSendsTotal.WithLabelValues("drop").Inc()
}
}
// a replicationMultiplexer sends to multiple replicators
type replicationMultiplexer []replicator
func (m replicationMultiplexer) send(key string, ps []DatabaseAddress, seen int64) {
for _, s := range m {
// each send is nonblocking
s.send(key, ps, seen)
}
}
// replicationListener acceptes incoming connections and reads replication
// items from them. Incoming items are applied to the KV store.
type replicationListener struct {
addr string
cert tls.Certificate
allowedIDs []protocol.DeviceID
db database
stop chan struct{}
}
func newReplicationListener(addr string, cert tls.Certificate, allowedIDs []protocol.DeviceID, db database) *replicationListener {
return &replicationListener{
addr: addr,
cert: cert,
allowedIDs: allowedIDs,
db: db,
stop: make(chan struct{}),
}
}
func (l *replicationListener) Serve() {
tlsCfg := &tls.Config{
Certificates: []tls.Certificate{l.cert},
ClientAuth: tls.RequestClientCert,
MinVersion: tls.VersionTLS12,
InsecureSkipVerify: true,
}
lst, err := tls.Listen("tcp", l.addr, tlsCfg)
if err != nil {
log.Println("Replication listen:", err)
return
}
defer lst.Close()
for {
select {
case <-l.stop:
return
default:
}
// Accept a connection
conn, err := lst.Accept()
if err != nil {
log.Println("Replication accept:", err)
return
}
// Figure out the other side device ID
remoteID, err := deviceID(conn.(*tls.Conn))
if err != nil {
log.Println("Replication accept:", err)
conn.SetWriteDeadline(time.Now().Add(time.Second))
conn.Close()
continue
}
// Verify it is in the set of allowed device IDs
if !deviceIDIn(remoteID, l.allowedIDs) {
log.Println("Replication accept: unexpected device ID:", remoteID)
conn.SetWriteDeadline(time.Now().Add(time.Second))
conn.Close()
continue
}
go l.handle(conn)
}
}
func (l *replicationListener) Stop() {
close(l.stop)
}
func (l *replicationListener) String() string {
return fmt.Sprintf("replicationListener(%q)", l.addr)
}
func (l *replicationListener) handle(conn net.Conn) {
defer func() {
conn.SetWriteDeadline(time.Now().Add(time.Second))
conn.Close()
}()
buf := make([]byte, 1024)
for {
select {
case <-l.stop:
return
default:
}
conn.SetReadDeadline(time.Now().Add(time.Minute))
// First four bytes are the size
if _, err := io.ReadFull(conn, buf[:4]); err != nil {
log.Println("Replication read size:", err)
replicationRecvsTotal.WithLabelValues("error").Inc()
return
}
// Read the rest of the record
size := int(binary.BigEndian.Uint32(buf[:4]))
if len(buf) < size {
buf = make([]byte, size)
}
if _, err := io.ReadFull(conn, buf[:size]); err != nil {
log.Println("Replication read record:", err)
replicationRecvsTotal.WithLabelValues("error").Inc()
return
}
// Unmarshal
var rec ReplicationRecord
if err := rec.Unmarshal(buf[:size]); err != nil {
log.Println("Replication unmarshal:", err)
replicationRecvsTotal.WithLabelValues("error").Inc()
continue
}
// Store
l.db.merge(rec.Key, rec.Addresses, rec.Seen)
replicationRecvsTotal.WithLabelValues("success").Inc()
}
}
func deviceID(conn *tls.Conn) (protocol.DeviceID, error) {
// Handshake may not be complete on the server side yet, which we need
// to get the client certificate.
if !conn.ConnectionState().HandshakeComplete {
if err := conn.Handshake(); err != nil {
return protocol.DeviceID{}, err
}
}
// We expect exactly one certificate.
certs := conn.ConnectionState().PeerCertificates
if len(certs) != 1 {
return protocol.DeviceID{}, fmt.Errorf("unexpected number of certificates (%d != 1)", len(certs))
}
return protocol.NewDeviceID(certs[0].Raw), nil
}
func deviceIDIn(id protocol.DeviceID, ids []protocol.DeviceID) bool {
for _, candidate := range ids {
if id == candidate {
return true
}
}
return false
}

View File

@@ -1,141 +1,108 @@
// Copyright (C) 2014-2015 Jakob Borg and Contributors (see the CONTRIBUTORS file).
// Copyright (C) 2018 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
package main
import (
"bytes"
"database/sql"
"fmt"
"io/ioutil"
"log"
"os"
"sync/atomic"
"time"
"github.com/prometheus/client_golang/prometheus"
)
type stats struct {
// Incremented atomically
announces int64
queries int64
answers int64
errors int64
}
func (s *stats) Announce() {
atomic.AddInt64(&s.announces, 1)
}
func (s *stats) Query() {
atomic.AddInt64(&s.queries, 1)
}
func (s *stats) Answer() {
atomic.AddInt64(&s.answers, 1)
}
func (s *stats) Error() {
atomic.AddInt64(&s.errors, 1)
}
// Reset returns a copy of the current stats and resets the counters to
// zero.
func (s *stats) Reset() stats {
// Create a copy of the stats using atomic reads
copy := stats{
announces: atomic.LoadInt64(&s.announces),
queries: atomic.LoadInt64(&s.queries),
answers: atomic.LoadInt64(&s.answers),
errors: atomic.LoadInt64(&s.errors),
}
// Reset the stats by subtracting the values that we copied
atomic.AddInt64(&s.announces, -copy.announces)
atomic.AddInt64(&s.queries, -copy.queries)
atomic.AddInt64(&s.answers, -copy.answers)
atomic.AddInt64(&s.errors, -copy.errors)
return copy
}
type statssrv struct {
intv time.Duration
file string
db *sql.DB
}
func (s *statssrv) Serve() {
lastReset := time.Now()
for {
time.Sleep(next(s.intv))
stats := globalStats.Reset()
d := time.Since(lastReset).Seconds()
lastReset = time.Now()
log.Printf("Stats: %.02f announces/s, %.02f queries/s, %.02f answers/s, %.02f errors/s",
float64(stats.announces)/d, float64(stats.queries)/d, float64(stats.answers)/d, float64(stats.errors)/d)
if s.file != "" {
s.writeToFile(stats, d)
}
}
}
func (s *statssrv) Stop() {
panic("stop unimplemented")
}
func (s *statssrv) writeToFile(stats stats, secs float64) {
newLine := []byte("\n")
var addrs int
row := s.db.QueryRow("SELECT COUNT(*) FROM Addresses")
if err := row.Scan(&addrs); err != nil {
log.Println("stats query:", err)
return
}
fd, err := os.OpenFile(s.file, os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
log.Println("stats file:", err)
return
}
defer func() {
err = fd.Close()
if err != nil {
log.Println("stats file:", err)
}
}()
bs, err := ioutil.ReadAll(fd)
if err != nil {
log.Println("stats file:", err)
return
}
lines := bytes.Split(bytes.TrimSpace(bs), newLine)
if len(lines) > 12 {
lines = lines[len(lines)-12:]
}
latest := fmt.Sprintf("%v: %6d addresses, %8.02f announces/s, %8.02f queries/s, %8.02f answers/s, %8.02f errors/s\n",
time.Now().UTC().Format(time.RFC3339), addrs,
float64(stats.announces)/secs, float64(stats.queries)/secs, float64(stats.answers)/secs, float64(stats.errors)/secs)
lines = append(lines, []byte(latest))
_, err = fd.Seek(0, 0)
if err != nil {
log.Println("stats file:", err)
return
}
err = fd.Truncate(0)
if err != nil {
log.Println("stats file:", err)
return
}
_, err = fd.Write(bytes.Join(lines, newLine))
if err != nil {
log.Println("stats file:", err)
return
}
var (
apiRequestsTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: "syncthing",
Subsystem: "discovery",
Name: "api_requests_total",
Help: "Number of API requests.",
}, []string{"type", "result"})
apiRequestsSeconds = prometheus.NewSummaryVec(
prometheus.SummaryOpts{
Namespace: "syncthing",
Subsystem: "discovery",
Name: "api_requests_seconds",
Help: "Latency of API requests.",
Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
}, []string{"type"})
lookupRequestsTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: "syncthing",
Subsystem: "discovery",
Name: "lookup_requests_total",
Help: "Number of lookup requests.",
}, []string{"result"})
announceRequestsTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: "syncthing",
Subsystem: "discovery",
Name: "announcement_requests_total",
Help: "Number of announcement requests.",
}, []string{"result"})
replicationSendsTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: "syncthing",
Subsystem: "discovery",
Name: "replication_sends_total",
Help: "Number of replication sends.",
}, []string{"result"})
replicationRecvsTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: "syncthing",
Subsystem: "discovery",
Name: "replication_recvs_total",
Help: "Number of replication receives.",
}, []string{"result"})
databaseKeys = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: "syncthing",
Subsystem: "discovery",
Name: "database_keys",
Help: "Number of database keys at last count.",
}, []string{"category"})
databaseStatisticsSeconds = prometheus.NewGauge(
prometheus.GaugeOpts{
Namespace: "syncthing",
Subsystem: "discovery",
Name: "database_statistics_seconds",
Help: "Time spent running the statistics routine.",
})
databaseOperations = prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: "syncthing",
Subsystem: "discovery",
Name: "database_operations_total",
Help: "Number of database operations.",
}, []string{"operation", "result"})
databaseOperationSeconds = prometheus.NewSummaryVec(
prometheus.SummaryOpts{
Namespace: "syncthing",
Subsystem: "discovery",
Name: "database_operation_seconds",
Help: "Latency of database operations.",
Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
}, []string{"operation"})
)
const (
dbOpGet = "get"
dbOpPut = "put"
dbOpMerge = "merge"
dbResSuccess = "success"
dbResNotFound = "not_found"
dbResError = "error"
dbResUnmarshalError = "unmarsh_err"
)
func init() {
prometheus.MustRegister(apiRequestsTotal, apiRequestsSeconds,
lookupRequestsTotal, announceRequestsTotal,
replicationSendsTotal, replicationRecvsTotal,
databaseKeys, databaseStatisticsSeconds,
databaseOperations, databaseOperationSeconds)
}

View File

@@ -40,6 +40,7 @@ import (
"github.com/syncthing/syncthing/lib/sync"
"github.com/syncthing/syncthing/lib/tlsutil"
"github.com/syncthing/syncthing/lib/upgrade"
"github.com/syncthing/syncthing/lib/versioner"
"github.com/vitrun/qart/qr"
"golang.org/x/crypto/bcrypt"
)
@@ -95,6 +96,8 @@ type modelIntf interface {
ResetFolder(folder string)
Availability(folder, file string, version protocol.Vector, block protocol.BlockInfo) []model.Availability
GetIgnores(folder string) ([]string, []string, error)
GetFolderVersions(folder string) (map[string][]versioner.FileVersion, error)
RestoreFolderVersions(folder string, versions map[string]time.Time) (map[string]string, error)
SetIgnores(folder string, content []string) error
DelayScan(folder string, next time.Duration)
ScanFolder(folder string) error
@@ -108,6 +111,7 @@ type modelIntf interface {
RemoteSequence(folder string) (int64, bool)
State(folder string) (string, time.Time, error)
UsageReportingStats(version int, preview bool) map[string]interface{}
PullErrors(folder string) ([]model.FileError, error)
}
type configIntf interface {
@@ -259,6 +263,8 @@ func (s *apiService) Serve() {
getRestMux.HandleFunc("/rest/db/remoteneed", s.getDBRemoteNeed) // device folder [perpage] [page]
getRestMux.HandleFunc("/rest/db/status", s.getDBStatus) // folder
getRestMux.HandleFunc("/rest/db/browse", s.getDBBrowse) // folder [prefix] [dirsonly] [levels]
getRestMux.HandleFunc("/rest/folder/versions", s.getFolderVersions) // folder
getRestMux.HandleFunc("/rest/folder/pullerrors", s.getPullErrors) // folder
getRestMux.HandleFunc("/rest/events", s.getIndexEvents) // [since] [limit] [timeout] [events]
getRestMux.HandleFunc("/rest/events/disk", s.getDiskEvents) // [since] [limit] [timeout]
getRestMux.HandleFunc("/rest/stats/device", s.getDeviceStats) // -
@@ -287,6 +293,7 @@ func (s *apiService) Serve() {
postRestMux.HandleFunc("/rest/db/ignores", s.postDBIgnores) // folder
postRestMux.HandleFunc("/rest/db/override", s.postDBOverride) // folder
postRestMux.HandleFunc("/rest/db/scan", s.postDBScan) // folder [sub...] [delay]
postRestMux.HandleFunc("/rest/folder/versions", s.postFolderVersionsRestore) // folder <body>
postRestMux.HandleFunc("/rest/system/config", s.postSystemConfig) // <body>
postRestMux.HandleFunc("/rest/system/error", s.postSystemError) // <body>
postRestMux.HandleFunc("/rest/system/error/clear", s.postSystemErrorClear) // -
@@ -660,25 +667,39 @@ func (s *apiService) getDBCompletion(w http.ResponseWriter, r *http.Request) {
return
}
comp := s.model.Completion(device, folder)
sendJSON(w, map[string]interface{}{
sendJSON(w, jsonCompletion(s.model.Completion(device, folder)))
}
func jsonCompletion(comp model.FolderCompletion) map[string]interface{} {
return map[string]interface{}{
"completion": comp.CompletionPct,
"needBytes": comp.NeedBytes,
"needItems": comp.NeedItems,
"globalBytes": comp.GlobalBytes,
"needDeletes": comp.NeedDeletes,
})
}
}
func (s *apiService) getDBStatus(w http.ResponseWriter, r *http.Request) {
qs := r.URL.Query()
folder := qs.Get("folder")
sendJSON(w, folderSummary(s.cfg, s.model, folder))
if sum, err := folderSummary(s.cfg, s.model, folder); err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
} else {
sendJSON(w, sum)
}
}
func folderSummary(cfg configIntf, m modelIntf, folder string) map[string]interface{} {
func folderSummary(cfg configIntf, m modelIntf, folder string) (map[string]interface{}, error) {
var res = make(map[string]interface{})
pullErrors, err := m.PullErrors(folder)
if err != nil && err != model.ErrFolderPaused {
// Stats from the db can still be obtained if the folder is just paused
return nil, err
}
res["pullErrors"] = len(pullErrors)
res["invalid"] = "" // Deprecated, retains external API for now
global := m.GlobalSize(folder)
@@ -692,7 +713,6 @@ func folderSummary(cfg configIntf, m modelIntf, folder string) map[string]interf
res["inSyncFiles"], res["inSyncBytes"] = global.Files-need.Files, global.Bytes-need.Bytes
var err error
res["state"], res["stateChanged"], err = m.State(folder)
if err != nil {
res["error"] = err.Error()
@@ -713,7 +733,7 @@ func folderSummary(cfg configIntf, m modelIntf, folder string) map[string]interf
}
}
return res
return res, nil
}
func (s *apiService) postDBOverride(w http.ResponseWriter, r *http.Request) {
@@ -1309,6 +1329,71 @@ func (s *apiService) getPeerCompletion(w http.ResponseWriter, r *http.Request) {
sendJSON(w, comp)
}
func (s *apiService) getFolderVersions(w http.ResponseWriter, r *http.Request) {
qs := r.URL.Query()
versions, err := s.model.GetFolderVersions(qs.Get("folder"))
if err != nil {
http.Error(w, err.Error(), 500)
return
}
sendJSON(w, versions)
}
func (s *apiService) postFolderVersionsRestore(w http.ResponseWriter, r *http.Request) {
qs := r.URL.Query()
bs, err := ioutil.ReadAll(r.Body)
r.Body.Close()
if err != nil {
http.Error(w, err.Error(), 500)
return
}
var versions map[string]time.Time
err = json.Unmarshal(bs, &versions)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
ferr, err := s.model.RestoreFolderVersions(qs.Get("folder"), versions)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
sendJSON(w, ferr)
}
func (s *apiService) getPullErrors(w http.ResponseWriter, r *http.Request) {
qs := r.URL.Query()
folder := qs.Get("folder")
page, perpage := getPagingParams(qs)
errors, err := s.model.PullErrors(folder)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
start := (page - 1) * perpage
if start >= len(errors) {
errors = nil
} else {
errors = errors[start:]
if perpage < len(errors) {
errors = errors[:perpage]
}
}
sendJSON(w, map[string]interface{}{
"folder": folder,
"errors": errors,
"page": page,
"perpage": perpage,
})
}
func (s *apiService) getSystemBrowse(w http.ResponseWriter, r *http.Request) {
qs := r.URL.Query()
current := qs.Get("current")

View File

@@ -770,10 +770,9 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
m := model.NewModel(cfg, myID, "syncthing", Version, ldb, protectedFiles)
if t := os.Getenv("STDEADLOCKTIMEOUT"); len(t) > 0 {
it, err := strconv.Atoi(t)
if err == nil {
m.StartDeadlockDetector(time.Duration(it) * time.Second)
if t := os.Getenv("STDEADLOCKTIMEOUT"); t != "" {
if secs, _ := strconv.Atoi(t); secs > 0 {
m.StartDeadlockDetector(time.Duration(secs) * time.Second)
}
} else if !IsRelease || IsBeta {
m.StartDeadlockDetector(20 * time.Minute)
@@ -916,6 +915,12 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
cleanConfigDirectory()
if cfg.Options().SetLowPriority {
if err := osutil.SetLowPriority(); err != nil {
l.Warnln("Failed to lower process priority:", err)
}
}
code := <-stop
mainService.Stop()

View File

@@ -14,6 +14,7 @@ import (
"github.com/syncthing/syncthing/lib/model"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/stats"
"github.com/syncthing/syncthing/lib/versioner"
)
type mockedModel struct{}
@@ -75,6 +76,14 @@ func (m *mockedModel) SetIgnores(folder string, content []string) error {
return nil
}
func (m *mockedModel) GetFolderVersions(folder string) (map[string][]versioner.FileVersion, error) {
return nil, nil
}
func (m *mockedModel) RestoreFolderVersions(folder string, versions map[string]time.Time) (map[string]string, error) {
return nil, nil
}
func (m *mockedModel) PauseDevice(device protocol.DeviceID) {
}
@@ -123,3 +132,7 @@ func (m *mockedModel) State(folder string) (string, time.Time, error) {
func (m *mockedModel) UsageReportingStats(version int, preview bool) map[string]interface{} {
return nil
}
func (m *mockedModel) PullErrors(folder string) ([]model.FileError, error) {
return nil, nil
}

View File

@@ -187,7 +187,10 @@ func (c *folderSummaryService) foldersToHandle() []string {
func (c *folderSummaryService) sendSummary(folder string) {
// The folder summary contains how many bytes, files etc
// are in the folder and how in sync we are.
data := folderSummary(c.cfg, c.model, folder)
data, err := folderSummary(c.cfg, c.model, folder)
if err != nil {
return
}
events.Default.Log(events.FolderSummary, map[string]interface{}{
"folder": folder,
"summary": data,
@@ -205,15 +208,10 @@ func (c *folderSummaryService) sendSummary(folder string) {
// Get completion percentage of this folder for the
// remote device.
comp := c.model.Completion(devCfg.DeviceID, folder)
events.Default.Log(events.FolderCompletion, map[string]interface{}{
"folder": folder,
"device": devCfg.DeviceID.String(),
"completion": comp.CompletionPct,
"needBytes": comp.NeedBytes,
"needItems": comp.NeedItems,
"globalBytes": comp.GlobalBytes,
})
comp := jsonCompletion(c.model.Completion(devCfg.DeviceID, folder))
comp["folder"] = folder
comp["device"] = devCfg.DeviceID.String()
events.Default.Log(events.FolderCompletion, comp)
}
}

View File

@@ -243,3 +243,7 @@ code.ng-binding{
.progress .frontal {
color: #222;
}
.fancytree-title {
color: #aaa !important;
}

View File

@@ -256,3 +256,6 @@ code.ng-binding{
color: #3fa9f0;
}
.fancytree-title {
color: #aaa !important;
}

View File

@@ -371,3 +371,7 @@ ul.three-columns li, ul.two-columns li {
.tab-content {
padding-top: 10px;
}
.fancytree-ext-table {
width: 100% !important;
}

View File

@@ -27,3 +27,9 @@
.panel-heading:hover, .panel-heading:focus {
text-decoration: none;
}
.fancytree-ext-filter-hide tr.fancytree-submatch span.fancytree-title,
.fancytree-ext-filter-hide span.fancytree-node.fancytree-submatch span.fancytree-title {
color: black !important;
font-weight: lighter !important;
}

View File

@@ -28,6 +28,7 @@
"Any devices configured on an introducer device will be added to this device as well.": "Устройства настроени да представят други устройства също ще бъдат добавени към това устройство.",
"Are you sure you want to remove device {%name%}?": "Сигурни ли сте, че искате да премахнете устройство {{name}}?",
"Are you sure you want to remove folder {%label%}?": "Сигурни ли сте, че искате да премахнете папка {{label}}?",
"Are you sure you want to restore {%count%} files?": "Сигурни ли сте, че искате да възстановите {{count}} фаила?",
"Auto Accept": "Автоматично приемане",
"Automatic upgrade now offers the choice between stable releases and release candidates.": "Автоматичното обновяване вече предлага избор между стабилни версии и кандидат версии.",
"Automatic upgrades": "Автоматично обновяване",
@@ -52,6 +53,7 @@
"Copyright © 2014-2017 the following Contributors:": "Всички правата запазени © 2014-2017. Сътрудници:",
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Създаване на шаблони за игнориране, презаписване на съществуващ файл в {{path}}.",
"Danger!": "Опасност!",
"Debugging Facilities": "Дебъгин Функционалост",
"Default Folder Path": "Път до папка по подразбиране",
"Deleted": "Изтрито",
"Device": "Устройство",
@@ -66,6 +68,8 @@
"Discovered": "Открит",
"Discovery": "Откриване",
"Discovery Failures": "Грешка в откриването",
"Do not restore": "Не възстановявай",
"Do not restore all": "Не възстановявай всички",
"Documentation": "Документация",
"Download Rate": "Скорост на сваляне",
"Downloaded": "Изтеглен",
@@ -94,6 +98,8 @@
"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.": "Защитава файловете от промени направени на други устройства, но промените направени на това устройство ще бъдат синхронизирани с останалите устройства.",
"Filesystem Notifications": "Известия на системата",
"Filter by date": "Филтриране по дата",
"Filter by name": "Филтриране по име",
"Folder": "Папка",
"Folder ID": "Идентификатор на папката",
"Folder Label": "Етикет на папката",
@@ -132,10 +138,15 @@
"Learn more": "Научете повече",
"Listeners": "Синхронизиращи устройства",
"Loading data...": "Зарежадне на информация...",
"Loading...": "Зареждане...",
"Local Discovery": "Локално откриване",
"Local State": "Локално състояние",
"Local State (Total)": "Локално състояние (общо)",
"Log": "Доклад",
"Log tailing paused. Click here to continue.": "Докладът е замразен. Натиснете, за да продължите.",
"Logs": "Доклади",
"Major Upgrade": "Основно Обновяване",
"Mass actions": "Действия за всички",
"Master": "Главен",
"Maximum Age": "Максимална възраст",
"Metadata Only": "Само мета информация",
@@ -196,6 +207,8 @@
"Restart": "Рестартирай",
"Restart Needed": "Изисква се рестартиране",
"Restarting": "Рестартиране",
"Restore": "Възстановяване",
"Restore Versions": "Възстановяване на версии",
"Resume": "Пусни",
"Resume All": "Пускане на всичко",
"Reused": "Повторно използван",
@@ -205,6 +218,8 @@
"See external versioner help for supported templated command line parameters.": "Прегледайте документацията на външното приложение за версии и поддържаните от него командни параметри. ",
"See external versioning help for supported templated command line parameters.": "Прегледайте външната документацията за поддържаните командни параметри. ",
"Select a version": "Изберте версия",
"Select latest version": "Избор на най-новата версия",
"Select oldest version": "Избор на най-старата версия",
"Select the devices to share this folder with.": "Изберете устройствата, с които да споделите папката.",
"Select the folders to share with this device.": "Изберете папките за споделяне с това устройство.",
"Send & Receive": "Изпращане & получаване",
@@ -227,6 +242,7 @@
"Single level wildcard (matches within a directory only)": "Маска на едно ниво (покрива само в папка)",
"Size": "Размер",
"Smallest First": "Първо най-малките",
"Some items could not be restored:": "Някои не могат да бъдат възстановени:",
"Source Code": "Сорс код",
"Stable releases and release candidates": "Стабилни версии и кандидати за стабилни версии",
"Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "Стабилните версии са забавени с две седмици. През това време те преминават през тестване като бъдат кандидат версии.",
@@ -292,6 +308,7 @@
"Usage reporting is always enabled for candidate releases.": "Докладът за ползването е винаги включен за кандидат нови версии.",
"Use HTTPS for GUI": "Използвай HTTPS за потребителския интерфейс",
"Version": "Версия",
"Versions": "Версии",
"Versions Path": "Път до версиите",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Версиите биват изтривани автоматично, когато са по-стари от максималната възраст или надминават броя файлове разрешени в даден интервал.",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Предупреждение, този път е по-горна директория на съществуващата папка \"{{otherFolder}}\".",

View File

@@ -28,6 +28,7 @@
"Any devices configured on an introducer device will be added to this device as well.": "Tots els dispositius configurats en un dispositiu presentador seràn afegits també a aquest dispositiu.",
"Are you sure you want to remove device {%name%}?": "Are you sure you want to remove device {{name}}?",
"Are you sure you want to remove folder {%label%}?": "Are you sure you want to remove folder {{label}}?",
"Are you sure you want to restore {%count%} files?": "Are you sure you want to restore {{count}} files?",
"Auto Accept": "Auto Accept",
"Automatic upgrade now offers the choice between stable releases and release candidates.": "L'actualització automàtica ara ofereix l'elecció entre les versions estables i les versions candidates.",
"Automatic upgrades": "Actualitzacions automàtiques",
@@ -52,6 +53,7 @@
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 els següents Col·laboradors:",
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Creant patrons a ignorar, sobreescriguent un fitxer que ja existeix a {{path}}.",
"Danger!": "Perill!",
"Debugging Facilities": "Debugging Facilities",
"Default Folder Path": "Default Folder Path",
"Deleted": "Esborrat",
"Device": "Dispositiu",
@@ -66,6 +68,8 @@
"Discovered": "Descobert",
"Discovery": "Descobriment",
"Discovery Failures": "Fallades al Descobriment",
"Do not restore": "Do not restore",
"Do not restore all": "Do not restore all",
"Documentation": "Documentació",
"Download Rate": "Velocitat de descàrrega",
"Downloaded": "Descarregat",
@@ -94,6 +98,8 @@
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Els fitxers són canviats a versions amb indicació de data en una carpeta \".stversions\" quant són reemplaçats o esborrats per 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.": "Els fitxers són protegits dels canvis fets en altres dispositius, però els canvis fets en aquest dispositiu seràn enviats a la resta del grup (cluster).",
"Filesystem Notifications": "Filesystem Notifications",
"Filter by date": "Filter by date",
"Filter by name": "Filter by name",
"Folder": "Carpeta",
"Folder ID": "ID de carpeta",
"Folder Label": "Etiqueta de la Carpeta",
@@ -132,10 +138,15 @@
"Learn more": "Saber més",
"Listeners": "Escoltants",
"Loading data...": "Loading data...",
"Loading...": "Loading...",
"Local Discovery": "Descobriment local",
"Local State": "Estat local",
"Local State (Total)": "Estat Local (Total)",
"Log": "Log",
"Log tailing paused. Click here to continue.": "Log tailing paused. Click here to continue.",
"Logs": "Logs",
"Major Upgrade": "Actualització important",
"Mass actions": "Mass actions",
"Master": "Mestre",
"Maximum Age": "Edat màxima",
"Metadata Only": "Sols metadades",
@@ -196,6 +207,8 @@
"Restart": "Reiniciar",
"Restart Needed": "Reinici necesari",
"Restarting": "Reiniciant",
"Restore": "Restore",
"Restore Versions": "Restore Versions",
"Resume": "Continuar",
"Resume All": "Continuar Tot",
"Reused": "Reutilitzat",
@@ -205,6 +218,8 @@
"See external versioner help for supported templated command line parameters.": "See external versioner help for supported templated command line parameters.",
"See external versioning help for supported templated command line parameters.": "See external versioning help for supported templated command line parameters.",
"Select a version": "Select a version",
"Select latest version": "Select latest version",
"Select oldest version": "Select oldest version",
"Select the devices to share this folder with.": "Selecciona els dispositius amb els que compartir aquesta carpeta.",
"Select the folders to share with this device.": "Selecciona les carpetes per a compartir amb aquest dispositiu.",
"Send & Receive": "Enviar i Rebre",
@@ -227,6 +242,7 @@
"Single level wildcard (matches within a directory only)": "Comodí de nivell únic (coincideix sols dins d'un directori)",
"Size": "Size",
"Smallest First": "El més xicotet primer",
"Some items could not be restored:": "Some items could not be restored:",
"Source Code": "Codi font",
"Stable releases and release candidates": "Versions estables i versions candidates",
"Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "Les versions estables es retrasen sobre dos setmanes. Durant aquest temps es fiquen a prova com versions candidates.",
@@ -292,6 +308,7 @@
"Usage reporting is always enabled for candidate releases.": "Els informes d'ús sempre estan activats per a les versions candidates.",
"Use HTTPS for GUI": "Utilitzar HTTPS per a l'Interfície Gràfica d'Usuari (GUI)",
"Version": "Versió",
"Versions": "Versions",
"Versions Path": "Ruta de les versions",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Les versions s'esborren automàticament si són més antigues que l'edat màxima o excedixen el nombre de fitxer permesos en un interval.",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Perill! Esta ruta és un directori pare d'una carpeta ja existent \"{{otherFolder}}\".",

View File

@@ -28,6 +28,7 @@
"Any devices configured on an introducer device will be added to this device as well.": "Jakékoliv přístroje nakonfigurované na zavaděči budou přidány také na tento přístroj.",
"Are you sure you want to remove device {%name%}?": "Skutečně chcete odebrat zařízení {{name}}?",
"Are you sure you want to remove folder {%label%}?": "Skutečně chcete odebrat adresář {{label}}?",
"Are you sure you want to restore {%count%} files?": "Opravdu chcete obnovit {{count}} souborů?",
"Auto Accept": "Přijmout automaticky",
"Automatic upgrade now offers the choice between stable releases and release candidates.": "Automatická aktualizace nyní nabízí volbu mezi stabilními vydáními a kandidáty na vydání.",
"Automatic upgrades": "Automatické aktualizace",
@@ -52,6 +53,7 @@
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 následující přispěvatelé:",
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Vytváření ignorovaných vzorů, přepisování existujícího souboru v {{path}}.",
"Danger!": "Pozor!",
"Debugging Facilities": "Nástroje pro ladění",
"Default Folder Path": "Výchozí cesta k adresáři",
"Deleted": "Smazáno",
"Device": "Zařízení",
@@ -66,6 +68,8 @@
"Discovered": "Nalezeno",
"Discovery": "Oznamování",
"Discovery Failures": "Selhání při oznamování",
"Do not restore": "Neobnovit",
"Do not restore all": "Neobnovit vše",
"Documentation": "Dokumentace",
"Download Rate": "Rychlost stahování",
"Downloaded": "Staženo",
@@ -94,6 +98,8 @@
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Po nahrazení nebo smazání aplikací Syncthing jsou soubory přesunuty do verzí označených daty ve složce .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.": "Soubory jsou chráněny před změnami na ostatních přístrojích, ale změny provedené z tohoto přístroje budou rozeslány na zbytek clusteru.",
"Filesystem Notifications": "Oznámení souborového systému",
"Filter by date": "Vybrat podle data",
"Filter by name": "Vybrat podle názvu",
"Folder": "Adresář",
"Folder ID": "ID adresáře",
"Folder Label": "Jmenovka adresáře",
@@ -132,10 +138,15 @@
"Learn more": "Zjistěte více",
"Listeners": "Naslouchající",
"Loading data...": "Nahrávání dat...",
"Loading...": "Načítání...",
"Local Discovery": "Místní oznamování",
"Local State": "Místní status",
"Local State (Total)": "Místní status (Celkem)",
"Log": "Log",
"Log tailing paused. Click here to continue.": "Log pozastaven. Klikněte zde pro pokračování.",
"Logs": "Logy",
"Major Upgrade": "Důležitá aktualizace",
"Mass actions": "Hromadné akce",
"Master": "Master",
"Maximum Age": "Maximální časový limit",
"Metadata Only": "Pouze metadata",
@@ -196,6 +207,8 @@
"Restart": "Restart",
"Restart Needed": "Je nutný restart",
"Restarting": "Restartuji",
"Restore": "Obnovit",
"Restore Versions": "Obnovené verze",
"Resume": "Pokračovat",
"Resume All": "Pokračovat (vše)",
"Reused": "Opakovaně použité",
@@ -205,6 +218,8 @@
"See external versioner help for supported templated command line parameters.": "Pro upřesnění požadovaných parametrů příkazu navštivte nápovědu pro externí verzování.",
"See external versioning help for supported templated command line parameters.": "Podporované šablonové parametry příkazové řádky jsou dostupné v nápovědě k externímu verzování.",
"Select a version": "Vyberte verzi",
"Select latest version": "Vybrat nejnovější verzi",
"Select oldest version": "Vybrat nejstarší verzi",
"Select the devices to share this folder with.": "Vybrat přístroje, se kterými sdílet tento adresář.",
"Select the folders to share with this device.": "Vybrat adresáře sdílené s tímto přístrojem.",
"Send & Receive": "Odeslat a přijmout",
@@ -227,6 +242,7 @@
"Single level wildcard (matches within a directory only)": "Jednoúrovňový zástupný znak (shody pouze uvnitř složky)",
"Size": "Velikost",
"Smallest First": "Od nejmenšího",
"Some items could not be restored:": "Některé položky nemohly být obnoveny:",
"Source Code": "Zdrojový kód",
"Stable releases and release candidates": "Stabilní vydání a kandidáti na vydání",
"Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "Stabilní vydání jsou opožděna zhruba o dva týdny. Po tuto dobu se testují jako kandidáti na vydání.",
@@ -292,6 +308,7 @@
"Usage reporting is always enabled for candidate releases.": "Hlášení o používání je pro kandidáty na vydání vždy zapnuto.",
"Use HTTPS for GUI": "Použít HTTPS pro grafické rozhraní",
"Version": "Verze",
"Versions": "Verze",
"Versions Path": "Cesta k verzím",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Verze jsou automaticky smazány, pokud jsou starší než maximální časový limit nebo překročí počet souborů povolených pro interval.",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Varování, tato cesta je nadřazenou složkou existujícího adresáře \"{{otherFolder}}\".",

View File

@@ -28,6 +28,7 @@
"Any devices configured on an introducer device will be added to this device as well.": "Alle enheder som er konfigueret som en introducerende enhed, vil også blive tilføjet til denne enhed.",
"Are you sure you want to remove device {%name%}?": "Are you sure you want to remove device {{name}}?",
"Are you sure you want to remove folder {%label%}?": "Are you sure you want to remove folder {{label}}?",
"Are you sure you want to restore {%count%} files?": "Are you sure you want to restore {{count}} files?",
"Auto Accept": "Auto Accept",
"Automatic upgrade now offers the choice between stable releases and release candidates.": "Den automatiske opdatering tilbyder nu valget mellem stabile - og udgivelses kandidater.",
"Automatic upgrades": "Automatisk opdatering",
@@ -52,6 +53,7 @@
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 de følgende bidragsydere:",
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Creating ignore patterns, overwriting an existing file at {{path}}.",
"Danger!": "Fare!",
"Debugging Facilities": "Debugging Facilities",
"Default Folder Path": "Default Folder Path",
"Deleted": "Slettet",
"Device": "Enhed",
@@ -66,6 +68,8 @@
"Discovered": "Opdaget",
"Discovery": "Opslag",
"Discovery Failures": "Opdagelses Fejl ",
"Do not restore": "Do not restore",
"Do not restore all": "Do not restore all",
"Documentation": "Dokumentation",
"Download Rate": "Downloadhastighed",
"Downloaded": "Downloadet",
@@ -94,6 +98,8 @@
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Filer flyttes til data-stemplede versioner i en .stversions mappe når de erstattes eller slettes af Syncthing.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Filer er beskyttet fra ændringer foretaget på andre enheder, men ændringerne på denne enhed vil blive sendt til alle andre tilknyttede enheder.",
"Filesystem Notifications": "Filesystem Notifications",
"Filter by date": "Filter by date",
"Filter by name": "Filter by name",
"Folder": "Mappe",
"Folder ID": "Mappe-ID",
"Folder Label": "Mappelabel",
@@ -132,10 +138,15 @@
"Learn more": "Lær mere",
"Listeners": "Lyttere",
"Loading data...": "Loading data...",
"Loading...": "Loading...",
"Local Discovery": "Lokal opslag",
"Local State": "Lokal tilstand",
"Local State (Total)": "Lokal tilstand (total)",
"Log": "Log",
"Log tailing paused. Click here to continue.": "Log tailing paused. Click here to continue.",
"Logs": "Logs",
"Major Upgrade": "Ny version",
"Mass actions": "Mass actions",
"Master": "Mester ",
"Maximum Age": "Maks alder",
"Metadata Only": "Kun metadata",
@@ -196,6 +207,8 @@
"Restart": "Genstart",
"Restart Needed": "Programmet kræver genstart",
"Restarting": "Genstarter",
"Restore": "Restore",
"Restore Versions": "Restore Versions",
"Resume": "Genoptag",
"Resume All": "Genoptag alt",
"Reused": "Genbrugt",
@@ -205,6 +218,8 @@
"See external versioner help for supported templated command line parameters.": "See external versioner help for supported templated command line parameters.",
"See external versioning help for supported templated command line parameters.": "See external versioning help for supported templated command line parameters.",
"Select a version": "Select a version",
"Select latest version": "Select latest version",
"Select oldest version": "Select oldest version",
"Select the devices to share this folder with.": "Vælg hvilke enheder du vil dele denne mappe med",
"Select the folders to share with this device.": "Vælg hvilke mapper du vil dele med denne enhed.",
"Send & Receive": "Send & Modtag",
@@ -227,6 +242,7 @@
"Single level wildcard (matches within a directory only)": "Enkeltnivau wildcard (matcher kun inden for en mapp)",
"Size": "Size",
"Smallest First": "Mindste først",
"Some items could not be restored:": "Some items could not be restored:",
"Source Code": "Kildekode",
"Stable releases and release candidates": "Stabile udgivelser og udgivelseskandidater ",
"Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "Stabile udgivelser er forsinket med omkring 2 uger. I denne periode gennemgår de tests som udgivelseskandidater. ",
@@ -292,6 +308,7 @@
"Usage reporting is always enabled for candidate releases.": "Forbrugsraportering er altid slået til for udgivelseskandidater.",
"Use HTTPS for GUI": "Anvend HTTPS til GUI adgang",
"Version": "Version",
"Versions": "Versions",
"Versions Path": "Versions-sti",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versioner slettes automatisk, hvis de er ældre end den satte maksimum alder eller overstiger det tilladte antal filer i et interval.",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Advarsel, denne sti er en rodmappe til den eksisterende mappe \"{{otherFolder}}\".",

View File

@@ -24,14 +24,15 @@
"An external command handles the versioning. It has to remove the file from the shared folder.": "Ein externer Befehl führt die Versionierung durch. Er muss die Datei aus dem geteilten Ordner entfernen.",
"An external command handles the versioning. It has to remove the file from the synced folder.": "Ein externer Programmaufruf handhabt die Versionierung. Es muss die Datei aus dem zu synchronisierendem Ordner entfernen.",
"Anonymous Usage Reporting": "Anonymer Nutzungsbericht",
"Anonymous usage report format has changed. Would you like to move to the new format?": "Das Format der anonymen Nutzungsberichte wurde geändert. Möchten Sie auf das neue Format umsteigen?",
"Anonymous usage report format has changed. Would you like to move to the new format?": "Das Format des anonymen Nutzungsberichts hat sich geändert. Möchten Sie auf das neue Format umsteigen?",
"Any devices configured on an introducer device will be added to this device as well.": "Alle Geräte, die beim Verteilergerät eingetragen sind, werden auch bei diesem Gerät hinzugefügt.",
"Are you sure you want to remove device {%name%}?": "Sind Sie sicher, dass sie das Gerät {{name}} entfernen möchten?",
"Are you sure you want to remove folder {%label%}?": "Sind Sie sicher, dass sie den Ordner {{label}} entfernen möchten?",
"Auto Accept": "Automatisches Annehmen",
"Are you sure you want to restore {%count%} files?": "Sind Sie sicher, dass Sie {{count}} Dateien wiederherstellen möchten?",
"Auto Accept": "Automatische Annahme",
"Automatic upgrade now offers the choice between stable releases and release candidates.": "Die automatische Aktualisierung bietet jetzt die Wahl zwischen stabilen Veröffentlichungen und Veröffentlichungskandidaten.",
"Automatic upgrades": "Automatische Aktualisierungen aktivieren",
"Automatically create or share folders that this device advertises at the default path.": "Automatisches erstellen oder teilen von Ordnern, die von dem Gerät über den Standard Pfad angeboten werden.",
"Automatically create or share folders that this device advertises at the default path.": "Automatisch Ordner erstellen oder freigeben, die dieses Gerät im Standardpfad ankündigt.",
"Be careful!": "Vorsicht!",
"Bugs": "Fehler",
"CPU Utilization": "Prozessorauslastung",
@@ -52,7 +53,8 @@
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 der folgenden Unterstützer:",
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Erstelle Ignoriermuster, welche die existierende Datei {{path}} überschreiben.",
"Danger!": "Achtung!",
"Default Folder Path": "Standardmäßiger Ordnerpfad ",
"Debugging Facilities": "Debugging-Möglichkeiten",
"Default Folder Path": "Standardmäßiger Ordnerpfad",
"Deleted": "Gelöscht",
"Device": "Gerät",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Gerät \"{{name}}\" ({{device}} {{address}}) möchte sich verbinden. Gerät hinzufügen?",
@@ -66,6 +68,8 @@
"Discovered": "Ermittelt",
"Discovery": "Gerätesuche",
"Discovery Failures": "Gerätesuchfehler",
"Do not restore": "Nicht wiederherstellen",
"Do not restore all": "Nicht alle wiederherstellen",
"Documentation": "Dokumentation",
"Download Rate": "Download",
"Downloaded": "Heruntergeladen",
@@ -94,6 +98,8 @@
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Dateien werden mit Datumsstempel versioniert und in den .stversions Ordner verschoben, wenn sie von Syncthing ersetzt oder gelöscht werden.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Dateien sind auf diesem Gerät schreibgeschützt. Auf diesem Gerät durchgeführte Veränderungen werden aber auf den Rest des Verbunds übertragen.",
"Filesystem Notifications": "Dateisystembenachrichtigungen",
"Filter by date": "Nach Datum sortieren",
"Filter by name": "Nach Name sortieren",
"Folder": "Ordner",
"Folder ID": "Ordnerkennung",
"Folder Label": "Ordnerbezeichnung",
@@ -132,10 +138,15 @@
"Learn more": "Mehr erfahren",
"Listeners": "Zuhörer",
"Loading data...": "Daten werden geladen...",
"Loading...": "Wird geladen...",
"Local Discovery": "Lokale Gerätesuche",
"Local State": "Lokaler Status",
"Local State (Total)": "Lokaler Status (Gesamt)",
"Log": "Protokoll",
"Log tailing paused. Click here to continue.": "Protokollaufzeichnungen sind pausiert. Klicken Sie hier um fortzufahren.",
"Logs": "Protokolle",
"Major Upgrade": "Hauptversionsaktualisierung",
"Mass actions": "Massenaktionen",
"Master": "Master",
"Maximum Age": "Höchstalter",
"Metadata Only": "Nur Metadaten",
@@ -196,15 +207,19 @@
"Restart": "Neustart",
"Restart Needed": "Neustart benötigt",
"Restarting": "Wird neu gestartet",
"Restore": "Wiederherstellen",
"Restore Versions": "Versionen wiederherstellen",
"Resume": "Fortsetzen",
"Resume All": "Alles fortsetzen",
"Reused": "Erneut benutzt",
"Save": "Speichern",
"Scan Time Remaining": "Zeit für Scan verbleibend",
"Scanning": "Scannen",
"See external versioner help for supported templated command line parameters.": "Siehe externe Versionshilfe für unterstützte Befehlszeilenparameter.",
"See external versioner help for supported templated command line parameters.": "Siehe Hilfe des externen Versionierers für unterstützte Befehlszeilenparameter.",
"See external versioning help for supported templated command line parameters.": "Siehe Hilfe zur externen Versionierung für unterstützte Befehlszeilenparameter.",
"Select a version": "Wählen Sie eine Version",
"Select latest version": "Letzte Version auswählen",
"Select oldest version": "Älteste Version auswählen",
"Select the devices to share this folder with.": "Wähle die Geräte aus, mit denen Du diesen Ordner teilen willst.",
"Select the folders to share with this device.": "Wähle die Ordner aus, die Du mit diesem Gerät teilen möchtest",
"Send & Receive": "Senden & empfangen",
@@ -227,6 +242,7 @@
"Single level wildcard (matches within a directory only)": "Einzelnes Maskenzeichen (wird für einen einzelnen Ordner verwendet)",
"Size": "Größe",
"Smallest First": "Kleinstes zuerst",
"Some items could not be restored:": "Einige Elemente konnten nicht wiederhergestellt werden:",
"Source Code": "Quellcode",
"Stable releases and release candidates": "Stabile Veröffentlichungen und Veröffentlichungskandidaten",
"Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "Stabile Veröffentlichungen werden ca. 2 Wochen verzögert. Während dieser Zeit durchlaufen sie eine Testphase als Veröffentlichungskandidaten.",
@@ -277,7 +293,7 @@
"Trash Can File Versioning": "Papierkorb Dateiversionierung",
"Type": "Typ",
"Unavailable": " Nicht verfügbar",
"Unavailable/Disabled by administrator or maintainer": "Nicht verfügbar / Deaktiviert von Administrator oder Eigentümer",
"Unavailable/Disabled by administrator or maintainer": "Nicht verfügbar/durch Administrator oder Betreuer deaktiviert",
"Undecided (will prompt)": "Unentschlossen (wird nachgefragt)",
"Unknown": "Unbekannt",
"Unshared": "Ungeteilt",
@@ -292,6 +308,7 @@
"Usage reporting is always enabled for candidate releases.": "Nutzungsbericht ist für Veröffentlichungskandidaten immer aktiviert.",
"Use HTTPS for GUI": "HTTPS für Benutzeroberfläche verwenden",
"Version": "Version",
"Versions": "Versionen",
"Versions Path": "Versionierungspfad",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Alte Dateiversionen werden automatisch gelöscht, wenn sie älter als das angegebene Höchstalter sind oder die angegebene Höchstzahl an Dateien erreicht ist.",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Warnung, dieser Pfad ist ein übergeordnetes Verzeichnis eines existierenden Ordners \"{{otherFolder}}\".",

View File

@@ -28,6 +28,7 @@
"Any devices configured on an introducer device will be added to this device as well.": "Αν δηλωθεί σαν «βασικός κόμβος», τότε όλες οι συσκευές που είναι δηλωμένες εκεί θα υπάρχουν και στον τοπικό κόμβο.",
"Are you sure you want to remove device {%name%}?": "Σίγουρα επιθυμείτε να αφαιρέσετε τη συσκευή {{name}};",
"Are you sure you want to remove folder {%label%}?": "Σίγουρα επιθυμείτε να αφαιρέσετε τον φάκελο {{label}};",
"Are you sure you want to restore {%count%} files?": "Σίγουρα επιθυμείτε να επαναφέρετε {{count}} αρχεία;",
"Auto Accept": "Αυτόματη αποδοχή",
"Automatic upgrade now offers the choice between stable releases and release candidates.": "Για τις αυτόματες αναβαθμίσεις μπορείτε πλέον να επιλέξετε μεταξύ σταθερών εκδόσεων και υποψήφιων εκδόσεων.",
"Automatic upgrades": "Αυτόματη αναβάθμιση",
@@ -52,6 +53,7 @@
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 για τους παρακάτω συνεισφέροντες:",
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Δημιουργία προτύπων αγνόησης, αντικατάσταση του υπάρχοντος αρχείου στο {{path}}.",
"Danger!": "Προσοχή!",
"Debugging Facilities": "Εργαλεία αποσφαλμάτωσης",
"Default Folder Path": "Προκαθορισμένη διαδρομή φακέλων",
"Deleted": "Διαγραμμένα",
"Device": "Συσκευή",
@@ -66,6 +68,8 @@
"Discovered": "Βάσει ανεύρεσης",
"Discovery": "Ανεύρεση συσκευών",
"Discovery Failures": "Αποτυχίες ανεύρεσης συσκευών",
"Do not restore": "Να μη γίνει επαναφορά",
"Do not restore all": "Να μη γίνει επαναφορά όλων",
"Documentation": "Τεκμηρίωση",
"Download Rate": "Ταχύτητα λήψης",
"Downloaded": "Έχει ληφθεί",
@@ -94,6 +98,8 @@
"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.": "Τα αρχεία προστατεύονται από αλλαγές που γίνονται σε άλλες συσκευές, αλλά όποιες αλλαγές γίνουν σε αυτή τη συσκευή θα αποσταλούν σε όλη τη συστάδα συσκευών.",
"Filesystem Notifications": "Ειδοποιήσεις συστήματος αρχείων",
"Filter by date": "Φιλτράρισμα κατά ημερομηνία",
"Filter by name": "Φιλτράρισμα κατά όνομα",
"Folder": "Φάκελος",
"Folder ID": "Ταυτότητα φακέλου",
"Folder Label": "Ετικέτα φακέλου",
@@ -132,10 +138,15 @@
"Learn more": "Μάθετε περισσότερα",
"Listeners": "Ακροατές",
"Loading data...": "Φόρτωση δεδομένων...",
"Loading...": "Φόρτωση...",
"Local Discovery": "Τοπική ανεύρεση",
"Local State": "Τοπική κατάσταση",
"Local State (Total)": "Τοπική κατάσταση (συνολικά)",
"Log": "Αρχείο καταγραφής",
"Log tailing paused. Click here to continue.": "Η αυτόματη κύλιση έχει διακοπεί. Πατήστε εδώ για να συνεχιστεί.",
"Logs": "Αρχεία καταγραφής",
"Major Upgrade": "Σημαντική αναβάθμιση",
"Mass actions": "Μαζικές ενέργειες",
"Master": "Βασικός κόμβος",
"Maximum Age": "Μέγιστη ηλικία",
"Metadata Only": "Μόνο μεταδεδομένα",
@@ -196,6 +207,8 @@
"Restart": "Επανεκκίνηση",
"Restart Needed": "Απαιτείται επανεκκίνηση",
"Restarting": "Επανεκκίνηση",
"Restore": "Επαναφορά",
"Restore Versions": "Επαναφορά εκδόσεων",
"Resume": "Συνέχεια",
"Resume All": "Συνέχιση όλων",
"Reused": "Χρησιμοποιήθηκε ξανά",
@@ -205,6 +218,8 @@
"See external versioner help for supported templated command line parameters.": "Ανατρέξτε στην τεκμηρίωση της εξωτερικής τήρησης εκδόσεων για πληροφορίες σχετικά με τις υποστηριζόμενες παραμέτρους της γραμμής εντολών.",
"See external versioning help for supported templated command line parameters.": "Ανατρέξτε στην τεκμηρίωση της εξωτερικής τήρησης εκδόσεων για πληροφορίες σχετικά με τις υποστηριζόμενες παραμέτρους της γραμμής εντολών.",
"Select a version": "Επιλογή έκδοσης",
"Select latest version": "Επιλογή τελευταίας έκδοσης",
"Select oldest version": "Επιλογή παλαιότερης έκδοσης",
"Select the devices to share this folder with.": "Διάλεξε τις συσκευές προς τις οποίες θα διαμοιράζεται αυτός ο φάκελος.",
"Select the folders to share with this device.": "Διάλεξε ποιοι φάκελοι θα διαμοιράζονται προς αυτή τη συσκευή.",
"Send & Receive": "Αποστολή και λήψη",
@@ -227,6 +242,7 @@
"Single level wildcard (matches within a directory only)": "Τελεστής μπαλαντέρ (*) για ένα επίπεδο (χρησιμοποιείται για έναν φάκελο μόνο)",
"Size": "Μέγεθος",
"Smallest First": "Το μικρότερο πρώτα",
"Some items could not be restored:": "Κάποια στοιχεία δεν μπόρεσαν να επαναφερθούν:",
"Source Code": "Πηγαίος κώδικας",
"Stable releases and release candidates": "Σταθερές εκδόσεις και υποψήφιες εκδόσεις.",
"Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "Οι σταθερές εκδόσεις βγαίνουν με καθυστέρηση περίπου δύο εβδομάδων. Σε αυτό το διάστημα δοκιμάζονται ως υποψήφιες εκδόσεις.",
@@ -292,6 +308,7 @@
"Usage reporting is always enabled for candidate releases.": "Η αποστολή αναφορών χρήσης είναι πάντα ενεργοποιημένη στις υποψήφιες εκδόσεις.",
"Use HTTPS for GUI": "Χρήση HTTPS για τη διεπαφή",
"Version": "Έκδοση",
"Versions": "Εκδόσεις",
"Versions Path": "Φάκελος τήρησης εκδόσεων",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Οι παλιές εκδόσεις θα σβήνονται αυτόματα όταν ξεπεράσουν τη μέγιστη ηλικία ή όταν ξεπεραστεί ο μέγιστος αριθμός αρχείων ανά περίοδο.",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Προσοχή, αυτό το μονοπάτι είναι γονικός φάκελος ενός υπάρχοντος φακέλου \"{{otherFolder}}\".",

View File

@@ -28,6 +28,7 @@
"Any devices configured on an introducer device will be added to this device as well.": "Any devices configured on an introducer device will be added to this device as well.",
"Are you sure you want to remove device {%name%}?": "Are you sure you want to remove device {{name}}?",
"Are you sure you want to remove folder {%label%}?": "Are you sure you want to remove folder {{label}}?",
"Are you sure you want to restore {%count%} files?": "Are you sure you want to restore {{count}} files?",
"Auto Accept": "Auto Accept",
"Automatic upgrade now offers the choice between stable releases and release candidates.": "Automatic upgrade now offers the choice between stable releases and release candidates.",
"Automatic upgrades": "Automatic upgrades",
@@ -52,6 +53,7 @@
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 the following Contributors:",
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Creating ignore patterns, overwriting an existing file at {{path}}.",
"Danger!": "Danger!",
"Debugging Facilities": "Debugging Facilities",
"Default Folder Path": "Default Folder Path",
"Deleted": "Deleted",
"Device": "Device",
@@ -66,6 +68,8 @@
"Discovered": "Discovered",
"Discovery": "Discovery",
"Discovery Failures": "Discovery Failures",
"Do not restore": "Do not restore",
"Do not restore all": "Do not restore all",
"Documentation": "Documentation",
"Download Rate": "Download Rate",
"Downloaded": "Downloaded",
@@ -94,6 +98,8 @@
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Files are moved to date stamped versions in a .stversions folder when replaced or deleted by 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.": "Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.",
"Filesystem Notifications": "Filesystem Notifications",
"Filter by date": "Filter by date",
"Filter by name": "Filter by name",
"Folder": "Folder",
"Folder ID": "Folder ID",
"Folder Label": "Folder Label",
@@ -132,10 +138,15 @@
"Learn more": "Learn more",
"Listeners": "Listeners",
"Loading data...": "Loading data...",
"Loading...": "Loading...",
"Local Discovery": "Local Discovery",
"Local State": "Local State",
"Local State (Total)": "Local State (Total)",
"Log": "Log",
"Log tailing paused. Click here to continue.": "Log tailing paused. Click here to continue.",
"Logs": "Logs",
"Major Upgrade": "Major Upgrade",
"Mass actions": "Mass actions",
"Master": "Master",
"Maximum Age": "Maximum Age",
"Metadata Only": "Metadata Only",
@@ -196,6 +207,8 @@
"Restart": "Restart",
"Restart Needed": "Restart Needed",
"Restarting": "Restarting",
"Restore": "Restore",
"Restore Versions": "Restore Versions",
"Resume": "Resume",
"Resume All": "Resume All",
"Reused": "Reused",
@@ -205,6 +218,8 @@
"See external versioner help for supported templated command line parameters.": "See external versioner help for supported templated command line parameters.",
"See external versioning help for supported templated command line parameters.": "See external versioning help for supported templated command line parameters.",
"Select a version": "Select a version",
"Select latest version": "Select latest version",
"Select oldest version": "Select oldest version",
"Select the devices to share this folder with.": "Select the devices to share this folder with.",
"Select the folders to share with this device.": "Select the folders to share with this device.",
"Send & Receive": "Send & Receive",
@@ -227,6 +242,7 @@
"Single level wildcard (matches within a directory only)": "Single level wildcard (matches within a directory only)",
"Size": "Size",
"Smallest First": "Smallest First",
"Some items could not be restored:": "Some items could not be restored:",
"Source Code": "Source Code",
"Stable releases and release candidates": "Stable releases and release candidates",
"Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.",
@@ -292,6 +308,7 @@
"Usage reporting is always enabled for candidate releases.": "Usage reporting is always enabled for candidate releases.",
"Use HTTPS for GUI": "Use HTTPS for GUI",
"Version": "Version",
"Versions": "Versions",
"Versions Path": "Versions Path",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Warning, this path is a parent directory of an existing folder \"{{otherFolder}}\".",

View File

@@ -28,6 +28,7 @@
"Any devices configured on an introducer device will be added to this device as well.": "Any devices configured on an introducer device will be added to this device as well.",
"Are you sure you want to remove device {%name%}?": "Are you sure you want to remove device {{name}}?",
"Are you sure you want to remove folder {%label%}?": "Are you sure you want to remove folder {{label}}?",
"Are you sure you want to restore {%count%} files?": "Are you sure you want to restore {{count}} files?",
"Auto Accept": "Auto Accept",
"Automatic upgrade now offers the choice between stable releases and release candidates.": "Automatic upgrade now offers the choice between stable releases and release candidates.",
"Automatic upgrades": "Automatic upgrades",
@@ -67,6 +68,8 @@
"Discovered": "Discovered",
"Discovery": "Discovery",
"Discovery Failures": "Discovery Failures",
"Do not restore": "Do not restore",
"Do not restore all": "Do not restore all",
"Documentation": "Documentation",
"Download Rate": "Download Rate",
"Downloaded": "Downloaded",
@@ -95,6 +98,8 @@
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Files are moved to date stamped versions in a .stversions folder when replaced or deleted by 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.": "Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.",
"Filesystem Notifications": "Filesystem Notifications",
"Filter by date": "Filter by date",
"Filter by name": "Filter by name",
"Folder": "Folder",
"Folder ID": "Folder ID",
"Folder Label": "Folder Label",
@@ -141,6 +146,7 @@
"Log tailing paused. Click here to continue.": "Log tailing paused. Click here to continue.",
"Logs": "Logs",
"Major Upgrade": "Major Upgrade",
"Mass actions": "Mass actions",
"Master": "Master",
"Maximum Age": "Maximum Age",
"Metadata Only": "Metadata Only",
@@ -201,6 +207,8 @@
"Restart": "Restart",
"Restart Needed": "Restart Needed",
"Restarting": "Restarting",
"Restore": "Restore",
"Restore Versions": "Restore Versions",
"Resume": "Resume",
"Resume All": "Resume All",
"Reused": "Reused",
@@ -210,6 +218,8 @@
"See external versioner help for supported templated command line parameters.": "See external versioner help for supported templated command line parameters.",
"See external versioning help for supported templated command line parameters.": "See external versioning help for supported templated command line parameters.",
"Select a version": "Select a version",
"Select latest version": "Select latest version",
"Select oldest version": "Select oldest version",
"Select the devices to share this folder with.": "Select the devices to share this folder with.",
"Select the folders to share with this device.": "Select the folders to share with this device.",
"Send \u0026 Receive": "Send \u0026 Receive",
@@ -232,6 +242,7 @@
"Single level wildcard (matches within a directory only)": "Single level wildcard (matches within a directory only)",
"Size": "Size",
"Smallest First": "Smallest First",
"Some items could not be restored:": "Some items could not be restored:",
"Source Code": "Source Code",
"Stable releases and release candidates": "Stable releases and release candidates",
"Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.",
@@ -297,6 +308,7 @@
"Usage reporting is always enabled for candidate releases.": "Usage reporting is always enabled for candidate releases.",
"Use HTTPS for GUI": "Use HTTPS for GUI",
"Version": "Version",
"Versions": "Versions",
"Versions Path": "Versions Path",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Warning, this path is a parent directory of an existing folder \"{{otherFolder}}\".",

View File

@@ -28,6 +28,7 @@
"Any devices configured on an introducer device will be added to this device as well.": "Ajna aparatoj agorditaj sur enkondukanto aparato estos ankaŭ aldonita al ĉi tiu aparato.",
"Are you sure you want to remove device {%name%}?": "Are you sure you want to remove device {{name}}?",
"Are you sure you want to remove folder {%label%}?": "Are you sure you want to remove folder {{label}}?",
"Are you sure you want to restore {%count%} files?": "Are you sure you want to restore {{count}} files?",
"Auto Accept": "Auto Accept",
"Automatic upgrade now offers the choice between stable releases and release candidates.": "Aŭtomata ĝisdatigo nun proponas la elekton inter stabilaj eldonoj kaj kandidataj eldonoj.",
"Automatic upgrades": "Aŭtomataj ĝisdatigoj",
@@ -52,6 +53,7 @@
"Copyright © 2014-2017 the following Contributors:": "Kopirajto © 2014-2017 por la sekvantaj Kontribuantoj:",
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Kreante ignori skemojn, anstataŭige ekzistantan dosieron ĉe {{path}}.",
"Danger!": "Danĝero!",
"Debugging Facilities": "Debugging Facilities",
"Default Folder Path": "Default Folder Path",
"Deleted": "Forigita",
"Device": "Aparato",
@@ -66,6 +68,8 @@
"Discovered": "Malkovrita",
"Discovery": "Malkovro",
"Discovery Failures": "Malkovritaj Fiaskoj",
"Do not restore": "Do not restore",
"Do not restore all": "Do not restore all",
"Documentation": "Dokumentado",
"Download Rate": "Elŝutado rapida",
"Downloaded": "Elŝutita",
@@ -94,6 +98,8 @@
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Dosieroj estas movigitaj al datostampitaj versioj en .stversions dosierujoj kiam ili estas anstataŭigitaj aŭ forigitaj en 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.": "Dosieroj estas protektataj kontraŭ ŝanĝoj faritaj en aliaj aparatoj, sed ŝanĝoj faritaj en ĉi tiu aparato estos senditaj al cetera parto de la grupo.",
"Filesystem Notifications": "Filesystem Notifications",
"Filter by date": "Filter by date",
"Filter by name": "Filter by name",
"Folder": "Dosierujo",
"Folder ID": "Dosieruja ID",
"Folder Label": "Dosieruja Etikedo",
@@ -132,10 +138,15 @@
"Learn more": "Lerni pli",
"Listeners": "Aŭskultantoj",
"Loading data...": "Loading data...",
"Loading...": "Loading...",
"Local Discovery": "Loka Malkovro",
"Local State": "Loka Stato",
"Local State (Total)": "Loka Stato (Tuta)",
"Log": "Log",
"Log tailing paused. Click here to continue.": "Log tailing paused. Click here to continue.",
"Logs": "Logs",
"Major Upgrade": "Ĉefa Ĝisdatigo",
"Mass actions": "Mass actions",
"Master": "Ĉefa",
"Maximum Age": "Maksimuma Aĝo",
"Metadata Only": "Nur Metadatumoj",
@@ -196,6 +207,8 @@
"Restart": "Restartu",
"Restart Needed": "Restarto Bezonata",
"Restarting": "Restartado",
"Restore": "Restore",
"Restore Versions": "Restore Versions",
"Resume": "Daŭrigu",
"Resume All": "Daŭrigu Ĉion",
"Reused": "Reuzita",
@@ -205,6 +218,8 @@
"See external versioner help for supported templated command line parameters.": "See external versioner help for supported templated command line parameters.",
"See external versioning help for supported templated command line parameters.": "See external versioning help for supported templated command line parameters.",
"Select a version": "Select a version",
"Select latest version": "Select latest version",
"Select oldest version": "Select oldest version",
"Select the devices to share this folder with.": "Elekti la aparatojn por komunigi ĉi tiun dosierujon.",
"Select the folders to share with this device.": "Elekti la dosierujojn por komunigi kun ĉi tiu aparato.",
"Send & Receive": "Sendi kaj Ricevi",
@@ -227,6 +242,7 @@
"Single level wildcard (matches within a directory only)": "Ununura nivelo ĵokero (egalas nur ene de dosierujo)",
"Size": "Size",
"Smallest First": "Plej Malgranda Unue",
"Some items could not be restored:": "Some items could not be restored:",
"Source Code": "Fontkodo",
"Stable releases and release candidates": "Stabilaj eldonoj kaj kandidataj eldonoj",
"Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "Stabilaj eldonoj prokrastas je ĉirkaŭ du semjanoj. Dum tiu tempo ili estos testataj kiel kandidataj eldonoj.",
@@ -292,6 +308,7 @@
"Usage reporting is always enabled for candidate releases.": "Uzada raportado ĉiam ŝaltita por kandidataj ĵetoj.",
"Use HTTPS for GUI": "Uzi HTTPS por grafika interfaco.",
"Version": "Versio",
"Versions": "Versions",
"Versions Path": "Vojo de Versioj",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versioj estas aŭtomate forigita se ili estas pli malnovaj ol la maksimuma aĝo aŭ superas la nombron da dosieroj permesita en intervalo.",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Averto, ĉi tiu vojo estas parenta dosierujo de ekzistanta dosierujo \"{{otherFolder}}\".",

View File

@@ -28,6 +28,7 @@
"Any devices configured on an introducer device will be added to this device as well.": "Cualquier dispositivo configurado en un dispositivo de introducción será añadido también.",
"Are you sure you want to remove device {%name%}?": "Are you sure you want to remove device {{name}}?",
"Are you sure you want to remove folder {%label%}?": "Are you sure you want to remove folder {{label}}?",
"Are you sure you want to restore {%count%} files?": "Are you sure you want to restore {{count}} files?",
"Auto Accept": "Auto Accept",
"Automatic upgrade now offers the choice between stable releases and release candidates.": "Ahora la actualización automática permite elegir entre versiones estables o versiones candidatas.",
"Automatic upgrades": "Actualizaciones automáticas",
@@ -52,6 +53,7 @@
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 Los siguientes colaboradores:",
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Crear patrones a ignorar, sobreescribiendo un fichero existente en {{path}}.",
"Danger!": "¡Peligro!",
"Debugging Facilities": "Debugging Facilities",
"Default Folder Path": "Default Folder Path",
"Deleted": "Eliminado",
"Device": "Dispositivo",
@@ -66,6 +68,8 @@
"Discovered": "Descubierto",
"Discovery": "Descubrimiento",
"Discovery Failures": "Fallos de Descubrimiento",
"Do not restore": "Do not restore",
"Do not restore all": "Do not restore all",
"Documentation": "Documentación",
"Download Rate": "Velocidad de descarga",
"Downloaded": "Descargado",
@@ -94,6 +98,8 @@
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Los ficheros son cambiados a versiones con indicación de fecha en una carpeta \".stversions\" cuando son reemplazados o borrados por 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.": "Los ficheros son protegidos por los cambios hechos en otros dispositivos, pero los cambios hechos en este dispositivo serán enviados al resto del grupo (cluster).",
"Filesystem Notifications": "Filesystem Notifications",
"Filter by date": "Filter by date",
"Filter by name": "Filter by name",
"Folder": "Carpeta",
"Folder ID": "ID de carpeta",
"Folder Label": "Etiqueta de la Carpeta",
@@ -132,10 +138,15 @@
"Learn more": "Saber más",
"Listeners": "Oyentes",
"Loading data...": "Loading data...",
"Loading...": "Loading...",
"Local Discovery": "Descubrimiento local",
"Local State": "Estado local",
"Local State (Total)": "Estado Local (Total)",
"Log": "Log",
"Log tailing paused. Click here to continue.": "Log tailing paused. Click here to continue.",
"Logs": "Logs",
"Major Upgrade": "Actualización importante",
"Mass actions": "Mass actions",
"Master": "Maestro",
"Maximum Age": "Edad máxima",
"Metadata Only": "Sólo metadatos",
@@ -196,6 +207,8 @@
"Restart": "Reiniciar",
"Restart Needed": "Reinicio necesario",
"Restarting": "Reiniciando",
"Restore": "Restore",
"Restore Versions": "Restore Versions",
"Resume": "Continuar",
"Resume All": "Continuar todo",
"Reused": "Reutilizado",
@@ -205,6 +218,8 @@
"See external versioner help for supported templated command line parameters.": "See external versioner help for supported templated command line parameters.",
"See external versioning help for supported templated command line parameters.": "See external versioning help for supported templated command line parameters.",
"Select a version": "Select a version",
"Select latest version": "Select latest version",
"Select oldest version": "Select oldest version",
"Select the devices to share this folder with.": "Selecciona los dispositivos con los que compartir esta carpeta.",
"Select the folders to share with this device.": "Selecciona las carpetas para compartir con este dispositivo.",
"Send & Receive": "Enviar y Recibir",
@@ -227,6 +242,7 @@
"Single level wildcard (matches within a directory only)": "Comodín de nivel único (coincide solamente dentro de un directorio)",
"Size": "Size",
"Smallest First": "El más pequeño primero",
"Some items could not be restored:": "Some items could not be restored:",
"Source Code": "Código fuente",
"Stable releases and release candidates": "Versiones estables y versiones candidatas",
"Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "Las versiones estables son publicadas cada dos semanas. Durante este tiempo son probadas como versiones candidatas.",
@@ -292,6 +308,7 @@
"Usage reporting is always enabled for candidate releases.": "El informe de uso está siempre habilitado en las versiones candidatas.",
"Use HTTPS for GUI": "Usar HTTPS para la Interfaz Gráfica de Usuario (GUI)",
"Version": "Versión",
"Versions": "Versions",
"Versions Path": "Ruta de las versiones",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Las versiones se borran automáticamente si son más antiguas que la edad máxima o exceden el número de ficheros permitidos en un intervalo.",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "¡Peligro! Esta ruta es un directorio principal de la carpeta ya existente \"{{otherFolder}}\".",

View File

@@ -1,5 +1,5 @@
{
"A device with that ID is already added.": "Ya se ha agregado un equipo con ese ID.",
"A device with that ID is already added.": "Ya se ha agregado un dispositivo con ese ID.",
"A negative number of days doesn't make sense.": "Un número negativo de días no tiene sentido.",
"A new major version may not be compatible with previous versions.": "Una nueva versión con cambios importantes puede no ser compatible con versiones anteriores.",
"API Key": "Clave del API",
@@ -24,14 +24,15 @@
"An external command handles the versioning. It has to remove the file from the shared folder.": "Un comando externo gestiona las versiones. Tiene que eliminar el fichero de la carpeta compartida.",
"An external command handles the versioning. It has to remove the file from the synced folder.": "Un comando externo controla la versión. El fichero debe ser eliminado de la carpeta sincronizada.",
"Anonymous Usage Reporting": "Informe anónimo de uso",
"Anonymous usage report format has changed. Would you like to move to the new format?": "Anonymous usage report format has changed. Would you like to move to the new format?",
"Anonymous usage report format has changed. Would you like to move to the new format?": "El formato del informe de uso anónimo a cambiado. ¿Desearía usar el nuevo formato?",
"Any devices configured on an introducer device will be added to this device as well.": "Cualquier dispositivo configurado en un dispositivo de introducción será añadido también.",
"Are you sure you want to remove device {%name%}?": "Are you sure you want to remove device {{name}}?",
"Are you sure you want to remove folder {%label%}?": "Are you sure you want to remove folder {{label}}?",
"Auto Accept": "Auto Accept",
"Are you sure you want to remove device {%name%}?": "¿Está seguro que desea eliminar el dispositivo {{name}}?",
"Are you sure you want to remove folder {%label%}?": "¿Está seguro que desea eliminar la carpeta {{label}}?",
"Are you sure you want to restore {%count%} files?": "¿Está seguro que desea restaurar {{count}} archivos?",
"Auto Accept": "Aceptar automáticamente",
"Automatic upgrade now offers the choice between stable releases and release candidates.": "Ahora la actualización automática permite elegir entre versiones estables o versiones candidatas.",
"Automatic upgrades": "Actualizaciones automáticas",
"Automatically create or share folders that this device advertises at the default path.": "Automatically create or share folders that this device advertises at the default path.",
"Automatically create or share folders that this device advertises at the default path.": "Crear o compartir automáticamente carpetas que este dispositivo anuncia en la ruta por defecto.",
"Be careful!": "¡Ten cuidado!",
"Bugs": "Errores",
"CPU Utilization": "Uso de CPU",
@@ -45,27 +46,30 @@
"Configured": "Configurado",
"Connection Error": "Error de conexión",
"Connection Type": "Tipo de conexión",
"Connections": "Connections",
"Connections": "Conexiones",
"Copied from elsewhere": "Copiado de otro sitio",
"Copied from original": "Copiado del original",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 los siguientes Colaboradores:",
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 Los siguientes colaboradores:",
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Crear patrones a ignorar, sobreescribiendo un fichero existente en {{path}}.",
"Danger!": "¡Peligro!",
"Default Folder Path": "Default Folder Path",
"Debugging Facilities": "Servicios de depuración",
"Default Folder Path": "Ruta de la carpeta por defecto",
"Deleted": "Eliminado",
"Device": "Dispositivo",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "El dispositivo \"{{name}}\" ({{device}} en la dirección {{address}}) quiere conectarse. Añadir nuevo dispositivo?",
"Device ID": "ID del Dispositivo",
"Device Identification": "Identificación del Dispositivo",
"Device Name": "Nombre del Dispositivo",
"Device that last modified the item": "Device that last modified the item",
"Device that last modified the item": "Dispositivo que modificó por última vez el ítem",
"Devices": "Dispositivos",
"Disabled": "Disabled",
"Disabled": "Deshabilitado",
"Disconnected": "Desconectado",
"Discovered": "Descubierto",
"Discovery": "Descubrimiento",
"Discovery Failures": "Fallos de Descubrimiento",
"Do not restore": "No restaurar",
"Do not restore all": "No restaurar todos",
"Documentation": "Documentación",
"Download Rate": "Velocidad de descarga",
"Downloaded": "Descargado",
@@ -93,7 +97,9 @@
"Files are moved to date stamped versions in a .stversions directory when replaced or deleted by Syncthing.": "Los ficheros son movidos a una carpeta .stversions a versiones con control de fecha cuando son reemplazados o borrados por Syncthing.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Los ficheros son cambiados a versiones con indicación de fecha en una carpeta \".stversions\" cuando son reemplazados o borrados por 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.": "Los ficheros son protegidos por los cambios hechos en otros dispositivos, pero los cambios hechos en este dispositivo serán enviados al resto del grupo (cluster).",
"Filesystem Notifications": "Filesystem Notifications",
"Filesystem Notifications": "Notificaciones del sistema de archivos",
"Filter by date": "Filtrar por fecha",
"Filter by name": "Filtrar por nombre",
"Folder": "Carpeta",
"Folder ID": "ID de carpeta",
"Folder Label": "Etiqueta de la Carpeta",
@@ -131,17 +137,22 @@
"Latest Change": "Último Cambio",
"Learn more": "Saber más",
"Listeners": "Oyentes",
"Loading data...": "Loading data...",
"Loading data...": "Cargando datos...",
"Loading...": "Cargando...",
"Local Discovery": "Descubrimiento local",
"Local State": "Estado local",
"Local State (Total)": "Estado Local (Total)",
"Log": "Registro",
"Log tailing paused. Click here to continue.": "Seguimiento del registro pausado. Haga clic aquí para continuar.",
"Logs": "Registros",
"Major Upgrade": "Actualización importante",
"Mass actions": "Acción masiva",
"Master": "Maestro",
"Maximum Age": "Edad máxima",
"Metadata Only": "Sólo metadatos",
"Minimum Free Disk Space": "Espacio mínimo libre en disco",
"Mod. Device": "Mod. Device",
"Mod. Time": "Mod. Time",
"Mod. Device": "Editar dispositivo",
"Mod. Time": "Editar Hora",
"Move to top of queue": "Mover al principio de la cola",
"Multi level wildcard (matches multiple directory levels)": "Comodín multinivel (coincide con múltiples niveles de directorio)",
"Never": "Nunca",
@@ -150,7 +161,7 @@
"Newest First": "El más nuevo primero",
"No": "No",
"No File Versioning": "Sin versionado de fichero",
"No files will be deleted as a result of this operation.": "No files will be deleted as a result of this operation.",
"No files will be deleted as a result of this operation.": "Ningún archivo será eliminado como resultado de esta operación.",
"No upgrades": "Sin actualizaciones",
"Normal": "Normal",
"Notice": "Aviso",
@@ -165,7 +176,7 @@
"Override Changes": "Anular cambios",
"Path": "Parche",
"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": "Ruta a la carpeta en la máquina local. Se creará si no existe. El carácter de la tilde (~) puede usarse como atajo.",
"Path where new auto accepted folders will be created, as well as the default suggested path when adding new folders via the UI. Tilde character (~) expands to {%tilde%}.": "Path where new auto accepted folders will be created, as well as the default suggested path when adding new folders via the UI. Tilde character (~) expands to {{tilde}}.",
"Path where new auto accepted folders will be created, as well as the default suggested path when adding new folders via the UI. Tilde character (~) expands to {%tilde%}.": "La ruta donde las nuevas carpetas automáticamente aceptadas será creada, así como la ruta por defecto sugerida al agregar nuevas carpetas mediante la UI. La letra (~) se expande a {{tilde}}.",
"Path where versions should be stored (leave empty for the default .stversions directory in the shared folder).": "La ruta donde las versiones deben ser almacenadas (dejar vacío para el directorio .stversions por defecto en la carpeta compartida).",
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Ruta donde se almacenarán las versiones (dejar vacío para usar la carpeta por defecto \".stversions\").",
"Pause": "Pausar",
@@ -181,14 +192,14 @@
"Quick guide to supported patterns": "Guía rápida de patrones soportados",
"RAM Utilization": "Uso de RAM",
"Random": "Aleatorio",
"Recent Changes": "Recent Changes",
"Recent Changes": "Cambios recientes",
"Reduced by ignore patterns": "Reducido por patrones de ignorar",
"Release Notes": "Notas de la versión",
"Release candidates contain the latest features and fixes. They are similar to the traditional bi-weekly Syncthing releases.": "Las versiones candidatas contienen las últimas funcionalidades y correcciones. Son similares a las tradicionales versiones bisemanales de Syncthing.",
"Remote Devices": "Otros dispositivos",
"Remove": "Eliminar",
"Remove Device": "Remove Device",
"Remove Folder": "Remove Folder",
"Remove Device": "Eliminar dispositivo",
"Remove Folder": "Remover carpeta",
"Required identifier for the folder. Must be the same on all cluster devices.": "Identificador requerido para la carpeta. Debe ser el mismo en todos los dispositivos del clúster.",
"Rescan": "Volver a analizar",
"Rescan All": "Volver a analizar Todo",
@@ -196,15 +207,19 @@
"Restart": "Reiniciar",
"Restart Needed": "Reinicio necesario",
"Restarting": "Reiniciando",
"Restore": "Restaurar",
"Restore Versions": "Restaurar versiones",
"Resume": "Continuar",
"Resume All": "Continuar todo",
"Reused": "Reutilizado",
"Save": "Guardar",
"Scan Time Remaining": "Tiempo Restante de Escaneo",
"Scanning": "Analizando",
"See external versioner help for supported templated command line parameters.": "See external versioner help for supported templated command line parameters.",
"See external versioning help for supported templated command line parameters.": "See external versioning help for supported templated command line parameters.",
"Select a version": "Select a version",
"See external versioner help for supported templated command line parameters.": "Vea la ayuda del gestor de versiones externo para los parámetros de linea de comandos que usan una plantilla.",
"See external versioning help for supported templated command line parameters.": "Vea la ayuda del gestor de versiones externo para los parámetros de linea de comandos que usan una plantilla.",
"Select a version": "Seleccione una versión",
"Select latest version": "Seleccione la última versión",
"Select oldest version": "Seleccione la versión más antigua",
"Select the devices to share this folder with.": "Selecciona los dispositivos con los que compartir esta carpeta.",
"Select the folders to share with this device.": "Selecciona las carpetas para compartir con este dispositivo.",
"Send & Receive": "Enviar y Recibir",
@@ -218,15 +233,16 @@
"Shared With": "Compartir con",
"Show ID": "Mostrar ID",
"Show QR": "Mostrar QR",
"Show diff with previous version": "Show diff with previous version",
"Show diff with previous version": "Mostrar la diferencia con la versión anterior",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Se muestra en lugar del ID del dispositivo en el estado del grupo (cluster). Se notificará a los otros dispositivos como nombre opcional por defecto.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Se muestra en lugar del ID del dispositivo en el estado del grupo (cluster). Se actualizará al nombre que el dispositivo anuncia si se deja vacío.",
"Shutdown": "Apagar",
"Shutdown Complete": "Apagar completamente",
"Simple File Versioning": "Versionado simple de fichero",
"Single level wildcard (matches within a directory only)": "Comodín de nivel único (coincide solamente dentro de un directorio)",
"Size": "Size",
"Size": "Tamaño",
"Smallest First": "El más pequeño primero",
"Some items could not be restored:": "Algunos ítemes no pudieron ser restaurados:",
"Source Code": "Código fuente",
"Stable releases and release candidates": "Versiones estables y versiones candidatas",
"Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "Las versiones estables son publicadas cada dos semanas. Durante este tiempo son probadas como versiones candidatas.",
@@ -273,12 +289,12 @@
"This is a major version upgrade.": "Hay una actualización importante.",
"This setting controls the free space required on the home (i.e., index database) disk.": "Este ajuste controla el espacio libre necesario en el disco principal (por ejemplo, el índice de la base de datos).",
"Time": "Hora",
"Time the item was last modified": "Time the item was last modified",
"Time the item was last modified": "Hora en que el ítem fue modificado por última vez",
"Trash Can File Versioning": "Versionado de archivos de la papelera",
"Type": "Tipo",
"Unavailable": "Unavailable",
"Unavailable/Disabled by administrator or maintainer": "Unavailable/Disabled by administrator or maintainer",
"Undecided (will prompt)": "Undecided (will prompt)",
"Unavailable": "No disponible",
"Unavailable/Disabled by administrator or maintainer": "No disponible/Deshabilitado por el administrador o mantenedor",
"Undecided (will prompt)": "No decidido (se preguntará)",
"Unknown": "Desconocido",
"Unshared": "No compartido",
"Unused": "No usado",
@@ -292,6 +308,7 @@
"Usage reporting is always enabled for candidate releases.": "El informe de uso está siempre habilitado en las versiones candidatas.",
"Use HTTPS for GUI": "Usar HTTPS para la Interfaz Gráfica de Usuario (GUI)",
"Version": "Versión",
"Versions": "Versiones",
"Versions Path": "Ruta de las versiones",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Las versiones se borran automáticamente si son más antiguas que la edad máxima o exceden el número de ficheros permitidos en un intervalo.",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "¡Peligro! Esta ruta es un directorio principal de la carpeta ya existente \"{{otherFolder}}\".",

View File

@@ -28,6 +28,7 @@
"Any devices configured on an introducer device will be added to this device as well.": "Sarrarazle deitzen duzun tresna batean gehitua izanen den edozein tresna, zurean ere gehitua izanen da.",
"Are you sure you want to remove device {%name%}?": "Are you sure you want to remove device {{name}}?",
"Are you sure you want to remove folder {%label%}?": "Are you sure you want to remove folder {{label}}?",
"Are you sure you want to restore {%count%} files?": "Are you sure you want to restore {{count}} files?",
"Auto Accept": "Auto Accept",
"Automatic upgrade now offers the choice between stable releases and release candidates.": "Eguneratze automatiko sistemak iraunkor bertsioen eta aitzineko bertsioen artean hautatzea proposatzen du",
"Automatic upgrades": "Eguneratze automatikoak",
@@ -52,6 +53,7 @@
"Copyright © 2014-2017 the following Contributors:": "Copyright 2014-2017, ekarle hauk:",
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Baztertze modelo batzuen sortzea, dagoen fitxategiari ordaina ezartzea: {{path}}",
"Danger!": "Lanjera !",
"Debugging Facilities": "Debugging Facilities",
"Default Folder Path": "Default Folder Path",
"Deleted": "Kendua",
"Device": "Tresna",
@@ -66,6 +68,8 @@
"Discovered": "Agertua",
"Discovery": "Agertzea",
"Discovery Failures": "Agertze hutsegiteak",
"Do not restore": "Do not restore",
"Do not restore all": "Do not restore all",
"Documentation": "Dokumentazioa",
"Download Rate": "Deskargatzearen bit-tasa",
"Downloaded": "Deskargatua",
@@ -94,6 +98,8 @@
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": ".stbersioak azpi karpetan lekutuko eta ordu-markatuko dira fitxeroak, egitura errelatibo berdin batean, Syncthing-ek aldatu edo ezeztatuko dituelarik.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Beste tresnetan eginak izanen diren aldaketetatik zainduak izanen dira fitxeroak; haatik, tresna huntan egindako aldaketak besteeri hedatuak izanen dira.",
"Filesystem Notifications": "Filesystem Notifications",
"Filter by date": "Filter by date",
"Filter by name": "Filter by name",
"Folder": "Partekatze",
"Folder ID": "ID partekatze",
"Folder Label": "Partekatzearen izena",
@@ -132,10 +138,15 @@
"Learn more": "Gehiago jakiteko",
"Listeners": "Entzungailuak",
"Loading data...": "Loading data...",
"Loading...": "Loading...",
"Local Discovery": "Lekuko aurkikuntza",
"Local State": "Lekuko egoera",
"Local State (Total)": "Lekuko egoera (Osoa)",
"Log": "Log",
"Log tailing paused. Click here to continue.": "Log tailing paused. Click here to continue.",
"Logs": "Logs",
"Major Upgrade": "Eguneratze nagusia",
"Mass actions": "Mass actions",
"Master": "Nagusia",
"Maximum Age": "Adin gehiena",
"Metadata Only": "Metadatuak bakarrik",
@@ -196,6 +207,8 @@
"Restart": "Berriz piztu",
"Restart Needed": "Berriz piztea beharrezkoa",
"Restarting": "Berriz piztea martxan",
"Restore": "Restore",
"Restore Versions": "Restore Versions",
"Resume": "Berriz hastea",
"Resume All": "Dena berriz hastea",
"Reused": "Berriz erabilia",
@@ -205,6 +218,8 @@
"See external versioner help for supported templated command line parameters.": "See external versioner help for supported templated command line parameters.",
"See external versioning help for supported templated command line parameters.": "See external versioning help for supported templated command line parameters.",
"Select a version": "Select a version",
"Select latest version": "Select latest version",
"Select oldest version": "Select oldest version",
"Select the devices to share this folder with.": " Tresnak hauta itzazu partekatze honekin sinkronizatzeko ",
"Select the folders to share with this device.": "Tresna honek erabiltzen dituen partekatzeak hauta itzazu",
"Send & Receive": "Igorri eta errezibitu",
@@ -227,6 +242,7 @@
"Single level wildcard (matches within a directory only)": "Hein bakar bateko jokerra (karpetaren barnean bakarrik dagokiona)",
"Size": "Size",
"Smallest First": "Ttipienak lehenik",
"Some items could not be restored:": "Some items could not be restored:",
"Source Code": "Iturri kodea",
"Stable releases and release candidates": "iraunkor eta aintzin-bertsioak",
"Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "Iraunkor bertsioak bi astez (nonbait han) gibelatuak dira. Bitartean, aintzin-bertsio gisa probatuak izanen dira.",
@@ -292,6 +308,7 @@
"Usage reporting is always enabled for candidate releases.": "Erabiltze estatiskitak aintzin-bertsioetan beti igorriak dira",
"Use HTTPS for GUI": "HTTPS-a erabil ezazu GUI-arentzat",
"Version": "Bertsioa",
"Versions": "Versions",
"Versions Path": "Bertsioen kokalekua",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Bertsioak automatikoki ezeztatuak izanen dira, kontserbatzeko iraupen denbora pasatua badute edo bitartean onartua den kopurua (fitxategi bakoitzean) gainditua balin bada",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Kasu, bide hau dagoen partekatze baten karpeta ahaidea da (adibidez, \"{{otherFolder}}\"). Segitzen baduzu, azpi-karpeta berri bat sortu behar duzu, bestenaz arazoak sortzen ahal dira, fitxategi kentzeak edo doblatzeak.",

View File

@@ -28,6 +28,7 @@
"Any devices configured on an introducer device will be added to this device as well.": "Kaikki esittelijäksi määritetyn laitteen tuntemat laitteet lisätään myös tähän laitteeseen.",
"Are you sure you want to remove device {%name%}?": "Are you sure you want to remove device {{name}}?",
"Are you sure you want to remove folder {%label%}?": "Are you sure you want to remove folder {{label}}?",
"Are you sure you want to restore {%count%} files?": "Are you sure you want to restore {{count}} files?",
"Auto Accept": "Auto Accept",
"Automatic upgrade now offers the choice between stable releases and release candidates.": "Automatic upgrade now offers the choice between stable releases and release candidates.",
"Automatic upgrades": "Automaattiset päivitykset",
@@ -52,6 +53,7 @@
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 the following Contributors:",
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Creating ignore patterns, overwriting an existing file at {{path}}.",
"Danger!": "Vaara!",
"Debugging Facilities": "Debugging Facilities",
"Default Folder Path": "Default Folder Path",
"Deleted": "Poistettu",
"Device": "Laite",
@@ -66,6 +68,8 @@
"Discovered": "Löydetty",
"Discovery": "Etsintä",
"Discovery Failures": "Discovery Failures",
"Do not restore": "Do not restore",
"Do not restore all": "Do not restore all",
"Documentation": "Dokumentaatio",
"Download Rate": "Latausmäärä",
"Downloaded": "Ladattu",
@@ -94,6 +98,8 @@
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Tiedostot siirretään päivämäärällä merkityiksi versioiksi .stversions-kansioon, kun Syncthing korvaa tai poistaa ne.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Tiedostot on suojattu muilla laitteilla tehdyiltä muutoksilta, mutta tällä laitteella tehdyt muutokset lähetetään muuhun ryhmään.",
"Filesystem Notifications": "Filesystem Notifications",
"Filter by date": "Filter by date",
"Filter by name": "Filter by name",
"Folder": "Kansio",
"Folder ID": "Kansion ID",
"Folder Label": "Kansion nimi",
@@ -132,10 +138,15 @@
"Learn more": "Learn more",
"Listeners": "Kuuntelijat",
"Loading data...": "Loading data...",
"Loading...": "Loading...",
"Local Discovery": "Paikallinen etsintä",
"Local State": "Paikallinen tila",
"Local State (Total)": "Paikallinen tila (Yhteensä)",
"Log": "Log",
"Log tailing paused. Click here to continue.": "Log tailing paused. Click here to continue.",
"Logs": "Logs",
"Major Upgrade": "Pääversion päivitys.",
"Mass actions": "Mass actions",
"Master": "Pääkansio",
"Maximum Age": "Maksimi-ikä",
"Metadata Only": "Vain metadata",
@@ -196,6 +207,8 @@
"Restart": "Käynnistä uudelleen",
"Restart Needed": "Uudelleenkäynnistys tarvitaan",
"Restarting": "Käynnistetään uudelleen",
"Restore": "Restore",
"Restore Versions": "Restore Versions",
"Resume": "Jatka",
"Resume All": "Resume All",
"Reused": "Uudelleenkäytetty",
@@ -205,6 +218,8 @@
"See external versioner help for supported templated command line parameters.": "See external versioner help for supported templated command line parameters.",
"See external versioning help for supported templated command line parameters.": "See external versioning help for supported templated command line parameters.",
"Select a version": "Select a version",
"Select latest version": "Select latest version",
"Select oldest version": "Select oldest version",
"Select the devices to share this folder with.": "Valitse laitteet, joiden kanssa tämä kansio jaetaan.",
"Select the folders to share with this device.": "Valitse kansiot jaettavaksi tämän laitteen kanssa.",
"Send & Receive": "Lähetä & vastaanota",
@@ -227,6 +242,7 @@
"Single level wildcard (matches within a directory only)": "Yksitasoinen jokerimerkki (vaikuttaa vain kyseisen kansion sisällä)",
"Size": "Size",
"Smallest First": "Pienin ensin",
"Some items could not be restored:": "Some items could not be restored:",
"Source Code": "Lähdekoodi",
"Stable releases and release candidates": "Vakaat julkaisut ja julkaisuehdokkaat",
"Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.",
@@ -292,6 +308,7 @@
"Usage reporting is always enabled for candidate releases.": "Usage reporting is always enabled for candidate releases.",
"Use HTTPS for GUI": "Käytä HTTPS:ää GUI:n kanssa",
"Version": "Versio",
"Versions": "Versions",
"Versions Path": "Versioiden polku",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versiot poistetaan automaattisesti mikäli ne ovat vanhempia kuin maksimi-ikä tai niiden määrä ylittää sallitun määrän tietyllä aikavälillä.",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Varoitus: tämä polku on olemassa olevan kansion \"{{otherFolder}}\" yläkansio.",

View File

@@ -28,6 +28,7 @@
"Any devices configured on an introducer device will be added to this device as well.": "Lui permettre d'ajouter et enlever des membres à toutes mes listes de membres de partages dont il fait partie (ceci permet de créer toutes les liaisons point à point possibles en complétant mes listes par les siennes, meilleur débit de réception par cumul des débits d'envoi, indépendance vis à vis de l'introducteur, etc).",
"Are you sure you want to remove device {%name%}?": "Are you sure you want to remove device {{name}}?",
"Are you sure you want to remove folder {%label%}?": "Are you sure you want to remove folder {{label}}?",
"Are you sure you want to restore {%count%} files?": "Are you sure you want to restore {{count}} files?",
"Auto Accept": "Auto Accept",
"Automatic upgrade now offers the choice between stable releases and release candidates.": "Le système de mise à jour automatique propose le choix entre versions stables et versions préliminaires.",
"Automatic upgrades": "Mises à jour automatiques",
@@ -52,6 +53,7 @@
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017, les contributeurs suivants :",
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Création de masques d'exclusion, remplacement du fichier existant : {{path}}.",
"Danger!": "Attention !",
"Debugging Facilities": "Debugging Facilities",
"Default Folder Path": "Default Folder Path",
"Deleted": "Supprimé",
"Device": "Appareil",
@@ -66,6 +68,8 @@
"Discovered": "Découvert",
"Discovery": "Découverte",
"Discovery Failures": "Échecs de découverte",
"Do not restore": "Do not restore",
"Do not restore all": "Do not restore all",
"Documentation": "Documentation",
"Download Rate": "Débit de réception",
"Downloaded": "Reçu",
@@ -94,6 +98,8 @@
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Quand ils sont remplacés ou supprimés par Syncthing, les fichiers sont déplacés et horodatés vers le sous-répertoire .stversions dans une arborescence relative identique à celle de l'original.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Les fichiers sont protégés des changements réalisés sur les autres appareils, mais les changements réalisés sur celui-ci seront transférés aux autres.",
"Filesystem Notifications": "Filesystem Notifications",
"Filter by date": "Filter by date",
"Filter by name": "Filter by name",
"Folder": "Partage",
"Folder ID": "ID du partage",
"Folder Label": "Nom du partage",
@@ -132,10 +138,15 @@
"Learn more": "En savoir plus",
"Listeners": "Systèmes à l'écoute",
"Loading data...": "Loading data...",
"Loading...": "Loading...",
"Local Discovery": "Découverte locale",
"Local State": "État local",
"Local State (Total)": "État local (Total)",
"Log": "Log",
"Log tailing paused. Click here to continue.": "Log tailing paused. Click here to continue.",
"Logs": "Logs",
"Major Upgrade": "Mise à jour majeure",
"Mass actions": "Mass actions",
"Master": "Maître",
"Maximum Age": "Ancienneté maximum",
"Metadata Only": "Métadonnées uniquement",
@@ -196,6 +207,8 @@
"Restart": "Redémarrer",
"Restart Needed": "Redémarrage nécessaire",
"Restarting": "Redémarrage en cours",
"Restore": "Restore",
"Restore Versions": "Restore Versions",
"Resume": "Reprise",
"Resume All": "Tout libérer",
"Reused": "Réutilisé",
@@ -205,6 +218,8 @@
"See external versioner help for supported templated command line parameters.": "See external versioner help for supported templated command line parameters.",
"See external versioning help for supported templated command line parameters.": "See external versioning help for supported templated command line parameters.",
"Select a version": "Select a version",
"Select latest version": "Select latest version",
"Select oldest version": "Select oldest version",
"Select the devices to share this folder with.": "Synchroniser avec :",
"Select the folders to share with this device.": "Sélectionner les partages auxquels participe cet appareil.",
"Send & Receive": "Envoi & réception",
@@ -227,6 +242,7 @@
"Single level wildcard (matches within a directory only)": "Joker à un seul niveau (correspond uniquement à lintérieur du répertoire)",
"Size": "Size",
"Smallest First": "Les plus petits d'abord",
"Some items could not be restored:": "Some items could not be restored:",
"Source Code": "Code source",
"Stable releases and release candidates": "Versions stables et préliminaires",
"Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "Les versions stables sont reportées d'environ deux semaines. Pendant ce temps elles sont testées en tant que versions préliminaires.",
@@ -292,6 +308,7 @@
"Usage reporting is always enabled for candidate releases.": "Les statistiques d'utilisation sont toujours envoyées pour les versions préliminaires.",
"Use HTTPS for GUI": "Utiliser l'HTTPS pour le GUI",
"Version": "Version",
"Versions": "Versions",
"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 plus anciennes versions seront supprimées automatiquement quand elles dépassent la durée maximum de conservation ou si leur nombre (par fichier) est supérieur à la limite prédéfinie pour l'intervalle.",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Attention, ce chemin est un répertoire parent d'au moins un partage existant (par exemple \"{{otherFolder}}\"). Si vous continuez, vous devriez créer un nouveau sous-répertoire, sinon ceci peut causer des problèmes tels que duplications et/ou suppressions intempestives de fichiers.",

View File

@@ -10,7 +10,7 @@
"Add Device": "Ajouter l'appareil",
"Add Folder": "Ajouter un partage",
"Add Remote Device": "Ajouter un appareil",
"Add devices from the introducer to our device list, for mutually shared folders.": "Lui permettre d'ajouter et enlever des membres à toutes mes listes de membres des partages dont il fait (ou fera !) partie (ceci permet de créer toutes les liaisons point à point possibles en complétant mes listes par les siennes, meilleur débit de réception par cumul des débits d'envoi, indépendance vis à vis de l'introducteur, etc).",
"Add devices from the introducer to our device list, for mutually shared folders.": "ATTENTION !!! Lui permettre d'ajouter et enlever des membres à toutes mes listes de membres des partages dont il fait (ou fera !) partie (ceci permet de créer automatiquement toutes les liaisons point à point possibles en complétant mes listes par les siennes, meilleur débit de réception par cumul des débits d'envoi, indépendance vis à vis de l'introducteur, etc).",
"Add new folder?": "Ajouter ce partage ?",
"Address": "Adresse",
"Addresses": "Adresses",
@@ -28,10 +28,11 @@
"Any devices configured on an introducer device will be added to this device as well.": "Lui permettre d'ajouter et enlever des membres à toutes mes listes de membres des partages dont il fait (ou fera !) partie (ceci permet de créer toutes les liaisons point à point possibles en complétant mes listes par les siennes, meilleur débit de réception par cumul des débits d'envoi, indépendance vis à vis de l'introducteur, etc).",
"Are you sure you want to remove device {%name%}?": "Êtes-vous sûr de vouloir supprimer l'appareil {{name}} ?",
"Are you sure you want to remove folder {%label%}?": "Êtes-vous sûr de vouloir supprimer le partage {{label}} ?",
"Are you sure you want to restore {%count%} files?": "Êtes-vous sûr de vouloir restaurer {{count}} fichiers ?",
"Auto Accept": "Accepter automatiquement",
"Automatic upgrade now offers the choice between stable releases and release candidates.": "Le système de mise à jour automatique propose le choix entre versions stables et versions préliminaires.",
"Automatic upgrades": "Mises à jour automatiques",
"Automatically create or share folders that this device advertises at the default path.": "Créer ou partager automatiquement dans le chemin par défaut les partages que cet appareil annonce.",
"Automatically create or share folders that this device advertises at the default path.": "ATTENTION !!! Créer ou partager automatiquement dans le chemin par défaut les partages que cet appareil annonce.",
"Be careful!": "Faites attention !",
"Bugs": "Bugs",
"CPU Utilization": "Utilisation du CPU",
@@ -52,6 +53,7 @@
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017, les contributeurs sont:",
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Création de masques d'exclusion, remplacement du fichier existant : {{path}}.",
"Danger!": "Attention !",
"Debugging Facilities": "Outils de debogage",
"Default Folder Path": "Chemin par défaut des partages",
"Deleted": "Supprimé",
"Device": "Appareil",
@@ -66,6 +68,8 @@
"Discovered": "Découvert",
"Discovery": "Découverte",
"Discovery Failures": "Échecs de découverte",
"Do not restore": "Ne pas restaurer",
"Do not restore all": "Ne pas tout restaurer",
"Documentation": "Documentation",
"Download Rate": "Débit de réception",
"Downloaded": "Reçu",
@@ -94,6 +98,8 @@
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Quand ils sont remplacés ou supprimés par Syncthing, les fichiers sont déplacés et horodatés vers le sous-répertoire .stversions dans une arborescence relative identique à celle de l'original.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Les fichiers sont protégés des changements réalisés sur les autres appareils, mais les changements réalisés sur celui-ci seront transférés aux autres.",
"Filesystem Notifications": "Notifications du système de fichiers",
"Filter by date": "Filtrer par date",
"Filter by name": "Filtrer par nom",
"Folder": "Partage",
"Folder ID": "ID du partage",
"Folder Label": "Nom du partage",
@@ -132,10 +138,15 @@
"Learn more": "En savoir plus",
"Listeners": "Systèmes en écoute",
"Loading data...": "Chargement des données...",
"Loading...": "Chargement...",
"Local Discovery": "Découverte locale",
"Local State": "État local",
"Local State (Total)": "État local (Total)",
"Log": "Journal",
"Log tailing paused. Click here to continue.": "La file d'attente du journal est en pause. Cliquer ici pour continuer.",
"Logs": "Journaux",
"Major Upgrade": "Mise à jour majeure",
"Mass actions": "Actions multiples",
"Master": "Maître",
"Maximum Age": "Ancienneté maximum",
"Metadata Only": "Métadonnées uniquement",
@@ -149,7 +160,7 @@
"New Folder": "Nouveau partage",
"Newest First": "Les plus récents en premier",
"No": "Non",
"No File Versioning": "Pas de préservation",
"No File Versioning": "Pas de poubelle",
"No files will be deleted as a result of this operation.": "Aucun fichier ne sera supprimé à la suite de cette opération.",
"No upgrades": "Pas de mises à jour",
"Normal": "Normal",
@@ -196,6 +207,8 @@
"Restart": "Redémarrer",
"Restart Needed": "Redémarrage nécessaire",
"Restarting": "Redémarrage en cours",
"Restore": "Restaurer",
"Restore Versions": "Restaurer par versions",
"Resume": "Reprise",
"Resume All": "Tout libérer",
"Reused": "Réutilisé",
@@ -205,7 +218,9 @@
"See external versioner help for supported templated command line parameters.": "Voir l'aide sur la préservation externe des fichiers pour les paramètres supportés en lignes de commande dans les modèles.",
"See external versioning help for supported templated command line parameters.": "Consulter l'aide à la gestion externe des versions pour voir les paramètres de ligne de commande supportés.",
"Select a version": "Choisissez une version",
"Select the devices to share this folder with.": "Synchroniser avec :",
"Select latest version": "Restaurer la dernière version",
"Select oldest version": "Restaurer la plus ancienne version",
"Select the devices to share this folder with.": "Choisir les participants :",
"Select the folders to share with this device.": "Choisir les partages auxquels cet appareil doit participer :",
"Send & Receive": "Envoi & réception",
"Send Only": "Envoi (lecture seule)",
@@ -215,7 +230,7 @@
"Share Folders With Device": "Participe à ces partages",
"Share With Devices": "Synchroniser avec des appareils",
"Share this folder?": "Acceptez-vous ce partage ?",
"Shared With": "Synchronisé avec",
"Shared With": "Participant(s)",
"Show ID": "Afficher mon ID",
"Show QR": "Afficher le QR",
"Show diff with previous version": "Afficher les différences avec la version précédente",
@@ -227,6 +242,7 @@
"Single level wildcard (matches within a directory only)": "Joker à un seul niveau (correspond uniquement à lintérieur du répertoire)",
"Size": "Taille",
"Smallest First": "Les plus petits en premier",
"Some items could not be restored:": "Certains éléments n'ont pas pu être restaurés :",
"Source Code": "Code source",
"Stable releases and release candidates": "Versions stables et préliminaires",
"Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "Les versions stables sont reportées d'environ deux semaines. Pendant ce temps elles sont testées en tant que versions préliminaires.",
@@ -278,7 +294,7 @@
"Type": "Type",
"Unavailable": "Indisponible",
"Unavailable/Disabled by administrator or maintainer": "Indisponible/Désactivé par l'administrateur ou le mainteneur",
"Undecided (will prompt)": "Choix différé (Une question sera affichée plus tard)",
"Undecided (will prompt)": "Non défini (Choisir plus tard)",
"Unknown": "Inconnu",
"Unshared": "Non partagé",
"Unused": "Non utilisé",
@@ -292,6 +308,7 @@
"Usage reporting is always enabled for candidate releases.": "Les statistiques d'utilisation sont toujours envoyées pour les versions préliminaires.",
"Use HTTPS for GUI": "Utiliser l'HTTPS pour le GUI",
"Version": "Version",
"Versions": "Versions",
"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 plus anciennes versions seront supprimées automatiquement quand elles dépassent la durée maximum de conservation ou si leur nombre (par fichier) est supérieur à la limite prédéfinie pour l'intervalle.",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Attention, ce chemin est un répertoire parent d'au moins un partage existant (par exemple \"{{otherFolder}}\"). Si c'est bien ce que vous souhaitez, vous devriez créer un nouveau sous-répertoire, sinon ceci peut causer des problèmes tels que duplications et/ou suppressions intempestives de fichiers.",

View File

@@ -26,12 +26,13 @@
"Anonymous Usage Reporting": "Anonym brûkensrapportaazje",
"Anonymous usage report format has changed. Would you like to move to the new format?": "It formaat fan de rapportaazje fan anonime gebrûksynformaasje is feroare. Wolle jo op dit nije formaat oerstappe?",
"Any devices configured on an introducer device will be added to this device as well.": "Alle apparaten die op in 'yntrodusearjend apparaat' ynstelt binne, wurde ek op dit apparaat taheakke.",
"Are you sure you want to remove device {%name%}?": "Are you sure you want to remove device {{name}}?",
"Are you sure you want to remove folder {%label%}?": "Are you sure you want to remove folder {{label}}?",
"Auto Accept": "Auto Accept",
"Are you sure you want to remove device {%name%}?": "Bist der wis fan datsto apparaat {{name}} fuortsmite wolst?",
"Are you sure you want to remove folder {%label%}?": "Bist der wis fan datsto map {{label}} fuortsmite wolst?",
"Are you sure you want to restore {%count%} files?": "Bist der wis fan datsto {{count}} triemen weromsette wolst?",
"Auto Accept": "Auto-akseptaasje",
"Automatic upgrade now offers the choice between stable releases and release candidates.": "Automatyske fernijing biedt no de kar tusken stabyle ferzjes en ferzje kandidaten",
"Automatic upgrades": "Automatyske fernijings",
"Automatically create or share folders that this device advertises at the default path.": "Automatically create or share folders that this device advertises at the default path.",
"Automatically create or share folders that this device advertises at the default path.": "Meitsje of diel automatysk mappen dy't dit apparaat advertearret op it standert paad.",
"Be careful!": "Tink derom!",
"Bugs": "Brekkings",
"CPU Utilization": "CPU-brûken",
@@ -45,27 +46,30 @@
"Configured": "Konfigureart",
"Connection Error": "Ferbiningsflater",
"Connection Type": "Ferbiningstype",
"Connections": "Connections",
"Connections": "Ferbinings",
"Copied from elsewhere": "Oernommen fan earne oars",
"Copied from original": "Oernommen fan orizjineel",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 de folgende bydragers:",
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 de folgende Bydragers:",
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Meitsje negear-patroanen dy in besteande triem oerskriuwe yn {{path}}.",
"Danger!": "Gefaar!",
"Default Folder Path": "Default Folder Path",
"Debugging Facilities": "Debug-foarsjennings",
"Default Folder Path": "Standert Map-paad",
"Deleted": "Fuortsmiten",
"Device": "Apparaat",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Apparaat \"{{name}}\" {{device}} op ({{address}}) wol ferbining meitsje. Nij apparaat taheakje?",
"Device ID": "Apparaat-ID",
"Device Identification": "Apparaatidentifikaasje",
"Device Name": "Apparaatnamme",
"Device that last modified the item": "Device that last modified the item",
"Device that last modified the item": "Apparaat dat dit item it lêst oanpast hat",
"Devices": "Apparaten",
"Disabled": "Utskeakele",
"Disconnected": "Ferbining ferbrutsen",
"Discovered": "Untdekt",
"Discovery": "Untdekking",
"Discovery Failures": "Untdekkingsflaters",
"Do not restore": "Net tebeksette",
"Do not restore all": "Hielendal net tebeksette",
"Documentation": "Dokumintaasje",
"Download Rate": "Downloadfluggens",
"Downloaded": "Downloaded",
@@ -94,6 +98,8 @@
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Triemen wurde ferset nei in mei datum stimpele ferzjes yn in .stversions map wannear troch Syncthing ferfangen of fuortsmiten.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Triemen binne ymmún foar feroarings makke troch oare apparaten, mar feroarings makke op dit apparaat wurde nei de rest fan 'e bondel ferstjoerd.",
"Filesystem Notifications": "Meldingen fan Triemsysteem",
"Filter by date": "Op datum filterje",
"Filter by name": "Op namme filterje",
"Folder": "Map",
"Folder ID": "Map-ID",
"Folder Label": "Map-opskrift",
@@ -106,7 +112,7 @@
"GUI Listen Address": "GUI-harkadres",
"GUI Listen Addresses": "Harkadres foar GUI",
"GUI Theme": "Ynterfaasjetema",
"General": "General",
"General": "Algemien",
"Generate": "Generearje",
"Global Changes": "Wrâldwide Feroarings",
"Global Discovery": "Wrâldwide ûntdekking",
@@ -131,17 +137,22 @@
"Latest Change": "Meast Resinte Feroarings",
"Learn more": "Mear witte",
"Listeners": "Harkers",
"Loading data...": "Loading data...",
"Loading data...": "Data oan it laden...",
"Loading...": "Oan it laden...",
"Local Discovery": "Lokale ûntdekking",
"Local State": "Lokale tastân",
"Local State (Total)": "Lokale tastân (Folledich)",
"Log": "Loch",
"Log tailing paused. Click here to continue.": "Loch-sturt skofte. Klik hjir om fjirder te gean.",
"Logs": "Lochs",
"Major Upgrade": "Wichtige fernijing",
"Mass actions": "Massa-aksjes",
"Master": "Master",
"Maximum Age": "Maksimale âldens",
"Metadata Only": "Allinnich metadata",
"Minimum Free Disk Space": "Minimale frije skiifromte",
"Mod. Device": "Mod. Device",
"Mod. Time": "Mod. Time",
"Mod. Device": "Fer. Apparaat",
"Mod. Time": "Fer. Tiid",
"Move to top of queue": "Fersette nei boppe oan de rige",
"Multi level wildcard (matches multiple directory levels)": "Multi-nivo jokerteken (wildcard) (fergelykt mei meardere map-nivo's)",
"Never": "Nea",
@@ -150,7 +161,7 @@
"Newest First": "Nijste earst",
"No": "Nee",
"No File Versioning": "Gjin triemferzjebehear",
"No files will be deleted as a result of this operation.": "No files will be deleted as a result of this operation.",
"No files will be deleted as a result of this operation.": "Der wurde gjin triemen fuortsmiten as gefolch fan dizze aksje.",
"No upgrades": "Gjin fernijings",
"Normal": "Normaal",
"Notice": "Notysje",
@@ -165,7 +176,7 @@
"Override Changes": "Feroarings oerskriuwe",
"Path": "Paad",
"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": "Paad nei de map op de lokale kompjûter. Wurd oanmakke as dizze net bestiet. It tilde teken (~) kin brûkt wurde as fluchkeppeling foar",
"Path where new auto accepted folders will be created, as well as the default suggested path when adding new folders via the UI. Tilde character (~) expands to {%tilde%}.": "Path where new auto accepted folders will be created, as well as the default suggested path when adding new folders via the UI. Tilde character (~) expands to {{tilde}}.",
"Path where new auto accepted folders will be created, as well as the default suggested path when adding new folders via the UI. Tilde character (~) expands to {%tilde%}.": "Paad wêr't de nije akseptearre mappen makke wurde, tagelyks it standert paad by it taheakken fan nije mappen fia de UI. Tilde-karakter (~) wreidet út nei {{tilde}}.",
"Path where versions should be stored (leave empty for the default .stversions directory in the shared folder).": "Paad dêr't de ferzjes bewarre wurde moatte (leech litte foar de standert .stversions-map yn de map).",
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Paad dêr't de ferzjes bewarre wurde moatte (leech litte foar de standert .stversions-map yn de map).",
"Pause": "Skoftsje",
@@ -187,8 +198,8 @@
"Release candidates contain the latest features and fixes. They are similar to the traditional bi-weekly Syncthing releases.": "Ferzje kandidaten hawwe de lêste mooglikheden en ferbetterings. Se binne allyksa de tradisjonele twa-wyklikse Syncthing ferzjes.",
"Remote Devices": "Apparaten op Ofstân",
"Remove": "Fuortsmite",
"Remove Device": "Remove Device",
"Remove Folder": "Remove Folder",
"Remove Device": "Apparaat Fuortsmite",
"Remove Folder": "Map Fourtsmite",
"Required identifier for the folder. Must be the same on all cluster devices.": "Ferplicht ID foar de map. Moat op alle bondelapparaten itselde wêze.",
"Rescan": "Sken opnij",
"Rescan All": "Sken alles opnij",
@@ -196,6 +207,8 @@
"Restart": "Werstarte",
"Restart Needed": "Werstart nedich",
"Restarting": "Oan it werstarten",
"Restore": "Tebeksette",
"Restore Versions": "Ferzjes Tebeksette",
"Resume": "Trochgean",
"Resume All": "Alles trochgean litte",
"Reused": "Opnij brûkt",
@@ -205,6 +218,8 @@
"See external versioner help for supported templated command line parameters.": "Sjoch de eksterne help fan fersjebehearder foar stipe foarbylden fan kommando-rige-parameters.",
"See external versioning help for supported templated command line parameters.": "Sjoch de eksterne help fan fersjebehearder foar stipe foarbylden fan kommando-rige-parameters.",
"Select a version": "Kies in ferzje",
"Select latest version": "Selektearje de nijste ferzje",
"Select oldest version": "Selektearje de âldste ferzje",
"Select the devices to share this folder with.": "Sykje de apparaten út om dizze map mei te dielen.",
"Select the folders to share with this device.": "Sykje de mappen út om mei dit apparaat te dielen.",
"Send & Receive": "Stjoere & Untfange",
@@ -225,8 +240,9 @@
"Shutdown Complete": "Ofsluten klear",
"Simple File Versioning": "Ienfâldich triemferzjebehear",
"Single level wildcard (matches within a directory only)": "Inkel-nivo jokerteken (wildcard) (fergeliket allinnich binnen in map)",
"Size": "Size",
"Size": "Grutte",
"Smallest First": "Lytste earst",
"Some items could not be restored:": "Guon uûnderdielen koenen net tebeksetten wurde.",
"Source Code": "Boarnekoade",
"Stable releases and release candidates": "Stabyle ferzjes en ferzje kanditaten",
"Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "Stabyle ferzjes wurde likernôch twa wiken útstelt. Yn die tiid wurde se testen as ferzje kandidaten.",
@@ -273,11 +289,11 @@
"This is a major version upgrade.": "Dit is in wichtige ferzjefernijing.",
"This setting controls the free space required on the home (i.e., index database) disk.": "Dizze ynstelling bepaalt de frije romte dy't noadich is op de home-skiif (fan de yndeks-databank).",
"Time": "Tiid",
"Time the item was last modified": "Time the item was last modified",
"Time the item was last modified": "Tiidstip dat it ûnderdiel foar it lest oanpast waard.",
"Trash Can File Versioning": "Jiskefet-triemferzjebehear",
"Type": "Type",
"Unavailable": "Unavailable",
"Unavailable/Disabled by administrator or maintainer": "Unavailable/Disabled by administrator or maintainer",
"Unavailable": "Net beskikber",
"Unavailable/Disabled by administrator or maintainer": "Net beskikber/Utsetten troch administrator of ûnderhâlder",
"Undecided (will prompt)": "Noch net beslist (wurd noch frege)",
"Unknown": "Unbekend",
"Unshared": "Net dielt",
@@ -292,6 +308,7 @@
"Usage reporting is always enabled for candidate releases.": "Brûkersrapportaazje stiet altyd oan foar ferzje kandidaten.",
"Use HTTPS for GUI": "Brûk HTTPS foar GUI",
"Version": "Ferzje",
"Versions": "Ferzjes",
"Versions Path": "Ferzjes-paad",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Ferzjes wurde automatysk fuortsmiten wannear't se âlder binne dan de maksimale âldens of wannear it tal fan triemen yn in ynterval grutter is dan tastean.",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Warskôging, dit paad is in boppelizzende triemtafel fan in besteande map \"{{otherFolder}}\".",

View File

@@ -28,6 +28,7 @@
"Any devices configured on an introducer device will be added to this device as well.": "A bevezető eszközön beállított minden eszköz hozzá lesz adva ehhez az eszközhöz is.",
"Are you sure you want to remove device {%name%}?": "Biztos, hogy el akarod távolítani az eszközt: {{name}}?",
"Are you sure you want to remove folder {%label%}?": "Biztos, hogy el akarod távolítani a mappát: {{label}}?",
"Are you sure you want to restore {%count%} files?": "Biztos, hogy vissza akarod állítani a(z) {{count}} fájlt?",
"Auto Accept": "Automatikus elfogadás",
"Automatic upgrade now offers the choice between stable releases and release candidates.": "Az automatikus frissítés most lehetőséget kínál a stabil és az előzetes kiadások közötti választásra.",
"Automatic upgrades": "Automatikus frissítések",
@@ -52,6 +53,7 @@
"Copyright © 2014-2017 the following Contributors:": "Szerzői jog © 2014-2017 az alábbi közreműködők:",
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Figyelmen kívül hagyás minták létrehozása, egy létező fájl felülírása itt: {{path}}.",
"Danger!": "Veszély!",
"Debugging Facilities": "Hibakeresési képességek",
"Default Folder Path": "Alapértelmezett mappa útvonala",
"Deleted": "Törölve",
"Device": "Eszköz",
@@ -66,6 +68,8 @@
"Discovered": "Felfedezett",
"Discovery": "Felfedezés",
"Discovery Failures": "Felfedezési hibák",
"Do not restore": "Ne legyen visszaállítva",
"Do not restore all": "Semmit se állítson vissza",
"Documentation": "Dokumentáció",
"Download Rate": "Letöltési sebesség",
"Downloaded": "Letöltve",
@@ -94,6 +98,8 @@
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Ha a Syncthing felülírja vagy törli a fájlokat, akkor azok a .stversions mappába lesznek áthelyezve, időbélyegzővel ellátva.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "A fájlok védve vannak a más eszközökön történt változásokkal szemben, de az ezen az eszközön történt változások érvényesek lesznek a többire.",
"Filesystem Notifications": "Fájlrendszer értesítések",
"Filter by date": "Szűrés dátumra",
"Filter by name": "Szűrés névre",
"Folder": "Mappa",
"Folder ID": "Mappaazonosító",
"Folder Label": "Mappacímke",
@@ -132,10 +138,15 @@
"Learn more": "Tudj meg többet",
"Listeners": "Kapcsolatok",
"Loading data...": "Adatok betöltése...",
"Loading...": "Betöltés...",
"Local Discovery": "Helyi felfedezés",
"Local State": "Helyi állapot",
"Local State (Total)": "Helyi állapot (teljes)",
"Log": "Napló",
"Log tailing paused. Click here to continue.": "Napló utolsó sorainak figyelése leállítva. Ide kattintva lehet folytatni.",
"Logs": "Naplfájlok",
"Major Upgrade": "Főverzió-frissítés",
"Mass actions": "Tömeges műveletek",
"Master": "Központi",
"Maximum Age": "Maximális kor",
"Metadata Only": "Csak metaadatok",
@@ -196,6 +207,8 @@
"Restart": "Újraindítás",
"Restart Needed": "Újraindítás szükséges",
"Restarting": "Újraindulás",
"Restore": "Visszaállítás",
"Restore Versions": "Visszaállítási verziók",
"Resume": "Folytatás",
"Resume All": "Mindent folytat",
"Reused": "Újrafelhasználva",
@@ -205,6 +218,8 @@
"See external versioner help for supported templated command line parameters.": "A támogatott parancssori paraméter sablonokat a külső verziókezelő súgójában találod.",
"See external versioning help for supported templated command line parameters.": "A támogatott parancssori paraméter sablonokat a külső verziókezelő súgójában találod.",
"Select a version": "Válassz egy verziót",
"Select latest version": "Legfrissebb verzió kijelölése",
"Select oldest version": "Legrégebbi verzió kijelölése",
"Select the devices to share this folder with.": "Eszközök, amelyekkel megosztandó a mappa",
"Select the folders to share with this device.": "Mappák, amelyek megosztandók ezzel az eszközzel.",
"Send & Receive": "Küldés és fogadás",
@@ -227,6 +242,7 @@
"Single level wildcard (matches within a directory only)": "Egyszintű helyettesítő karakter (csak egy mappára érvényes)",
"Size": "Méret",
"Smallest First": "Kisebb először",
"Some items could not be restored:": "Néhány elemet nem sikerült visszaállítani:",
"Source Code": "Forráskód",
"Stable releases and release candidates": "Stabil és előzetes kiadások ",
"Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "A stabil kiadások nagyjából két héttel el vannak csúsztatva. Ez alatt előzetes kiadásként tesztelésen mennek keresztül.",
@@ -292,6 +308,7 @@
"Usage reporting is always enabled for candidate releases.": "Az előzetes kiadásokban a használati jelentés mindig engedélyezett.",
"Use HTTPS for GUI": "HTTPS használata a grafikus felülethez",
"Version": "Verzió",
"Versions": "Verziók",
"Versions Path": "Verziók útvonala",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "A régi verziók automatikusan törlődnek, amennyiben öregebbek, mint a maximum kor, vagy már több van belőlük, mint az adott időszakban megtartható maximum.",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Figyelem, ez az útvonal a meglévő „{{otherFolder}}” mappa szülőmappája.",

View File

@@ -26,8 +26,9 @@
"Anonymous Usage Reporting": "Statistiche Anonime di Utilizzo",
"Anonymous usage report format has changed. Would you like to move to the new format?": "Il formato delle statistiche anonime di utilizzo è cambiato. Vuoi passare al nuovo formato?",
"Any devices configured on an introducer device will be added to this device as well.": "Qualsiasi dispositivo configurato in un introduttore verrà aggiunto anche a questo dispositivo.",
"Are you sure you want to remove device {%name%}?": "Are you sure you want to remove device {{name}}?",
"Are you sure you want to remove folder {%label%}?": "Are you sure you want to remove folder {{label}}?",
"Are you sure you want to remove device {%name%}?": "Sei sicuro di voler rimuovere il dispositivo {{name}}?",
"Are you sure you want to remove folder {%label%}?": "Sei sicuro di voler rimuovere la cartella {{label}}?",
"Are you sure you want to restore {%count%} files?": "Sei sicuro di voler ripristinare {{count}} file?",
"Auto Accept": "Auto Accept",
"Automatic upgrade now offers the choice between stable releases and release candidates.": "Aggiornamenti automatici offrono la scelta tra rilasci stabili e candidati di rilascio.",
"Automatic upgrades": "Aggiornamenti automatici",
@@ -52,6 +53,7 @@
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 i seguenti Collaboratori:",
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Creazione di schemi di esclusione, sovrascrivendo un file esistente in {{path}}.",
"Danger!": "Pericolo!",
"Debugging Facilities": "Debugging Facilities",
"Default Folder Path": "Default Folder Path",
"Deleted": "Cancellato",
"Device": "Dispositivo",
@@ -66,6 +68,8 @@
"Discovered": "Individuato",
"Discovery": "Individuazione",
"Discovery Failures": "Individuazione Fallita",
"Do not restore": "Do not restore",
"Do not restore all": "Do not restore all",
"Documentation": "Documentazione",
"Download Rate": "Velocità Download",
"Downloaded": "Scaricato",
@@ -94,6 +98,8 @@
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "I file sostituiti o eliminati da Syncthing vengono datati e spostati in una cartella .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.": "I file sono protetti dalle modifiche effettuate negli altri dispositivi, ma le modifiche effettuate in questo dispositivo verranno inviate anche al resto del cluster.",
"Filesystem Notifications": "Notifiche del filesystem",
"Filter by date": "Filter by date",
"Filter by name": "Filter by name",
"Folder": "Cartella",
"Folder ID": "ID Cartella",
"Folder Label": "Etichetta per la Cartella",
@@ -132,10 +138,15 @@
"Learn more": "Impara di piu",
"Listeners": "In Ascolto",
"Loading data...": "Loading data...",
"Loading...": "Loading...",
"Local Discovery": "Individuazione Locale",
"Local State": "Stato Locale",
"Local State (Total)": "Stato Locale (Totale)",
"Log": "Log",
"Log tailing paused. Click here to continue.": "Log tailing paused. Click here to continue.",
"Logs": "Logs",
"Major Upgrade": "Aggiornamento Principale",
"Mass actions": "Mass actions",
"Master": "Principale",
"Maximum Age": "Durata Massima",
"Metadata Only": "Solo i Metadati",
@@ -196,6 +207,8 @@
"Restart": "Riavvia",
"Restart Needed": "Riavvio Necessario",
"Restarting": "Riavvio",
"Restore": "Restore",
"Restore Versions": "Restore Versions",
"Resume": "Riprendi",
"Resume All": "Riprendi Tutti",
"Reused": "Riutilizzato",
@@ -205,6 +218,8 @@
"See external versioner help for supported templated command line parameters.": "Consultare la guida al controllo di versione per i modelli dei parametri di riga di comando supportati.",
"See external versioning help for supported templated command line parameters.": "Consultare la guida al controllo di versione per i modelli dei parametri di riga di comando supportati.",
"Select a version": "Seleziona una versione",
"Select latest version": "Select latest version",
"Select oldest version": "Select oldest version",
"Select the devices to share this folder with.": "Seleziona i dispositivi con i quali condividere questa cartella.",
"Select the folders to share with this device.": "Seleziona le cartelle da condividere con questo dispositivo.",
"Send & Receive": "Invia & Ricevi",
@@ -227,6 +242,7 @@
"Single level wildcard (matches within a directory only)": "Metacarattere di singolo livello (per corrispondenze solo all'interno di una cartella)",
"Size": "Size",
"Smallest First": "Prima il più piccolo",
"Some items could not be restored:": "Some items could not be restored:",
"Source Code": "Codice Sorgente",
"Stable releases and release candidates": "Rilasci stabili e candidati di rilascio",
"Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "Rilasci stabili sono in ritardo di circa due settimane. Durante questo tempo verranno testati come candidati di rilascio.",
@@ -292,6 +308,7 @@
"Usage reporting is always enabled for candidate releases.": "Segnalazioni di utilizzo sono sempre abilitati per candidati di rilascio.",
"Use HTTPS for GUI": "Utilizza HTTPS per l'interfaccia grafica",
"Version": "Versione",
"Versions": "Versions",
"Versions Path": "Percorso Cartella Versioni",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Le versioni vengono eliminate automaticamente se superano la durata massima o il numero di file permessi in un determinato intervallo temporale.",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Attenzione, questo percorso è una cartella superiore di una cartella esistente \"{{otherFolder}}\".",

View File

@@ -28,6 +28,7 @@
"Any devices configured on an introducer device will be added to this device as well.": "紹介者デバイス上で設定されたデバイスは、このデバイス上にも追加されます。",
"Are you sure you want to remove device {%name%}?": "Are you sure you want to remove device {{name}}?",
"Are you sure you want to remove folder {%label%}?": "Are you sure you want to remove folder {{label}}?",
"Are you sure you want to restore {%count%} files?": "Are you sure you want to restore {{count}} files?",
"Auto Accept": "Auto Accept",
"Automatic upgrade now offers the choice between stable releases and release candidates.": "自動アップグレードは、安定版とリリース候補版のいずれかを選べるようになりました。",
"Automatic upgrades": "自動アップグレード",
@@ -52,6 +53,7 @@
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 the following Contributors:",
"Creating ignore patterns, overwriting an existing file at {%path%}.": "無視パターンを作成中。既存のファイルが {{path}} にある場合は上書きされます。",
"Danger!": "危険!",
"Debugging Facilities": "Debugging Facilities",
"Default Folder Path": "Default Folder Path",
"Deleted": "削除",
"Device": "デバイス",
@@ -66,6 +68,8 @@
"Discovered": "探索結果",
"Discovery": "探索サーバー",
"Discovery Failures": "探索サーバーへの接続失敗",
"Do not restore": "Do not restore",
"Do not restore all": "Do not restore all",
"Documentation": "マニュアル",
"Download Rate": "ダウンロード速度",
"Downloaded": "ダウンロード済",
@@ -94,6 +98,8 @@
"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.": "ファイルを他のデバイスによる変更から保護します。一方、このデバイスでの変更は他のデバイスに送信されます。",
"Filesystem Notifications": "Filesystem Notifications",
"Filter by date": "Filter by date",
"Filter by name": "Filter by name",
"Folder": "フォルダー",
"Folder ID": "フォルダーID",
"Folder Label": "フォルダー名",
@@ -132,10 +138,15 @@
"Learn more": "詳細を確認する",
"Listeners": "待ち受けポート",
"Loading data...": "Loading data...",
"Loading...": "Loading...",
"Local Discovery": "LAN内で探索",
"Local State": "ローカル状態",
"Local State (Total)": "ローカル状態 (合計)",
"Log": "Log",
"Log tailing paused. Click here to continue.": "Log tailing paused. Click here to continue.",
"Logs": "Logs",
"Major Upgrade": "メジャーアップグレード",
"Mass actions": "Mass actions",
"Master": "マスター",
"Maximum Age": "最大保存日数",
"Metadata Only": "メタデータのみ",
@@ -196,6 +207,8 @@
"Restart": "再起動",
"Restart Needed": "再起動が必要です",
"Restarting": "再起動中",
"Restore": "Restore",
"Restore Versions": "Restore Versions",
"Resume": "再開",
"Resume All": "すべて再開",
"Reused": "中断後再利用",
@@ -205,6 +218,8 @@
"See external versioner help for supported templated command line parameters.": "See external versioner help for supported templated command line parameters.",
"See external versioning help for supported templated command line parameters.": "使用可能なコマンドラインパラメータについてはヘルプの外部バージョン管理の項目を参照してください。",
"Select a version": "バージョンを選択してください",
"Select latest version": "Select latest version",
"Select oldest version": "Select oldest version",
"Select the devices to share this folder with.": "このフォルダーを共有するデバイスを選択してください。",
"Select the folders to share with this device.": "このデバイスと共有するフォルダーを選択してください。",
"Send & Receive": "送受信",
@@ -227,6 +242,7 @@
"Single level wildcard (matches within a directory only)": "ワイルドカード (単一のディレクトリ内だけでマッチします)",
"Size": "Size",
"Smallest First": "小さい順",
"Some items could not be restored:": "Some items could not be restored:",
"Source Code": "ソースコード",
"Stable releases and release candidates": "安定版とリリース候補版",
"Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "安定版は、リリース候補版としてのテスト後およそ2週間でリリースされます。",
@@ -292,6 +308,7 @@
"Usage reporting is always enabled for candidate releases.": "リリース候補版では常に使用状況レポートが送信されます。",
"Use HTTPS for GUI": "GUIにHTTPSを使用する",
"Version": "バージョン",
"Versions": "Versions",
"Versions Path": "古いバージョンを保存するパス",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "古いバージョンは、最大保存日数もしくは期間ごとの最大保存数を超えた場合、自動的に削除されます。",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "警告: 入力されたパスは、設定済みのフォルダー「{{otherFolder}}」の親ディレクトリです。",

View File

@@ -28,6 +28,7 @@
"Any devices configured on an introducer device will be added to this device as well.": "유도 장치에 추가된 기기들은 이 기기에도 동시에 추가됩니다.",
"Are you sure you want to remove device {%name%}?": "Are you sure you want to remove device {{name}}?",
"Are you sure you want to remove folder {%label%}?": "Are you sure you want to remove folder {{label}}?",
"Are you sure you want to restore {%count%} files?": "Are you sure you want to restore {{count}} files?",
"Auto Accept": "Auto Accept",
"Automatic upgrade now offers the choice between stable releases and release candidates.": "자동 업데이트를 이제 안정 버전과 출시 후보 사이에 선택 할 수 있게 됩니다.",
"Automatic upgrades": "자동 업데이트",
@@ -52,6 +53,7 @@
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 the following Contributors:",
"Creating ignore patterns, overwriting an existing file at {%path%}.": "무시 패턴 만들기, {{path}}에 존재하는 파일을 덮어쓰기 합니다",
"Danger!": "경고!",
"Debugging Facilities": "Debugging Facilities",
"Default Folder Path": "Default Folder Path",
"Deleted": "삭제됨",
"Device": "기기",
@@ -66,6 +68,8 @@
"Discovered": "탐색됨",
"Discovery": "탐색",
"Discovery Failures": "탐색 실패",
"Do not restore": "Do not restore",
"Do not restore all": "Do not restore all",
"Documentation": "문서",
"Download Rate": "다운로드 속도",
"Downloaded": "다운로드됨",
@@ -94,6 +98,8 @@
"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.": "다른 장치가 파일을 편집할 수 없으며 반드시 이 장치의 내용을 기준으로 동기화합니다.",
"Filesystem Notifications": "파일시스템 알림",
"Filter by date": "Filter by date",
"Filter by name": "Filter by name",
"Folder": "폴더",
"Folder ID": "폴더 ID",
"Folder Label": "폴더 라벨",
@@ -132,10 +138,15 @@
"Learn more": "더 알아보기",
"Listeners": "수신자",
"Loading data...": "Loading data...",
"Loading...": "Loading...",
"Local Discovery": "로컬 노드 검색",
"Local State": "로컬 상태",
"Local State (Total)": "로컬 상태 (합계)",
"Log": "Log",
"Log tailing paused. Click here to continue.": "Log tailing paused. Click here to continue.",
"Logs": "Logs",
"Major Upgrade": "메이저 업데이트",
"Mass actions": "Mass actions",
"Master": "마스터",
"Maximum Age": "최대 보존 기간",
"Metadata Only": "메타데이터만",
@@ -196,6 +207,8 @@
"Restart": "재시작",
"Restart Needed": "재시작 필요함",
"Restarting": "재시작 중",
"Restore": "Restore",
"Restore Versions": "Restore Versions",
"Resume": "재개",
"Resume All": "모두 재개",
"Reused": "재개",
@@ -205,6 +218,8 @@
"See external versioner help for supported templated command line parameters.": "지원되는 템플릿 명령 행 매개 변수에 대해서는 외부 버전 도움말을 참조하십시오.",
"See external versioning help for supported templated command line parameters.": "지원되는 템플릿 명령 행 매개 변수에 대해서는 외부 버전 도움말을 참조하십시오.",
"Select a version": "버전 선택",
"Select latest version": "Select latest version",
"Select oldest version": "Select oldest version",
"Select the devices to share this folder with.": "이 폴더를 공유할 장치를 선택합니다.",
"Select the folders to share with this device.": "이 장치와 공유할 폴더를 선택합니다.",
"Send & Receive": "송신 & 수신",
@@ -227,6 +242,7 @@
"Single level wildcard (matches within a directory only)": "단일 레벨 와일드카드 (하나의 디렉토리만 일치하는 경우)",
"Size": "Size",
"Smallest First": "작은 파일순",
"Some items could not be restored:": "Some items could not be restored:",
"Source Code": "소스 코드",
"Stable releases and release candidates": "안정 버전과 출시 후보",
"Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "안정 버전은 약 2주 정도 지연되어 출시됩니다. 그 기간 동안 출시 후보에 대한 테스트를 거칩니다.",
@@ -292,6 +308,7 @@
"Usage reporting is always enabled for candidate releases.": "출시 후보 버전에서는 사용 보고서가 항상 활성화 됩니다.",
"Use HTTPS for GUI": "GUI에서 HTTPS 프로토콜 사용",
"Version": "버전",
"Versions": "Versions",
"Versions Path": "버전 저장 경로",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "최대 보존 기간보다 오래되었거나 지정한 개수를 넘긴 버전은 자동으로 삭제됩니다.",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "경고, 이 경로는 현재 존재하는 폴더 \"{{otherFolder}}\" 의 상위 폴더 입니다.",

View File

@@ -28,6 +28,7 @@
"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.",
"Are you sure you want to remove device {%name%}?": "Ar tikrai norite pašalinti įrenginį {{name}}?",
"Are you sure you want to remove folder {%label%}?": "Ar tikrai norite pašalinti aplanką {{label}}?",
"Are you sure you want to restore {%count%} files?": "Ar tikrai norite atkurti {{count}} failų(-us)?",
"Auto Accept": "Automatiškai priimti",
"Automatic upgrade now offers the choice between stable releases and release candidates.": "Automatiniai atnaujinimai dabar siūlo pasirinkimą tarp stabilių versijų ir kandidatinių versijų.",
"Automatic upgrades": "Automatiniai atnaujinimai",
@@ -52,6 +53,7 @@
"Copyright © 2014-2017 the following Contributors:": "Autorių teisės © 2014-2017 šių bendraautorių:",
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Kuriami nepaisomi šablonai, perrašomas esamas failas, esantis {{path}}.",
"Danger!": "Pavojus!",
"Debugging Facilities": "Derinimo priemonės",
"Default Folder Path": "Numatytojo aplanko kelias",
"Deleted": "Ištrinta",
"Device": "Įrenginys",
@@ -66,6 +68,8 @@
"Discovered": "Atrastas",
"Discovery": "Lokacija",
"Discovery Failures": "Matomumo nesėkmės",
"Do not restore": "Neatkurti",
"Do not restore all": "Neatkurti visus",
"Documentation": "Aprašymas",
"Download Rate": "Parsisiuntimo greitis",
"Downloaded": "Parsisiųstas",
@@ -94,6 +98,8 @@
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Programai Syncthing pakeičiant ar ištrinant failus, jie yra perkeliami į datomis pažymėtas versijas, aplanke .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.": "Failai yra apsaugoti nuo kituose įrenginiuose atliktų pakeitimų, bet pakeitimai šiame įrenginyje bus nusiųsti kitiems įrenginiams.",
"Filesystem Notifications": "Failų sistemos pranešimai",
"Filter by date": "Filtruoti pagal datą",
"Filter by name": "Filtruoti pagal pavadinimą",
"Folder": "Aplankas",
"Folder ID": "Aplanko ID",
"Folder Label": "Aplanko etiketė",
@@ -132,10 +138,15 @@
"Learn more": "Sužinoti daugiau",
"Listeners": "Klausytojai",
"Loading data...": "Įkeliami duomenys...",
"Loading...": "Įkeliama...",
"Local Discovery": "Vietinis matomumas",
"Local State": "Vietinė būsena",
"Local State (Total)": "Vietinė būsena (Bendrai)",
"Log": "Žurnalas",
"Log tailing paused. Click here to continue.": "Žurnalo galas pristabdytas. Spustelėkite čia, norėdami tęsti.",
"Logs": "Žurnalai",
"Major Upgrade": "Stambus atnaujinimas",
"Mass actions": "Masiniai veiksmai",
"Master": "Pagrindinis",
"Maximum Age": "Maksimalus amžius",
"Metadata Only": "Metaduomenims",
@@ -196,6 +207,8 @@
"Restart": "Perleisti",
"Restart Needed": "Reikalingas perleidimas",
"Restarting": "Persileidžia",
"Restore": "Atkurti",
"Restore Versions": "Atkurti versijas",
"Resume": "Pratęsti",
"Resume All": "Pratęsti visus",
"Reused": "Pakartotinas",
@@ -205,6 +218,8 @@
"See external versioner help for supported templated command line parameters.": "Palaikomiems šabloniniams komandų eilutės parametrams, žiūrėkite išorinį versijų valdymo programos žinyną.",
"See external versioning help for supported templated command line parameters.": "Palaikomiems šabloniniams komandų eilutės parametrams, žiūrėkite išorinį versijų valdymo žinyną.",
"Select a version": "Pasirinkti versiją",
"Select latest version": "Pasirinkti paskiausią versiją",
"Select oldest version": "Pasirinkti seniausią versiją",
"Select the devices to share this folder with.": "Pasirinkite įrenginius, su kuriais dalinsitės šį aplanką.",
"Select the folders to share with this device.": "Pasirinkite aplankus kuriais norite dalintis su šiuo įrenginiu.",
"Send & Receive": "Siųsti ir gauti",
@@ -227,6 +242,7 @@
"Single level wildcard (matches within a directory only)": "Vieno lygio pakaitos simbolis (atitinka tik vieną katalogo lygį)",
"Size": "Dydis",
"Smallest First": "Mažiausi pirmiau",
"Some items could not be restored:": "Kai kurių elementų atkurti nepavyko:",
"Source Code": "Išeities kodas",
"Stable releases and release candidates": "Stabilios versijos ir kandidatinės versijos",
"Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "Stabilios versijos pasirodo maždaug dvi savaites vėliau. Per tą laiką, jos pereina testavimą kaip kandidatinės versijos.",
@@ -292,6 +308,7 @@
"Usage reporting is always enabled for candidate releases.": "Naudojimo ataskaita kandidatinėms versijoms visada yra įjungta.",
"Use HTTPS for GUI": "Valdymo skydeliui naudoti saugų ryšį ",
"Version": "Versija",
"Versions": "Versijos",
"Versions Path": "Kelias iki versijos",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versijos ištrinamos jei senesnės už nustatyta maksimalų amžių arba jei viršytas maksimalus failų skaičius per nustatytą laiko tarpą.",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Įspėjimas, šis kelias yra esamo aplanko \"{{otherFolder}}\" virškatalogis.",

View File

@@ -28,10 +28,11 @@
"Any devices configured on an introducer device will be added to this device as well.": "Enheter satt opp på en introduksjonsenhet vil også bli lagt til denne enheten.",
"Are you sure you want to remove device {%name%}?": "Er du sikker på at du ønsker å fjerne enheten {{name}}?",
"Are you sure you want to remove folder {%label%}?": "Er du sikker på at du ønsker å fjerne mappen {{label}}?",
"Auto Accept": "Auto Accept",
"Are you sure you want to restore {%count%} files?": "Er du sikker på at du ønsker å gjenopprette {{count}} filer?",
"Auto Accept": "Godta automatisk",
"Automatic upgrade now offers the choice between stable releases and release candidates.": "Automatisk oppgradering lar deg nå få valget mellom ferdige utgaver og utgivelseskandidater.",
"Automatic upgrades": "Automatiske oppdateringer",
"Automatically create or share folders that this device advertises at the default path.": "Automatically create or share folders that this device advertises at the default path.",
"Automatically create or share folders that this device advertises at the default path.": "Opprett eller del mapper automatisk i mapper som denne enheten melder som forvalgt mappe.",
"Be careful!": "Vær forsiktig!",
"Bugs": "Programfeil",
"CPU Utilization": "CPU-utnyttelse",
@@ -45,27 +46,30 @@
"Configured": "Oppsatt",
"Connection Error": "Tilkoblingsfeil",
"Connection Type": "Tilkoblingstype",
"Connections": "Connections",
"Connections": "Tilkoblinger",
"Copied from elsewhere": "Kopiert fra et annet sted",
"Copied from original": "Kopiert fra original",
"Copyright © 2014-2016 the following Contributors:": "Opphavsrett © 2014-2016 for følgende bidragsytere:",
"Copyright © 2014-2017 the following Contributors:": "Opphavsrett © 2014-2017 for følgende bidragsytere:",
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Oppretter ignoreringsmønster, overskriver eksisterende fil i {{path}}.",
"Danger!": "Fare!",
"Default Folder Path": "Default Folder Path",
"Debugging Facilities": "Feilrettingsverktøy",
"Default Folder Path": "Forvalgt mappeplassering",
"Deleted": "Slettet",
"Device": "Enhet",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Enhet \"{{name}}\" ({{device}} på {{address}}) ønsker å koble til. Legge til ny enhet?",
"Device ID": "Enhets-ID",
"Device Identification": "Enhetskjennemerke",
"Device Name": "Navn på enhet",
"Device that last modified the item": "Device that last modified the item",
"Device that last modified the item": "Enheten som sist endret elementet",
"Devices": "Enheter",
"Disabled": "Avskrudd",
"Disconnected": "Frakoblet",
"Discovered": "Oppdaget",
"Discovery": "Oppslag",
"Discovery Failures": "Oppslagsfeil",
"Do not restore": "Ikke gjenopprett",
"Do not restore all": "Ikke gjenopprett alle",
"Documentation": "Dokumentasjon",
"Download Rate": "Nedlastingsrate",
"Downloaded": "Lastet ned",
@@ -94,6 +98,8 @@
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Filer flyttes til en datostemplet versjon i .stversions-mappa når den oppdateres eller slettes av Syncthing.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Filer er beskyttet mot endringer som er gjort på andre enheter, men endringer som er gjort på denne enheten blir sendt til resten av gruppen.",
"Filesystem Notifications": "Filsystemvarsler ",
"Filter by date": "Filtrer etter dato",
"Filter by name": "Filtrer etter navn",
"Folder": "Mappe",
"Folder ID": "Mappe-ID",
"Folder Label": "Merkelapp for mappe",
@@ -106,7 +112,7 @@
"GUI Listen Address": "Lytteadresse for grafisk brukergrensesnitt",
"GUI Listen Addresses": "GUI-lytteadresse",
"GUI Theme": "GUI-tema",
"General": "General",
"General": "Hovedinnstillinger",
"Generate": "Generer",
"Global Changes": "Globale endringer",
"Global Discovery": "Globalt oppslag",
@@ -131,17 +137,22 @@
"Latest Change": "Sist endret",
"Learn more": "Lær mer",
"Listeners": "Lyttere",
"Loading data...": "Loading data...",
"Loading data...": "Laster inn data",
"Loading...": "Laster…",
"Local Discovery": "Lokalt oppslag",
"Local State": "Lokal tilstand",
"Local State (Total)": "Lokal tilstand (total)",
"Log": "Logg",
"Log tailing paused. Click here to continue.": "Tail-utdata pauset. Klikk her for å fortsette.",
"Logs": "Logger",
"Major Upgrade": "Storoppgradering",
"Mass actions": "Massehandlinger",
"Master": "Beskyttet",
"Maximum Age": "Maksimal levetid",
"Metadata Only": "Kun metadata",
"Minimum Free Disk Space": "Nødvendig ledig diskplass",
"Mod. Device": "Mod. Device",
"Mod. Time": "Mod. Time",
"Mod. Device": "Endringsenhet",
"Mod. Time": "Endringstid",
"Move to top of queue": "Flytt fremst i køen",
"Multi level wildcard (matches multiple directory levels)": "Multinivåsøk (søker på flere mappenivå)",
"Never": "Aldri",
@@ -165,7 +176,7 @@
"Override Changes": "Overstyr endringer",
"Path": "Sti",
"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": "Plasseringen av mappen på datamaskinen. Denne vil bli opprettet dersom den ikke finnes. Krøllstrektegnet (~) kan brukes som forkortelse for",
"Path where new auto accepted folders will be created, as well as the default suggested path when adding new folders via the UI. Tilde character (~) expands to {%tilde%}.": "Path where new auto accepted folders will be created, as well as the default suggested path when adding new folders via the UI. Tilde character (~) expands to {{tilde}}.",
"Path where new auto accepted folders will be created, as well as the default suggested path when adding new folders via the UI. Tilde character (~) expands to {%tilde%}.": "Mappe der automatisk godkjente mapper vil oppretes, såvel også forvalgt anbefalt mappe ved tillegg av nye mapper via det grafiske brukergrensesnittet. Tilde-tegn (~) utvides til {{tilde}}.",
"Path where versions should be stored (leave empty for the default .stversions directory in the shared folder).": "Plasseringen for lagrede versjoner (la denne være tom for å bruke den forvalgte .stversions-mappa i den delte mappa).",
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Plasseringen for lagrede versjoner (la denne være tom for å bruke standard .stversions-mappen i mappen).",
"Pause": "Oppholde",
@@ -196,6 +207,8 @@
"Restart": "Omstart",
"Restart Needed": "Omstart kreves",
"Restarting": "Starter på nytt",
"Restore": "Gjenopprett",
"Restore Versions": "Gjenopprett versjoner",
"Resume": "Gjenoppta",
"Resume All": "Gjenoppta alt",
"Reused": "Gjenbrukt",
@@ -205,6 +218,8 @@
"See external versioner help for supported templated command line parameters.": "Se ekstern versjoneringshjelp for støttede mal-baserte kommandolinjeparameter.",
"See external versioning help for supported templated command line parameters.": "Se ekstern versjoneringshjelp for støttede mal-baserte kommandolinjeparameter.",
"Select a version": "Velg en versjon",
"Select latest version": "Velg siste versjon",
"Select oldest version": "Velg eldste versjon",
"Select the devices to share this folder with.": "Velg enhetene du vil dele denne mappen med.",
"Select the folders to share with this device.": "Velg hvilke mapper som skal deles med denne enheten.",
"Send & Receive": "Sende og motta",
@@ -225,8 +240,9 @@
"Shutdown Complete": "Avslutning fullført",
"Simple File Versioning": "Enkel versjonskontroll",
"Single level wildcard (matches within a directory only)": "Enkeltnivåsøk (søker kun i en mappe)",
"Size": "Size",
"Size": "Størrelse",
"Smallest First": "Den minste først",
"Some items could not be restored:": "Noen elementer kunne ikke gjenopprettes:",
"Source Code": "Kildekode",
"Stable releases and release candidates": "Ferdige utgaver og utgivelseskandidater",
"Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "Ferdige utgaver blir holdt tilbake i to uker. I løpet av denne tiden blir de testet som utgivelseskandidater.",
@@ -273,11 +289,11 @@
"This is a major version upgrade.": "Dette er en storoppgradering",
"This setting controls the free space required on the home (i.e., index database) disk.": "Denne innstillingen kontrollerer ledig diskplass krevd på hjemme- (f.eks. indekseringsdatabase-) disken.",
"Time": "Klokkeslett",
"Time the item was last modified": "Time the item was last modified",
"Time the item was last modified": "Tidspunktet elementet sist ble endret",
"Trash Can File Versioning": "Papirkurv versjonskontroll",
"Type": "Type",
"Unavailable": "Unavailable",
"Unavailable/Disabled by administrator or maintainer": "Unavailable/Disabled by administrator or maintainer",
"Unavailable": "Utilgjengelig",
"Unavailable/Disabled by administrator or maintainer": "Utilgjengelig/avskrudd av administrator eller vedlikeholder",
"Undecided (will prompt)": "Ikke bestemt (vil spørre)",
"Unknown": "Ukjent",
"Unshared": "Ikke delt",
@@ -292,6 +308,7 @@
"Usage reporting is always enabled for candidate releases.": "Bruksrapportering er alltid påslått for utgivelseskandidater",
"Use HTTPS for GUI": "Bruk HTTPS for GUI",
"Version": "Versjon",
"Versions": "Versjoner",
"Versions Path": "Plassering av versjoner",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versjoner blir automatisk slettet når maksimal levetid er nådd eller når antall filer er oversteget.",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Advarsel, denne stien er en foreldremappe for en eksisterende mappe \"{{otherFolder}}\".",

View File

@@ -28,6 +28,7 @@
"Any devices configured on an introducer device will be added to this device as well.": "Apparaten die geconfigureerd worden op een introductieapparaat zullen ook aan dit apparaat worden toegevoegd.",
"Are you sure you want to remove device {%name%}?": "Weet je zeker dat je apparaat {{name}} wil verwijderen?",
"Are you sure you want to remove folder {%label%}?": "Weet je zeker dat je map {{label}} wil verwijderen?",
"Are you sure you want to restore {%count%} files?": "Weet je zeker dat je {{count}} bestanden wil herstellen?",
"Auto Accept": "Automatisch aanvaarden",
"Automatic upgrade now offers the choice between stable releases and release candidates.": "Automatisch bijwerken biedt nu de keuze tussen stabiele uitgaven en uitgavekandidaten.",
"Automatic upgrades": "Automatische upgrades",
@@ -52,6 +53,7 @@
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 voor de volgende bijdragers:",
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Negeerpatronen worden aangemaakt, bestaand bestand wordt overschreven op {{path}}.",
"Danger!": "Let op!",
"Debugging Facilities": "Debugmogelijkheden",
"Default Folder Path": "Standaardmaplocatie",
"Deleted": "Verwijderd",
"Device": "Apparaat",
@@ -66,6 +68,8 @@
"Discovered": "Ontdekt",
"Discovery": "Ontdekken",
"Discovery Failures": "Ontdekkingsproblemen",
"Do not restore": "Niet herstellen",
"Do not restore all": "Niet alles herstellen",
"Documentation": "Documentatie",
"Download Rate": "Downloadsnelheid",
"Downloaded": "Gedownload",
@@ -94,6 +98,8 @@
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Bestanden worden niet door Syncthing vervangen of verwijderd, maar verplaatst naar de map .stversions.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Bestanden zijn beschermt tegen aanpassingen die gemaakt zijn door andere apparaten, maar aanpassingen op dit apparaat worden doorgestuurd naar de rest van het cluster.",
"Filesystem Notifications": "Bestandssysteemmeldingen",
"Filter by date": "Filteren op datum",
"Filter by name": "Filteren op naam",
"Folder": "Map",
"Folder ID": "Map-ID",
"Folder Label": "Map label",
@@ -132,10 +138,15 @@
"Learn more": "Lees meer",
"Listeners": "Luisteraars",
"Loading data...": "Gegevens laden...",
"Loading...": "Laden...",
"Local Discovery": "Lokaal zoeken",
"Local State": "Lokale status",
"Local State (Total)": "Lokale status (totaal)",
"Log": "Logboek",
"Log tailing paused. Click here to continue.": "Log tailing paused. Click here to continue.",
"Logs": "Logboeken",
"Major Upgrade": "Grote update",
"Mass actions": "Mass actions",
"Master": "Master",
"Maximum Age": "Maximum leeftijd",
"Metadata Only": "Alleen metadata",
@@ -196,6 +207,8 @@
"Restart": "Herstart",
"Restart Needed": "Herstart nodig",
"Restarting": "Herstarten",
"Restore": "Herstellen",
"Restore Versions": "Versies herstellen",
"Resume": "Hervatten",
"Resume All": "Alles hervatten",
"Reused": "Hergebruikt",
@@ -205,6 +218,8 @@
"See external versioner help for supported templated command line parameters.": "Zie de documentatie van de externe versie voor ondersteunde command line parameters.",
"See external versioning help for supported templated command line parameters.": "Bekijk de documentatie van het extern versiebeheer voor ondersteunde opdrachtregelparameters.",
"Select a version": "Selecteer een versie",
"Select latest version": "Meest recente versie selecteren",
"Select oldest version": "Oudste versie selecteren",
"Select the devices to share this folder with.": "Selecteer de apparaten om deze map mee te delen.",
"Select the folders to share with this device.": "Selecteer de mappen om met dit apparaat te delen.",
"Send & Receive": "Verzenden & Ontvangen",
@@ -227,6 +242,7 @@
"Single level wildcard (matches within a directory only)": "Wildcard op enkelvoudig niveau (alleen toepasbaar binnen een map)",
"Size": "Grootte",
"Smallest First": "Kleinste eerst",
"Some items could not be restored:": "Sommige items konden niet hersteld worden.",
"Source Code": "Broncode",
"Stable releases and release candidates": "Stabiele versies en release candidates",
"Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "Stabiele uitgaven zijn met ongeveer twee weken vertraagd. Tijdens deze periode worden ze als uitgavekandidaten getest.",
@@ -292,6 +308,7 @@
"Usage reporting is always enabled for candidate releases.": "Rapportage van gebruik is altijd ingeschakeld voor de candidate releases.",
"Use HTTPS for GUI": "Gebruik HTTPS voor de GUI",
"Version": "Versie",
"Versions": "Versies",
"Versions Path": "Bestandspad versies",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versies worden automatisch verwijderd als deze ouder zijn dan de maximale leeftijd of als ze het maximaal aantal toegestane bestanden per interval overschrijden. ",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Waarschuwing, dit pad is een bovenliggende map van een bestaande map \"{{otherFolder}}\".",

View File

@@ -1,315 +0,0 @@
{
"A device with that ID is already added.": "Ei eining med den ID-en er allereie lagt til.",
"A negative number of days doesn't make sense.": "Eit negativt tal dagar gir ikkje meining.",
"A new major version may not be compatible with previous versions.": "Ein ny hovudversjon er ikkje nødvendigvis kompatibel med eldre versjonar. ",
"API Key": "API-nøkkel",
"About": "Om",
"Action": "Handling",
"Actions": "Handlingar",
"Add": "Legg til",
"Add Device": "Legg Til Eining",
"Add Folder": "Legg Til Mappe",
"Add Remote Device": "Legg til ekstern eining",
"Add devices from the introducer to our device list, for mutually shared folders.": "Add devices from the introducer to our device list, for mutually shared folders.",
"Add new folder?": "Leggja til ny mappe?",
"Address": "Adresse",
"Addresses": "Adresser",
"Advanced": "Avansert",
"Advanced Configuration": "Avansert konfigurasjon",
"Advanced settings": "Avanserte innstillingar",
"All Data": "Alle data",
"Allow Anonymous Usage Reporting?": "Tillata anonymisert bruksrapportering?",
"Allowed Networks": "Allowed Networks",
"Alphabetic": "Alfabetisk",
"An external command handles the versioning. It has to remove the file from the shared folder.": "An external command handles the versioning. It has to remove the file from the shared folder.",
"An external command handles the versioning. It has to remove the file from the synced folder.": "Ein ekstern kommando handterer filutgåver. Den må sørga for at fila blir fjerna frå den synkroniserte mappa.",
"Anonymous Usage Reporting": "Anonymisert bruksrapportering",
"Anonymous usage report format has changed. Would you like to move to the new format?": "Anonymous usage report format has changed. Would you like to move to the new format?",
"Any devices configured on an introducer device will be added to this device as well.": "Einingar konfigurert på ei introduksjonseining vil òg verta lagt til denne eininga.",
"Are you sure you want to remove device {%name%}?": "Are you sure you want to remove device {{name}}?",
"Are you sure you want to remove folder {%label%}?": "Are you sure you want to remove folder {{label}}?",
"Auto Accept": "Auto Accept",
"Automatic upgrade now offers the choice between stable releases and release candidates.": "Automatic upgrade now offers the choice between stable releases and release candidates.",
"Automatic upgrades": "Automatiske oppdateringar",
"Automatically create or share folders that this device advertises at the default path.": "Automatically create or share folders that this device advertises at the default path.",
"Be careful!": "Ver varsam!",
"Bugs": "Programfeil",
"CPU Utilization": "CPU-utnytting",
"Changelog": "Endringslogg",
"Clean out after": "Tøm etter",
"Click to see discovery failures": "Click to see discovery failures",
"Close": "Lukk",
"Command": "Kommando",
"Comment, when used at the start of a line": "Kommentar, når brukt i starten av linja",
"Compression": "Komprimering",
"Configured": "Konfigurert",
"Connection Error": "Tilkoplingsfeil",
"Connection Type": "Tilkoplingstype",
"Connections": "Connections",
"Copied from elsewhere": "Kopiert frå ein annan stad",
"Copied from original": "Kopiert frå originalen",
"Copyright © 2014-2016 the following Contributors:": "Opphavsrett © 2014-2016 for følgjande bidragsyterar:",
"Copyright © 2014-2017 the following Contributors:": "Opphavsrett © 2014-2017 for følgjande bidragsyterar:",
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Creating ignore patterns, overwriting an existing file at {{path}}.",
"Danger!": "Fare!",
"Default Folder Path": "Default Folder Path",
"Deleted": "Sletta",
"Device": "Eining",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Eininga «{{name}}» {{device}} ({{address}}) vil kopla seg til. Vil du leggja ho til?",
"Device ID": "Eining ID",
"Device Identification": "Einingskjennemerke",
"Device Name": "Namn På Eining",
"Device that last modified the item": "Device that last modified the item",
"Devices": "Einingar",
"Disabled": "Disabled",
"Disconnected": "Fråkopla",
"Discovered": "Oppdaga",
"Discovery": "Oppdaging",
"Discovery Failures": "Discovery Failures",
"Documentation": "Dokumentasjon",
"Download Rate": "Nedlastingsfart",
"Downloaded": "Lasta ned",
"Downloading": "Lastar ned",
"Edit": "Rediger",
"Edit Device": "Rediger enhet",
"Edit Folder": "Rediger katalog",
"Editing": "Redigerer",
"Editing {%path%}.": "Editing {{path}}.",
"Enable NAT traversal": "Slå på NAT-gjennomgang",
"Enable Relaying": "Aktiver relé",
"Enter a non-negative number (e.g., \"2.35\") and select a unit. Percentages are as part of the total disk size.": "Enter a non-negative number (e.g., \"2.35\") and select a unit. Percentages are as part of the total disk size.",
"Enter a non-privileged port number (1024 - 65535).": "Enter a non-privileged port number (1024 - 65535).",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Skriv inn adresser med komma mellom kvar adresse («tcp://ip:port», «tcp://host:port»), eller «dynamic» for å automatisk søkja opp adressa.",
"Enter ignore patterns, one per line.": "Skriv inn mønster som skal utelatast, eitt per linje.",
"Error": "Feilmelding",
"External File Versioning": "Ekstern filutgåvehandtering",
"Failed Items": "Feilande element",
"Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": "Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.",
"File Pull Order": "Henterekkefølge for filer",
"File Versioning": "Filutgåvekontroll",
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Informasjon om filrettar vert ignorert når det blir leita etter endringar. Bruk på FAT-filsystem. ",
"Files are moved to .stversions directory when replaced or deleted by Syncthing.": "Files are moved to .stversions directory when replaced or deleted by Syncthing.",
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Filer som Syncthing slettar eller skriv over vert flytta til katalogen .stversions.",
"Files are moved to date stamped versions in a .stversions directory when replaced or deleted by Syncthing.": "Files are moved to date stamped versions in a .stversions directory when replaced or deleted by Syncthing.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Filer som Syncthing oppdaterer eller slettar vert flytta til ein datostempla versjon i .stversions-katalogen.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Filer er beskytta mot endringar gjort på andre einingar, men endringar gjort på denne eininga vert sende til resten av klyngja.",
"Filesystem Notifications": "Filesystem Notifications",
"Folder": "Mappe",
"Folder ID": "Mappe ID",
"Folder Label": "Merkelapp for mappa",
"Folder Path": "Mappeplassering",
"Folder Type": "Mappetype",
"Folders": "Mapper",
"GUI": "grafisk brukargrensesnitt",
"GUI Authentication Password": "GUI Passord",
"GUI Authentication User": "GUI Brukar",
"GUI Listen Address": "GUI Listen Address",
"GUI Listen Addresses": "GUI Lytteadresse",
"GUI Theme": "GUI Theme",
"General": "General",
"Generate": "Generer",
"Global Changes": "Global Changes",
"Global Discovery": "Global søking",
"Global Discovery Servers": "Globale søkjetenarar",
"Global State": "Global Tilstand",
"Help": "Hjelp",
"Home page": "Heimeside",
"Ignore": "Ignorer",
"Ignore Patterns": "Utelatingsmønster",
"Ignore Permissions": "Ignorer tilgangar",
"Incoming Rate Limit (KiB/s)": "Innkomande hastigheitsgrense (KiB/s)",
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Feil innstillingar kan skada innhaldet i dine delte katalogar og hindra Syncthing i å fungera.",
"Introduced By": "Introduced By",
"Introducer": "Introduktør",
"Inversion of the given condition (i.e. do not exclude)": "Det motsette av den gitte tilstanden (dvs. ekskluder ikkje)",
"Keep Versions": "Behald Versjonar",
"Largest First": "Største fyrst",
"Last File Received": "Siste mottatte fila",
"Last Scan": "Siste skanning",
"Last seen": "Sist sett",
"Later": "Seinare",
"Latest Change": "Siste endringar",
"Learn more": "Learn more",
"Listeners": "Lyttarar",
"Loading data...": "Loading data...",
"Local Discovery": "Lokal oppdaging",
"Local State": "Lokal Tilstand",
"Local State (Total)": "Lokal tilstand (total)",
"Major Upgrade": "Hovudoppgradering",
"Master": "Styrar",
"Maximum Age": "Maksimal Levetid",
"Metadata Only": "Berre metadata",
"Minimum Free Disk Space": "Naudsynt ledig diskplass",
"Mod. Device": "Mod. Device",
"Mod. Time": "Mod. Time",
"Move to top of queue": "Flytt øvst i køen",
"Multi level wildcard (matches multiple directory levels)": "Fleirnivå-jokerteikn (søkjer på fleire mappenivå)",
"Never": "Aldri",
"New Device": "Ny eining",
"New Folder": "Ny mappe",
"Newest First": "Nyaste fyrst",
"No": "Nei",
"No File Versioning": "Inga filutgåvehandtering",
"No files will be deleted as a result of this operation.": "No files will be deleted as a result of this operation.",
"No upgrades": "No upgrades",
"Normal": "Normal",
"Notice": "Merknad",
"OK": "OK",
"Off": "Av",
"Oldest First": "Eldste fyrst",
"Optional descriptive label for the folder. Can be different on each device.": "Valfri merkelapp på katalogen. Denne kan vera ulik på andre einingar.",
"Options": "Val",
"Out of Sync": "Ikkje synkronisert",
"Out of Sync Items": "Ikkje-synkroniserte element",
"Outgoing Rate Limit (KiB/s)": "Utgåande hastigheitsgrense (KiB/s)",
"Override Changes": "Overstyr endringar",
"Path": "Sti",
"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": "Plasseringa av mappa på datamaskinen. Vert oppretta om ho ikkje finst. Krøllstrekteiknet (~) kan brukast som forkorting for",
"Path where new auto accepted folders will be created, as well as the default suggested path when adding new folders via the UI. Tilde character (~) expands to {%tilde%}.": "Path where new auto accepted folders will be created, as well as the default suggested path when adding new folders via the UI. Tilde character (~) expands to {{tilde}}.",
"Path where versions should be stored (leave empty for the default .stversions directory in the shared folder).": "Path where versions should be stored (leave empty for the default .stversions directory in the shared folder).",
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Plasseringa for lagra versjonar (la denne vera tom for å bruka standardmappa .stversions i mappa).",
"Pause": "Stans",
"Pause All": "Pause All",
"Paused": "Stansa",
"Please consult the release notes before performing a major upgrade.": "Sjå utgjevingsmerknadene før ei hovudoppgradering vert utført.",
"Please set a GUI Authentication User and Password in the Settings dialog.": "Ver vennleg å laga ein GUI-brukar og eit passord i Innstillingar-dialogen.",
"Please wait": "Gjer vel og vent",
"Prefix indicating that the file can be deleted if preventing directory removal": "Prefix indicating that the file can be deleted if preventing directory removal",
"Prefix indicating that the pattern should be matched without case sensitivity": "Prefix indicating that the pattern should be matched without case sensitivity",
"Preview": "Førehandsvisning",
"Preview Usage Report": "Førehandsvis bruksrapporten",
"Quick guide to supported patterns": "Kjapp innføring i godkjente mønster",
"RAM Utilization": "Minnebruk",
"Random": "Tilfeldig",
"Recent Changes": "Recent Changes",
"Reduced by ignore patterns": "Reduser med utelatelsesmønster",
"Release Notes": "Utgjevingsnotat",
"Release candidates contain the latest features and fixes. They are similar to the traditional bi-weekly Syncthing releases.": "Release candidates contain the latest features and fixes. They are similar to the traditional bi-weekly Syncthing releases.",
"Remote Devices": "Eksterne einingar",
"Remove": "Fjern",
"Remove Device": "Remove Device",
"Remove Folder": "Remove Folder",
"Required identifier for the folder. Must be the same on all cluster devices.": "Påkravd identifikator for katalogen. Denne må vera lik på alle einingane i same klynge.",
"Rescan": "Skann På Ny",
"Rescan All": "Skann alle på nytt",
"Rescan Interval": "Skanneintervall",
"Restart": "Omstart",
"Restart Needed": "Omstart Trengs",
"Restarting": "Startar På Ny",
"Resume": "Start",
"Resume All": "Resume All",
"Reused": "Gjenbrukt",
"Save": "Lagre",
"Scan Time Remaining": "Gjenståande skannetid",
"Scanning": "Skannar",
"See external versioner help for supported templated command line parameters.": "See external versioner help for supported templated command line parameters.",
"See external versioning help for supported templated command line parameters.": "See external versioning help for supported templated command line parameters.",
"Select a version": "Select a version",
"Select the devices to share this folder with.": "Vel einingane du vil dela denne mappa med.",
"Select the folders to share with this device.": "Vel mappene du vil dela med denne eininga.",
"Send & Receive": "Sende og motta",
"Send Only": "Bare sende",
"Settings": "Innstillingar",
"Share": "Del",
"Share Folder": "Del mappe",
"Share Folders With Device": "Del mapper med eininga",
"Share With Devices": "Del Med Einingar",
"Share this folder?": "Dela denne mappa?",
"Shared With": "Delt Med",
"Show ID": "Vis ID",
"Show QR": "Vis QR",
"Show diff with previous version": "Show diff with previous version",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Vist i staden for einings-ID-en i klyngjestatusen. Vil verta kringkasta til dei andre einingane som eit valfritt standardnamn.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Vist i staden for mappe-ID-en i klyngjestatuses. Vil verta oppdatert til namnet eininga kringkastar dersom tomt.",
"Shutdown": "Slå Av",
"Shutdown Complete": "Slått av",
"Simple File Versioning": "Enkel filutgåvehandtering",
"Single level wildcard (matches within a directory only)": "Enkeltnivå-jokerteikn (søkjer berre i éi mappe)",
"Size": "Size",
"Smallest First": "Minste fyrst",
"Source Code": "Kildekode",
"Stable releases and release candidates": "Stable releases and release candidates",
"Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.",
"Stable releases only": "Stable releases only",
"Staggered File Versioning": "Forskuva filutgåvehandtering",
"Start Browser": "Start Nettlesar",
"Statistics": "Statistikk",
"Stopped": "Stoppa",
"Support": "Brukarstøtte",
"Sync Protocol Listen Addresses": "Lytteadresse For Synkroniseringsprotokoll",
"Syncing": "Synkroniserer",
"Syncthing has been shut down.": "Syncthing har blitt slått av.",
"Syncthing includes the following software or portions thereof:": "Syncthing inkluderer føljande programvare heilt eller delvis:",
"Syncthing is restarting.": "Syncthing startar på ny.",
"Syncthing is upgrading.": "Syncthing oppgraderer.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing ser ut til å vera nede, eller så er det eit problem med nettilkoplinga di. Prøvar på ny …",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing ser ut til å ha støtt på eit problem under behandling av førespurnaden din. Ver vennleg å oppfriska nettlesaren eller starta Syncthing på nytt om problemet varer ved.",
"The Syncthing admin interface is configured to allow remote access without a password.": "Administreringsgrensesnittet til Syncthing er sett opp til å tillata ekstern tilgang utan passord.",
"The aggregated statistics are publicly available at the URL below.": "Samla statistikk er opent tilgjengeleg på URL-en nedanfor.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Instillingane har blitt lagra men ikkje aktivert. Syncthing må starta på ny for å aktivera dei nye instillingane.",
"The device ID cannot be blank.": "Eining ID kan ikkje vera tom.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Einings-ID-en som skal oppgjevast her kan hentast fram via menyvalet «Handlingar > Vis ID» på den andre eininga. Mellomrom og bindestrek er valfritt (blir ignorert).",
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Den krypterte bruksrapporten vert send dagleg. Han vert nytta til å spora vanlege plattformer, mappestorleikar og programutgåvene. Om datasettet endrar seg, vil dette meldingsvindauget dukka opp att, og du vil verta beden om å godkjenna det.",
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Einings-ID-en er ikkje gyldig. Han må vera på 52 eller 56 teikn og vera samansett av bokstavar og tal med valfrie mellomrom og bindestrekar.",
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "Den første kommandolinjeparameteren er mappebana og den andre syner den relative bana i mappa.",
"The folder ID cannot be blank.": "Mappe ID kan ikkje vera tom.",
"The folder ID must be unique.": "Mappe ID må vera unik.",
"The folder path cannot be blank.": "Mappeplasseringa kan ikkje vera tom.",
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "Desse intervalla vert nytta: den fyrste timen vert ei utgåve lagra kvart 30. sekund, den fyrste dagen vert ei utgåve lagra kvar time, dei fyrste 30 dagane vert ei utgåve lagra kvar dag, og inntil høgaste alderen vert ei utgåve lagra kvar veke.",
"The following items could not be synchronized.": "Fylgjande filer kunne ikkje synkroniserast.",
"The maximum age must be a number and cannot be blank.": "Maksimal levetid må vera eit tal og kan ikkje vera tomt.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Høgaste tidsrom å spara på ei utgåve (i dagar; set til 0 for å spara på versjonane for alltid).",
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "Nødvendig ledig diskplass må vera eit tal mellom 0 og 100.",
"The number of days must be a number and cannot be blank.": "Tal på dagar må vera eit tal, og kan ikkje vera tomt.",
"The number of days to keep files in the trash can. Zero means forever.": "Antall dagar å behalda filer i søppelkorga. Null betyr for alltid.",
"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)": "Hastigheitsgrensa må vera eit positivt tall (0: inga grense)",
"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.": "Desse vil bli prøvd på nytt automatisk og vil bli synkronisert når feilen har blitt utbetra.",
"This Device": "Denne eininga",
"This can easily give hackers access to read and change any files on your computer.": "Dette kan lett gje datasnokar tilgang til å lesa og endra vilkårlege filer på denne maskina.",
"This is a major version upgrade.": "Dette er ei hovudoppgradering",
"This setting controls the free space required on the home (i.e., index database) disk.": "This setting controls the free space required on the home (i.e., index database) disk.",
"Time": "Time",
"Time the item was last modified": "Time the item was last modified",
"Trash Can File Versioning": "Papirkorg-filutgåvehandtering",
"Type": "Type",
"Unavailable": "Unavailable",
"Unavailable/Disabled by administrator or maintainer": "Unavailable/Disabled by administrator or maintainer",
"Undecided (will prompt)": "Undecided (will prompt)",
"Unknown": "Ukjent",
"Unshared": "Ikkje delt",
"Unused": "Ubrukt",
"Up to Date": "Oppdatert",
"Updated": "Oppdatert",
"Upgrade": "Oppgrader",
"Upgrade To {%version%}": "Oppgrader Til {{version}}",
"Upgrading": "Oppgraderer",
"Upload Rate": "Opplastingsfart",
"Uptime": "Oppetid",
"Usage reporting is always enabled for candidate releases.": "Usage reporting is always enabled for candidate releases.",
"Use HTTPS for GUI": "Bruk HTTPS ved grafisk grensesnitt",
"Version": "Versjon",
"Versions Path": "Utgåvebane",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Utgåver vert automatisk sletta når maksimal levetid er nådd eller når det høgaste tillate talet på filer innan eit intervall vert overskride.",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Warning, this path is a parent directory of an existing folder \"{{otherFolder}}\".",
"Warning, this path is a parent directory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Warning, this path is a parent directory of an existing folder \"{{otherFolderLabel}}\" ({{otherFolder}}).",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Åtvaring, denne bana er ei undermappe av den eksisterande mappa «{{otherFolder}}».",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Warning, this path is a subdirectory of an existing folder \"{{otherFolderLabel}}\" ({{otherFolder}}).",
"When adding a new device, keep in mind that this device must be added on the other side too.": "Hugs at når ei ny eining vert lagt til, må ho òg leggjast til på andre sida.",
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Hugs at når ei ny mappe vert lagt til, vert mappe-ID-en brukt til å binda saman mappene mellom einingane. Det er skilnad på store og små bokstavar, så ID-ane må vera identiske på alle einingane.",
"Yes": "Ja",
"You can also select one of these nearby devices:": "You can also select one of these nearby devices:",
"You can change your choice at any time in the Settings dialog.": "You can change your choice at any time in the Settings dialog.",
"You can read more about the two release channels at the link below.": "You can read more about the two release channels at the link below.",
"You must keep at least one version.": "Du må behalda minst ein versjon.",
"days": "dagar",
"directories": "katalogar",
"files": "filer",
"full documentation": "all dokumentasjon",
"items": "element",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} ønskjer å dela mappa \"{{folder}}\".",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} ønskjer å dela mappa «{{folderLabel}}» ({{folder}})."
}

View File

@@ -28,6 +28,7 @@
"Any devices configured on an introducer device will be added to this device as well.": "Wszystkie urządzenia skonfigurowane na urządzeniu wprowadzającym zostaną dodane także do tego urządzenia.",
"Are you sure you want to remove device {%name%}?": "Are you sure you want to remove device {{name}}?",
"Are you sure you want to remove folder {%label%}?": "Are you sure you want to remove folder {{label}}?",
"Are you sure you want to restore {%count%} files?": "Are you sure you want to restore {{count}} files?",
"Auto Accept": "Auto Accept",
"Automatic upgrade now offers the choice between stable releases and release candidates.": "Automatyczne aktualizacje pozwalają teraz wybrać pomiędzy wydaniami stabilnymi a wersjami kandydującymi.",
"Automatic upgrades": "Automatyczne aktualizacje",
@@ -52,6 +53,7 @@
"Copyright © 2014-2017 the following Contributors:": "Prawa autorskie © 2014-2017 dla następujących autorów:",
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Ustawienie wzorów ignorowania, nadpisze istniejący plik w {{path}}.",
"Danger!": "Niebezpieczne!",
"Debugging Facilities": "Debugging Facilities",
"Default Folder Path": "Default Folder Path",
"Deleted": "Usunięto",
"Device": "Urządzenie",
@@ -66,6 +68,8 @@
"Discovered": "Odkryte",
"Discovery": "Odnajdywanie",
"Discovery Failures": "Błędy odnajdowania",
"Do not restore": "Do not restore",
"Do not restore all": "Do not restore all",
"Documentation": "Dokumentacja",
"Download Rate": "Prędkość pobierania",
"Downloaded": "Pobrane",
@@ -94,6 +98,8 @@
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Pliki przenoszone są do wersji oznaczonych datą w folderze .stversions kiedy są zastępowane bądź usuwane przez Syncthing",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Pliki są zabezpieczone przed zmianami na innym urządzeniu, jednak zmiany w tym urządzeniu będą wysłane do reszty.",
"Filesystem Notifications": "Powiadomienia systemowe",
"Filter by date": "Filter by date",
"Filter by name": "Filter by name",
"Folder": "Folder",
"Folder ID": "ID folderu",
"Folder Label": "Etykieta folderu",
@@ -132,10 +138,15 @@
"Learn more": "Zobacz więcej",
"Listeners": "Nasłuchujący",
"Loading data...": "Loading data...",
"Loading...": "Loading...",
"Local Discovery": "Lokalne odnajdywanie",
"Local State": "Status lokalny",
"Local State (Total)": "Status lokalny (suma)",
"Log": "Log",
"Log tailing paused. Click here to continue.": "Log tailing paused. Click here to continue.",
"Logs": "Logs",
"Major Upgrade": "Ważna aktualizacja",
"Mass actions": "Mass actions",
"Master": "Główny",
"Maximum Age": "Maksymalny wiek",
"Metadata Only": "Tylko metadane",
@@ -196,6 +207,8 @@
"Restart": "Uruchom ponownie",
"Restart Needed": "Wymagane ponowne uruchomienie",
"Restarting": "Uruchamianie ponowne",
"Restore": "Restore",
"Restore Versions": "Restore Versions",
"Resume": "Wznów",
"Resume All": "Wznów wszystkie",
"Reused": "Ponownie użyte",
@@ -205,6 +218,8 @@
"See external versioner help for supported templated command line parameters.": "Dostępne zmienne dla polecenia opisane są w dokumentacji w sekcji Zewnętrzne wersjonowanie plików.",
"See external versioning help for supported templated command line parameters.": "Dostępne zmienne dla polecenia opisane są w dokumentacji w sekcji Zewnętrzne wersjonowanie plików.",
"Select a version": "Wybierz wersję",
"Select latest version": "Select latest version",
"Select oldest version": "Select oldest version",
"Select the devices to share this folder with.": "Wybierz urządzenie, któremu udostępnić folder.",
"Select the folders to share with this device.": "Wybierz foldery do współdzielenia z tym urządzeniem.",
"Send & Receive": "Wyślij i odbierz",
@@ -227,6 +242,7 @@
"Single level wildcard (matches within a directory only)": "Wieloznaczność na poziomie plików (uwzględnia nazwy plików)",
"Size": "Size",
"Smallest First": "Najmniejsze na początku",
"Some items could not be restored:": "Some items could not be restored:",
"Source Code": "Kod źródłowy",
"Stable releases and release candidates": "Wydania stabilne i wydania kandydujące",
"Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "Wydania stabilne są opóźnione ok. dwa tygodnie. W tym czasie są testowane jako wydania kandydujące.",
@@ -292,6 +308,7 @@
"Usage reporting is always enabled for candidate releases.": "Raportowanie użycia dla wydań kandydujących jest zawsze włączone.",
"Use HTTPS for GUI": "Używaj HTTPS",
"Version": "Wersja",
"Versions": "Versions",
"Versions Path": "Ścieżka wersji",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Wersje zostają automatycznie usunięte jeżeli są starsze niż maksymalny wiek lub przekraczają liczbę dopuszczalnych wersji.",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Uwaga, ta ścieżka jest nadrzędnym folderem istniejącego folderu \"{{otherFolder}}\".",

View File

@@ -28,6 +28,7 @@
"Any devices configured on an introducer device will be added to this device as well.": "Quaisquer dispositivos configurados em um apresentador também serão adicionados a este dispositivo.",
"Are you sure you want to remove device {%name%}?": "Are you sure you want to remove device {{name}}?",
"Are you sure you want to remove folder {%label%}?": "Are you sure you want to remove folder {{label}}?",
"Are you sure you want to restore {%count%} files?": "Are you sure you want to restore {{count}} files?",
"Auto Accept": "Auto Accept",
"Automatic upgrade now offers the choice between stable releases and release candidates.": "A atualização automática agora oferece a escolha entre versões estáveis e candidatas ao lançamento.",
"Automatic upgrades": "Atualizações automáticas",
@@ -52,6 +53,7 @@
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 dos seguintes Colaboradores:",
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Criando filtros, sobrescrevendo o arquivo {{path}}.",
"Danger!": "Perigo!",
"Debugging Facilities": "Debugging Facilities",
"Default Folder Path": "Default Folder Path",
"Deleted": "Apagado",
"Device": "Dispositivo",
@@ -66,6 +68,8 @@
"Discovered": "Descoberto",
"Discovery": "Descoberta",
"Discovery Failures": "Falhas na descoberta",
"Do not restore": "Do not restore",
"Do not restore all": "Do not restore all",
"Documentation": "Documentação",
"Download Rate": "Velocidade de recepção",
"Downloaded": "Recebido",
@@ -94,6 +98,8 @@
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Os arquivos são renomeados com suas datas na pasta .stversions quando substituídos ou removidos pelo 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.": "Os arquivos estão protegidos contra alterações feitas em outros dispositivos, mas alterações feitas neste dispositivo serão enviadas ao resto dos dispositivos.",
"Filesystem Notifications": "Notificação do Sistema de Arquivos",
"Filter by date": "Filter by date",
"Filter by name": "Filter by name",
"Folder": "Pasta",
"Folder ID": "ID da pasta",
"Folder Label": "Rótulo da pasta",
@@ -132,10 +138,15 @@
"Learn more": "Saiba mais",
"Listeners": "Escutadores",
"Loading data...": "Loading data...",
"Loading...": "Loading...",
"Local Discovery": "Descoberta local",
"Local State": "Estado local",
"Local State (Total)": "Estado local (total)",
"Log": "Log",
"Log tailing paused. Click here to continue.": "Log tailing paused. Click here to continue.",
"Logs": "Logs",
"Major Upgrade": "Atualização \"major\"",
"Mass actions": "Mass actions",
"Master": "Mestre",
"Maximum Age": "Idade máxima",
"Metadata Only": "Somente metadados",
@@ -196,6 +207,8 @@
"Restart": "Reiniciar",
"Restart Needed": "É necessário reiniciar",
"Restarting": "Reiniciando",
"Restore": "Restore",
"Restore Versions": "Restore Versions",
"Resume": "Resumir",
"Resume All": "Resumir Todas",
"Reused": "Reutilizado",
@@ -205,6 +218,8 @@
"See external versioner help for supported templated command line parameters.": "Consulte a ajuda sobre versionamento externo para modelos de parâmetros de linha de comando aceitos.",
"See external versioning help for supported templated command line parameters.": "Consulte a ajuda sobre versionamento externo para modelos de parâmetros de linha de comando aceitos.",
"Select a version": "Selecione uma versão",
"Select latest version": "Select latest version",
"Select oldest version": "Select oldest version",
"Select the devices to share this folder with.": "Selecione os dispositivos com os quais esta pasta será compartilhada.",
"Select the folders to share with this device.": "Selecione as pastas a serem compartilhadas com este dispositivo.",
"Send & Receive": "Envia e recebe",
@@ -227,6 +242,7 @@
"Single level wildcard (matches within a directory only)": "Coringa de único nível (faz corresponder apenas dentro de uma pasta)",
"Size": "Size",
"Smallest First": "Menor primeiro",
"Some items could not be restored:": "Some items could not be restored:",
"Source Code": "Código-fonte",
"Stable releases and release candidates": "Versões estáveis e candidatas ao lançamento",
"Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "Versões estáveis são lançadas aproximadamente a cada duas semanas. Durante esse tempo elas são testadas como candidatas.",
@@ -292,6 +308,7 @@
"Usage reporting is always enabled for candidate releases.": "O relatório de uso está sempre habilitado em versões candidatas ao lançamento",
"Use HTTPS for GUI": "Usar HTTPS para a interface web",
"Version": "Versão",
"Versions": "Versions",
"Versions Path": "Caminho do versionamento",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "As versões são automaticamente apagadas se elas são mais antigas do que a idade máxima ou excederem o número de arquivos permitido em um intervalo.",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Aviso: este caminho é o diretório pai da pasta \"{{otherFolder}}\".",

View File

@@ -28,10 +28,11 @@
"Any devices configured on an introducer device will be added to this device as well.": "Quaisquer dispositivos configurados num dispositivo apresentador serão também adicionados a este dispositivo.",
"Are you sure you want to remove device {%name%}?": "Tem a certeza que quer remover o dispositivo {{name}}?",
"Are you sure you want to remove folder {%label%}?": "Tem a certeza que quer remover a pasta {{label}}?",
"Are you sure you want to restore {%count%} files?": "Tem a certeza que quer restaurar {{count}} ficheiros?",
"Auto Accept": "Aceitar automaticamente",
"Automatic upgrade now offers the choice between stable releases and release candidates.": "A actualização automática agora oferece a escolha entre versões estáveis e candidatas a lançamento.",
"Automatic upgrades": "Actualizações automáticas",
"Automatically create or share folders that this device advertises at the default path.": "Criar ou partilhar automaticamente pastas que este dispositivo publicita no caminho predefinido.",
"Automatically create or share folders that this device advertises at the default path.": "Criar ou partilhar, de forma automática e no caminho predefinido, pastas que este dispositivo publicita.",
"Be careful!": "Tenha cuidado!",
"Bugs": "Erros",
"CPU Utilization": "Utilização da CPU",
@@ -52,6 +53,7 @@
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 dos seguintes contribuidores:",
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Criando padrões de exclusão, sobrescrevendo um ficheiro existente em {{path}}.",
"Danger!": "Perigo!",
"Debugging Facilities": "Recursos de depuração",
"Default Folder Path": "Caminho da pasta predefinida",
"Deleted": "Eliminado",
"Device": "Dispositivo",
@@ -59,13 +61,15 @@
"Device ID": "ID do dispositivo",
"Device Identification": "Identificação do dispositivo",
"Device Name": "Nome do dispositivo",
"Device that last modified the item": "Dispositivo que modificou o item pela última vez",
"Device that last modified the item": "Último dispositivo a modificar o item",
"Devices": "Dispositivos",
"Disabled": "Desactivado",
"Disconnected": "Desconectado",
"Discovered": "Descoberto",
"Discovery": "Pesquisa",
"Discovery Failures": "Falhas da pesquisa",
"Do not restore": "Não restaurar",
"Do not restore all": "Não restaurar todos",
"Documentation": "Documentação",
"Download Rate": "Velocidade de recepção",
"Downloaded": "Recebido",
@@ -94,6 +98,8 @@
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Os ficheiros são movidos para versões carimbadas com o tempo numa pasta .stversions, ao serem substituídos ou apagados pelo 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.": "Os ficheiros estão protegidos contra alterações feitas noutros dispositivos, mas alterações feitas neste dispositivo serão enviadas ao resto do grupo.",
"Filesystem Notifications": "Notificações do sistema de ficheiros",
"Filter by date": "Filtrar por data",
"Filter by name": "Filtrar por nome",
"Folder": "Pasta",
"Folder ID": "ID da pasta",
"Folder Label": "Etiqueta da pasta",
@@ -132,10 +138,15 @@
"Learn more": "Saiba mais",
"Listeners": "Auscultadores",
"Loading data...": "Carregando dados...",
"Loading...": "Carregando...",
"Local Discovery": "Pesquisa local",
"Local State": "Estado local",
"Local State (Total)": "Estado local (total)",
"Log": "Registo",
"Log tailing paused. Click here to continue.": "O acompanhamento do final do registo está em pausa. Clique aqui para continuar.",
"Logs": "Registos",
"Major Upgrade": "Actualização importante",
"Mass actions": "Acções em massa",
"Master": "Mestre",
"Maximum Age": "Idade máxima",
"Metadata Only": "Metadados apenas",
@@ -196,6 +207,8 @@
"Restart": "Reiniciar",
"Restart Needed": "É preciso reiniciar",
"Restarting": "Reiniciando",
"Restore": "Restaurar",
"Restore Versions": "Restaurar versões",
"Resume": "Retomar",
"Resume All": "Retomar todas",
"Reused": "Reutilizado",
@@ -205,6 +218,8 @@
"See external versioner help for supported templated command line parameters.": "Veja a ajuda do gestor de versões externo para saber que parâmetros da linha de comandos são suportados.",
"See external versioning help for supported templated command line parameters.": "Veja a ajuda externa sobre gestão de versões para ver os modelos suportados de parâmetros para a linha de comandos.",
"Select a version": "Seleccione uma versão",
"Select latest version": "Seleccione a última versão",
"Select oldest version": "Seleccione a versão mais antiga",
"Select the devices to share this folder with.": "Seleccione os dispositivos com os quais vai partilhar esta pasta.",
"Select the folders to share with this device.": "Seleccione as pastas a partilhar com este dispositivo.",
"Send & Receive": "Envia e recebe",
@@ -227,6 +242,7 @@
"Single level wildcard (matches within a directory only)": "Caractere polivalente de um só nível (faz corresponder apenas dentro de uma pasta)",
"Size": "Tamanho",
"Smallest First": "Primeiro os menores",
"Some items could not be restored:": "Não foi possível restaurar alguns dos itens:",
"Source Code": "Código fonte",
"Stable releases and release candidates": "Versões estáveis e versões candidatas a lançamento",
"Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "Versões estáveis são adiadas por cerca de duas semanas. Durante esse período são submetidas a testes sob a forma de versões candidatas a lançamento.",
@@ -277,7 +293,7 @@
"Trash Can File Versioning": "Reciclagem",
"Type": "Tipo",
"Unavailable": "Indisponível",
"Unavailable/Disabled by administrator or maintainer": "Indisponível/Desactivado pelo administrador ou responsável de manutenção",
"Unavailable/Disabled by administrator or maintainer": "Indisponíveis ou desactivadas pelo administrador ou responsável de manutenção",
"Undecided (will prompt)": "Não definido (será inquirido na altura)",
"Unknown": "Desconhecido",
"Unshared": "Não partilhada",
@@ -292,6 +308,7 @@
"Usage reporting is always enabled for candidate releases.": "O relatório de utilização está sempre activado nas versões candidatas a lançamento.",
"Use HTTPS for GUI": "Utilizar HTTPS na interface gráfica",
"Version": "Versão",
"Versions": "Versões",
"Versions Path": "Caminho das versões",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "As versões são eliminadas automaticamente se forem mais antigas do que a idade máxima ou excederem o número máximo de ficheiros permitido num intervalo.",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Aviso: Este caminho é uma pasta mãe duma pasta \"{{otherFolder}}\" já existente.",

View File

@@ -28,6 +28,7 @@
"Any devices configured on an introducer device will be added to this device as well.": "Все устройства, подключённые к устройству-рекомендателю, будут добавлены к текущему устройству.",
"Are you sure you want to remove device {%name%}?": "Are you sure you want to remove device {{name}}?",
"Are you sure you want to remove folder {%label%}?": "Are you sure you want to remove folder {{label}}?",
"Are you sure you want to restore {%count%} files?": "Are you sure you want to restore {{count}} files?",
"Auto Accept": "Auto Accept",
"Automatic upgrade now offers the choice between stable releases and release candidates.": "Автоматическое обновление теперь предлагает выбор между стабильными выпусками и кандидатами в релизы.",
"Automatic upgrades": "Автообновление",
@@ -45,27 +46,30 @@
"Configured": "Сконфигурировано",
"Connection Error": "Ошибка подключения",
"Connection Type": "Тип соединения",
"Connections": "Connections",
"Connections": "Подключения",
"Copied from elsewhere": "Скопировано из другого места",
"Copied from original": "Скопировано с оригинала",
"Copyright © 2014-2016 the following Contributors:": "Авторские права © 20142016 принадлежат:",
"Copyright © 2014-2017 the following Contributors:": "Авторские права © 2014—2017 следующие участники:",
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Creating ignore patterns, overwriting an existing file at {{path}}.",
"Danger!": "Опасно!",
"Default Folder Path": "Default Folder Path",
"Debugging Facilities": "Средства отладки",
"Default Folder Path": "Путь для папок",
"Deleted": "Удалено",
"Device": "Устройство",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Устройство «{{name}}» ({{device}} на {{address}}) хочет подключиться. Добавить новое устройство?",
"Device ID": "ID устройства",
"Device Identification": "Идентификация устройства",
"Device Name": "Имя устройства",
"Device that last modified the item": "Device that last modified the item",
"Device that last modified the item": "Устройство, последним изменившее объект",
"Devices": "Устройства",
"Disabled": "Отключено",
"Disconnected": "Нет соединения",
"Discovered": "Обнаружено",
"Discovery": "Обнаружение",
"Discovery Failures": "Discovery Failures",
"Discovery Failures": "Ошибки обнаружения",
"Do not restore": "Do not restore",
"Do not restore all": "Do not restore all",
"Documentation": "Документация",
"Download Rate": "Скорость загрузки",
"Downloaded": "Загружено",
@@ -94,6 +98,8 @@
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Файлы с временнОй меткой версии помещаются в папку .stversions при их замене или удалении 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.": "Файлы защищены от изменений сделанных на других устройствах, но изменения сделанные на этом устройстве будут отправлены всему кластеру.",
"Filesystem Notifications": "Уведомления файловой системы",
"Filter by date": "Filter by date",
"Filter by name": "Filter by name",
"Folder": "Папка",
"Folder ID": "ID папки",
"Folder Label": "Ярлык папки",
@@ -103,10 +109,10 @@
"GUI": "Интерфейс",
"GUI Authentication Password": "Пароль для доступа к панели управления",
"GUI Authentication User": "Имя пользователя для доступа к панели управления",
"GUI Listen Address": "GUI Listen Address",
"GUI Listen Address": "Адрес GUI",
"GUI Listen Addresses": "Адрес панели управления",
"GUI Theme": "Тема оформления",
"General": "General",
"General": "Общие",
"Generate": "Сгенерировать",
"Global Changes": "Глобальные изменения",
"Global Discovery": "Глобальное обнаружение",
@@ -131,11 +137,16 @@
"Latest Change": "Последнее изменение",
"Learn more": "Узнать больше",
"Listeners": "Прослушиватель",
"Loading data...": "Loading data...",
"Loading data...": "Загрузка данных...",
"Loading...": "Загрузка...",
"Local Discovery": "Локальное обнаружение",
"Local State": "Локальное состояние",
"Local State (Total)": "Локальное состояние (всего)",
"Log": "Журнал",
"Log tailing paused. Click here to continue.": "Log tailing paused. Click here to continue.",
"Logs": "Журналы",
"Major Upgrade": "Обновление основной версии",
"Mass actions": "Mass actions",
"Master": "Мастер",
"Maximum Age": "Максимальный срок",
"Metadata Only": "Только метаданные",
@@ -181,14 +192,14 @@
"Quick guide to supported patterns": "Краткое руководство по поддерживаемым шаблонам",
"RAM Utilization": "Использование памяти",
"Random": "Случайно",
"Recent Changes": "Recent Changes",
"Recent Changes": "Последние изменения",
"Reduced by ignore patterns": "Уменьшено шаблонами игнорирования",
"Release Notes": "Примечания к выпуску",
"Release candidates contain the latest features and fixes. They are similar to the traditional bi-weekly Syncthing releases.": "Кандидаты в релизы содержат последние улучшения и исправления. Они похожи на традиционные двухнедельные выпуски Syncthing.",
"Remote Devices": "Удалённые устройства",
"Remove": "Удалить",
"Remove Device": "Remove Device",
"Remove Folder": "Remove Folder",
"Remove Device": "Удалить устройство",
"Remove Folder": "Удалить папку",
"Required identifier for the folder. Must be the same on all cluster devices.": "Обязательный идентификатор папки. Должен быть одним и тем же на всех устройствах кластера.",
"Rescan": "Пересканировать",
"Rescan All": "Пересканировать все",
@@ -196,6 +207,8 @@
"Restart": "Перезапустить",
"Restart Needed": "Требуется перезапуск",
"Restarting": "Перезапуск",
"Restore": "Restore",
"Restore Versions": "Restore Versions",
"Resume": "Возобновить",
"Resume All": "Возобновить все",
"Reused": "Повторно использовано",
@@ -205,6 +218,8 @@
"See external versioner help for supported templated command line parameters.": "See external versioner help for supported templated command line parameters.",
"See external versioning help for supported templated command line parameters.": "See external versioning help for supported templated command line parameters.",
"Select a version": "Выберите версию",
"Select latest version": "Select latest version",
"Select oldest version": "Select oldest version",
"Select the devices to share this folder with.": "Выберите устройства, для которых будет доступна эта папка.",
"Select the folders to share with this device.": "Выберите папки, которые будут доступны этому устройству.",
"Send & Receive": "Отправить и получить",
@@ -225,8 +240,9 @@
"Shutdown Complete": "Выключение",
"Simple File Versioning": "Простое управление версиями файлов",
"Single level wildcard (matches within a directory only)": "Одноуровневая маска (поиск совпадений только внутри папки)",
"Size": "Size",
"Size": "Размер",
"Smallest First": "Сначала маленькие",
"Some items could not be restored:": "Some items could not be restored:",
"Source Code": "Исходный код",
"Stable releases and release candidates": "Стабильные выпуски и кандидаты в релизы",
"Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "Стабильные выпуски выходят с задержкой около двух недель. За это время они проходят тестирование в качестве кандидатов в релизы.",
@@ -273,10 +289,10 @@
"This is a major version upgrade.": "Это обновление основной версии продукта.",
"This setting controls the free space required on the home (i.e., index database) disk.": "Эта настройка управляет свободным местом, необходимым на домашнем диске (например, для базы индексов).",
"Time": "Время",
"Time the item was last modified": "Time the item was last modified",
"Time the item was last modified": "Время последней модификации объекта",
"Trash Can File Versioning": "Использовать версионность для файлов в Корзине",
"Type": "Тип",
"Unavailable": "Unavailable",
"Unavailable": "Недоступно",
"Unavailable/Disabled by administrator or maintainer": "Unavailable/Disabled by administrator or maintainer",
"Undecided (will prompt)": "Undecided (will prompt)",
"Unknown": "Неизвестно",
@@ -292,6 +308,7 @@
"Usage reporting is always enabled for candidate releases.": "Отправка отчётов об использовании всегда включена для кандидатов в релизы.",
"Use HTTPS for GUI": "Использовать HTTPS для панели управления",
"Version": "Версия",
"Versions": "Versions",
"Versions Path": "Путь к версиям",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Версии удаляются автоматически, если они существуют дольше максимального срока или превышают разрешённое количество файлов за интервал.",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Внимание! Этот путь — родительская директория уже существующей папки «{{otherFolder}}».",

View File

@@ -10,7 +10,7 @@
"Add Device": "Pridať zariadenie",
"Add Folder": "Pridať adresár",
"Add Remote Device": "Pridať vzdialené zariadenie",
"Add devices from the introducer to our device list, for mutually shared folders.": "Pre vzájomne zdielané adresáre pridaj zariadenie od zavádzača do svojho zoznamu zariadení.",
"Add devices from the introducer to our device list, for mutually shared folders.": "Pre vzájomne zdieľané adresáre pridaj zariadenie od zavádzača do svojho zoznamu zariadení.",
"Add new folder?": "Pridať nový adresár?",
"Address": "Adresa",
"Addresses": "Adresy",
@@ -26,12 +26,13 @@
"Anonymous Usage Reporting": "Anonymné hlásenie o používaní",
"Anonymous usage report format has changed. Would you like to move to the new format?": "Anonymous usage report format has changed. Would you like to move to the new format?",
"Any devices configured on an introducer device will be added to this device as well.": "Všetky zariadenia nakonfigurované na uvádzači budú tiež pridané na tomto zariadení.",
"Are you sure you want to remove device {%name%}?": "Are you sure you want to remove device {{name}}?",
"Are you sure you want to remove folder {%label%}?": "Are you sure you want to remove folder {{label}}?",
"Auto Accept": "Auto Accept",
"Are you sure you want to remove device {%name%}?": "Určite chcete odobrať zariadenie {{name}}?",
"Are you sure you want to remove folder {%label%}?": "Určite chcete odobrať adresár {{label}}?",
"Are you sure you want to restore {%count%} files?": "Určite chcete obnoviť {{count}} súborov?",
"Auto Accept": "Automatické prijatie",
"Automatic upgrade now offers the choice between stable releases and release candidates.": "Automatická aktualizácia teraz ponúka voľbu medzi stabilnými vydaniami a kandidátmi na vydanie.",
"Automatic upgrades": "Automatické aktualizácie",
"Automatically create or share folders that this device advertises at the default path.": "Automatically create or share folders that this device advertises at the default path.",
"Automatically create or share folders that this device advertises at the default path.": "Automaticky vytvoriť alebo zdieľať adresáre, ktoré toto zariadenie ohlasuje, v predvolenom adresári.",
"Be careful!": "Buď opatrný!",
"Bugs": "Chyby",
"CPU Utilization": "Využitie CPU",
@@ -45,27 +46,30 @@
"Configured": "Nakonfigurované",
"Connection Error": "Chyba pripojenia",
"Connection Type": "Typ pripojenia",
"Connections": "Connections",
"Connections": "Spojenia",
"Copied from elsewhere": "Skoprírované odinakiaľ",
"Copied from original": "Skopírované z originálu",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 následujúci prispivatelia:",
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 následujúci prispivatelia:",
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Vytváranie vzorov ignorovania, prepísanie existujúceho súboru v {{path}}.",
"Danger!": "Pozor!",
"Default Folder Path": "Default Folder Path",
"Debugging Facilities": "Debugging Facilities",
"Default Folder Path": "Predvolená adresárová cesta",
"Deleted": "Zmazané",
"Device": "Zariadenie",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Zariadenie \"{{name}}\" ({{device}} na {{address}}) sa chce pripojiť. Pridať nové zariadenie?",
"Device ID": "ID zariadenia",
"Device Identification": "Identifikácia zariadenia",
"Device Name": "Názov zariadenia",
"Device that last modified the item": "Device that last modified the item",
"Device that last modified the item": "Zariadenie, ktoré naposledy pozmenilo položku",
"Devices": "Zariadenia",
"Disabled": "Disabled",
"Disabled": "Odpojené",
"Disconnected": "Odpojené",
"Discovered": "Zistené",
"Discovery": "Zisťovanie",
"Discovery Failures": "Zlyhania zisťovania",
"Do not restore": "Neobnovovať",
"Do not restore all": "Neobnovovať všetko",
"Documentation": "Dokumentácia",
"Download Rate": "Rýchlosť sťahovania",
"Downloaded": "Stiahnuté",
@@ -93,7 +97,9 @@
"Files are moved to date stamped versions in a .stversions directory when replaced or deleted by Syncthing.": "Súbory premiestnené alebo zmazané aplikáciou Sycthing sú presunuté do verzií označených dátumov v adresári .stversions.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Súbory premiestnené alebo zmazané aplikáciou Syncthing sú premenované na verziu s dátumom a presunuté do adresára .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.": "Soubory sú chránené pred zmenami na ostatních zariadeniach, ale zmeny provedené z tohto zariadenia budú rozoslané na zvyšok klastra.",
"Filesystem Notifications": "Filesystem Notifications",
"Filesystem Notifications": "Oznamy súborového systému",
"Filter by date": "Filtrovanie podľa dátumu",
"Filter by name": "Filtrovanie podľa mena",
"Folder": "Adresár",
"Folder ID": "ID adresára",
"Folder Label": "Označenie adresára",
@@ -106,7 +112,7 @@
"GUI Listen Address": "Adresa pre prístup do GUI",
"GUI Listen Addresses": "Adresa pre prístup do GUI",
"GUI Theme": "Grafická téma GUI",
"General": "General",
"General": "Všeobecné",
"Generate": "Generovať",
"Global Changes": "Globálne zmeny",
"Global Discovery": "Globálne vyhľadávanie",
@@ -131,17 +137,22 @@
"Latest Change": "Posledná zmena",
"Learn more": "Zisti viac",
"Listeners": "Načúvajúci",
"Loading data...": "Loading data...",
"Loading data...": "Načítavanie údajov...",
"Loading...": "Načítavanie...",
"Local Discovery": "Lokálne vyhľadávanie",
"Local State": "Lokálny status",
"Local State (Total)": "Lokálny status (celkový)",
"Log": "Záznam",
"Log tailing paused. Click here to continue.": "Posúvanie záznamu prerušené. Klikni pre pokračovanie.",
"Logs": "Záznamy",
"Major Upgrade": "Hlavná aktualizácia",
"Mass actions": "Hromadná akcia",
"Master": "Master",
"Maximum Age": "Maximálny časový limit",
"Metadata Only": "Iba metadáta",
"Minimum Free Disk Space": "Minimálna veľkosť voľného miesta na disku",
"Mod. Device": "Mod. Device",
"Mod. Time": "Mod. Time",
"Mod. Device": "Mod. zariadenie",
"Mod. Time": "Mod. čas",
"Move to top of queue": "Presun na začiatok poradia",
"Multi level wildcard (matches multiple directory levels)": "Viacúrovňový zástupný znak (zhoda naprieč viacerými úrovňami adresára)",
"Never": "Nikdy",
@@ -150,7 +161,7 @@
"Newest First": "Najnovší najprv",
"No": "Nie",
"No File Versioning": "Bez verzií súbor",
"No files will be deleted as a result of this operation.": "No files will be deleted as a result of this operation.",
"No files will be deleted as a result of this operation.": "Ako výsledok tejto operáciu nebudú zmazané žiadne súbory.",
"No upgrades": "Bez aktualizácií",
"Normal": "Normalny",
"Notice": "Oznámenie",
@@ -165,7 +176,7 @@
"Override Changes": "Prepísať zmeny",
"Path": "Cesta",
"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": "Cesta k adresáru na lokálnom počítači. Ak neexistuje, bude vytvorená. Znak vlnovky (~) môže byť použitý ako skratka pre",
"Path where new auto accepted folders will be created, as well as the default suggested path when adding new folders via the UI. Tilde character (~) expands to {%tilde%}.": "Path where new auto accepted folders will be created, as well as the default suggested path when adding new folders via the UI. Tilde character (~) expands to {{tilde}}.",
"Path where new auto accepted folders will be created, as well as the default suggested path when adding new folders via the UI. Tilde character (~) expands to {%tilde%}.": "Cesta, kde budú vytvorené nové, automaticky prijaté adresáre, rovnako aj predvolená cesta, kam budú pridané nové adresáre cez UI. Vlnovka (~) smeruje do {{tilde}}.",
"Path where versions should be stored (leave empty for the default .stversions directory in the shared folder).": "Cesta, kde budú uložené verzie (ponechajte prázdne pre predvolený adresár .stversions v zdieľanom adresari)",
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Cesta, kde budú uložené verzie (ponechajte prázdne pre predvolený adresár .stversions v adresári).",
"Pause": "Pozastaviť",
@@ -181,14 +192,14 @@
"Quick guide to supported patterns": "Rýchly sprievodca podporovanými vzormi",
"RAM Utilization": "Využitie RAM",
"Random": "Náhodne",
"Recent Changes": "Recent Changes",
"Recent Changes": "Nedávne zmeny",
"Reduced by ignore patterns": "Znížené o ignorované vzory",
"Release Notes": "Poznámky k vydaniu",
"Release candidates contain the latest features and fixes. They are similar to the traditional bi-weekly Syncthing releases.": "Kandidáti na vydanie obsahujú najnovšie vlastnosti a opravy. Sú podobné tradičným dvojtýždenným vydaniam programu Syncthing.",
"Remote Devices": "Vzdialené zariadenia",
"Remove": "Odstrániť",
"Remove Device": "Remove Device",
"Remove Folder": "Remove Folder",
"Remove Device": "Odstrániť zariadenie",
"Remove Folder": "Odstrániť adresár",
"Required identifier for the folder. Must be the same on all cluster devices.": "Potrebný identifikátor pre adresár. Musí byť rovnaký na všetkých zariadeniach v skupine.",
"Rescan": "Opakovať skenovanie",
"Rescan All": "Opakovať skenovanie všetkých",
@@ -196,6 +207,8 @@
"Restart": "Reštart",
"Restart Needed": "Potrebný reštart",
"Restarting": "Reštartovanie",
"Restore": "Obnoviť",
"Restore Versions": "Obnoviť verzie",
"Resume": "Pokračovať",
"Resume All": "Pokračuj so všetkými",
"Reused": "Opakovane použité",
@@ -204,7 +217,9 @@
"Scanning": "Skenovanie",
"See external versioner help for supported templated command line parameters.": "See external versioner help for supported templated command line parameters.",
"See external versioning help for supported templated command line parameters.": "See external versioning help for supported templated command line parameters.",
"Select a version": "Select a version",
"Select a version": "Zvoliť verziu",
"Select latest version": "Zvoliť najnovšiu verziu",
"Select oldest version": "Zvoliť najstaršiu verziu",
"Select the devices to share this folder with.": "Vyberte zariadenia s ktorými chcete zdieľať tento adresár.",
"Select the folders to share with this device.": "Vyberte adresáre ktoré chcete zdieľať s týmto zariadením.",
"Send & Receive": "Prijímať a odosielať",
@@ -218,15 +233,16 @@
"Shared With": "Zdieľané s",
"Show ID": "Zobraziť ID",
"Show QR": "Zobraziť QR",
"Show diff with previous version": "Show diff with previous version",
"Show diff with previous version": "Ukázať rozdiely s predchádzajúcou verziou",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Zobrazované namiesto ID zariadenia v štatúte klastra. Toto pomenovanie bude oznamovať ostatným zariadeniam ako voliteľné predvolené pomenovanie.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Zobrazované namiesto ID zariadenia v klastri. Ak je ponechané prázdne, bude nahradené pomenovaním, ktoré oznamuje zariadenie.",
"Shutdown": "Vypnutie",
"Shutdown Complete": "Vypnutie ukončené",
"Simple File Versioning": "Jednoduché verzie súborov",
"Single level wildcard (matches within a directory only)": "Single level wildcard (matches within a directory only)",
"Size": "Size",
"Size": "Veľkosť",
"Smallest First": "Najmenší najprv",
"Some items could not be restored:": "Niektoré položky nemôžu byť obnovené:",
"Source Code": "Zdrojový kód",
"Stable releases and release candidates": "Stabilné verzie a kandidáti na vydanie",
"Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "Stabilné vydania sú odložené asi o dva týždne. Počas tejto doby prechádzajú testovaním ako kandidáti na vydanie.",
@@ -273,11 +289,11 @@
"This is a major version upgrade.": "Toto je hlavná aktualizácia.",
"This setting controls the free space required on the home (i.e., index database) disk.": "Toto nastavenie kontroluje voľné miesto požadované na domovskom disku (napr. indexová databáza).",
"Time": "Čas",
"Time the item was last modified": "Time the item was last modified",
"Time the item was last modified": "Čas poslednej zmeny položky",
"Trash Can File Versioning": "Verzie súborov v koši",
"Type": "Typ",
"Unavailable": "Unavailable",
"Unavailable/Disabled by administrator or maintainer": "Unavailable/Disabled by administrator or maintainer",
"Unavailable": "Nedostupné",
"Unavailable/Disabled by administrator or maintainer": "Nedostupné/Zakázané administrátorom alebo správcom",
"Undecided (will prompt)": "Undecided (will prompt)",
"Unknown": "Neznáme",
"Unshared": "Nezdieľané",
@@ -290,8 +306,9 @@
"Upload Rate": "Rýchlosť odosielania",
"Uptime": "Doba prevádzky",
"Usage reporting is always enabled for candidate releases.": "Hlásenia o používaní sú pri kandidátoch na vydanie vždy povolené.",
"Use HTTPS for GUI": "Use HTTPS for GUI",
"Use HTTPS for GUI": "Použiť HTTPS pre grafické rozhranie",
"Version": "Verzia",
"Versions": "Verzie",
"Versions Path": "Cesta k verziám",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Verzie sú automaticky zmazané ak sú staršie ako je maximálny časový limit alebo presiahnu počet súborov povolených v danom intervale.",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Warning, this path is a parent directory of an existing folder \"{{otherFolder}}\".",

View File

@@ -28,6 +28,7 @@
"Any devices configured on an introducer device will be added to this device as well.": "Alla enheter konfigurerade på en introduktör enhet kommer också att läggas till den här enheten.",
"Are you sure you want to remove device {%name%}?": "Är du säker på att du vill ta bort enheten {{name}}?",
"Are you sure you want to remove folder {%label%}?": "Är du säker på att du vill ta bort mappen {{label}}?",
"Are you sure you want to restore {%count%} files?": "Är du säker på att du vill återställa {{count}} filer?",
"Auto Accept": "Auto acceptera",
"Automatic upgrade now offers the choice between stable releases and release candidates.": "Automatisk uppgradering erbjuder nu valet mellan stabila utgåvor och utgåvskandidater.",
"Automatic upgrades": "Automatiska uppgraderingar",
@@ -52,6 +53,7 @@
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 följande bidragande:",
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Skapa ignorera mönster, skriver över en existerande fil på {{path}}.",
"Danger!": "Fara!",
"Debugging Facilities": "Felsökningsanläggningar",
"Default Folder Path": "Standard mappsökväg",
"Deleted": "Tog bort",
"Device": "Enhet",
@@ -66,6 +68,8 @@
"Discovered": "Upptäckt",
"Discovery": "Annonsering",
"Discovery Failures": "Upptäcktsmisslyckanden",
"Do not restore": "Återställ inte",
"Do not restore all": "Återställ inte allt",
"Documentation": "Dokumentation",
"Download Rate": "Nedladdningshastighet",
"Downloaded": "Hämtat",
@@ -76,7 +80,7 @@
"Editing": "Redigerar",
"Editing {%path%}.": "Redigerar {{path}}.",
"Enable NAT traversal": "Aktivera NAT traversering",
"Enable Relaying": "Aktivera reläa",
"Enable Relaying": "Aktivera vidarebefordra",
"Enter a non-negative number (e.g., \"2.35\") and select a unit. Percentages are as part of the total disk size.": "Ange ett icke-negativt antal (t.ex., \"2.35\") och välj en enhet. Procenttalen är som en del av den totala diskstorleken.",
"Enter a non-privileged port number (1024 - 65535).": "Ange ett icke-privilegierat portnummer (1024 - 65535).",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Ange kommaseparerade (\"tcp://ip:port\", \"tcp://host:port\")-adresser eller ordet \"dynamic\" för att använda automatisk uppslagning.",
@@ -94,6 +98,8 @@
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Filer flyttas till datummärkta versioner i en .stversions mapp när de ersätts eller tas bort av Syncthing.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Filer skyddas från ändringar gjorda på andra enheter, men ändringar som görs på den här noden skickas till de andra klustermedlemmarna.",
"Filesystem Notifications": "filsystemsnotifieringar",
"Filter by date": "Filtrera efter datum",
"Filter by name": "Filtrera efter namn",
"Folder": "Mapp",
"Folder ID": "Mapp-ID",
"Folder Label": "Mapp-etikett",
@@ -101,11 +107,11 @@
"Folder Type": "Mapptyp",
"Folders": "Mappar",
"GUI": "Grafiskt gränssnitt",
"GUI Authentication Password": "GUI-autentiseringslösenord",
"GUI Authentication User": "GUI-autentiseringsanvändare",
"GUI Listen Address": "GUI-lyssnaradress",
"GUI Listen Addresses": "GUI-lyssnaradresser",
"GUI Theme": "GUI-tema",
"GUI Authentication Password": "GUI autentiseringslösenord",
"GUI Authentication User": "GUI autentiseringsanvändare",
"GUI Listen Address": "GUI lyssnaradress",
"GUI Listen Addresses": "GUI lyssnaradresser",
"GUI Theme": "GUI tema",
"General": "Allmänt",
"Generate": "Generera",
"Global Changes": "Globala ändringar",
@@ -132,10 +138,15 @@
"Learn more": "Ta reda på mer",
"Listeners": "Lyssnare",
"Loading data...": "Laddar data...",
"Loading...": "Laddar...",
"Local Discovery": "Lokal annonsering",
"Local State": "Lokalt tillstånd",
"Local State (Total)": "Lokalt tillstånd (totalt)",
"Log": "Logg",
"Log tailing paused. Click here to continue.": "Log tailing paused. Click here to continue.",
"Logs": "Loggar",
"Major Upgrade": "Större uppgradering",
"Mass actions": "Mass actions",
"Master": "Huvud",
"Maximum Age": "Maximum ålder",
"Metadata Only": "Endast metadata",
@@ -196,6 +207,8 @@
"Restart": "Starta om",
"Restart Needed": "Omstart behövs",
"Restarting": "Startar om",
"Restore": "Återställ",
"Restore Versions": "Restore Versions",
"Resume": "Återuppta",
"Resume All": "Återuppta alla",
"Reused": "Återanvänds",
@@ -205,6 +218,8 @@
"See external versioner help for supported templated command line parameters.": "Se hjälp för extern version för stödda mallade kommandoradsparametrar.",
"See external versioning help for supported templated command line parameters.": "Se hjälp för extern version för stödda mallade kommandoradsparametrar.",
"Select a version": "Välj en version",
"Select latest version": "Välj senaste versionen",
"Select oldest version": "Välj äldsta versionen",
"Select the devices to share this folder with.": "Ange enheterna som den här mappen ska delas med.",
"Select the folders to share with this device.": "Välj mapparna som ska delas med den här enheten.",
"Send & Receive": "Skicka & ta emot",
@@ -227,6 +242,7 @@
"Single level wildcard (matches within a directory only)": "Jokertecken som representerar noll eller fler godtyckliga tecken i ett filnamn.",
"Size": "Storlek",
"Smallest First": "Minst först",
"Some items could not be restored:": "Vissa objekt kunde inte återställas:",
"Source Code": "Källkod",
"Stable releases and release candidates": "Stabila utgåvor och utgåvskandidater",
"Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "Stabila utgåvor är försenade med cirka två veckor. Under denna tid de går igenom tester som utgåvskandidater.",
@@ -292,6 +308,7 @@
"Usage reporting is always enabled for candidate releases.": "Användningsrapportering är alltid aktiverad för kandidatutgåvor.",
"Use HTTPS for GUI": "Använd HTTPS för GUI",
"Version": "Version",
"Versions": "Versioner",
"Versions Path": "Sökväg för versioner",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versioner tas bort automatiskt när de är äldre än den maximala åldersgränsen eller överstiger frekvensen i intervallet.",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Varning, denna sökväg är en överordnad mapp av en befintlig mapp \"{{otherFolder}}\".",

View File

@@ -28,6 +28,7 @@
"Any devices configured on an introducer device will be added to this device as well.": "Tanıtıcı bir cihaz üzerinde yapılandırılan cihazlar bu cihaza da eklenecektir.",
"Are you sure you want to remove device {%name%}?": "Are you sure you want to remove device {{name}}?",
"Are you sure you want to remove folder {%label%}?": "Are you sure you want to remove folder {{label}}?",
"Are you sure you want to restore {%count%} files?": "Are you sure you want to restore {{count}} files?",
"Auto Accept": "Auto Accept",
"Automatic upgrade now offers the choice between stable releases and release candidates.": "Kendiliğinden yükseltme artık kararlı dağıtımlar ve sürüm adayları arasında seçim yapmayı sağlıyor.",
"Automatic upgrades": "Kendiliğinden yükseltmeler",
@@ -52,6 +53,7 @@
"Copyright © 2014-2017 the following Contributors:": "Telif Hakkı © 2014-2017 takip eden Katkıcılar:",
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Creating ignore patterns, overwriting an existing file at {{path}}.",
"Danger!": "Tehlike!",
"Debugging Facilities": "Debugging Facilities",
"Default Folder Path": "Default Folder Path",
"Deleted": "Silindi",
"Device": "Aygıt",
@@ -66,6 +68,8 @@
"Discovered": "Keşfedildi",
"Discovery": "Keşif",
"Discovery Failures": "Discovery Failures",
"Do not restore": "Do not restore",
"Do not restore all": "Do not restore all",
"Documentation": "Belgeleme",
"Download Rate": "İndirme Hızı",
"Downloaded": "İndirilmiş",
@@ -94,6 +98,8 @@
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Dosyalar Syncthing tarafından yeri değiştirildiğinde ya da silindiğinde, tarih damgalı sürümleri .stversions klasörüne taşınır.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Dosyalar diğer aygıtlarda yapılan değişikliklerden korunur, ancak bu aygıttaki değişiklikler kümedeki diğer aygıtlara gönderilir.",
"Filesystem Notifications": "Filesystem Notifications",
"Filter by date": "Filter by date",
"Filter by name": "Filter by name",
"Folder": "Klasör",
"Folder ID": "Klasör ID",
"Folder Label": "Klasör Etiketi",
@@ -132,10 +138,15 @@
"Learn more": "Daha çoğunu öğren",
"Listeners": "Dinleyiciler",
"Loading data...": "Loading data...",
"Loading...": "Loading...",
"Local Discovery": "Yerel Discovery",
"Local State": "Yerel Durum",
"Local State (Total)": "Yerel Durum (Toplamı)",
"Log": "Log",
"Log tailing paused. Click here to continue.": "Log tailing paused. Click here to continue.",
"Logs": "Logs",
"Major Upgrade": "Birincil Yükseltme",
"Mass actions": "Mass actions",
"Master": "Ana",
"Maximum Age": "Azami Süre",
"Metadata Only": "Yalnızca Üstveri",
@@ -196,6 +207,8 @@
"Restart": "Yeniden Başlat",
"Restart Needed": "Yeniden başlatma gereklidir",
"Restarting": "Yeniden başlatılıyor",
"Restore": "Restore",
"Restore Versions": "Restore Versions",
"Resume": "Devam Et/Sürdür",
"Resume All": "Tümünü Devam Ettir",
"Reused": "Yeniden Kullanılan",
@@ -205,6 +218,8 @@
"See external versioner help for supported templated command line parameters.": "See external versioner help for supported templated command line parameters.",
"See external versioning help for supported templated command line parameters.": "See external versioning help for supported templated command line parameters.",
"Select a version": "Select a version",
"Select latest version": "Select latest version",
"Select oldest version": "Select oldest version",
"Select the devices to share this folder with.": "Bu klasörü paylaşacağın aygıtları seç.",
"Select the folders to share with this device.": "Bu aygıtla paylaşılacak klasörleri seç.",
"Send & Receive": "Gönder & Al",
@@ -227,6 +242,7 @@
"Single level wildcard (matches within a directory only)": "Tekli düzey wildcard (yalnızca bir dizin içinde eşleşme)",
"Size": "Size",
"Smallest First": "En küçük olan önce",
"Some items could not be restored:": "Some items could not be restored:",
"Source Code": "Kaynak Kodu",
"Stable releases and release candidates": "Kararlı dağıtımlar ve sürüm adayları",
"Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "Kararlı dağıtımlar aşağı yukarı iki hafta gecikir. Bu zaman zarfında sürüm adayı olarak sınanırlar.",
@@ -292,6 +308,7 @@
"Usage reporting is always enabled for candidate releases.": "Kullanım raporu sürüm adayları için her zaman etkindir.",
"Use HTTPS for GUI": "GUI için HTTPS kullan",
"Version": "Sürüm",
"Versions": "Versions",
"Versions Path": "Sürümler için Klasör Yolu",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Sürümler, tanımlı azami süre veya belirlenen zaman aralığı için izin verilen dosya sayısıılmışsa kendiliğinden silinir.",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Uyarı ! Bu yol varolan \"{{otherFolder}}\" klasörünün üst dizinidir.",

View File

@@ -28,6 +28,7 @@
"Any devices configured on an introducer device will be added to this device as well.": "Усі пристрої, налаштовані на пристрої-рекомендувачі, будуть додані до поточного пристрою.",
"Are you sure you want to remove device {%name%}?": "Are you sure you want to remove device {{name}}?",
"Are you sure you want to remove folder {%label%}?": "Are you sure you want to remove folder {{label}}?",
"Are you sure you want to restore {%count%} files?": "Are you sure you want to restore {{count}} files?",
"Auto Accept": "Auto Accept",
"Automatic upgrade now offers the choice between stable releases and release candidates.": "Автоматиче оновлення зараз дозволяє обирати між стабільними випусками та реліз-кандидатами.",
"Automatic upgrades": "Автоматичні оновлення",
@@ -52,6 +53,7 @@
"Copyright © 2014-2017 the following Contributors:": "© 2014-2017 Всі права застережено, вклад внесли:",
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Створення шаблонів винятків з перезаписом існуючого файлу {{path}}.",
"Danger!": "Небезпечно!",
"Debugging Facilities": "Debugging Facilities",
"Default Folder Path": "Default Folder Path",
"Deleted": "Видалене",
"Device": "Пристрій",
@@ -66,6 +68,8 @@
"Discovered": "Виявлено",
"Discovery": "Сервери координації NAT",
"Discovery Failures": "Помилки виявлення",
"Do not restore": "Do not restore",
"Do not restore all": "Do not restore all",
"Documentation": "Документація",
"Download Rate": "Швидкість завантаження",
"Downloaded": "Завантажено",
@@ -94,6 +98,8 @@
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Файли будуть поміщатися у директорію .stversions із відповідною позначкою часу, коли вони будуть замінятися або видалятися програмою.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Вміст папки захищено від змін, зроблених на інших пристроях, але зміни зроблені на цьому пристрої можна розіслати решті пристроїв кластеру.",
"Filesystem Notifications": "Повідомлення файлової системи",
"Filter by date": "Filter by date",
"Filter by name": "Filter by name",
"Folder": "Директорія",
"Folder ID": "ID директорії",
"Folder Label": "Мітка директорії",
@@ -132,10 +138,15 @@
"Learn more": "Дізнатися більше",
"Listeners": "Приймачі (TCP & Relay)",
"Loading data...": "Loading data...",
"Loading...": "Loading...",
"Local Discovery": "Локальне виявлення (LAN)",
"Local State": "Локальний статус",
"Local State (Total)": "Локальний статус (загалом)",
"Log": "Log",
"Log tailing paused. Click here to continue.": "Log tailing paused. Click here to continue.",
"Logs": "Logs",
"Major Upgrade": "Мажорне оновлення",
"Mass actions": "Mass actions",
"Master": "Головний",
"Maximum Age": "Максимальний вік",
"Metadata Only": "Тільки метадані",
@@ -196,6 +207,8 @@
"Restart": "Перезапуск",
"Restart Needed": "Необхідний перезапуск",
"Restarting": "Відбувається перезапуск",
"Restore": "Restore",
"Restore Versions": "Restore Versions",
"Resume": "Продовжити",
"Resume All": "Продовжити всі",
"Reused": "Використано вдруге",
@@ -205,6 +218,8 @@
"See external versioner help for supported templated command line parameters.": "Переглянути допомогу по зовнішньому версіонуванню для підтримуваних шаблонних параметрів командного рядка.",
"See external versioning help for supported templated command line parameters.": "Переглянути допомогу по зовнішньому версіонуванню для підтримуваних шаблонних параметрів командного рядка.",
"Select a version": "Обрати версію",
"Select latest version": "Select latest version",
"Select oldest version": "Select oldest version",
"Select the devices to share this folder with.": "Оберіть пристрої, які матимуть доступ до цієї директорії.",
"Select the folders to share with this device.": "Оберіть директорії до яких матиме доступ цей пристрій.",
"Send & Receive": "Відправити та отримати",
@@ -227,6 +242,7 @@
"Single level wildcard (matches within a directory only)": "Однорівнева маска (пошук збігів лише в середині директорії) ",
"Size": "Size",
"Smallest First": "Спершу найменші",
"Some items could not be restored:": "Some items could not be restored:",
"Source Code": "Сирцевий код",
"Stable releases and release candidates": "Стабільні випуски та реліз-кандидати",
"Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "Стабільні випуски затримуються на два тижні. У цей час вони тестуються як реліз-кандидати.",
@@ -292,6 +308,7 @@
"Usage reporting is always enabled for candidate releases.": "Звіти про користування завжди увімкнені для реліз-кандидатів.",
"Use HTTPS for GUI": "Використовувати HTTPS для доступу до панелі управління",
"Version": "Версія",
"Versions": "Versions",
"Versions Path": "Шлях до версій",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Версії автоматично видаляються, якщо вони старше, ніж максимальний вік, або перевищують допустиму кількість файлів за інтервал.",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Увага, цей шлях є батьківським каталогом директорії \"{{otherFolder}}\", що й так синхронізується .",

View File

@@ -28,6 +28,7 @@
"Any devices configured on an introducer device will be added to this device as well.": "在中介设备上添加的任何“远程设备”,也会被自动添加到本机的“远程设备”列表。",
"Are you sure you want to remove device {%name%}?": "您确定要移除设备 {{name}} 吗?",
"Are you sure you want to remove folder {%label%}?": "您确定要移除文件夹 {{label}} 吗?",
"Are you sure you want to restore {%count%} files?": "您确定要恢复这 {{count}} 个文件吗?",
"Auto Accept": "自动接受",
"Automatic upgrade now offers the choice between stable releases and release candidates.": "自动升级现在提供了稳定版本和发布候选版之间的选择。",
"Automatic upgrades": "自动升级",
@@ -52,6 +53,7 @@
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 以下贡献者:",
"Creating ignore patterns, overwriting an existing file at {%path%}.": "正在创建忽略列表,覆盖位于 {{path}} 的已有文件。",
"Danger!": "危险!",
"Debugging Facilities": "调试功能",
"Default Folder Path": "默认文件夹路径",
"Deleted": "已删除",
"Device": "设备",
@@ -66,6 +68,8 @@
"Discovered": "已发现",
"Discovery": "设备发现",
"Discovery Failures": "设备发现错误",
"Do not restore": "不要恢复",
"Do not restore all": "不要全部恢复",
"Documentation": "文档",
"Download Rate": "下载速度",
"Downloaded": "已下载",
@@ -94,6 +98,8 @@
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "当某个文件在其他设备被替换或删除时,本设备将会在 .stversions 文件夹中保留该文件的备份,并在文件名中加入时间戳信息。",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "在其它设备中对该文件夹内文件的修改并不会被同步到本机,但是在本机上对其的修改,则会被同步到其它设备中。",
"Filesystem Notifications": "文件系统通知",
"Filter by date": "按日期筛选",
"Filter by name": "按名称筛选",
"Folder": "文件夹",
"Folder ID": "文件夹 ID",
"Folder Label": "文件夹标签",
@@ -132,10 +138,15 @@
"Learn more": "了解更多",
"Listeners": "侦听程序",
"Loading data...": "正在载入数据…",
"Loading...": "正在载入…",
"Local Discovery": "本地发现",
"Local State": "本地状态",
"Local State (Total)": "本地状态汇总",
"Log": "日志",
"Log tailing paused. Click here to continue.": "已暂停日志跟踪。点击此处以继续。",
"Logs": "日志",
"Major Upgrade": "重大更新",
"Mass actions": "批量操作",
"Master": "自主",
"Maximum Age": "最长保留时间",
"Metadata Only": "仅元数据",
@@ -196,6 +207,8 @@
"Restart": "重启 Syncthing",
"Restart Needed": "需要重启 Syncthing",
"Restarting": "重启中",
"Restore": "恢复",
"Restore Versions": "恢复历史版本",
"Resume": "恢复",
"Resume All": "全部恢复",
"Reused": "复用",
@@ -205,6 +218,8 @@
"See external versioner help for supported templated command line parameters.": "有关支持的命令行参数模板,请参阅外部的版本控制器帮助。",
"See external versioning help for supported templated command line parameters.": "有关受支持的模板命令行参数,请参阅外部版本控制帮助。",
"Select a version": "选择版本",
"Select latest version": "选择最新的版本",
"Select oldest version": "选择最旧的版本",
"Select the devices to share this folder with.": "选择将本文件夹共享给哪些设备。",
"Select the folders to share with this device.": "选择与该设备共享的文件夹。",
"Send & Receive": "发送与接收",
@@ -227,6 +242,7 @@
"Single level wildcard (matches within a directory only)": "单级通配符(仅匹配单层文件夹)",
"Size": "大小",
"Smallest First": "小文件优先",
"Some items could not be restored:": "有些项目无法被恢复:",
"Source Code": "源代码",
"Stable releases and release candidates": "稳定版本和发布候选版",
"Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "稳定版本约延迟两个星期。这段时间它们将作为发布候选版来测试。",
@@ -292,6 +308,7 @@
"Usage reporting is always enabled for candidate releases.": "发布候选版总是会启用使用报告。",
"Use HTTPS for GUI": "使用加密连接到图形管理页面",
"Version": "版本",
"Versions": "历史版本",
"Versions Path": "历史版本路径",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "超过最长保留时间,或者不满足下列条件的历史版本,将会被删除。",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "警告,该路径是已有文件夹\"{{otherFolder}}\"的上级目录。",

View File

@@ -26,12 +26,13 @@
"Anonymous Usage Reporting": "匿名的使用資訊回報",
"Anonymous usage report format has changed. Would you like to move to the new format?": "匿名的使用資訊回報格式已經改變,你想要移至新格式嗎?",
"Any devices configured on an introducer device will be added to this device as well.": "任何在引入者裝置所設置的裝置將會一併新增至此裝置",
"Are you sure you want to remove device {%name%}?": "Are you sure you want to remove device {{name}}?",
"Are you sure you want to remove folder {%label%}?": "Are you sure you want to remove folder {{label}}?",
"Auto Accept": "Auto Accept",
"Are you sure you want to remove device {%name%}?": "確定要移除 {{name}} 裝置?",
"Are you sure you want to remove folder {%label%}?": "確定要移除 {{label}} 資料夾?",
"Are you sure you want to restore {%count%} files?": "Are you sure you want to restore {{count}} files?",
"Auto Accept": "自動接受",
"Automatic upgrade now offers the choice between stable releases and release candidates.": "自動更新目前有穩定發行版及發行候選版可供選擇。",
"Automatic upgrades": "自動升級",
"Automatically create or share folders that this device advertises at the default path.": "Automatically create or share folders that this device advertises at the default path.",
"Automatically create or share folders that this device advertises at the default path.": "自動在預設資料夾路徑建立或分享該裝置推薦的資料夾。",
"Be careful!": "請小心!",
"Bugs": "程式錯誤",
"CPU Utilization": "CPU 使用",
@@ -45,27 +46,30 @@
"Configured": "已設定",
"Connection Error": "連線錯誤",
"Connection Type": "連接類型",
"Connections": "Connections",
"Connections": "連線",
"Copied from elsewhere": "從別處複製",
"Copied from original": "從原處複製",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 下列貢獻者:",
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 下列貢獻者:",
"Creating ignore patterns, overwriting an existing file at {%path%}.": "建立忽略樣式,覆蓋已存在的 {{path}}。",
"Danger!": "危險!",
"Default Folder Path": "Default Folder Path",
"Debugging Facilities": "Debugging Facilities",
"Default Folder Path": "預設資料夾路徑",
"Deleted": "已刪除",
"Device": "裝置",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "裝置 \"{{name}}\" ({{device}} 位於 {{address}}) 想要連線。 要增加新裝置嗎?",
"Device ID": "裝置識別碼",
"Device Identification": "裝置識別",
"Device Name": "裝置名稱",
"Device that last modified the item": "Device that last modified the item",
"Device that last modified the item": "前次修改裝置",
"Devices": "裝置",
"Disabled": "關閉",
"Disconnected": "斷線",
"Discovered": "已發現",
"Discovery": "探索",
"Discovery Failures": "探索失敗",
"Do not restore": "Do not restore",
"Do not restore all": "Do not restore all",
"Documentation": "說明文件",
"Download Rate": "下載速率",
"Downloaded": "已下載",
@@ -94,6 +98,8 @@
"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.": "其他裝置做的改變不會影響到此裝置的檔案,但在此裝置上的變化將被發送到叢集中的其他部分。",
"Filesystem Notifications": "檔案系統通知",
"Filter by date": "Filter by date",
"Filter by name": "Filter by name",
"Folder": "資料夾",
"Folder ID": "資料夾識別碼",
"Folder Label": "資料夾標籤",
@@ -106,7 +112,7 @@
"GUI Listen Address": "GUI 監聽位址",
"GUI Listen Addresses": "GUI 監聽位址",
"GUI Theme": "主題",
"General": "General",
"General": "一般",
"Generate": "產生",
"Global Changes": "全域變動",
"Global Discovery": "全域探索",
@@ -132,16 +138,21 @@
"Learn more": "瞭解更多",
"Listeners": "監聽者",
"Loading data...": "Loading data...",
"Loading...": "Loading...",
"Local Discovery": "本機探索",
"Local State": "本機狀態",
"Local State (Total)": "本機狀態 (總結)",
"Log": "Log",
"Log tailing paused. Click here to continue.": "Log tailing paused. Click here to continue.",
"Logs": "Logs",
"Major Upgrade": "重大更新",
"Mass actions": "Mass actions",
"Master": "主要的",
"Maximum Age": "最長保留時間",
"Metadata Only": "僅中繼資料",
"Minimum Free Disk Space": "最少閒置磁碟空間",
"Mod. Device": "Mod. Device",
"Mod. Time": "Mod. Time",
"Mod. Device": "修改裝置",
"Mod. Time": "修改時間",
"Move to top of queue": "移到隊列頂端",
"Multi level wildcard (matches multiple directory levels)": "多階層萬用字元 (可比對多層資料夾)",
"Never": "從未",
@@ -150,7 +161,7 @@
"Newest First": "最新的優先",
"No": "否",
"No File Versioning": "無檔案版本控制",
"No files will be deleted as a result of this operation.": "No files will be deleted as a result of this operation.",
"No files will be deleted as a result of this operation.": "此操作將不會移除您的檔案。",
"No upgrades": "不更新",
"Normal": "正常的",
"Notice": "注意",
@@ -165,7 +176,7 @@
"Override Changes": "置換改變",
"Path": "路徑",
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "資料夾在本機的路徑。若資料夾不存在則會建立。波浪符號 (~) 可用作下列資料夾的捷徑:",
"Path where new auto accepted folders will be created, as well as the default suggested path when adding new folders via the UI. Tilde character (~) expands to {%tilde%}.": "Path where new auto accepted folders will be created, as well as the default suggested path when adding new folders via the UI. Tilde character (~) expands to {{tilde}}.",
"Path where new auto accepted folders will be created, as well as the default suggested path when adding new folders via the UI. Tilde character (~) expands to {%tilde%}.": "新自動接受的資料夾所在路徑,也是從 UI 中加入資料夾的預設推薦路徑。波浪符字元 (~) 將被展開為 {{tilde}}",
"Path where versions should be stored (leave empty for the default .stversions directory in the shared folder).": "儲存歷史版本的路徑(若為空,則預設使用資料夾中的 .stversions 資料夾)。",
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "儲存歷史版本的路徑 (若為空,則預設使用資料夾中的 .stversions 資料夾)。",
"Pause": "暫停",
@@ -181,14 +192,14 @@
"Quick guide to supported patterns": "可支援樣式的快速指南",
"RAM Utilization": "記憶體使用",
"Random": "隨機",
"Recent Changes": "Recent Changes",
"Recent Changes": "最近變動",
"Reduced by ignore patterns": "已由忽略樣式縮減",
"Release Notes": "版本資訊",
"Release candidates contain the latest features and fixes. They are similar to the traditional bi-weekly Syncthing releases.": "發行候選版包含最新的功能及修補。與傳統 Syncthing 雙週發行版相似。",
"Remote Devices": "遠端裝置",
"Remove": "移除",
"Remove Device": "Remove Device",
"Remove Folder": "Remove Folder",
"Remove Device": "移除裝置",
"Remove Folder": "移除資料夾",
"Required identifier for the folder. Must be the same on all cluster devices.": "資料夾的識別字。必須在叢集內所有的裝置上皆相同。",
"Rescan": "重新掃描",
"Rescan All": "全部重新掃描",
@@ -196,6 +207,8 @@
"Restart": "重新啟動",
"Restart Needed": "需要重新啟動",
"Restarting": "正在重新啟動",
"Restore": "Restore",
"Restore Versions": "Restore Versions",
"Resume": "繼續",
"Resume All": "全部繼續",
"Reused": "重用",
@@ -205,6 +218,8 @@
"See external versioner help for supported templated command line parameters.": "關於命令列模板參數請參閱外部版本管理說明。",
"See external versioning help for supported templated command line parameters.": "查看關於命令列模板參數請參閱外部版本管理說明。",
"Select a version": "選擇一個版本",
"Select latest version": "Select latest version",
"Select oldest version": "Select oldest version",
"Select the devices to share this folder with.": "選擇要共享這個資料夾的裝置。",
"Select the folders to share with this device.": "選擇要共享這個資料夾的裝置。",
"Send & Receive": "傳送及接收",
@@ -227,6 +242,7 @@
"Single level wildcard (matches within a directory only)": "單階層萬用字元 (只在單個資料夾階層內比對)",
"Size": "Size",
"Smallest First": "最小的優先",
"Some items could not be restored:": "Some items could not be restored:",
"Source Code": "原始碼",
"Stable releases and release candidates": "穩定發行版及發行候選版",
"Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "穩定發行版大約延遲兩週發佈。這段期間將作為發行候選版來測試。",
@@ -273,11 +289,11 @@
"This is a major version upgrade.": "這是一個主要版本更新。",
"This setting controls the free space required on the home (i.e., index database) disk.": "此設定控制家目錄(即:索引資料庫)的必須可用空間。",
"Time": "時間",
"Time the item was last modified": "Time the item was last modified",
"Time the item was last modified": "前次修改時間",
"Trash Can File Versioning": "垃圾筒式檔案版本控制",
"Type": "類型",
"Unavailable": "Unavailable",
"Unavailable/Disabled by administrator or maintainer": "Unavailable/Disabled by administrator or maintainer",
"Unavailable": "無法使用",
"Unavailable/Disabled by administrator or maintainer": "無法使用 / 被系統管理員或維護者停用",
"Undecided (will prompt)": "未定的(將會提示)",
"Unknown": "未知",
"Unshared": "未共享",
@@ -292,6 +308,7 @@
"Usage reporting is always enabled for candidate releases.": "發行候選版永遠回報使用數據",
"Use HTTPS for GUI": "為 GUI 使用 HTTPS",
"Version": "版本",
"Versions": "Versions",
"Versions Path": "歷史版本路徑",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "當檔案歷史版本的存留時間大於設定的最大值,或是其數量在一段時間內超出允許值時,則會被刪除。",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "警告,此路徑是現存資料夾 \"{{otherFolder}}\" 的上級目錄。",

View File

@@ -1 +1 @@
var langPrettyprint = {"bg":"Bulgarian","ca@valencia":"Catalan (Valencian)","cs":"Czech","da":"Danish","de":"German","el":"Greek","en":"English","en-GB":"English (United Kingdom)","eo":"Esperanto","es":"Spanish","es-ES":"Spanish (Spain)","eu":"Basque","fi":"Finnish","fr":"French","fr-CA":"French (Canada)","fy":"Western Frisian","hu":"Hungarian","it":"Italian","ja":"Japanese","ko-KR":"Korean (Korea)","lt":"Lithuanian","nb":"Norwegian Bokmål","nl":"Dutch","nn":"Norwegian Nynorsk","pl":"Polish","pt-BR":"Portuguese (Brazil)","pt-PT":"Portuguese (Portugal)","ru":"Russian","sk":"Slovak","sv":"Swedish","tr":"Turkish","uk":"Ukrainian","zh-CN":"Chinese (China)","zh-TW":"Chinese (Taiwan)"}
var langPrettyprint = {"bg":"Bulgarian","ca@valencia":"Catalan (Valencian)","cs":"Czech","da":"Danish","de":"German","el":"Greek","en":"English","en-GB":"English (United Kingdom)","eo":"Esperanto","es":"Spanish","es-ES":"Spanish (Spain)","eu":"Basque","fi":"Finnish","fr":"French","fr-CA":"French (Canada)","fy":"Western Frisian","hu":"Hungarian","it":"Italian","ja":"Japanese","ko-KR":"Korean (Korea)","lt":"Lithuanian","nb":"Norwegian Bokmål","nl":"Dutch","pl":"Polish","pt-BR":"Portuguese (Brazil)","pt-PT":"Portuguese (Portugal)","ru":"Russian","sk":"Slovak","sv":"Swedish","tr":"Turkish","uk":"Ukrainian","zh-CN":"Chinese (China)","zh-TW":"Chinese (Taiwan)"}

View File

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

View File

@@ -19,10 +19,12 @@
<title ng-bind="thisDeviceName() + ' | Syncthing'"></title>
<link href="vendor/bootstrap/css/bootstrap.css" rel="stylesheet"/>
<link href="vendor/bootstrap/css/daterangepicker.css" rel="stylesheet"/>
<link href="assets/font/raleway.css" rel="stylesheet"/>
<link href="vendor/font-awesome/css/font-awesome.css" rel="stylesheet"/>
<link href="assets/css/overrides.css" rel="stylesheet"/>
<link href="assets/css/theme.css" rel="stylesheet"/>
<link href="vendor/fancytree/css/ui.fancytree.css" rel="stylesheet"/>
</head>
<body>
@@ -353,7 +355,7 @@
<th><span class="fa fa-fw fa-exclamation-circle"></span>&nbsp;<span translate>Failed Items</span></th>
<!-- Show the number of failed items as a link to bring up the list. -->
<td class="text-right">
<a href="" ng-click="showFailed(folder.id)">{{failed[folder.id].length | alwaysNumber}}&nbsp;<span translate>items</span></a>
<a href="" ng-click="showFailed(folder.id)">{{model[folder.id].pullErrors | alwaysNumber}}&nbsp;<span translate>items</span></a>
</td>
</tr>
<tr ng-if="folder.type != 'readwrite'">
@@ -434,6 +436,9 @@
<button ng-if="folder.paused" type="button" class="btn btn-sm btn-default" ng-click="setFolderPause(folder.id, false)">
<span class="fa fa-play"></span>&nbsp;<span translate>Resume</span>
</button>
<button type="button" class="btn btn-default btn-sm" ng-click="restoreVersions.show(folder.id)" ng-if="folder.versioning.type">
<span class="fa fa-undo"></span>&nbsp;<span translate>Versions</span>
</button>
<button type="button" class="btn btn-sm btn-default" ng-click="rescanFolder(folder.id)" ng-show="['idle', 'stopped', 'unshared'].indexOf(folderStatus(folder)) > -1">
<span class="fa fa-refresh"></span>&nbsp;<span translate>Rescan</span>
</button>
@@ -723,6 +728,8 @@
<ng-include src="'syncthing/device/globalChangesModalView.html'"></ng-include>
<ng-include src="'syncthing/folder/editFolderModalView.html'"></ng-include>
<ng-include src="'syncthing/folder/editIgnoresModalView.html'"></ng-include>
<ng-include src="'syncthing/folder/restoreVersionsModalView.html'"></ng-include>
<ng-include src="'syncthing/folder/restoreVersionsConfirmation.html'"></ng-include>
<ng-include src="'syncthing/settings/settingsModalView.html'"></ng-include>
<ng-include src="'syncthing/settings/advancedSettingsModalView.html'"></ng-include>
<ng-include src="'syncthing/usagereport/usageReportModalView.html'"></ng-include>
@@ -744,7 +751,10 @@
<script type="text/javascript" src="vendor/angular/angular-translate.js"></script>
<script type="text/javascript" src="vendor/angular/angular-translate-loader-static-files.js"></script>
<script type="text/javascript" src="vendor/angular/angular-dirPagination.js"></script>
<script type="text/javascript" src="vendor/moment/moment.js"></script>
<script type="text/javascript" src="vendor/bootstrap/js/bootstrap.js"></script>
<script type="text/javascript" src="vendor/bootstrap/js/daterangepicker.js"></script>
<script type="text/javascript" src="vendor/fancytree/jquery.fancytree-all-deps.js"></script>
<!-- / vendor scripts -->
<!-- gui application code -->

View File

@@ -134,3 +134,75 @@ function debounce(func, wait) {
return result;
};
}
function buildTree(children) {
/* Converts
*
* {
* 'foo/bar': [...],
* 'foo/baz': [...]
* }
*
* to
*
* [
* {
* title: 'foo',
* children: [
* {
* title: 'bar',
* versions: [...],
* ...
* },
* {
* title: 'baz',
* versions: [...],
* ...
* }
* ],
* }
* ]
*/
var root = {
children: []
}
$.each(children, function(path, data) {
var parts = path.split('/');
var name = parts.splice(-1)[0];
var keySoFar = [];
var parent = root;
while (parts.length > 0) {
var part = parts.shift();
keySoFar.push(part);
var found = false;
for (var i = 0; i < parent.children.length; i++) {
if (parent.children[i].title == part) {
parent = parent.children[i];
found = true;
break;
}
}
if (!found) {
var child = {
title: part,
key: keySoFar.join('/'),
folder: true,
children: []
}
parent.children.push(child);
parent = child;
}
}
parent.children.push({
title: name,
key: path,
folder: false,
versions: data,
});
});
return root.children;
}

View File

@@ -2,7 +2,7 @@ angular.module('syncthing.core')
.config(function($locationProvider) {
$locationProvider.html5Mode({enabled: true, requireBase: false}).hashPrefix('!');
})
.controller('SyncthingController', function ($scope, $http, $location, LocaleService, Events, $filter, $q, $interval) {
.controller('SyncthingController', function ($scope, $http, $location, LocaleService, Events, $filter, $q, $compile, $timeout, $rootScope) {
'use strict';
// private/helper definitions
@@ -48,8 +48,6 @@ angular.module('syncthing.core')
$scope.neededCurrentPage = 1;
$scope.neededPageSize = 10;
$scope.failed = {};
$scope.failedCurrentPage = 1;
$scope.failedPageSize = 10;
$scope.scanProgress = {};
$scope.themes = [];
$scope.globalChangeEvents = {};
@@ -198,13 +196,6 @@ angular.module('syncthing.core')
$scope.model[data.folder].state = data.to;
$scope.model[data.folder].error = data.error;
// If a folder has started syncing, then any old list of
// errors is obsolete. We may get a new list of errors very
// shortly though.
if (data.to === 'syncing') {
$scope.failed[data.folder] = [];
}
// If a folder has started scanning, then any scan progress is
// also obsolete.
if (data.to === 'scanning') {
@@ -344,8 +335,7 @@ angular.module('syncthing.core')
});
$scope.$on(Events.FOLDER_ERRORS, function (event, arg) {
var data = arg.data;
$scope.failed[data.folder] = data.errors;
$scope.model[arg.data.folder].pullErrors = arg.data.errors.length;
});
$scope.$on(Events.FOLDER_SCAN_PROGRESS, function (event, arg) {
@@ -495,7 +485,7 @@ angular.module('syncthing.core')
} else {
$scope.completion[device]._total = Math.floor(100 * (1 - needed / total));
$scope.completion[device]._needBytes = needed
$scope.completion[device]._needItems = items;
$scope.completion[device]._needItems = items + deletes;
}
if (needed == 0 && deletes > 0) {
@@ -657,12 +647,12 @@ angular.module('syncthing.core')
refreshNeed($scope.neededFolder);
};
$scope.failedPageChanged = function (page) {
$scope.failedCurrentPage = page;
};
$scope.failedChangePageSize = function (perpage) {
$scope.failedPageSize = perpage;
$scope.refreshFailed = function (page, perpage) {
var url = urlbase + '/folder/pullerrors?folder=' + encodeURIComponent($scope.failed.folder);
url += "&page=" + page + "&perpage=" + perpage;
$http.get(url).success(function (data) {
$scope.failed = data;
}).error($scope.emitHTTPError);
};
$scope.refreshRemoteNeed = function (folder, page, perpage) {
@@ -1018,14 +1008,6 @@ angular.module('syncthing.core')
return '?';
};
$scope.deviceCompletion = function (deviceCfg) {
var conn = $scope.connections[deviceCfg.deviceID];
if (conn) {
return conn.completion + '%';
}
return '';
};
$scope.friendlyNameFromShort = function (shortID) {
var matches = $scope.devices.filter(function (n) {
return n.deviceID.substr(0, 7) === shortID;
@@ -1107,9 +1089,9 @@ angular.module('syncthing.core')
},
show: function() {
$scope.logging.refreshFacilities();
$scope.logging.timer = $interval($scope.logging.fetch, 0, 1);
$scope.logging.timer = $timeout($scope.logging.fetch);
$('#logViewer').modal().on('hidden.bs.modal', function () {
$interval.cancel($scope.logging.timer);
$timeout.cancel($scope.logging.timer);
$scope.logging.timer = null;
$scope.logging.entries = [];
});
@@ -1138,7 +1120,7 @@ angular.module('syncthing.core')
var textArea = $('#logViewerText');
if (textArea.is(":focus")) {
if (!$scope.logging.timer) return;
$scope.logging.timer = $interval($scope.logging.fetch, 500, 1);
$scope.logging.timer = $timeout($scope.logging.fetch, 500);
return;
}
@@ -1149,7 +1131,7 @@ angular.module('syncthing.core')
$http.get(urlbase + '/system/log' + (last ? '?since=' + encodeURIComponent(last) : '')).success(function (data) {
if (!$scope.logging.timer) return;
$scope.logging.timer = $interval($scope.logging.fetch, 2000, 1);
$scope.logging.timer = $timeout($scope.logging.fetch, 2000);
if (!textArea.is(":focus")) {
if (data.messages) {
$scope.logging.entries.push.apply($scope.logging.entries, data.messages);
@@ -1767,6 +1749,233 @@ angular.module('syncthing.core')
});
};
function resetRestoreVersions() {
$scope.restoreVersions = {
folder: null,
selections: {},
versions: null,
tree: null,
errors: null,
filters: {},
massAction: function (name, action) {
$.each($scope.restoreVersions.versions, function(key) {
if (key.startsWith(name + '/') && (!$scope.restoreVersions.filters.text || key.indexOf($scope.restoreVersions.filters.text) > -1)) {
if (action == 'unset') {
delete $scope.restoreVersions.selections[key];
return;
}
var availableVersions = [];
$.each($scope.restoreVersions.filterVersions($scope.restoreVersions.versions[key]), function(idx, version) {
availableVersions.push(version.versionTime);
})
if (availableVersions.length) {
availableVersions.sort(function (a, b) { return a - b; });
if (action == 'latest') {
$scope.restoreVersions.selections[key] = availableVersions.pop();
} else if (action == 'oldest') {
$scope.restoreVersions.selections[key] = availableVersions.shift();
}
}
}
});
},
filterVersions: function(versions) {
var filteredVersions = [];
$.each(versions, function (idx, version) {
if (moment(version.versionTime).isBetween($scope.restoreVersions.filters['start'], $scope.restoreVersions.filters['end'], null, '[]')) {
filteredVersions.push(version);
}
});
return filteredVersions;
},
selectionCount: function() {
var count = 0;
$.each($scope.restoreVersions.selections, function(key, value) {
if (value) {
count++;
}
});
return count;
},
restore: function() {
$scope.restoreVersions.tree.clear();
$scope.restoreVersions.tree = null;
$scope.restoreVersions.versions = null;
var selections = {};
$.each($scope.restoreVersions.selections, function(key, value) {
if (value) {
selections[key] = value;
}
});
$scope.restoreVersions.selections = {};
$http.post(urlbase + '/folder/versions?folder=' + encodeURIComponent($scope.restoreVersions.folder), selections).success(function (data) {
if (Object.keys(data).length == 0) {
$('#restoreVersions').modal('hide');
} else {
$scope.restoreVersions.errors = data;
}
});
},
show: function(folder) {
$scope.restoreVersions.folder = folder;
var closed = false;
var modalShown = $q.defer();
$('#restoreVersions').modal().on('hidden.bs.modal', function () {
closed = true;
resetRestoreVersions();
}).on('shown.bs.modal', function() {
modalShown.resolve();
});
var dataReceived = $http.get(urlbase + '/folder/versions?folder=' + encodeURIComponent($scope.restoreVersions.folder))
.success(function (data) {
$.each(data, function(key, values) {
$.each(values, function(idx, value) {
value.modTime = new Date(value.modTime);
value.versionTime = new Date(value.versionTime);
});
});
if (closed) return;
$scope.restoreVersions.versions = data;
});
$q.all([dataReceived, modalShown.promise]).then(function() {
if (closed) {
resetRestoreVersions();
return;
}
$scope.restoreVersions.tree = $("#restoreTree").fancytree({
extensions: ["table", "filter"],
quicksearch: true,
filter: {
autoApply: true,
counter: true,
hideExpandedCounter: true,
hideExpanders: true,
highlight: true,
leavesOnly: false,
nodata: true,
mode: "hide"
},
table: {
indentation: 20,
nodeColumnIdx: 0,
},
debugLevel: 2,
source: buildTree($scope.restoreVersions.versions),
renderColumns: function(event, data) {
var node = data.node,
$tdList = $(node.tr).find(">td"),
template;
if (node.folder) {
template = '<div ng-include="\'syncthing/folder/restoreVersionsMassActions.html\'" class="pull-right"/>';
} else {
template = '<div ng-include="\'syncthing/folder/restoreVersionsVersionSelector.html\'" class="pull-right"/>';
}
var scope = $rootScope.$new(true);
scope.key = node.key;
scope.restoreVersions = $scope.restoreVersions;
$tdList.eq(1).html(
$compile(template)(scope)
);
// Force angular to redraw.
$timeout(function() {
$scope.$apply();
});
}
}).fancytree("getTree");
var minDate = moment(),
maxDate = moment(0, 'X'),
date;
// Find version window.
$.each($scope.restoreVersions.versions, function(key) {
$.each($scope.restoreVersions.versions[key], function(idx, version) {
date = moment(version.versionTime);
if (date.isBefore(minDate)) {
minDate = date;
}
if (date.isAfter(maxDate)) {
maxDate = date;
}
});
});
$scope.restoreVersions.filters['start'] = minDate;
$scope.restoreVersions.filters['end'] = maxDate;
var ranges = {
'All time': [minDate, maxDate],
'Today': [moment(), moment()],
'Yesterday': [moment().subtract(1, 'days'), moment().subtract(1, 'days')],
'Last 7 Days': [moment().subtract(6, 'days'), moment()],
'Last 30 Days': [moment().subtract(29, 'days'), moment()],
'This Month': [moment().startOf('month'), moment().endOf('month')],
'Last Month': [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')]
};
// Filter out invalid ranges.
$.each(ranges, function(key, range) {
if (!range[0].isBetween(minDate, maxDate, null, '[]') && !range[1].isBetween(minDate, maxDate, null, '[]')) {
delete ranges[key];
}
});
$("#restoreVersionDateRange").daterangepicker({
timePicker: true,
timePicker24Hour: true,
timePickerSeconds: true,
autoUpdateInput: true,
opens: "left",
drops: "up",
startDate: minDate,
endDate: maxDate,
minDate: minDate,
maxDate: maxDate,
ranges: ranges,
locale: {
format: 'YYYY/MM/DD HH:mm:ss',
}
}).on('apply.daterangepicker', function(ev, picker) {
$scope.restoreVersions.filters['start'] = picker.startDate;
$scope.restoreVersions.filters['end'] = picker.endDate;
// Events for this UI element are not managed by angular.
// Force angular to wake up.
$timeout(function() {
$scope.$apply();
});
});
});
}
};
}
resetRestoreVersions();
$scope.$watchCollection('restoreVersions.filters', function() {
if (!$scope.restoreVersions.tree) return;
$scope.restoreVersions.tree.filterNodes(function (node) {
if (node.folder) return false;
if ($scope.restoreVersions.filters.text && node.key.indexOf($scope.restoreVersions.filters.text) < 0) {
return false;
}
if ($scope.restoreVersions.filterVersions(node.data.versions).length == 0) {
return false;
}
return true;
});
});
$scope.editIgnoresOnAddingFolder = function () {
if ($scope.editingExisting) {
return;
@@ -1827,7 +2036,8 @@ angular.module('syncthing.core')
resetRemoteNeed();
$scope.remoteNeedDevice = device;
$scope.deviceFolders(device).forEach(function(folder) {
if ($scope.completion[device.deviceID][folder].needItems === 0) {
var comp = $scope.completion[device.deviceID][folder];
if (comp !== undefined && comp.needItems + comp.needDeletes === 0) {
return;
}
$scope.remoteNeedFolders.push(folder);
@@ -1839,24 +2049,18 @@ angular.module('syncthing.core')
};
$scope.showFailed = function (folder) {
$scope.failedCurrent = $scope.failed[folder];
$scope.failedFolderPath = $scope.folders[folder].path;
if ($scope.failedFolderPath[$scope.failedFolderPath.length - 1] !== $scope.system.pathSeparator) {
$scope.failedFolderPath += $scope.system.pathSeparator;
}
$scope.failed.folder = folder;
$scope.failed = $scope.refreshFailed(1, 10);
$('#failed').modal().on('hidden.bs.modal', function () {
$scope.failedCurrent = undefined;
$scope.failed = {};
});
};
$scope.hasFailedFiles = function (folder) {
if (!$scope.failed[folder]) {
if (!$scope.model[folder]) {
return false;
}
if ($scope.failed[folder].length === 0) {
return false;
}
return true;
return $scope.model[folder].pullErrors !== 0;
};
$scope.override = function (folder) {

View File

@@ -1,15 +1,15 @@
<modal id="remove-device-confirmation" status="warning" icon="exclamation-circle" heading="{{'Remove Device' | translate}}" large="no" closeable="yes">
<div class="modal-body">
<p ng-model="currentDevice.name" style=" overflow : hidden; text-overflow: ellipsis; white-space: nowrap;">
<span translate translate-value-name="{{currentDevice.name}}">Are you sure you want to remove device {%name%}?</span>
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-warning pull-left btn-sm" data-dismiss="modal" ng-click="deleteDevice()">
<span class="fa fa-minus-circle"></span>&nbsp;<span translate>Yes</span>
</button>
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
<span class="fa fa-times"></span>&nbsp;<span translate>No</span>
</button>
</div>
<div class="modal-body">
<p ng-model="currentDevice.name" style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
<span translate translate-value-name="{{currentDevice.name}}">Are you sure you want to remove device {%name%}?</span>
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-warning pull-left btn-sm" data-dismiss="modal" ng-click="deleteDevice()">
<span class="fa fa-minus-circle"></span>&nbsp;<span translate>Yes</span>
</button>
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
<span class="fa fa-times"></span>&nbsp;<span translate>No</span>
</button>
</div>
</modal>

View File

@@ -1,18 +1,18 @@
<modal id="remove-folder-confirmation" status="warning" icon="exclamation-circle" heading="{{'Remove Folder' | translate}}" large="no" closeable="yes">
<div class="modal-body">
<p ng-model="currentFolder.label" style=" overflow : hidden; text-overflow: ellipsis; white-space: nowrap;">
<span translate translate-value-label="{{currentFolder.label}}">Are you sure you want to remove folder {%label%}?</span>
</p>
<p translate>
No files will be deleted as a result of this operation.
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-warning pull-left btn-sm" data-dismiss="modal" ng-click="deleteFolder(currentFolder.id)">
<span class="fa fa-minus-circle"></span>&nbsp;<span translate>Yes</span>
</button>
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
<span class="fa fa-times"></span>&nbsp;<span translate>No</span>
</button>
</div>
<div class="modal-body">
<p style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
<span translate translate-value-label="{{currentFolder.label}}">Are you sure you want to remove folder {%label%}?</span>
</p>
<p translate>
No files will be deleted as a result of this operation.
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-warning pull-left btn-sm" data-dismiss="modal" ng-click="deleteFolder(currentFolder.id)">
<span class="fa fa-minus-circle"></span>&nbsp;<span translate>Yes</span>
</button>
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
<span class="fa fa-times"></span>&nbsp;<span translate>No</span>
</button>
</div>
</modal>

View File

@@ -0,0 +1,15 @@
<modal id="restore-versions-confirmation" status="warning" icon="exclamation-circle" heading="{{'Restore Versions' | translate}}" large="no" closeable="yes">
<div class="modal-body">
<p style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
<span translate-value-count="{{restoreVersions.selectionCount()}}" translate>Are you sure you want to restore {%count%} files?</span>
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-warning pull-left btn-sm" data-dismiss="modal" ng-click="restoreVersions.restore()">
<span class="fa fa-check"></span>&nbsp;<span translate>Yes</span>
</button>
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
<span class="fa fa-times"></span>&nbsp;<span translate>No</span>
</button>
</div>
</modal>

View File

@@ -0,0 +1,11 @@
<div class="dropdown">
<button class="btn btn-default btn-xs dropdown-toggle" type="button" data-toggle="dropdown">
<span translate>Mass actions</span>
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li><a href="#" ng-click="restoreVersions.massAction(key, 'unset')" translate>Do not restore all</a></li>
<li><a href="#" ng-click="restoreVersions.massAction(key, 'latest')" translate>Select latest version</a></li>
<li><a href="#" ng-click="restoreVersions.massAction(key, 'oldest')" translate>Select oldest version</a></li>
</ul>
</div>

View File

@@ -0,0 +1,51 @@
<modal id="restoreVersions" status="default" heading="{{'Restore Versions' | translate}}" large="yes" closeable="yes">
<div class="modal-body">
<span translate ng-if="!restoreVersions.versions && !restoreVersions.errors">Loading data...</span>
<div ng-if="restoreVersions.versions">
<table id="restoreTree">
<thead>
<tr>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<hr/>
<div class="row form-inline">
<div class="col-md-6">
<div class="form-group">
<label translate for="restoreVersionSearch">Filter by name</label>:&nbsp
<input id="restoreVersionSearch" class="form-control" type="text" ng-model="restoreVersions.filters.text">
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label translate for="restoreVersionDate">Filter by date</label>:&nbsp
<input id="restoreVersionDateRange" class="form-control">
</div>
</div>
</div>
</div>
<div ng-if="restoreVersions.errors">
<label><span translate>Some items could not be restored:</span></label>
<table class="table table-condensed table-striped">
<tbody>
<tr ng-repeat="(file, error) in restoreVersions.errors">
<td>{{ file }}</td>
<td>{{ error }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary btn-sm" data-toggle="modal" data-target="#restore-versions-confirmation" ng-if="restoreVersions.versions" ng-disabled="restoreVersions.selectionCount() < 1">
<span class="fa fa-check"></span>&nbsp;<span translate>Restore</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>
</modal>

View File

@@ -0,0 +1,17 @@
<div class="dropdown">
<button class="btn btn-default btn-xs dropdown-toggle" type="button" data-toggle="dropdown">
<span ng-if="!restoreVersions.selections[key]" translate>Do not restore</span>
<span ng-if="restoreVersions.selections[key]">{{ restoreVersions.selections[key] | date:"yyyy/MM/dd HH:mm:ss" }}</span>
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li>
<a href="#" ng-click="restoreVersions.selections[key] = undefined" translate>Do not restore</a>
</li>
<li ng-repeat="version in restoreVersions.filterVersions(restoreVersions.versions[key])">
<a href="#" ng-click="restoreVersions.selections[key] = version.versionTime">
{{ version.versionTime | date:"yyyy/MM/dd HH:mm:ss" }} {{ version.size | binary }}B
</a>
</li>
</ul>
</div>

View File

@@ -5,15 +5,15 @@
<span translate>They are retried automatically and will be synced when the error is resolved.</span>
</p>
<table class="table table-striped table-dynamic">
<tr dir-paginate="e in failedCurrent | itemsPerPage: failedPageSize" current-page="failedCurrentPage" pagination-id="failed">
<td>{{failedFolderPath}}{{e.path}}</td>
<tr dir-paginate="e in failed.errors | itemsPerPage: failed.perpage" current-page="failed.page" total-items="model[failed.folder].pullErrors" pagination-id="failed">
<td>{{e.path}}</td>
<td><abbr tooltip data-original-title="{{e.error}}">{{e.error | lastErrorComponent}}</abbr></td>
</tr>
</table>
<dir-pagination-controls on-page-change="failedPageChanged(newPageNumber)" pagination-id="failed"></dir-pagination-controls>
<dir-pagination-controls on-page-change="refreshFailed(newPageNumber, failed.perpage)" pagination-id="failed"></dir-pagination-controls>
<ul class="pagination pull-right">
<li ng-repeat="option in [10, 25, 50]" ng-class="{ active: failedPageSize == option }">
<a href="#" ng-click="failedChangePageSize(option)">{{option}}</a>
<li ng-repeat="option in [10, 25, 50]" ng-class="{ active: failed.page == option }">
<a href="#" ng-click="refreshFailed(failed.page, option)">{{option}}</a>
</li>
</ul>
<div class="clearfix"></div>

View File

@@ -1,4 +1,4 @@
é<modal id="remoteNeed" status="info" icon="exchange" heading="{{'Out of Sync Items' | translate}} - {{deviceName(remoteNeedDevice)}}" large="yes" closeable="yes">
<modal id="remoteNeed" status="info" icon="exchange" heading="{{'Out of Sync Items' | translate}} - {{deviceName(remoteNeedDevice)}}" large="yes" closeable="yes">
<div class="modal-body">
<div ng-if="sizeOf(remoteNeed) == 0">
<span translate>Loading data...</span>
@@ -12,7 +12,7 @@
</button>
<div id="remoteNeed-{{folder}}" class="panel-collapse collapse">
<div class="panel-body">
<table class="table table-striped table-dynamic">
<table class="table table-striped">
<thead>
<tr>
<th translate>Path</th>

View File

@@ -0,0 +1,269 @@
.daterangepicker {
position: absolute;
color: inherit;
background-color: #fff;
border-radius: 4px;
width: 278px;
padding: 4px;
margin-top: 1px;
top: 100px;
left: 20px;
/* Calendars */ }
.daterangepicker:before, .daterangepicker:after {
position: absolute;
display: inline-block;
border-bottom-color: rgba(0, 0, 0, 0.2);
content: ''; }
.daterangepicker:before {
top: -7px;
border-right: 7px solid transparent;
border-left: 7px solid transparent;
border-bottom: 7px solid #ccc; }
.daterangepicker:after {
top: -6px;
border-right: 6px solid transparent;
border-bottom: 6px solid #fff;
border-left: 6px solid transparent; }
.daterangepicker.opensleft:before {
right: 9px; }
.daterangepicker.opensleft:after {
right: 10px; }
.daterangepicker.openscenter:before {
left: 0;
right: 0;
width: 0;
margin-left: auto;
margin-right: auto; }
.daterangepicker.openscenter:after {
left: 0;
right: 0;
width: 0;
margin-left: auto;
margin-right: auto; }
.daterangepicker.opensright:before {
left: 9px; }
.daterangepicker.opensright:after {
left: 10px; }
.daterangepicker.dropup {
margin-top: -5px; }
.daterangepicker.dropup:before {
top: initial;
bottom: -7px;
border-bottom: initial;
border-top: 7px solid #ccc; }
.daterangepicker.dropup:after {
top: initial;
bottom: -6px;
border-bottom: initial;
border-top: 6px solid #fff; }
.daterangepicker.dropdown-menu {
max-width: none;
z-index: 3001; }
.daterangepicker.single .ranges, .daterangepicker.single .calendar {
float: none; }
.daterangepicker.show-calendar .calendar {
display: block; }
.daterangepicker .calendar {
display: none;
max-width: 270px;
margin: 4px; }
.daterangepicker .calendar.single .calendar-table {
border: none; }
.daterangepicker .calendar th, .daterangepicker .calendar td {
white-space: nowrap;
text-align: center;
min-width: 32px; }
.daterangepicker .calendar-table {
border: 1px solid #fff;
padding: 4px;
border-radius: 4px;
background-color: #fff; }
.daterangepicker table {
width: 100%;
margin: 0; }
.daterangepicker td, .daterangepicker th {
text-align: center;
width: 20px;
height: 20px;
border-radius: 4px;
border: 1px solid transparent;
white-space: nowrap;
cursor: pointer; }
.daterangepicker td.available:hover, .daterangepicker th.available:hover {
background-color: #eee;
border-color: transparent;
color: inherit; }
.daterangepicker td.week, .daterangepicker th.week {
font-size: 80%;
color: #ccc; }
.daterangepicker td.off, .daterangepicker td.off.in-range, .daterangepicker td.off.start-date, .daterangepicker td.off.end-date {
background-color: #fff;
border-color: transparent;
color: #999; }
.daterangepicker td.in-range {
background-color: #ebf4f8;
border-color: transparent;
color: #000;
border-radius: 0; }
.daterangepicker td.start-date {
border-radius: 4px 0 0 4px; }
.daterangepicker td.end-date {
border-radius: 0 4px 4px 0; }
.daterangepicker td.start-date.end-date {
border-radius: 4px; }
.daterangepicker td.active, .daterangepicker td.active:hover {
background-color: #357ebd;
border-color: transparent;
color: #fff; }
.daterangepicker th.month {
width: auto; }
.daterangepicker td.disabled, .daterangepicker option.disabled {
color: #999;
cursor: not-allowed;
text-decoration: line-through; }
.daterangepicker select.monthselect, .daterangepicker select.yearselect {
font-size: 12px;
padding: 1px;
height: auto;
margin: 0;
cursor: default; }
.daterangepicker select.monthselect {
margin-right: 2%;
width: 56%; }
.daterangepicker select.yearselect {
width: 40%; }
.daterangepicker select.hourselect, .daterangepicker select.minuteselect, .daterangepicker select.secondselect, .daterangepicker select.ampmselect {
width: 50px;
margin-bottom: 0; }
.daterangepicker .input-mini {
border: 1px solid #ccc;
border-radius: 4px;
color: #555;
height: 30px;
line-height: 30px;
display: block;
vertical-align: middle;
margin: 0 0 5px 0;
padding: 0 6px 0 28px;
width: 100%; }
.daterangepicker .input-mini.active {
border: 1px solid #08c;
border-radius: 4px; }
.daterangepicker .daterangepicker_input {
position: relative; }
.daterangepicker .daterangepicker_input i {
position: absolute;
left: 8px;
top: 8px; }
.daterangepicker.rtl .input-mini {
padding-right: 28px;
padding-left: 6px; }
.daterangepicker.rtl .daterangepicker_input i {
left: auto;
right: 8px; }
.daterangepicker .calendar-time {
text-align: center;
margin: 5px auto;
line-height: 30px;
position: relative;
padding-left: 28px; }
.daterangepicker .calendar-time select.disabled {
color: #ccc;
cursor: not-allowed; }
.ranges {
font-size: 11px;
float: none;
margin: 4px;
text-align: left; }
.ranges ul {
list-style: none;
margin: 0 auto;
padding: 0;
width: 100%; }
.ranges li {
font-size: 13px;
background-color: #f5f5f5;
border: 1px solid #f5f5f5;
border-radius: 4px;
color: #08c;
padding: 3px 12px;
margin-bottom: 8px;
cursor: pointer; }
.ranges li:hover {
background-color: #08c;
border: 1px solid #08c;
color: #fff; }
.ranges li.active {
background-color: #08c;
border: 1px solid #08c;
color: #fff; }
/* Larger Screen Styling */
@media (min-width: 564px) {
.daterangepicker {
width: auto; }
.daterangepicker .ranges ul {
width: 160px; }
.daterangepicker.single .ranges ul {
width: 100%; }
.daterangepicker.single .calendar.left {
clear: none; }
.daterangepicker.single.ltr .ranges, .daterangepicker.single.ltr .calendar {
float: left; }
.daterangepicker.single.rtl .ranges, .daterangepicker.single.rtl .calendar {
float: right; }
.daterangepicker.ltr {
direction: ltr;
text-align: left; }
.daterangepicker.ltr .calendar.left {
clear: left;
margin-right: 0; }
.daterangepicker.ltr .calendar.left .calendar-table {
border-right: none;
border-top-right-radius: 0;
border-bottom-right-radius: 0; }
.daterangepicker.ltr .calendar.right {
margin-left: 0; }
.daterangepicker.ltr .calendar.right .calendar-table {
border-left: none;
border-top-left-radius: 0;
border-bottom-left-radius: 0; }
.daterangepicker.ltr .left .daterangepicker_input {
padding-right: 12px; }
.daterangepicker.ltr .calendar.left .calendar-table {
padding-right: 12px; }
.daterangepicker.ltr .ranges, .daterangepicker.ltr .calendar {
float: left; }
.daterangepicker.rtl {
direction: rtl;
text-align: right; }
.daterangepicker.rtl .calendar.left {
clear: right;
margin-left: 0; }
.daterangepicker.rtl .calendar.left .calendar-table {
border-left: none;
border-top-left-radius: 0;
border-bottom-left-radius: 0; }
.daterangepicker.rtl .calendar.right {
margin-right: 0; }
.daterangepicker.rtl .calendar.right .calendar-table {
border-right: none;
border-top-right-radius: 0;
border-bottom-right-radius: 0; }
.daterangepicker.rtl .left .daterangepicker_input {
padding-left: 12px; }
.daterangepicker.rtl .calendar.left .calendar-table {
padding-left: 12px; }
.daterangepicker.rtl .ranges, .daterangepicker.rtl .calendar {
text-align: right;
float: right; } }
@media (min-width: 730px) {
.daterangepicker .ranges {
width: auto; }
.daterangepicker.ltr .ranges {
float: left; }
.daterangepicker.rtl .ranges {
float: right; }
.daterangepicker .calendar.left {
clear: none !important; } }

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,663 @@
/*!
* Fancytree "Lion" skin.
*
* DON'T EDIT THE CSS FILE DIRECTLY, since it is automatically generated from
* the LESS templates.
*/
/*
Lion colors:
gray highlight bar: #D4D4D4
blue highlight-bar and -border #3875D7
*/
/*******************************************************************************
* Common Styles for Fancytree Skins.
*
* This section is automatically generated from the `skin-common.less` template.
******************************************************************************/
/*------------------------------------------------------------------------------
* Helpers
*----------------------------------------------------------------------------*/
.ui-helper-hidden {
display: none;
}
/*------------------------------------------------------------------------------
* Container and UL / LI
*----------------------------------------------------------------------------*/
ul.fancytree-container {
font-family: tahoma, arial, helvetica;
font-size: 10pt;
white-space: nowrap;
padding: 3px;
margin: 0;
background-color: white;
border: 1px dotted gray;
min-height: 0%;
position: relative;
}
ul.fancytree-container ul {
padding: 0 0 0 16px;
margin: 0;
}
ul.fancytree-container ul > li:before {
content: none;
}
ul.fancytree-container li {
list-style-image: none;
list-style-position: outside;
list-style-type: none;
-moz-background-clip: border;
-moz-background-inline-policy: continuous;
-moz-background-origin: padding;
background-attachment: scroll;
background-color: transparent;
background-position: 0px 0px;
background-repeat: repeat-y;
background-image: none;
margin: 0;
}
ul.fancytree-container li.fancytree-lastsib {
background-image: none;
}
.ui-fancytree-disabled ul.fancytree-container {
opacity: 0.5;
background-color: silver;
}
ul.fancytree-connectors.fancytree-container li {
background-image: url("../skin-lion/vline.gif");
background-position: 0 0;
}
ul.fancytree-container li.fancytree-lastsib,
ul.fancytree-no-connector > li {
background-image: none;
}
li.fancytree-animating {
position: relative;
}
/*------------------------------------------------------------------------------
* Common icon definitions
*----------------------------------------------------------------------------*/
span.fancytree-empty,
span.fancytree-vline,
span.fancytree-expander,
span.fancytree-icon,
span.fancytree-checkbox,
span.fancytree-drag-helper-img,
#fancytree-drop-marker {
width: 16px;
height: 16px;
display: inline-block;
vertical-align: top;
background-repeat: no-repeat;
background-position: left;
background-image: url("../skin-lion/icons.gif");
background-position: 0px 0px;
}
span.fancytree-icon,
span.fancytree-checkbox,
span.fancytree-expander,
span.fancytree-custom-icon {
margin-top: 0px;
}
/* Used by icon option: */
span.fancytree-custom-icon {
width: 16px;
height: 16px;
display: inline-block;
margin-left: 3px;
background-position: 0px 0px;
}
/* Used by 'icon' node option: */
img.fancytree-icon {
width: 16px;
height: 16px;
margin-left: 3px;
margin-top: 0px;
vertical-align: top;
border-style: none;
}
/*------------------------------------------------------------------------------
* Expander icon
*
* Note: IE6 doesn't correctly evaluate multiples class names,
* so we create combined class names that can be used in the CSS.
*
* Prefix: fancytree-exp-
* 1st character: 'e': expanded, 'c': collapsed, 'n': no children
* 2nd character (optional): 'd': lazy (Delayed)
* 3rd character (optional): 'l': Last sibling
*----------------------------------------------------------------------------*/
span.fancytree-expander {
cursor: pointer;
}
.fancytree-exp-n span.fancytree-expander,
.fancytree-exp-nl span.fancytree-expander {
background-image: none;
cursor: default;
}
.fancytree-connectors .fancytree-exp-n span.fancytree-expander,
.fancytree-connectors .fancytree-exp-nl span.fancytree-expander {
background-image: url("../skin-lion/icons.gif");
margin-top: 0;
}
.fancytree-connectors .fancytree-exp-n span.fancytree-expander,
.fancytree-connectors .fancytree-exp-n span.fancytree-expander:hover {
background-position: 0px -64px;
}
.fancytree-connectors .fancytree-exp-nl span.fancytree-expander,
.fancytree-connectors .fancytree-exp-nl span.fancytree-expander:hover {
background-position: -16px -64px;
}
.fancytree-exp-c span.fancytree-expander {
background-position: 0px -80px;
}
.fancytree-exp-c span.fancytree-expander:hover {
background-position: -16px -80px;
}
.fancytree-exp-cl span.fancytree-expander {
background-position: 0px -96px;
}
.fancytree-exp-cl span.fancytree-expander:hover {
background-position: -16px -96px;
}
.fancytree-exp-cd span.fancytree-expander {
background-position: -64px -80px;
}
.fancytree-exp-cd span.fancytree-expander:hover {
background-position: -80px -80px;
}
.fancytree-exp-cdl span.fancytree-expander {
background-position: -64px -96px;
}
.fancytree-exp-cdl span.fancytree-expander:hover {
background-position: -80px -96px;
}
.fancytree-exp-e span.fancytree-expander,
.fancytree-exp-ed span.fancytree-expander {
background-position: -32px -80px;
}
.fancytree-exp-e span.fancytree-expander:hover,
.fancytree-exp-ed span.fancytree-expander:hover {
background-position: -48px -80px;
}
.fancytree-exp-el span.fancytree-expander,
.fancytree-exp-edl span.fancytree-expander {
background-position: -32px -96px;
}
.fancytree-exp-el span.fancytree-expander:hover,
.fancytree-exp-edl span.fancytree-expander:hover {
background-position: -48px -96px;
}
/* Fade out expanders, when container is not hovered or active */
.fancytree-fade-expander span.fancytree-expander {
transition: opacity 1.5s;
opacity: 0;
}
.fancytree-fade-expander:hover span.fancytree-expander,
.fancytree-fade-expander.fancytree-treefocus span.fancytree-expander,
.fancytree-fade-expander .fancytree-treefocus span.fancytree-expander,
.fancytree-fade-expander [class*='fancytree-statusnode-'] span.fancytree-expander {
transition: opacity 0.6s;
opacity: 1;
}
/*------------------------------------------------------------------------------
* Checkbox icon
*----------------------------------------------------------------------------*/
span.fancytree-checkbox {
margin-left: 3px;
background-position: 0px -32px;
}
span.fancytree-checkbox:hover {
background-position: -16px -32px;
}
span.fancytree-checkbox.fancytree-radio {
background-position: 0px -48px;
}
span.fancytree-checkbox.fancytree-radio:hover {
background-position: -16px -48px;
}
.fancytree-partsel span.fancytree-checkbox {
background-position: -64px -32px;
}
.fancytree-partsel span.fancytree-checkbox:hover {
background-position: -80px -32px;
}
.fancytree-partsel span.fancytree-checkbox.fancytree-radio {
background-position: -64px -48px;
}
.fancytree-partsel span.fancytree-checkbox.fancytree-radio:hover {
background-position: -80px -48px;
}
.fancytree-selected span.fancytree-checkbox {
background-position: -32px -32px;
}
.fancytree-selected span.fancytree-checkbox:hover {
background-position: -48px -32px;
}
.fancytree-selected span.fancytree-checkbox.fancytree-radio {
background-position: -32px -48px;
}
.fancytree-selected span.fancytree-checkbox.fancytree-radio:hover {
background-position: -48px -48px;
}
.fancytree-unselectable span.fancytree-checkbox {
opacity: 0.4;
filter: alpha(opacity=40);
}
.fancytree-unselectable span.fancytree-checkbox:hover {
background-position: 0px -32px;
}
.fancytree-unselectable.fancytree-partsel span.fancytree-checkbox:hover {
background-position: -64px -32px;
}
.fancytree-unselectable.fancytree-selected span.fancytree-checkbox:hover {
background-position: -32px -32px;
}
/*------------------------------------------------------------------------------
* Node type icon
* Note: IE6 doesn't correctly evaluate multiples class names,
* so we create combined class names that can be used in the CSS.
*
* Prefix: fancytree-ico-
* 1st character: 'e': expanded, 'c': collapsed
* 2nd character (optional): 'f': folder
*----------------------------------------------------------------------------*/
span.fancytree-icon {
margin-left: 3px;
background-position: 0px 0px;
}
/* Documents */
.fancytree-ico-c span.fancytree-icon:hover {
background-position: -16px 0px;
}
.fancytree-has-children.fancytree-ico-c span.fancytree-icon {
background-position: -32px 0px;
}
.fancytree-has-children.fancytree-ico-c span.fancytree-icon:hover {
background-position: -48px 0px;
}
.fancytree-ico-e span.fancytree-icon {
background-position: -64px 0px;
}
.fancytree-ico-e span.fancytree-icon:hover {
background-position: -80px 0px;
}
/* Folders */
.fancytree-ico-cf span.fancytree-icon {
background-position: 0px -16px;
}
.fancytree-ico-cf span.fancytree-icon:hover {
background-position: -16px -16px;
}
.fancytree-has-children.fancytree-ico-cf span.fancytree-icon {
background-position: -32px -16px;
}
.fancytree-has-children.fancytree-ico-cf span.fancytree-icon:hover {
background-position: -48px -16px;
}
.fancytree-ico-ef span.fancytree-icon {
background-position: -64px -16px;
}
.fancytree-ico-ef span.fancytree-icon:hover {
background-position: -80px -16px;
}
.fancytree-loading span.fancytree-expander,
.fancytree-loading span.fancytree-expander:hover,
.fancytree-statusnode-loading span.fancytree-icon,
.fancytree-statusnode-loading span.fancytree-icon:hover {
background-image: url("../skin-lion/loading.gif");
background-position: 0px 0px;
}
/* Status node icons */
.fancytree-statusnode-error span.fancytree-icon,
.fancytree-statusnode-error span.fancytree-icon:hover {
background-position: 0px -112px;
}
/*------------------------------------------------------------------------------
* Node titles and highlighting
*----------------------------------------------------------------------------*/
span.fancytree-node {
/* See #117 */
display: inherit;
width: 100%;
margin-top: 1px;
min-height: 16px;
}
span.fancytree-title {
color: black;
cursor: pointer;
display: inline-block;
vertical-align: top;
min-height: 16px;
padding: 0 3px 0 3px;
margin: 0px 0 0 3px;
border: 1px solid transparent;
-webkit-border-radius: 0px;
-moz-border-radius: 0px;
-ms-border-radius: 0px;
-o-border-radius: 0px;
border-radius: 0px;
}
span.fancytree-node.fancytree-error span.fancytree-title {
color: red;
}
/*------------------------------------------------------------------------------
* Drag'n'drop support
*----------------------------------------------------------------------------*/
div.fancytree-drag-helper span.fancytree-childcounter,
div.fancytree-drag-helper span.fancytree-dnd-modifier {
display: inline-block;
color: #fff;
background: #337ab7;
border: 1px solid gray;
min-width: 10px;
height: 10px;
line-height: 1;
vertical-align: baseline;
border-radius: 10px;
padding: 2px;
text-align: center;
font-size: 9px;
}
div.fancytree-drag-helper span.fancytree-childcounter {
position: absolute;
top: -6px;
right: -6px;
}
div.fancytree-drag-helper span.fancytree-dnd-modifier {
background: #5cb85c;
border: none;
font-weight: bolder;
}
div.fancytree-drag-helper.fancytree-drop-accept span.fancytree-drag-helper-img {
background-position: -32px -112px;
}
div.fancytree-drag-helper.fancytree-drop-reject span.fancytree-drag-helper-img {
background-position: -16px -112px;
}
/*** Drop marker icon *********************************************************/
#fancytree-drop-marker {
width: 32px;
position: absolute;
background-position: 0px -128px;
margin: 0;
}
#fancytree-drop-marker.fancytree-drop-after,
#fancytree-drop-marker.fancytree-drop-before {
width: 64px;
background-position: 0px -144px;
}
#fancytree-drop-marker.fancytree-drop-copy {
background-position: -64px -128px;
}
#fancytree-drop-marker.fancytree-drop-move {
background-position: -32px -128px;
}
/*** Source node while dragging ***********************************************/
span.fancytree-drag-source.fancytree-drag-remove {
opacity: 0.15;
}
/*** Target node while dragging cursor is over it *****************************/
/*------------------------------------------------------------------------------
* 'rtl' option
*----------------------------------------------------------------------------*/
.fancytree-container.fancytree-rtl .fancytree-title {
/*unicode-bidi: bidi-override;*/
/* optional: reverse title letters */
}
.fancytree-container.fancytree-rtl span.fancytree-connector,
.fancytree-container.fancytree-rtl span.fancytree-expander,
.fancytree-container.fancytree-rtl span.fancytree-icon,
.fancytree-container.fancytree-rtl span.fancytree-drag-helper-img,
.fancytree-container.fancytree-rtl #fancytree-drop-marker {
background-image: url("../skin-lion/icons-rtl.gif");
}
.fancytree-container.fancytree-rtl .fancytree-exp-n span.fancytree-expander,
.fancytree-container.fancytree-rtl .fancytree-exp-nl span.fancytree-expander {
background-image: none;
}
.fancytree-container.fancytree-rtl.fancytree-connectors .fancytree-exp-n span.fancytree-expander,
.fancytree-container.fancytree-rtl.fancytree-connectors .fancytree-exp-nl span.fancytree-expander {
background-image: url("../skin-lion/icons-rtl.gif");
}
ul.fancytree-container.fancytree-rtl ul {
padding: 0 16px 0 0;
}
ul.fancytree-container.fancytree-rtl.fancytree-connectors li {
background-position: right 0;
background-image: url("../skin-lion/vline-rtl.gif");
}
ul.fancytree-container.fancytree-rtl li.fancytree-lastsib,
ul.fancytree-container.fancytree-rtl.fancytree-no-connector > li {
background-image: none;
}
/*------------------------------------------------------------------------------
* 'table' extension
*----------------------------------------------------------------------------*/
table.fancytree-ext-table {
border-collapse: collapse;
}
table.fancytree-ext-table span.fancytree-node {
display: inline-block;
box-sizing: border-box;
}
/*------------------------------------------------------------------------------
* 'columnview' extension
*----------------------------------------------------------------------------*/
table.fancytree-ext-columnview tbody tr td {
position: relative;
border: 1px solid gray;
vertical-align: top;
overflow: auto;
}
table.fancytree-ext-columnview tbody tr td > ul {
padding: 0;
}
table.fancytree-ext-columnview tbody tr td > ul li {
list-style-image: none;
list-style-position: outside;
list-style-type: none;
-moz-background-clip: border;
-moz-background-inline-policy: continuous;
-moz-background-origin: padding;
background-attachment: scroll;
background-color: transparent;
background-position: 0px 0px;
background-repeat: repeat-y;
background-image: none;
/* no v-lines */
margin: 0;
}
table.fancytree-ext-columnview span.fancytree-node {
position: relative;
/* allow positioning of embedded spans */
display: inline-block;
}
table.fancytree-ext-columnview span.fancytree-node.fancytree-expanded {
background-color: #CBE8F6;
}
table.fancytree-ext-columnview .fancytree-has-children span.fancytree-cv-right {
position: absolute;
right: 3px;
background-position: 0px -80px;
}
table.fancytree-ext-columnview .fancytree-has-children span.fancytree-cv-right:hover {
background-position: -16px -80px;
}
/*------------------------------------------------------------------------------
* 'filter' extension
*----------------------------------------------------------------------------*/
.fancytree-ext-filter-dimm span.fancytree-node span.fancytree-title {
color: silver;
font-weight: lighter;
}
.fancytree-ext-filter-dimm tr.fancytree-submatch span.fancytree-title,
.fancytree-ext-filter-dimm span.fancytree-node.fancytree-submatch span.fancytree-title {
color: black;
font-weight: normal;
}
.fancytree-ext-filter-dimm tr.fancytree-match span.fancytree-title,
.fancytree-ext-filter-dimm span.fancytree-node.fancytree-match span.fancytree-title {
color: black;
font-weight: bold;
}
.fancytree-ext-filter-hide tr.fancytree-hide,
.fancytree-ext-filter-hide span.fancytree-node.fancytree-hide {
display: none;
}
.fancytree-ext-filter-hide tr.fancytree-submatch span.fancytree-title,
.fancytree-ext-filter-hide span.fancytree-node.fancytree-submatch span.fancytree-title {
color: silver;
font-weight: lighter;
}
.fancytree-ext-filter-hide tr.fancytree-match span.fancytree-title,
.fancytree-ext-filter-hide span.fancytree-node.fancytree-match span.fancytree-title {
color: black;
font-weight: normal;
}
/* Hide expanders if all child nodes are hidden by filter */
.fancytree-ext-filter-hide-expanders tr.fancytree-match span.fancytree-expander,
.fancytree-ext-filter-hide-expanders span.fancytree-node.fancytree-match span.fancytree-expander {
visibility: hidden;
}
.fancytree-ext-filter-hide-expanders tr.fancytree-submatch span.fancytree-expander,
.fancytree-ext-filter-hide-expanders span.fancytree-node.fancytree-submatch span.fancytree-expander {
visibility: visible;
}
.fancytree-ext-childcounter span.fancytree-icon,
.fancytree-ext-filter span.fancytree-icon {
position: relative;
}
.fancytree-ext-childcounter span.fancytree-childcounter,
.fancytree-ext-filter span.fancytree-childcounter {
color: #fff;
background: #777;
border: 1px solid gray;
position: absolute;
top: -6px;
right: -6px;
min-width: 10px;
height: 10px;
line-height: 1;
vertical-align: baseline;
border-radius: 10px;
padding: 2px;
text-align: center;
font-size: 9px;
}
/*------------------------------------------------------------------------------
* 'wide' extension
*----------------------------------------------------------------------------*/
ul.fancytree-ext-wide {
position: relative;
min-width: 100%;
z-index: 2;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
ul.fancytree-ext-wide span.fancytree-node > span {
position: relative;
z-index: 2;
}
ul.fancytree-ext-wide span.fancytree-node span.fancytree-title {
position: absolute;
z-index: 1;
left: 0px;
min-width: 100%;
margin-left: 0;
margin-right: 0;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
/*------------------------------------------------------------------------------
* 'fixed' extension
*----------------------------------------------------------------------------*/
.fancytree-ext-fixed-wrapper .fancytree-ext-fixed-hidden {
display: none;
}
.fancytree-ext-fixed-wrapper div.fancytree-ext-fixed-scroll-border-bottom {
border-bottom: 3px solid rgba(0, 0, 0, 0.75);
}
.fancytree-ext-fixed-wrapper div.fancytree-ext-fixed-scroll-border-right {
border-right: 3px solid rgba(0, 0, 0, 0.75);
}
.fancytree-ext-fixed-wrapper div.fancytree-ext-fixed-wrapper-tl {
position: absolute;
overflow: hidden;
z-index: 3;
top: 0px;
left: 0px;
}
.fancytree-ext-fixed-wrapper div.fancytree-ext-fixed-wrapper-tr {
position: absolute;
overflow: hidden;
z-index: 2;
top: 0px;
}
.fancytree-ext-fixed-wrapper div.fancytree-ext-fixed-wrapper-bl {
position: absolute;
overflow: hidden;
z-index: 2;
left: 0px;
}
.fancytree-ext-fixed-wrapper div.fancytree-ext-fixed-wrapper-br {
position: absolute;
overflow: scroll;
z-index: 1;
}
/*******************************************************************************
* Styles specific to this skin.
*
* This section is automatically generated from the `ui-fancytree.less` template.
******************************************************************************/
/*******************************************************************************
* Node titles
*/
span.fancytree-title {
border: 1px solid transparent;
border-radius: 0;
}
span.fancytree-focused span.fancytree-title {
outline: 1px dotted black;
}
span.fancytree-selected span.fancytree-title,
span.fancytree-active span.fancytree-title {
background-color: #D4D4D4;
}
span.fancytree-selected span.fancytree-title {
font-style: italic;
}
.fancytree-treefocus span.fancytree-selected span.fancytree-title,
.fancytree-treefocus span.fancytree-active span.fancytree-title {
color: white;
background-color: #3875D7;
}
/*******************************************************************************
* 'table' extension
*/
table.fancytree-ext-table {
border-collapse: collapse;
}
table.fancytree-ext-table tbody tr.fancytree-focused {
background-color: #99DEFD;
}
table.fancytree-ext-table tbody tr.fancytree-active {
background-color: royalblue;
}
table.fancytree-ext-table tbody tr.fancytree-selected {
background-color: #99DEFD;
}
/*******************************************************************************
* 'columnview' extension
*/
table.fancytree-ext-columnview tbody tr td {
border: 1px solid gray;
}
table.fancytree-ext-columnview span.fancytree-node.fancytree-expanded {
background-color: #ccc;
}
table.fancytree-ext-columnview span.fancytree-node.fancytree-active {
background-color: royalblue;
}

View File

File diff suppressed because it is too large Load Diff

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 852 B

4517
gui/default/vendor/moment/moment.js vendored Normal file
View File

File diff suppressed because it is too large Load Diff

View File

@@ -53,16 +53,14 @@ var (
// DefaultDiscoveryServersV4 should be substituted when the configuration
// contains <globalAnnounceServer>default-v4</globalAnnounceServer>.
DefaultDiscoveryServersV4 = []string{
"https://discovery-v4-2.syncthing.net/v2/?id=DVU36WY-H3LVZHW-E6LLFRE-YAFN5EL-HILWRYP-OC2M47J-Z4PE62Y-ADIBDQC", // 45.55.230.38, USA
"https://discovery-v4-3.syncthing.net/v2/?id=VK6HNJ3-VVMM66S-HRVWSCR-IXEHL2H-U4AQ4MW-UCPQBWX-J2L2UBK-NVZRDQZ", // 128.199.95.124, Singapore
"https://discovery-v4-4.syncthing.net/v2/?id=LYXKCHX-VI3NYZR-ALCJBHF-WMZYSPK-QG6QJA3-MPFYMSO-U56GTUK-NA2MIAW", // 95.85.19.244, NL
"https://discovery.syncthing.net/v2/?noannounce&id=LYXKCHX-VI3NYZR-ALCJBHF-WMZYSPK-QG6QJA3-MPFYMSO-U56GTUK-NA2MIAW",
"https://discovery-v4.syncthing.net/v2/?nolookup&id=LYXKCHX-VI3NYZR-ALCJBHF-WMZYSPK-QG6QJA3-MPFYMSO-U56GTUK-NA2MIAW",
}
// DefaultDiscoveryServersV6 should be substituted when the configuration
// contains <globalAnnounceServer>default-v6</globalAnnounceServer>.
DefaultDiscoveryServersV6 = []string{
"https://discovery-v6-2.syncthing.net/v2/?id=DVU36WY-H3LVZHW-E6LLFRE-YAFN5EL-HILWRYP-OC2M47J-Z4PE62Y-ADIBDQC", // 2604:a880:800:10::182:a001, USA
"https://discovery-v6-3.syncthing.net/v2/?id=VK6HNJ3-VVMM66S-HRVWSCR-IXEHL2H-U4AQ4MW-UCPQBWX-J2L2UBK-NVZRDQZ", // 2400:6180:0:d0::d9:d001, Singapore
"https://discovery-v6-4.syncthing.net/v2/?id=LYXKCHX-VI3NYZR-ALCJBHF-WMZYSPK-QG6QJA3-MPFYMSO-U56GTUK-NA2MIAW", // 2a03:b0c0:0:1010::4ed:3001, NL
"https://discovery.syncthing.net/v2/?noannounce&id=LYXKCHX-VI3NYZR-ALCJBHF-WMZYSPK-QG6QJA3-MPFYMSO-U56GTUK-NA2MIAW",
"https://discovery-v6.syncthing.net/v2/?nolookup&id=LYXKCHX-VI3NYZR-ALCJBHF-WMZYSPK-QG6QJA3-MPFYMSO-U56GTUK-NA2MIAW",
}
// DefaultDiscoveryServers should be substituted when the configuration
// contains <globalAnnounceServer>default</globalAnnounceServer>.

View File

@@ -76,6 +76,7 @@ func TestDefaultValues(t *testing.T) {
KCPUpdateIntervalMs: 25,
KCPFastResend: false,
DefaultFolderPath: "~",
SetLowPriority: true,
}
cfg := New(device1)
@@ -224,6 +225,7 @@ func TestOverriddenValues(t *testing.T) {
KCPUpdateIntervalMs: 1000,
KCPFastResend: true,
DefaultFolderPath: "/media/syncthing",
SetLowPriority: false,
}
os.Unsetenv("STNOUPGRADE")

View File

@@ -14,6 +14,7 @@ import (
"github.com/syncthing/syncthing/lib/fs"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/util"
"github.com/syncthing/syncthing/lib/versioner"
)
var (
@@ -98,6 +99,18 @@ func (f FolderConfiguration) Filesystem() fs.Filesystem {
return f.cachedFilesystem
}
func (f FolderConfiguration) Versioner() versioner.Versioner {
if f.Versioning.Type == "" {
return nil
}
versionerFactory, ok := versioner.Factories[f.Versioning.Type]
if !ok {
l.Fatalf("Requested versioning type %q that does not exist", f.Versioning.Type)
}
return versionerFactory(f.ID, f.Filesystem(), f.Versioning.Params)
}
func (f *FolderConfiguration) CreateMarker() error {
if err := f.CheckPath(); err != errMarkerMissing {
return err

View File

@@ -143,6 +143,7 @@ type OptionsConfiguration struct {
KCPSendWindowSize int `xml:"kcpSendWindowSize" json:"kcpSendWindowSize" default:"128"`
KCPReceiveWindowSize int `xml:"kcpReceiveWindowSize" json:"kcpReceiveWindowSize" default:"128"`
DefaultFolderPath string `xml:"defaultFolderPath" json:"defaultFolderPath" default:"~"`
SetLowPriority bool `xml:"setLowPriority" json:"setLowPriority" default:"true"`
DeprecatedUPnPEnabled bool `xml:"upnpEnabled,omitempty" json:"-"`
DeprecatedUPnPLeaseM int `xml:"upnpLeaseMinutes,omitempty" json:"-"`

View File

@@ -44,5 +44,6 @@
<kcpUpdateIntervalMs>1000</kcpUpdateIntervalMs>
<kcpFastResend>true</kcpFastResend>
<defaultFolderPath>/media/syncthing</defaultFolderPath>
<setLowPriority>false</setLowPriority>
</options>
</configuration>

View File

@@ -163,7 +163,7 @@ next:
// because there are implementations out there that don't support
// protocol negotiation (iOS for one...).
if !cs.NegotiatedProtocolIsMutual || cs.NegotiatedProtocol != s.bepProtocolName {
l.Infof("Peer %s did not negotiate bep/1.0", c.RemoteAddr())
l.Infof("Peer at %s did not negotiate bep/1.0", c)
}
// We should have received exactly one certificate from the other
@@ -171,7 +171,7 @@ 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, c.RemoteAddr())
l.Infof("Got peer certificate list of length %d != 1 from peer at %s; protocol error", cl, c)
c.Close()
continue
}
@@ -182,7 +182,7 @@ next:
// though, especially in the presence of NAT hairpinning, multiple
// clients between the same NAT gateway, and global discovery.
if remoteID == s.myID {
l.Infof("Connected to myself (%s) - should not happen", remoteID)
l.Infof("Connected to myself (%s) at %s - should not happen", remoteID, c)
c.Close()
continue
}
@@ -206,7 +206,7 @@ next:
warningFor(remoteID, msg)
} else {
// It's something else - connection reset or whatever
l.Infof("Failed to exchange Hello messages with %s (%s): %s", remoteID, c.RemoteAddr(), err)
l.Infof("Failed to exchange Hello messages with %s at %s: %s", remoteID, c, err)
}
c.Close()
continue
@@ -256,7 +256,7 @@ 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, c.RemoteAddr(), err)
l.Warnf("Bad certificate from %s at %s: %v", remoteID, c, err)
c.Close()
continue next
}
@@ -268,11 +268,10 @@ next:
wr := s.limiter.newWriteLimiter(c, isLAN)
rd := s.limiter.newReadLimiter(c, isLAN)
name := fmt.Sprintf("%s-%s (%s)", c.LocalAddr(), c.RemoteAddr(), c.Type())
protoConn := protocol.NewConnection(remoteID, rd, wr, s.model, name, deviceCfg.Compression)
protoConn := protocol.NewConnection(remoteID, rd, wr, s.model, c.String(), deviceCfg.Compression)
modelConn := completeConn{c, protoConn}
l.Infof("Established secure connection to %s at %s (%s)", remoteID, name, tlsCipherSuiteNames[c.ConnectionState().CipherSuite])
l.Infof("Established secure connection to %s at %s (%s)", remoteID, c, tlsCipherSuiteNames[c.ConnectionState().CipherSuite])
s.model.AddConnection(modelConn, hello)
continue next
@@ -779,11 +778,9 @@ func dialParallel(deviceID protocol.DeviceID, dialTargets []dialTarget) (interna
}()
// Wait for the first connection, or for channel closure.
conn, ok := <-res
// Got a connection, means more might come back, hence spawn a
// routine that will do the discarding.
if ok {
if conn, ok := <-res; ok {
// Got a connection, means more might come back, hence spawn a
// routine that will do the discarding.
l.Debugln("connected to", deviceID, prio, "using", conn, conn.priority)
go func(deviceID protocol.DeviceID, prio int) {
wg.Wait()
@@ -793,10 +790,9 @@ func dialParallel(deviceID protocol.DeviceID, dialTargets []dialTarget) (interna
}
}(deviceID, prio)
return conn, ok
} else {
// Failed to connect, report that fact.
l.Debugln("failed to connect to", deviceID, prio)
}
// Failed to connect, report that fact.
l.Debugln("failed to connect to", deviceID, prio)
}
return internalConn{}, false
}

View File

@@ -28,6 +28,7 @@ type Connection interface {
Transport() string
RemoteAddr() net.Addr
Priority() int
String() string
}
// completeConn is the aggregation of an internalConn and the
@@ -114,7 +115,7 @@ func (c internalConn) Transport() string {
}
func (c internalConn) String() string {
return fmt.Sprintf("%s-%s/%s", c.LocalAddr(), c.RemoteAddr(), c.connType.String())
return fmt.Sprintf("%s-%s/%s", c.LocalAddr(), c.RemoteAddr(), c.Type())
}
type dialerFactory interface {

View File

@@ -30,6 +30,7 @@ type globalClient struct {
announceClient httpClient
queryClient httpClient
noAnnounce bool
noLookup bool
stop chan struct{}
errorHolder
}
@@ -52,6 +53,7 @@ type announcement struct {
type serverOptions struct {
insecure bool // don't check certificate
noAnnounce bool // don't announce
noLookup bool // don't use for lookups
id string // expected server device ID
}
@@ -119,15 +121,26 @@ func NewGlobal(server string, cert tls.Certificate, addrList AddressLister) (Fin
announceClient: announceClient,
queryClient: queryClient,
noAnnounce: opts.noAnnounce,
noLookup: opts.noLookup,
stop: make(chan struct{}),
}
cl.setError(errors.New("not announced"))
if !opts.noAnnounce {
// If we are supposed to annonce, it's an error until we've done so.
cl.setError(errors.New("not announced"))
}
return cl, nil
}
// Lookup returns the list of addresses where the given device is available
func (c *globalClient) Lookup(device protocol.DeviceID) (addresses []string, err error) {
if c.noLookup {
return nil, lookupError{
error: errors.New("lookups not supported"),
cacheFor: time.Hour,
}
}
qURL, err := url.Parse(c.server)
if err != nil {
return nil, err
@@ -201,7 +214,6 @@ func (c *globalClient) Serve() {
}
func (c *globalClient) sendAnnouncement(timer *time.Timer) {
var ann announcement
if c.addrList != nil {
ann.Addresses = c.addrList.ExternalAddresses()
@@ -287,6 +299,7 @@ func parseOptions(dsn string) (server string, opts serverOptions, err error) {
opts.id = q.Get("id")
opts.insecure = opts.id != "" || queryBool(q, "insecure")
opts.noAnnounce = queryBool(q, "noannounce")
opts.noLookup = queryBool(q, "nolookup")
// Check for disallowed combinations
if p.Scheme == "http" {

View File

@@ -299,6 +299,18 @@ func (f *BasicFilesystem) URI() string {
return strings.TrimPrefix(f.root, `\\?\`)
}
func (f *BasicFilesystem) SameFile(fi1, fi2 FileInfo) bool {
// Like os.SameFile, we always return false unless fi1 and fi2 were created
// by this package's Stat/Lstat method.
f1, ok1 := fi1.(fsFileInfo)
f2, ok2 := fi2.(fsFileInfo)
if !ok1 || !ok2 {
return false
}
return os.SameFile(f1.FileInfo, f2.FileInfo)
}
// fsFile implements the fs.File interface on top of an os.File
type fsFile struct {
*os.File

View File

@@ -28,7 +28,7 @@ func (BasicFilesystem) ReadSymlink(path string) (string, error) {
return "", errNotSupported
}
func (BasicFilesystem) CreateSymlink(path, target string) error {
func (BasicFilesystem) CreateSymlink(target, name string) error {
return errNotSupported
}

View File

@@ -20,7 +20,7 @@ type errorFilesystem struct {
func (fs *errorFilesystem) Chmod(name string, mode FileMode) error { return fs.err }
func (fs *errorFilesystem) Chtimes(name string, atime time.Time, mtime time.Time) error { return fs.err }
func (fs *errorFilesystem) Create(name string) (File, error) { return nil, fs.err }
func (fs *errorFilesystem) CreateSymlink(name, target string) error { return fs.err }
func (fs *errorFilesystem) CreateSymlink(target, name string) error { return fs.err }
func (fs *errorFilesystem) DirNames(name string) ([]string, error) { return nil, fs.err }
func (fs *errorFilesystem) Lstat(name string) (FileInfo, error) { return nil, fs.err }
func (fs *errorFilesystem) Mkdir(name string, perm FileMode) error { return fs.err }
@@ -42,6 +42,7 @@ func (fs *errorFilesystem) Roots() ([]string, error)
func (fs *errorFilesystem) Usage(name string) (Usage, error) { return Usage{}, fs.err }
func (fs *errorFilesystem) Type() FilesystemType { return fs.fsType }
func (fs *errorFilesystem) URI() string { return fs.uri }
func (fs *errorFilesystem) SameFile(fi1, fi2 FileInfo) bool { return false }
func (fs *errorFilesystem) Watch(path string, ignore Matcher, ctx context.Context, ignorePerms bool) (<-chan Event, error) {
return nil, fs.err
}

View File

@@ -21,7 +21,7 @@ type Filesystem interface {
Chmod(name string, mode FileMode) error
Chtimes(name string, atime time.Time, mtime time.Time) error
Create(name string) (File, error)
CreateSymlink(name, target string) error
CreateSymlink(target, name string) error
DirNames(name string) ([]string, error)
Lstat(name string) (FileInfo, error)
Mkdir(name string, perm FileMode) error
@@ -43,6 +43,7 @@ type Filesystem interface {
Usage(name string) (Usage, error)
Type() FilesystemType
URI() string
SameFile(fi1, fi2 FileInfo) bool
}
// The File interface abstracts access to a regular file, being a somewhat

View File

@@ -44,9 +44,9 @@ func (fs *logFilesystem) Create(name string) (File, error) {
return file, err
}
func (fs *logFilesystem) CreateSymlink(name, target string) error {
err := fs.Filesystem.CreateSymlink(name, target)
l.Debugln(getCaller(), fs.Type(), fs.URI(), "CreateSymlink", name, target, err)
func (fs *logFilesystem) CreateSymlink(target, name string) error {
err := fs.Filesystem.CreateSymlink(target, name)
l.Debugln(getCaller(), fs.Type(), fs.URI(), "CreateSymlink", target, name, err)
return err
}

View File

@@ -46,7 +46,7 @@ func IsTemporary(name string) bool {
return false
}
func TempName(name string) string {
func TempNameWithPrefix(name, prefix string) string {
tdir := filepath.Dir(name)
tbase := filepath.Base(name)
if len(tbase) > maxFilenameLength {
@@ -54,6 +54,10 @@ func TempName(name string) string {
hash.Write([]byte(name))
tbase = fmt.Sprintf("%x", hash.Sum(nil))
}
tname := fmt.Sprintf("%s%s.tmp", TempPrefix, tbase)
tname := fmt.Sprintf("%s%s.tmp", prefix, tbase)
return filepath.Join(tdir, tname)
}
func TempName(name string) string {
return TempNameWithPrefix(name, TempPrefix)
}

View File

@@ -147,7 +147,7 @@ func TestRecorder(t *testing.T) {
t.Fatalf("unexpected line count: %d", len(lines))
}
if lines[0].Message != "hah" {
t.Errorf("incorrect line: %s", lines[0])
t.Errorf("incorrect line: %s", lines[0].Message)
}
}

View File

@@ -38,6 +38,16 @@ import (
"github.com/thejerf/suture"
)
var locationLocal *time.Location
func init() {
var err error
locationLocal, err = time.LoadLocation("Local")
if err != nil {
panic(err.Error())
}
}
// How many files to send in each Index/IndexUpdate message.
const (
maxBatchSizeBytes = 250 * 1024 // Aim for making index messages no larger than 250 KiB (uncompressed)
@@ -54,6 +64,7 @@ type service interface {
Serve()
Stop()
CheckHealth() error
PullErrors() []FileError
getState() (folderState, time.Time, error)
setState(state folderState)
@@ -109,7 +120,7 @@ var (
errDeviceUnknown = errors.New("unknown device")
errDevicePaused = errors.New("device is paused")
errDeviceIgnored = errors.New("device is ignored")
errFolderPaused = errors.New("folder is paused")
ErrFolderPaused = errors.New("folder is paused")
errFolderNotRunning = errors.New("folder is not running")
errFolderMissing = errors.New("no such folder")
errNetworkNotAllowed = errors.New("network not allowed")
@@ -232,21 +243,13 @@ func (m *Model) startFolderLocked(folder string) config.FolderType {
}
}
var ver versioner.Versioner
if len(cfg.Versioning.Type) > 0 {
versionerFactory, ok := versioner.Factories[cfg.Versioning.Type]
if !ok {
l.Fatalf("Requested versioning type %q that does not exist", cfg.Versioning.Type)
}
ver = versionerFactory(folder, cfg.Filesystem(), cfg.Versioning.Params)
if service, ok := ver.(suture.Service); ok {
// The versioner implements the suture.Service interface, so
// expects to be run in the background in addition to being called
// when files are going to be archived.
token := m.Add(service)
m.folderRunnerTokens[folder] = append(m.folderRunnerTokens[folder], token)
}
ver := cfg.Versioner()
if service, ok := ver.(suture.Service); ok {
// The versioner implements the suture.Service interface, so
// expects to be run in the background in addition to being called
// when files are going to be archived.
token := m.Add(service)
m.folderRunnerTokens[folder] = append(m.folderRunnerTokens[folder], token)
}
ffs := fs.MtimeFS()
@@ -1094,15 +1097,17 @@ func (m *Model) handleIntroductions(introducerCfg config.DeviceConfiguration, cm
foldersDevices := make(folderDeviceSet)
for _, folder := range cm.Folders {
// We don't have this folder, skip.
if _, ok := m.folderDevices[folder.ID]; !ok {
continue
}
// Adds devices which we do not have, but the introducer has
// for the folders that we have in common. Also, shares folders
// with devices that we have in common, yet are currently not sharing
// the folder.
fcfg, ok := m.cfg.Folder(folder.ID)
if !ok {
// Don't have this folder, carry on.
continue
}
nextDevice:
for _, device := range folder.Devices {
// No need to share with self.
@@ -1116,22 +1121,29 @@ func (m *Model) handleIntroductions(introducerCfg config.DeviceConfiguration, cm
// The device is currently unknown. Add it to the config.
m.introduceDevice(device, introducerCfg)
changed = true
}
for _, er := range m.deviceFolders[device.ID] {
if er == folder.ID {
// We already share the folder with this device, so
// nothing to do.
continue nextDevice
} else {
for _, dev := range fcfg.DeviceIDs() {
if dev == device.ID {
// We already share the folder with this device, so
// nothing to do.
continue nextDevice
}
}
}
// We don't yet share this folder with this device. Add the device
// to sharing list of the folder.
l.Infof("Sharing folder %s with %v (vouched for by introducer %v)", folder.Description(), device.ID, introducerCfg.DeviceID)
m.shareFolderWithDeviceLocked(device.ID, folder.ID, introducerCfg.DeviceID)
fcfg.Devices = append(fcfg.Devices, config.FolderDeviceConfiguration{
DeviceID: device.ID,
IntroducedBy: introducerCfg.DeviceID,
})
changed = true
}
if changed {
m.cfg.SetFolder(fcfg)
}
}
return foldersDevices, changed
@@ -1193,7 +1205,7 @@ func (m *Model) handleDeintroductions(introducerCfg config.DeviceConfiguration,
// handleAutoAccepts handles adding and sharing folders for devices that have
// AutoAcceptFolders set to true.
func (m *Model) handleAutoAccepts(deviceCfg config.DeviceConfiguration, folder protocol.Folder) bool {
if _, ok := m.cfg.Folder(folder.ID); !ok {
if cfg, ok := m.cfg.Folder(folder.ID); !ok {
defaultPath := m.cfg.Options().DefaultFolderPath
defaultPathFs := fs.NewFilesystem(fs.FilesystemTypeBasic, defaultPath)
for _, path := range []string{folder.Label, folder.ID} {
@@ -1202,7 +1214,9 @@ func (m *Model) handleAutoAccepts(deviceCfg config.DeviceConfiguration, folder p
}
fcfg := config.NewFolderConfiguration(m.id, folder.ID, folder.Label, fs.FilesystemTypeBasic, filepath.Join(defaultPath, path))
fcfg.Devices = append(fcfg.Devices, config.FolderDeviceConfiguration{
DeviceID: deviceCfg.DeviceID,
})
// Need to wait for the waiter, as this calls CommitConfiguration,
// which sets up the folder and as we return from this call,
// ClusterConfig starts poking at m.folderFiles and other things
@@ -1210,29 +1224,26 @@ func (m *Model) handleAutoAccepts(deviceCfg config.DeviceConfiguration, folder p
w, _ := m.cfg.SetFolder(fcfg)
w.Wait()
// This needs to happen under a lock.
m.fmut.Lock()
w = m.shareFolderWithDeviceLocked(deviceCfg.DeviceID, folder.ID, protocol.DeviceID{})
m.fmut.Unlock()
w.Wait()
l.Infof("Auto-accepted %s folder %s at path %s", deviceCfg.DeviceID, folder.Description(), fcfg.Path)
return true
}
l.Infof("Failed to auto-accept folder %s from %s due to path conflict", folder.Description(), deviceCfg.DeviceID)
return false
} else {
for _, device := range cfg.DeviceIDs() {
if device == deviceCfg.DeviceID {
// Already shared nothing todo.
return false
}
}
cfg.Devices = append(cfg.Devices, config.FolderDeviceConfiguration{
DeviceID: deviceCfg.DeviceID,
})
w, _ := m.cfg.SetFolder(cfg)
w.Wait()
l.Infof("Shared %s with %s due to auto-accept", folder.ID, deviceCfg.DeviceID)
return true
}
// Folder does not exist yet.
if m.folderSharedWith(folder.ID, deviceCfg.DeviceID) {
return false
}
m.fmut.Lock()
w := m.shareFolderWithDeviceLocked(deviceCfg.DeviceID, folder.ID, protocol.DeviceID{})
m.fmut.Unlock()
w.Wait()
l.Infof("Shared %s with %s due to auto-accept", folder.ID, deviceCfg.DeviceID)
return true
}
func (m *Model) introduceDevice(device protocol.Device, introducerCfg config.DeviceConfiguration) {
@@ -1263,19 +1274,6 @@ func (m *Model) introduceDevice(device protocol.Device, introducerCfg config.Dev
m.cfg.SetDevice(newDeviceCfg)
}
func (m *Model) shareFolderWithDeviceLocked(deviceID protocol.DeviceID, folder string, introducer protocol.DeviceID) config.Waiter {
m.deviceFolders[deviceID] = append(m.deviceFolders[deviceID], folder)
m.folderDevices.set(deviceID, folder)
folderCfg := m.cfg.Folders()[folder]
folderCfg.Devices = append(folderCfg.Devices, config.FolderDeviceConfiguration{
DeviceID: deviceID,
IntroducedBy: introducer,
})
w, _ := m.cfg.SetFolder(folderCfg)
return w
}
// Closed is called when a connection has been closed
func (m *Model) Closed(conn protocol.Connection, err error) {
device := conn.ID()
@@ -1293,7 +1291,7 @@ func (m *Model) Closed(conn protocol.Connection, err error) {
delete(m.closed, device)
m.pmut.Unlock()
l.Infof("Connection to %s closed: %v", device, err)
l.Infof("Connection to %s at %s closed: %v", device, conn.Name(), err)
events.Default.Log(events.DeviceDisconnected, map[string]string{
"id": device.String(),
"error": err.Error(),
@@ -1568,7 +1566,7 @@ func (m *Model) AddConnection(conn connections.Connection, hello protocol.HelloR
events.Default.Log(events.DeviceConnected, event)
l.Infof(`Device %s client is "%s %s" named "%s"`, deviceID, hello.ClientName, hello.ClientVersion, hello.DeviceName)
l.Infof(`Device %s client is "%s %s" named "%s" at %s`, deviceID, hello.ClientName, hello.ClientVersion, hello.DeviceName, conn)
conn.Start()
m.pmut.Unlock()
@@ -1647,11 +1645,10 @@ func (m *Model) receivedFile(folder string, file protocol.FileInfo) {
func sendIndexes(conn protocol.Connection, folder string, fs *db.FileSet, ignores *ignore.Matcher, startSequence int64, dbLocation string, dropSymlinks bool) {
deviceID := conn.ID()
name := conn.Name()
var err error
l.Debugf("sendIndexes for %s-%s/%q starting (slv=%d)", deviceID, name, folder, startSequence)
defer l.Debugf("sendIndexes for %s-%s/%q exiting: %v", deviceID, name, folder, err)
l.Debugf("Starting sendIndexes for %s to %s at %s (slv=%d)", folder, deviceID, conn, startSequence)
defer l.Debugf("Exiting sendIndexes for %s to %s at %s: %v", folder, deviceID, conn, err)
minSequence, err := sendIndexTo(startSequence, conn, folder, fs, ignores, dbLocation, dropSymlinks)
@@ -1687,12 +1684,14 @@ func sendIndexes(conn protocol.Connection, folder string, fs *db.FileSet, ignore
func sendIndexTo(minSequence int64, conn protocol.Connection, folder string, fs *db.FileSet, ignores *ignore.Matcher, dbLocation string, dropSymlinks bool) (int64, error) {
deviceID := conn.ID()
name := conn.Name()
batch := make([]protocol.FileInfo, 0, maxBatchSizeFiles)
batchSizeBytes := 0
initial := minSequence == 0
maxSequence := minSequence
var err error
debugMsg := func(t string) string {
return fmt.Sprintf("Sending indexes for %s to %s at %s: %d files (<%d bytes) (%s)", folder, deviceID, conn, len(batch), batchSizeBytes, t)
}
sorter := NewIndexSorter(dbLocation)
defer sorter.Close()
@@ -1725,13 +1724,13 @@ func sendIndexTo(minSequence int64, conn protocol.Connection, folder string, fs
if err = conn.Index(folder, batch); err != nil {
return false
}
l.Debugf("sendIndexes for %s-%s/%q: %d files (<%d bytes) (initial index)", deviceID, name, folder, len(batch), batchSizeBytes)
l.Debugln(debugMsg("initial index"))
initial = false
} else {
if err = conn.IndexUpdate(folder, batch); err != nil {
return false
}
l.Debugf("sendIndexes for %s-%s/%q: %d files (<%d bytes) (batched update)", deviceID, name, folder, len(batch), batchSizeBytes)
l.Debugln(debugMsg("batched update"))
}
batch = make([]protocol.FileInfo, 0, maxBatchSizeFiles)
@@ -1746,12 +1745,12 @@ func sendIndexTo(minSequence int64, conn protocol.Connection, folder string, fs
if initial && err == nil {
err = conn.Index(folder, batch)
if err == nil {
l.Debugf("sendIndexes for %s-%s/%q: %d files (small initial index)", deviceID, name, folder, len(batch))
l.Debugln(debugMsg("small initial index"))
}
} else if len(batch) > 0 && err == nil {
err = conn.IndexUpdate(folder, batch)
if err == nil {
l.Debugf("sendIndexes for %s-%s/%q: %d files (last batch)", deviceID, name, folder, len(batch))
l.Debugln(debugMsg("last batch"))
}
}
@@ -2228,6 +2227,15 @@ func (m *Model) State(folder string) (string, time.Time, error) {
return state.String(), changed, err
}
func (m *Model) PullErrors(folder string) ([]FileError, error) {
m.fmut.RLock()
defer m.fmut.RUnlock()
if err := m.checkFolderRunningLocked(folder); err != nil {
return nil, err
}
return m.folderRunners[folder].PullErrors(), nil
}
func (m *Model) Override(folder string) {
m.fmut.RLock()
fs, ok := m.folderFiles[folder]
@@ -2376,6 +2384,132 @@ func (m *Model) GlobalDirectoryTree(folder, prefix string, levels int, dirsonly
return output
}
func (m *Model) GetFolderVersions(folder string) (map[string][]versioner.FileVersion, error) {
fcfg, ok := m.cfg.Folder(folder)
if !ok {
return nil, errFolderMissing
}
files := make(map[string][]versioner.FileVersion)
filesystem := fcfg.Filesystem()
err := filesystem.Walk(".stversions", func(path string, f fs.FileInfo, err error) error {
// Skip root (which is ok to be a symlink)
if path == ".stversions" {
return nil
}
// Ignore symlinks
if f.IsSymlink() {
return fs.SkipDir
}
// No records for directories
if f.IsDir() {
return nil
}
// Strip .stversions prefix.
path = strings.TrimPrefix(path, ".stversions"+string(fs.PathSeparator))
name, tag := versioner.UntagFilename(path)
// Something invalid
if name == "" || tag == "" {
return nil
}
name = osutil.NormalizedFilename(name)
versionTime, err := time.ParseInLocation(versioner.TimeFormat, tag, locationLocal)
if err != nil {
return nil
}
files[name] = append(files[name], versioner.FileVersion{
VersionTime: versionTime.Truncate(time.Second),
ModTime: f.ModTime().Truncate(time.Second),
Size: f.Size(),
})
return nil
})
if err != nil {
return nil, err
}
return files, nil
}
func (m *Model) RestoreFolderVersions(folder string, versions map[string]time.Time) (map[string]string, error) {
fcfg, ok := m.cfg.Folder(folder)
if !ok {
return nil, errFolderMissing
}
filesystem := fcfg.Filesystem()
ver := fcfg.Versioner()
restore := make(map[string]string)
errors := make(map[string]string)
// Validation
for file, version := range versions {
file = osutil.NativeFilename(file)
tag := version.In(locationLocal).Truncate(time.Second).Format(versioner.TimeFormat)
versionedTaggedFilename := filepath.Join(".stversions", versioner.TagFilename(file, tag))
// Check that the thing we've been asked to restore is actually a file
// and that it exists.
if info, err := filesystem.Lstat(versionedTaggedFilename); err != nil {
errors[file] = err.Error()
continue
} else if !info.IsRegular() {
errors[file] = "not a file"
continue
}
// Check that the target location of where we are supposed to restore
// either does not exist, or is actually a file.
if info, err := filesystem.Lstat(file); err == nil && !info.IsRegular() {
errors[file] = "cannot replace a non-file"
continue
} else if err != nil && !fs.IsNotExist(err) {
errors[file] = err.Error()
continue
}
restore[file] = versionedTaggedFilename
}
// Execution
var err error
for target, source := range restore {
err = nil
if _, serr := filesystem.Lstat(target); serr == nil {
if ver != nil {
err = osutil.InWritableDir(ver.Archive, filesystem, target)
} else {
err = osutil.InWritableDir(filesystem.Remove, filesystem, target)
}
}
filesystem.MkdirAll(filepath.Dir(target), 0755)
if err == nil {
err = osutil.Copy(filesystem, source, target)
}
if err != nil {
errors[target] = err.Error()
continue
}
}
// Trigger scan
if !fcfg.FSWatcherEnabled {
m.ScanFolder(folder)
}
return errors, nil
}
func (m *Model) Availability(folder, file string, version protocol.Vector, block protocol.BlockInfo) []Availability {
// The slightly unusual locking sequence here is because we need to hold
// pmut for the duration (as the value returned from foldersFiles can
@@ -2533,7 +2667,7 @@ func (m *Model) checkFolderRunningLocked(folder string) error {
if cfg, ok := m.cfg.Folder(folder); !ok {
return errFolderMissing
} else if cfg.Paused {
return errFolderPaused
return ErrFolderPaused
}
return errFolderNotRunning
@@ -2647,48 +2781,30 @@ func readOffsetIntoBuf(fs fs.Filesystem, file string, offset int64, buf []byte)
// The exists function is expected to return true for all known paths
// (excluding "" and ".")
func unifySubs(dirs []string, exists func(dir string) bool) []string {
subs := trimUntilParentKnown(dirs, exists)
sort.Strings(subs)
return simplifySortedPaths(subs)
}
func trimUntilParentKnown(dirs []string, exists func(dir string) bool) []string {
var subs []string
for _, sub := range dirs {
for sub != "" && !fs.IsInternal(sub) {
sub = filepath.Clean(sub)
parent := filepath.Dir(sub)
if parent == "." || exists(parent) {
break
}
sub = parent
if sub == "." || sub == string(filepath.Separator) {
// Shortcut. We are going to scan the full folder, so we can
// just return an empty list of subs at this point.
return nil
}
}
if sub == "" {
return nil
}
subs = append(subs, sub)
if len(dirs) == 0 {
return nil
}
return subs
}
func simplifySortedPaths(subs []string) []string {
var cleaned []string
next:
for _, sub := range subs {
for _, existing := range cleaned {
if sub == existing || strings.HasPrefix(sub, existing+string(fs.PathSeparator)) {
continue next
}
}
cleaned = append(cleaned, sub)
sort.Strings(dirs)
if dirs[0] == "" || dirs[0] == "." || dirs[0] == string(fs.PathSeparator) {
return nil
}
return cleaned
prev := "./" // Anything that can't be parent of a clean path
for i := 0; i < len(dirs); {
dir := filepath.Clean(dirs[i])
if dir == prev || strings.HasPrefix(dir, prev+string(fs.PathSeparator)) {
dirs = append(dirs[:i], dirs[i+1:]...)
continue
}
parent := filepath.Dir(dir)
for parent != "." && parent != string(fs.PathSeparator) && !exists(parent) {
dir = parent
parent = filepath.Dir(dir)
}
dirs[i] = dir
prev = dir
i++
}
return dirs
}
// makeForgetUpdate takes an index update and constructs a download progress update

View File

@@ -29,9 +29,11 @@ import (
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/fs"
"github.com/syncthing/syncthing/lib/ignore"
"github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/protocol"
srand "github.com/syncthing/syncthing/lib/rand"
"github.com/syncthing/syncthing/lib/scanner"
"github.com/syncthing/syncthing/lib/versioner"
)
var device1, device2 protocol.DeviceID
@@ -58,6 +60,9 @@ func init() {
defaultConfig = config.Wrap("/tmp/test", _defaultConfig)
defaultAutoAcceptCfg = config.Configuration{
Devices: []config.DeviceConfiguration{
{
DeviceID: protocol.LocalDeviceID, // self
},
{
DeviceID: device1,
AutoAcceptFolders: true,
@@ -108,7 +113,9 @@ func newState(cfg config.Configuration) (*config.Wrapper, *Model) {
m := NewModel(wcfg, protocol.LocalDeviceID, "syncthing", "dev", db, nil)
for _, folder := range cfg.Folders {
m.AddFolder(folder)
if !folder.Paused {
m.AddFolder(folder)
}
}
m.ServeBackground()
m.AddConnection(&fakeConnection{id: device1}, protocol.HelloResult{})
@@ -281,6 +288,10 @@ func (f *fakeConnection) Name() string {
return ""
}
func (f *fakeConnection) String() string {
return ""
}
func (f *fakeConnection) Option(string) string {
return ""
}
@@ -988,7 +999,9 @@ func TestIntroducer(t *testing.T) {
func TestAutoAcceptRejected(t *testing.T) {
// Nothing happens if AutoAcceptFolders not set
tcfg := defaultAutoAcceptCfg.Copy()
tcfg.Devices[0].AutoAcceptFolders = false
for i := range tcfg.Devices {
tcfg.Devices[i].AutoAcceptFolders = false
}
wcfg, m := newState(tcfg)
id := srand.String(8)
defer os.RemoveAll(filepath.Join("testdata", id))
@@ -1056,6 +1069,7 @@ func TestAutoAcceptExistingFolder(t *testing.T) {
id := srand.String(8)
idOther := srand.String(8) // To check that path does not get changed.
defer os.RemoveAll(filepath.Join("testdata", id))
defer os.RemoveAll(filepath.Join("testdata", idOther))
tcfg := defaultAutoAcceptCfg.Copy()
tcfg.Folders = []config.FolderConfiguration{
@@ -1216,6 +1230,115 @@ func TestAutoAcceptFallsBackToID(t *testing.T) {
}
}
func TestAutoAcceptPausedWhenFolderConfigChanged(t *testing.T) {
// Existing folder
id := srand.String(8)
idOther := srand.String(8) // To check that path does not get changed.
defer os.RemoveAll(filepath.Join("testdata", id))
defer os.RemoveAll(filepath.Join("testdata", idOther))
tcfg := defaultAutoAcceptCfg.Copy()
fcfg := config.NewFolderConfiguration(protocol.LocalDeviceID, id, "", fs.FilesystemTypeBasic, filepath.Join("testdata", idOther))
fcfg.Paused = true
// The order of devices here is wrong (cfg.clean() sorts them), which will cause the folder to restart.
// Because of the restart, folder gets removed from m.deviceFolder, which means that generateClusterConfig will not panic.
// This wasn't an issue before, yet keeping the test case to prove that it still isn't.
fcfg.Devices = append(fcfg.Devices, config.FolderDeviceConfiguration{
DeviceID: device1,
})
tcfg.Folders = []config.FolderConfiguration{fcfg}
wcfg, m := newState(tcfg)
if _, ok := wcfg.Folder(id); !ok || m.folderSharedWith(id, device1) {
t.Error("missing folder, or shared", id)
}
if _, ok := m.folderRunners[id]; ok {
t.Fatal("folder running?")
}
m.ClusterConfig(device1, protocol.ClusterConfig{
Folders: []protocol.Folder{
{
ID: id,
Label: id,
},
},
})
m.generateClusterConfig(device1)
if fcfg, ok := wcfg.Folder(id); !ok {
t.Error("missing folder")
} else if fcfg.Path != filepath.Join("testdata", idOther) {
t.Error("folder path changed")
} else {
for _, dev := range fcfg.DeviceIDs() {
if dev == device1 {
return
}
}
t.Error("device missing")
}
if _, ok := m.folderDevices[id]; ok {
t.Error("folder started")
}
}
func TestAutoAcceptPausedWhenFolderConfigNotChanged(t *testing.T) {
// Existing folder
id := srand.String(8)
idOther := srand.String(8) // To check that path does not get changed.
defer os.RemoveAll(filepath.Join("testdata", id))
defer os.RemoveAll(filepath.Join("testdata", idOther))
tcfg := defaultAutoAcceptCfg.Copy()
fcfg := config.NewFolderConfiguration(protocol.LocalDeviceID, id, "", fs.FilesystemTypeBasic, filepath.Join("testdata", idOther))
fcfg.Paused = true
// The new folder is exactly the same as the one constructed by handleAutoAccept, which means
// the folder will not be restarted (even if it's paused), yet handleAutoAccept used to add the folder
// to m.deviceFolders which had caused panics when calling generateClusterConfig, as the folder
// did not have a file set.
fcfg.Devices = append([]config.FolderDeviceConfiguration{
{
DeviceID: device1,
},
}, fcfg.Devices...) // Need to ensure this device order to avoid folder restart.
tcfg.Folders = []config.FolderConfiguration{fcfg}
wcfg, m := newState(tcfg)
if _, ok := wcfg.Folder(id); !ok || m.folderSharedWith(id, device1) {
t.Error("missing folder, or shared", id)
}
if _, ok := m.folderRunners[id]; ok {
t.Fatal("folder running?")
}
m.ClusterConfig(device1, protocol.ClusterConfig{
Folders: []protocol.Folder{
{
ID: id,
Label: id,
},
},
})
m.generateClusterConfig(device1)
if fcfg, ok := wcfg.Folder(id); !ok {
t.Error("missing folder")
} else if fcfg.Path != filepath.Join("testdata", idOther) {
t.Error("folder path changed")
} else {
for _, dev := range fcfg.DeviceIDs() {
if dev == device1 {
return
}
}
t.Error("device missing")
}
if _, ok := m.folderDevices[id]; ok {
t.Error("folder started")
}
}
func changeIgnores(t *testing.T, m *Model, expected []string) {
arrEqual := func(a, b []string) bool {
if len(a) != len(b) {
@@ -1964,12 +2087,14 @@ func benchmarkTree(b *testing.B, n1, n2 int) {
b.ReportAllocs()
}
func TestUnifySubs(t *testing.T) {
cases := []struct {
in []string // input to unifySubs
exists []string // paths that exist in the database
out []string // expected output
}{
type unifySubsCase struct {
in []string // input to unifySubs
exists []string // paths that exist in the database
out []string // expected output
}
func unifySubsCases() []unifySubsCase {
cases := []unifySubsCase{
{
// 0. trailing slashes are cleaned, known paths are just passed on
[]string{"foo/", "bar//"},
@@ -2051,16 +2176,24 @@ func TestUnifySubs(t *testing.T) {
}
}
return cases
}
func unifyExists(f string, tc unifySubsCase) bool {
for _, e := range tc.exists {
if f == e {
return true
}
}
return false
}
func TestUnifySubs(t *testing.T) {
cases := unifySubsCases()
for i, tc := range cases {
exists := func(f string) bool {
for _, e := range tc.exists {
if f == e {
return true
}
}
return false
return unifyExists(f, tc)
}
out := unifySubs(tc.in, exists)
if diff, equal := messagediff.PrettyDiff(tc.out, out); !equal {
t.Errorf("Case %d failed; got %v, expected %v, diff:\n%s", i, out, tc.out, diff)
@@ -2068,6 +2201,20 @@ func TestUnifySubs(t *testing.T) {
}
}
func BenchmarkUnifySubs(b *testing.B) {
cases := unifySubsCases()
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
for _, tc := range cases {
exists := func(f string) bool {
return unifyExists(f, tc)
}
unifySubs(tc.in, exists)
}
}
}
func TestIssue3028(t *testing.T) {
// Create two files that we'll delete, one with a name that is a prefix of the other.
@@ -2871,6 +3018,217 @@ func TestIssue4475(t *testing.T) {
}
}
func TestVersionRestore(t *testing.T) {
// We create a bunch of files which we restore
// In each file, we write the filename as the content
// We verify that the content matches at the expected filenames
// after the restore operation.
dir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
dbi := db.OpenMemory()
fcfg := config.NewFolderConfiguration(protocol.LocalDeviceID, "default", "default", fs.FilesystemTypeBasic, dir)
fcfg.Versioning.Type = "simple"
fcfg.FSWatcherEnabled = false
filesystem := fcfg.Filesystem()
rawConfig := config.Configuration{
Folders: []config.FolderConfiguration{fcfg},
}
cfg := config.Wrap("/tmp/test", rawConfig)
m := NewModel(cfg, protocol.LocalDeviceID, "syncthing", "dev", dbi, nil)
m.AddFolder(fcfg)
m.StartFolder("default")
m.ServeBackground()
defer m.Stop()
m.ScanFolder("default")
sentinel, err := time.ParseInLocation(versioner.TimeFormat, "20200101-010101", locationLocal)
if err != nil {
t.Fatal(err)
}
sentinelTag := sentinel.Format(versioner.TimeFormat)
for _, file := range []string{
// Versions directory
".stversions/file~20171210-040404.txt", // will be restored
".stversions/existing~20171210-040404", // exists, should expect to be archived.
".stversions/something~20171210-040404", // will become directory, hence error
".stversions/dir/file~20171210-040404.txt",
".stversions/dir/file~20171210-040405.txt",
".stversions/dir/file~20171210-040406.txt",
".stversions/very/very/deep/one~20171210-040406.txt", // lives deep down, no directory exists.
".stversions/dir/existing~20171210-040406.txt", // exists, should expect to be archived.
".stversions/dir/file.txt~20171210-040405", // incorrect tag format, ignored.
".stversions/dir/cat", // incorrect tag format, ignored.
// "file.txt" will be restored
"existing",
"something/file", // Becomes directory
"dir/file.txt",
"dir/existing.txt",
} {
if runtime.GOOS == "windows" {
file = filepath.FromSlash(file)
}
dir := filepath.Dir(file)
if err := filesystem.MkdirAll(dir, 0755); err != nil {
t.Fatal(err)
}
if fd, err := filesystem.Create(file); err != nil {
t.Fatal(err)
} else if _, err := fd.Write([]byte(file)); err != nil {
t.Fatal(err)
} else if err := fd.Close(); err != nil {
t.Fatal(err)
} else if err := filesystem.Chtimes(file, sentinel, sentinel); err != nil {
t.Fatal(err)
}
}
versions, err := m.GetFolderVersions("default")
if err != nil {
t.Fatal(err)
}
expectedVersions := map[string]int{
"file.txt": 1,
"existing": 1,
"something": 1,
"dir/file.txt": 3,
"dir/existing.txt": 1,
"very/very/deep/one.txt": 1,
}
for name, vers := range versions {
cnt, ok := expectedVersions[name]
if !ok {
t.Errorf("unexpected %s", name)
}
if len(vers) != cnt {
t.Errorf("%s: %d != %d", name, cnt, len(vers))
}
// Delete, so we can check if we didn't hit something we expect afterwards.
delete(expectedVersions, name)
}
for name := range expectedVersions {
t.Errorf("not found expected %s", name)
}
// Restoring non existing folder fails.
_, err = m.RestoreFolderVersions("does not exist", nil)
if err == nil {
t.Errorf("expected an error")
}
makeTime := func(s string) time.Time {
tm, err := time.ParseInLocation(versioner.TimeFormat, s, locationLocal)
if err != nil {
t.Error(err)
}
return tm.Truncate(time.Second)
}
restore := map[string]time.Time{
"file.txt": makeTime("20171210-040404"),
"existing": makeTime("20171210-040404"),
"something": makeTime("20171210-040404"),
"dir/file.txt": makeTime("20171210-040406"),
"dir/existing.txt": makeTime("20171210-040406"),
"very/very/deep/one.txt": makeTime("20171210-040406"),
}
ferr, err := m.RestoreFolderVersions("default", restore)
if err != nil {
t.Fatal(err)
}
if err, ok := ferr["something"]; len(ferr) > 1 || !ok || err != "cannot replace a non-file" {
t.Fatalf("incorrect error or count: %d %s", len(ferr), ferr)
}
// Failed items are not expected to be restored.
// Remove them from expectations
for name := range ferr {
delete(restore, name)
}
// Check that content of files matches to the version they've been restored.
for file, version := range restore {
if runtime.GOOS == "windows" {
file = filepath.FromSlash(file)
}
tag := version.In(locationLocal).Truncate(time.Second).Format(versioner.TimeFormat)
taggedName := filepath.Join(".stversions", versioner.TagFilename(file, tag))
fd, err := filesystem.Open(file)
if err != nil {
t.Error(err)
}
defer fd.Close()
content, err := ioutil.ReadAll(fd)
if err != nil {
t.Error(err)
}
if !bytes.Equal(content, []byte(taggedName)) {
t.Errorf("%s: %s != %s", file, string(content), taggedName)
}
}
// Simple versioner uses modtime for timestamp generation, so we can check
// if existing stuff was correctly archived as we restored.
expectArchived := map[string]struct{}{
"existing": {},
"dir/file.txt": {},
"dir/existing.txt": {},
}
// Even if they are at the archived path, content should have the non
// archived name.
for file := range expectArchived {
if runtime.GOOS == "windows" {
file = filepath.FromSlash(file)
}
taggedName := versioner.TagFilename(file, sentinelTag)
taggedArchivedName := filepath.Join(".stversions", taggedName)
fd, err := filesystem.Open(taggedArchivedName)
if err != nil {
t.Fatal(err)
}
defer fd.Close()
content, err := ioutil.ReadAll(fd)
if err != nil {
t.Error(err)
}
if !bytes.Equal(content, []byte(file)) {
t.Errorf("%s: %s != %s", file, string(content), file)
}
}
// Check for other unexpected things that are tagged.
filesystem.Walk(".", func(path string, f fs.FileInfo, err error) error {
if !f.IsRegular() {
return nil
}
if strings.Contains(path, sentinelTag) {
path = osutil.NormalizedFilename(path)
name, _ := versioner.UntagFilename(path)
name = strings.TrimPrefix(name, ".stversions/")
if _, ok := expectArchived[name]; !ok {
t.Errorf("unexpected file with sentinel tag: %s", name)
}
}
return nil
})
}
func TestPausedFolders(t *testing.T) {
// Create a separate wrapper not to pollute other tests.
cfg := defaultConfig.RawCopy()
@@ -2895,7 +3253,7 @@ func TestPausedFolders(t *testing.T) {
}
w.Wait()
if err := m.ScanFolder("default"); err != errFolderPaused {
if err := m.ScanFolder("default"); err != ErrFolderPaused {
t.Errorf("Expected folder paused error, received: %v", err)
}

View File

@@ -66,3 +66,7 @@ func (f *sendOnlyFolder) Serve() {
func (f *sendOnlyFolder) String() string {
return fmt.Sprintf("sendOnlyFolder/%s@%p", f.folderID, f)
}
func (f *sendOnlyFolder) PullErrors() []FileError {
return nil
}

View File

@@ -7,6 +7,7 @@
package model
import (
"bytes"
"errors"
"fmt"
"math/rand"
@@ -24,6 +25,7 @@ import (
"github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/scanner"
"github.com/syncthing/syncthing/lib/sha256"
"github.com/syncthing/syncthing/lib/sync"
"github.com/syncthing/syncthing/lib/versioner"
"github.com/syncthing/syncthing/lib/weakhash"
@@ -284,7 +286,7 @@ func (f *sendReceiveFolder) pull(prevIgnoreHash string) (curIgnoreHash string, s
// we're not making it. Probably there are write
// errors preventing us. Flag this with a warning and
// wait a bit longer before retrying.
if folderErrors := f.currentErrors(); len(folderErrors) > 0 {
if folderErrors := f.PullErrors(); len(folderErrors) > 0 {
events.Default.Log(events.FolderErrors, map[string]interface{}{
"folder": f.folderID,
"errors": folderErrors,
@@ -560,7 +562,7 @@ nextFile:
// we can just do a rename instead.
key := string(fi.Blocks[0].Hash)
for i, candidate := range buckets[key] {
if scanner.BlocksEqual(candidate.Blocks, fi.Blocks) {
if blocksEqual(candidate.Blocks, fi.Blocks) {
// Remove the candidate from the bucket
lidx := len(buckets[key]) - 1
buckets[key][i] = buckets[key][lidx]
@@ -615,6 +617,21 @@ nextFile:
return changed
}
// blocksEqual returns whether two slices of blocks are exactly the same hash
// and index pair wise.
func blocksEqual(src, tgt []protocol.BlockInfo) bool {
if len(tgt) != len(src) {
return false
}
for i, sblk := range src {
if !bytes.Equal(sblk.Hash, tgt[i].Hash) {
return false
}
}
return true
}
// handleDir creates or updates the given directory
func (f *sendReceiveFolder) handleDir(file protocol.FileInfo, dbUpdateChan chan<- dbUpdateJob) {
// Used in the defer closure below, updated by the function body. Take
@@ -976,7 +993,7 @@ func (f *sendReceiveFolder) renameFile(source, target protocol.FileInfo, dbUpdat
func (f *sendReceiveFolder) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocksState, finisherChan chan<- *sharedPullerState, dbUpdateChan chan<- dbUpdateJob) {
curFile, hasCurFile := f.model.CurrentFolderFile(f.folderID, file.Name)
have, need := scanner.BlockDiff(curFile.Blocks, file.Blocks)
have, need := blockDiff(curFile.Blocks, file.Blocks)
if hasCurFile && len(need) == 0 {
// We are supposed to copy the entire file, and then fetch nothing. We
@@ -1013,18 +1030,18 @@ func (f *sendReceiveFolder) handleFile(file protocol.FileInfo, copyChan chan<- c
tempName := fs.TempName(file.Name)
scanner.PopulateOffsets(file.Blocks)
populateOffsets(file.Blocks)
var blocks []protocol.BlockInfo
blocks := make([]protocol.BlockInfo, 0, len(file.Blocks))
var blocksSize int64
var reused []int32
reused := make([]int32, 0, len(file.Blocks))
// Check for an old temporary file which might have some blocks we could
// reuse.
tempBlocks, err := scanner.HashFile(f.ctx, f.fs, tempName, protocol.BlockSize, nil, false)
if err == nil {
// Check for any reusable blocks in the temp file
tempCopyBlocks, _ := scanner.BlockDiff(tempBlocks, file.Blocks)
tempCopyBlocks, _ := blockDiff(tempBlocks, file.Blocks)
// block.String() returns a string unique to the block
existingBlocks := make(map[string]struct{}, len(tempCopyBlocks))
@@ -1108,6 +1125,45 @@ func (f *sendReceiveFolder) handleFile(file protocol.FileInfo, copyChan chan<- c
copyChan <- cs
}
// blockDiff returns lists of common and missing (to transform src into tgt)
// blocks. Both block lists must have been created with the same block size.
func blockDiff(src, tgt []protocol.BlockInfo) ([]protocol.BlockInfo, []protocol.BlockInfo) {
if len(tgt) == 0 {
return nil, nil
}
if len(src) == 0 {
// Copy the entire file
return nil, tgt
}
have := make([]protocol.BlockInfo, 0, len(src))
need := make([]protocol.BlockInfo, 0, len(tgt))
for i := range tgt {
if i >= len(src) {
return have, append(need, tgt[i:]...)
}
if !bytes.Equal(tgt[i].Hash, src[i].Hash) {
// Copy differing block
need = append(need, tgt[i])
} else {
have = append(have, tgt[i])
}
}
return have, need
}
// populateOffsets sets the Offset field on each block
func populateOffsets(blocks []protocol.BlockInfo) {
var offset int64
for i := range blocks {
blocks[i].Offset = offset
offset += int64(blocks[i].Size)
}
}
// shortcutFile sets file mode and modification time, when that's the only
// thing that has changed.
func (f *sendReceiveFolder) shortcutFile(file protocol.FileInfo) error {
@@ -1200,13 +1256,14 @@ func (f *sendReceiveFolder) copierRoutine(in <-chan copyBlocksState, pullChan ch
// Pretend we copied it.
state.copiedFromOrigin()
state.copyDone(block)
continue
}
buf = buf[:int(block.Size)]
found, err := weakHashFinder.Iterate(block.WeakHash, buf, func(offset int64) bool {
if _, err := scanner.VerifyBuffer(buf, block); err != nil {
if _, err := verifyBuffer(buf, block); err != nil {
return true
}
@@ -1241,7 +1298,7 @@ func (f *sendReceiveFolder) copierRoutine(in <-chan copyBlocksState, pullChan ch
return false
}
hash, err := scanner.VerifyBuffer(buf, block)
hash, err := verifyBuffer(buf, block)
if err != nil {
if hash != nil {
l.Debugf("Finder block mismatch in %s:%s:%d expected %q got %q", folder, path, index, block.Hash, hash)
@@ -1291,6 +1348,24 @@ func (f *sendReceiveFolder) copierRoutine(in <-chan copyBlocksState, pullChan ch
}
}
func verifyBuffer(buf []byte, block protocol.BlockInfo) ([]byte, error) {
if len(buf) != int(block.Size) {
return nil, fmt.Errorf("length mismatch %d != %d", len(buf), block.Size)
}
hf := sha256.New()
_, err := hf.Write(buf)
if err != nil {
return nil, err
}
hash := hf.Sum(nil)
if !bytes.Equal(hash, block.Hash) {
return hash, fmt.Errorf("hash mismatch %x != %x", hash, block.Hash)
}
return hash, nil
}
func (f *sendReceiveFolder) pullerRoutine(in <-chan pullBlockState, out chan<- *sharedPullerState) {
for state := range in {
if state.failed() != nil {
@@ -1345,7 +1420,7 @@ func (f *sendReceiveFolder) pullerRoutine(in <-chan pullBlockState, out chan<- *
// Verify that the received block matches the desired hash, if not
// try pulling it from another device.
_, lastError = scanner.VerifyBuffer(buf, state.block)
_, lastError = verifyBuffer(buf, state.block)
if lastError != nil {
l.Debugln("request:", f.folderID, state.file.Name, state.block.Offset, state.block.Size, "hash mismatch")
continue
@@ -1451,7 +1526,7 @@ func (f *sendReceiveFolder) performFinish(ignores *ignore.Matcher, state *shared
// file before we replace it. Archiving a non-existent file is not
// an error.
if err = f.versioner.Archive(state.file.Name); err != nil {
if err = osutil.InWritableDir(f.versioner.Archive, f.fs, state.file.Name); err != nil {
return err
}
}
@@ -1728,11 +1803,11 @@ func (f *sendReceiveFolder) clearErrors() {
f.errorsMut.Unlock()
}
func (f *sendReceiveFolder) currentErrors() []fileError {
func (f *sendReceiveFolder) PullErrors() []FileError {
f.errorsMut.Lock()
errors := make([]fileError, 0, len(f.errors))
errors := make([]FileError, 0, len(f.errors))
for path, err := range f.errors {
errors = append(errors, fileError{path, err})
errors = append(errors, FileError{path, err})
}
sort.Sort(fileErrorList(errors))
f.errorsMut.Unlock()
@@ -1811,13 +1886,13 @@ func (f *sendReceiveFolder) deleteDir(dir string, ignores *ignore.Matcher, scanC
return err
}
// A []fileError is sent as part of an event and will be JSON serialized.
type fileError struct {
// A []FileError is sent as part of an event and will be JSON serialized.
type FileError struct {
Path string `json:"path"`
Err string `json:"error"`
}
type fileErrorList []fileError
type fileErrorList []FileError
func (l fileErrorList) Len() int {
return len(l)

View File

@@ -58,6 +58,26 @@ var blocks = []protocol.BlockInfo{
var folders = []string{"default"}
var diffTestData = []struct {
a string
b string
s int
d []protocol.BlockInfo
}{
{"contents", "contents", 1024, []protocol.BlockInfo{}},
{"", "", 1024, []protocol.BlockInfo{}},
{"contents", "contents", 3, []protocol.BlockInfo{}},
{"contents", "cantents", 3, []protocol.BlockInfo{{Offset: 0, Size: 3}}},
{"contents", "contants", 3, []protocol.BlockInfo{{Offset: 3, Size: 3}}},
{"contents", "cantants", 3, []protocol.BlockInfo{{Offset: 0, Size: 3}, {Offset: 3, Size: 3}}},
{"contents", "", 3, []protocol.BlockInfo{{Offset: 0, Size: 0}}},
{"", "contents", 3, []protocol.BlockInfo{{Offset: 0, Size: 3}, {Offset: 3, Size: 3}, {Offset: 6, Size: 2}}},
{"con", "contents", 3, []protocol.BlockInfo{{Offset: 3, Size: 3}, {Offset: 6, Size: 2}}},
{"contents", "con", 3, nil},
{"contents", "cont", 3, []protocol.BlockInfo{{Offset: 3, Size: 1}}},
{"cont", "contents", 3, []protocol.BlockInfo{{Offset: 3, Size: 3}, {Offset: 6, Size: 2}}},
}
func setUpFile(filename string, blockNumbers []int) protocol.FileInfo {
// Create existing file
existingBlocks := make([]protocol.BlockInfo, len(blockNumbers))
@@ -651,3 +671,61 @@ func TestIssue3164(t *testing.T) {
t.Fatal(err)
}
}
func TestDiff(t *testing.T) {
for i, test := range diffTestData {
a, _ := scanner.Blocks(context.TODO(), bytes.NewBufferString(test.a), test.s, -1, nil, false)
b, _ := scanner.Blocks(context.TODO(), bytes.NewBufferString(test.b), test.s, -1, nil, false)
_, d := blockDiff(a, b)
if len(d) != len(test.d) {
t.Fatalf("Incorrect length for diff %d; %d != %d", i, len(d), len(test.d))
} else {
for j := range test.d {
if d[j].Offset != test.d[j].Offset {
t.Errorf("Incorrect offset for diff %d block %d; %d != %d", i, j, d[j].Offset, test.d[j].Offset)
}
if d[j].Size != test.d[j].Size {
t.Errorf("Incorrect length for diff %d block %d; %d != %d", i, j, d[j].Size, test.d[j].Size)
}
}
}
}
}
func BenchmarkDiff(b *testing.B) {
testCases := make([]struct{ a, b []protocol.BlockInfo }, 0, len(diffTestData))
for _, test := range diffTestData {
a, _ := scanner.Blocks(context.TODO(), bytes.NewBufferString(test.a), test.s, -1, nil, false)
b, _ := scanner.Blocks(context.TODO(), bytes.NewBufferString(test.b), test.s, -1, nil, false)
testCases = append(testCases, struct{ a, b []protocol.BlockInfo }{a, b})
}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
for _, tc := range testCases {
blockDiff(tc.a, tc.b)
}
}
}
func TestDiffEmpty(t *testing.T) {
emptyCases := []struct {
a []protocol.BlockInfo
b []protocol.BlockInfo
need int
have int
}{
{nil, nil, 0, 0},
{[]protocol.BlockInfo{{Offset: 3, Size: 1}}, nil, 0, 0},
{nil, []protocol.BlockInfo{{Offset: 3, Size: 1}}, 1, 0},
}
for _, emptyCase := range emptyCases {
h, n := blockDiff(emptyCase.a, emptyCase.b)
if len(h) != emptyCase.have {
t.Errorf("incorrect have: %d != %d", len(h), emptyCase.have)
}
if len(n) != emptyCase.need {
t.Errorf("incorrect have: %d != %d", len(h), emptyCase.have)
}
}
}

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