Compare commits

...

84 Commits

Author SHA1 Message Date
Simon Frei
0f532a5607 lib/db: Don't get blocklists on drop and missing continue (ref #6457) (#6502) 2020-04-07 13:14:03 +02:00
Jakob Borg
0275cbd66a Revert "cmd/syncthing: Do auto-upgrade before startup (fixes #6384) (#6385)"
This reverts commit c101a04179.
2020-04-07 12:55:25 +02:00
Jakob Borg
670a9809fa lib/ur: Correct freaky error handling (fixes #6499) (#6500) 2020-04-07 12:54:06 +02:00
Jakob Borg
f1b253fc00 lib/db: Don't whack blocks when putting truncated file (#6434)
As of the latest database checker we are again putting files without
blocks. I'm not 100% convinced that's a great idea, but we also do it
for ignored files apparently so it looks like we probably should support
it. This adds an escape hatch that must be manually enabled...
2020-03-20 12:07:29 +01:00
Simon Frei
b33d5e57c6 lib/db, lib/syncthing: Repair db once on upgrade (ref #6425, #6427) (#6429) 2020-03-19 16:00:05 +01:00
Simon Frei
0060840249 lib/db: Fix removeFromGlobal and no filenames in error (fixes #6427) (#6428) 2020-03-19 16:00:05 +01:00
Simon Frei
71faae67f2 lib/db: Remove emptied global list in checkGlobals (fixes #6425) (#6426) 2020-03-19 15:59:49 +01:00
Simon Frei
00b2340f9a lib/db: Checkpoint during schema updates (fixes #6422) (#6424) 2020-03-18 20:33:43 +01:00
Simon Frei
cc2a55892f lib: Repair sequence inconsistencies (#6367) 2020-03-18 17:34:46 +01:00
Jakob Borg
80107d5f5e lib/config: Correct spelling of address in LDAP config (#6420)
Literally noone uses this so I don't see a need to call this out or
trigger a 1.5 release for it.
2020-03-18 10:44:00 +00:00
Mario Majila
f10e85d0c2 gui: Display folder name in modal (fixes #5380) (#6407) 2020-03-18 11:13:58 +01:00
Kevin Bushiri
f4a6e4439a gui: Add folder name for restore version modal (fixes #5380) (#6418) 2020-03-18 11:11:40 +01:00
Jakob Borg
2ae3ea0d52 gui, man, authors: Update docs, translations, and contributors 2020-03-18 07:45:30 +01:00
Alex Xu
1e68ab3f90 lib/beacon: Only send to appropriately flagged interfaces (ref #5957) (#6404)
saves some traffic (potential mobile wakeups), may help with #5957.
2020-03-17 09:40:37 +01:00
Simon Mwepu
f08d09f607 gui: Display device name in modal (fixes #5380) (#6405) 2020-03-17 08:23:45 +01:00
Jakob Borg
e053db6a5e lib/protocol: Zero pad index ID strings 2020-03-17 07:40:52 +01:00
Simon Frei
1bd4ea0cbb lib/db: Don't ignore failures unmarshaling version lists (#6411) 2020-03-16 10:09:27 +01:00
Simon Frei
a1cb1d70c4 lib/db: Use need func in withNeed and simplify (#6412) 2020-03-16 08:45:45 +01:00
Simon Frei
c101a04179 cmd/syncthing: Do auto-upgrade before startup (fixes #6384) (#6385) 2020-03-16 08:12:32 +01:00
Simon Frei
16698b12b1 lib/db: Extend set test with second remote (#6402) 2020-03-11 08:15:45 +01:00
Jakob Borg
0bc571b2fd gui, man, authors: Update docs, translations, and contributors 2020-03-11 07:45:28 +01:00
Jakob Borg
20aaa5927b lib/protocol: Use BlocksHash to compare block lists when available (#6401)
This is an optimization for faster equal checks on block lists.
2020-03-10 14:46:49 +01:00
Jakob Borg
d612c35290 lib/api: Ignore that one file that always shows up in git status 2020-03-07 11:46:54 +01:00
Jakob Borg
5ab257fb60 Merge branch 'release'
* release:
  lib/db: Be more lenient during migration (fixes #6397) (#6398)
2020-03-06 20:53:55 +01:00
Jakob Borg
db02545ef3 lib/db: Be more lenient during migration (fixes #6397) (#6398) 2020-03-06 20:52:22 +01:00
Jakob Borg
2faa1ad360 lib/db: Be more lenient during migration (fixes #6397) (#6398) 2020-03-06 20:50:55 +01:00
Jakob Borg
860ae7f395 cmd/ursrv: Analytics for Synology dist 2020-03-06 07:46:11 +01:00
Jakob Borg
135c71ca87 build: Build image should use Go 1.13 for now 2020-03-05 11:53:07 +01:00
Jakob Borg
c7d6a6d780 gui, lib/api: Remove CPU & RAM measurements (fixes #6249) (#6393) 2020-03-04 20:27:48 +01:00
Jakob Borg
92533dd9f0 gui, man, authors: Update docs, translations, and contributors 2020-03-04 07:45:31 +01:00
Jakob Borg
dd92b2b8f4 all: Tweak error creation (#6391)
- In the few places where we wrap errors, use the new Go 1.13 "%w"
  construction instead of %s or %v.

- Where we create errors with constant strings, consistently use
  errors.New and not fmt.Errorf.

- Remove capitalization from errors in the few places where we had that.
2020-03-03 22:40:00 +01:00
Jakob Borg
eddc8d3ff2 authors: Cleanup on request 2020-03-02 16:31:29 +01:00
Jakob Borg
dfdd5af7a6 build: We can now use Go 1.13 2020-03-01 12:59:49 +01:00
Jakob Borg
6b5c281dd5 Merge branch 'release'
* release:
  lib/db: Prevent GC concurrently with migration (fixes #6389) (#6390)
  build: Fix syso creation (fixes #6386) (#6387)
2020-02-29 19:58:49 +01:00
Jakob Borg
52e72e0122 lib/db: Prevent GC concurrently with migration (fixes #6389) (#6390) 2020-02-29 19:51:48 +01:00
Jakob Borg
c08e253e7c lib/db: Prevent GC concurrently with migration (fixes #6389) (#6390) 2020-02-29 19:51:32 +01:00
Evgeny Kuznetsov
d1e0a38c04 build: Fix syso creation (fixes #6386) (#6387) 2020-02-29 19:48:42 +01:00
Evgeny Kuznetsov
ac19cdb2cd build: Fix syso creation (fixes #6386) (#6387) 2020-02-28 20:40:14 +01:00
Jakob Borg
58607486af Merge branch 'release'
* release:
  lib/db: Correct metadata recalculation (fixes #6381) (#6382)
2020-02-28 11:21:51 +01:00
Jakob Borg
0b610017ea lib/db: Correct metadata recalculation (fixes #6381) (#6382)
If we decide to recalculate the metadata we shouldn't start from
whatever we loaded from the database, as that data is wrong. We should
start from a clean slate.
2020-02-28 11:17:02 +01:00
Jakob Borg
5de6f6d349 lib/db: Correct metadata recalculation (fixes #6381) (#6382)
If we decide to recalculate the metadata we shouldn't start from
whatever we loaded from the database, as that data is wrong. We should
start from a clean slate.
2020-02-28 11:16:33 +01:00
Jakob Borg
daf05c6509 Merge branch 'release'
* release:
  lib/db: Remove reference to env var that never existed
  lib/db: Slightly improve indirection (ref #6372) (#6373)
2020-02-27 11:24:18 +01:00
Jakob Borg
9a1df97c69 lib/db: Remove reference to env var that never existed 2020-02-27 11:22:09 +01:00
Jakob Borg
ee61da5b6a lib/db: Slightly improve indirection (ref #6372) (#6373)
I was working on indirecting version vectors, and that resulted in some
refactoring and improving the existing block indirection stuff. We may
or may not end up doing the version vector indirection, but I think
these changes are reasonable anyhow and will simplify the diff
significantly if we do go there. The main points are:

- A bunch of renaming to make the indirection and GC not about "blocks"
  but about "indirection".

- Adding a cutoff so that we don't actually indirect for small block
  lists. This gets us better performance when handling small files as it
  cuts out the indirection for quite small loss in space efficiency.

- Being paranoid and always recalculating the hash on put. This costs
  some CPU, but the consequences if a buggy or malicious implementation
  silently substituted the block list by lying about the hash would be bad.
2020-02-27 11:22:01 +01:00
Jakob Borg
883497966e lib/db: Remove reference to env var that never existed 2020-02-27 11:21:35 +01:00
Jakob Borg
4f7a77597e lib/db: Slightly improve indirection (ref #6372) (#6373)
I was working on indirecting version vectors, and that resulted in some
refactoring and improving the existing block indirection stuff. We may
or may not end up doing the version vector indirection, but I think
these changes are reasonable anyhow and will simplify the diff
significantly if we do go there. The main points are:

- A bunch of renaming to make the indirection and GC not about "blocks"
  but about "indirection".

- Adding a cutoff so that we don't actually indirect for small block
  lists. This gets us better performance when handling small files as it
  cuts out the indirection for quite small loss in space efficiency.

- Being paranoid and always recalculating the hash on put. This costs
  some CPU, but the consequences if a buggy or malicious implementation
  silently substituted the block list by lying about the hash would be bad.
2020-02-27 11:19:21 +01:00
Jakob Borg
c4b9046eaa build: Forked github.com/spaolacci/murmur3 for unsafe (ref #6371) 2020-02-26 20:25:24 +01:00
Simon Frei
299a80d328 cmd/syncthing: Do not truncate/rotate logs at start (#6359) 2020-02-26 13:49:03 +01:00
Jakob Borg
4e4b9a872a lib/dialer: Preserve nilness in error handling (fixes #6368) (#6369)
Also the call site where it shouldn't anyway be looking at the conn when
the err is non-nil.
2020-02-26 13:16:18 +01:00
Simon Frei
cb624dbf5d cmd/syncthing: Add indication that reset db happened (#6364) 2020-02-26 12:38:43 +01:00
Audrius Butkevicius
71aecc5cd4 lib/dialer: Bring back address faking connection (fixes #6289) (#6363) 2020-02-26 12:37:23 +01:00
Jakob Borg
10af09e4b4 gui, man, authors: Update docs, translations, and contributors 2020-02-26 07:45:29 +01:00
Simon Frei
680b0b14db lib/connections: Refactor status for testing (ref #6361) (#6362) 2020-02-25 21:18:31 +01:00
Jakob Borg
55238e3b5b lib/connections: Actually record connection errors (#6361) 2020-02-25 16:56:24 +01:00
Simon Frei
f0e33d052a lib: More contextification (#6343) 2020-02-24 21:57:15 +01:00
Jakob Borg
7b8622c2e9 build: Simplify build image for snaps 2020-02-23 08:40:42 +01:00
Jakob Borg
40e1835927 Merge branch 'release'
* release:
  lib/db: Allow put partial FileInfo without blocks (ref #6353)
  lib/db: Don't panic on incorrect BlocksHash (fixes #6353) (#6355)
2020-02-22 19:17:38 +01:00
Jakob Borg
a5e12a0a3d lib/db: Allow put partial FileInfo without blocks (ref #6353) 2020-02-22 17:49:23 +01:00
Jakob Borg
10cb14fcb8 lib/db: Allow put partial FileInfo without blocks (ref #6353) 2020-02-22 17:44:34 +01:00
Simon Frei
4f29180e7c lib/db: Don't panic on incorrect BlocksHash (fixes #6353) (#6355) 2020-02-22 16:52:34 +01:00
Simon Frei
32e12abb43 lib/db: Don't panic on incorrect BlocksHash (fixes #6353) (#6355) 2020-02-22 16:51:23 +01:00
Jakob Borg
4cc1b7f42c Merge branch 'release'
* release:
  lib/db: Schema update to repair sequence index (ref #6304) (#6350)
  lib: Modify FileInfos consistently (ref #6321) (#6349)
2020-02-22 09:40:27 +01:00
Simon Frei
0fb2cd52ff lib/db: Schema update to repair sequence index (ref #6304) (#6350) 2020-02-22 09:37:21 +01:00
Simon Frei
6489feb1d7 lib/db: Schema update to repair sequence index (ref #6304) (#6350) 2020-02-22 09:36:59 +01:00
Simon Frei
a4bd4d118a lib: Modify FileInfos consistently (ref #6321) (#6349) 2020-02-22 09:31:26 +01:00
Simon Frei
fae7425bbf lib: Modify FileInfos consistently (ref #6321) (#6349) 2020-02-19 16:58:09 +01:00
Jakob Borg
7b5551248a gui, man, authors: Update docs, translations, and contributors 2020-02-19 07:45:29 +01:00
Tyler Kropp
4026625c2d lib/config, gui: Set unix socket permissions for GUI listen address (fixes #5979) (#6310) 2020-02-18 08:52:12 +01:00
Jakob Borg
3e0241ea31 build: Dockerfile for the builder image 2020-02-14 10:44:31 +01:00
Jakob Borg
bb375b1aff lib/model: Stop summary sender faster (ref #6319) (#6341)
One of the causes of "panic: database is closed" is that we try to send
summaries after it's been closed. Calculating summaries can take a long
time and if we have a lot of folders it's not unreasonable to think
that we might be stopped in this loop, so prepare to bail here.

* push
2020-02-14 08:11:54 +01:00
Simon Frei
05e23f1991 lib/db: Don't call backend.Commit twice (ref #6337) (#6342) 2020-02-14 08:11:24 +01:00
Jakob Borg
71de6fe290 lib/upnp: Exit quicker (#6339)
During NAT discovery we block for 10s (NatTimeoutS) before returning.
This is mostly noticeable when Ctrl-C:ing a Syncthing directly after
startup as we wait for those ten seconds before shutting down. This
makes it check the context a little bit more frequently.
2020-02-13 15:39:36 +01:00
Jakob Borg
6a840a040b lib/db: Keep metadata better in sync (ref #6335) (#6337)
This adds metadata updates to the same write batch as the underlying
file change. The odds of a metadata update going missing is greatly
reduced.

Bonus change: actually commit the transaction in recalcMeta.
2020-02-13 15:23:08 +01:00
Simon Frei
c3637f2191 lib: Faster termination on exit (ref #6319) (#6329) 2020-02-13 14:43:00 +01:00
Simon Frei
ca90f4e6af lib/db: Use flags from arg not LocalFlags() updating meta (#6333) 2020-02-13 14:02:30 +01:00
Jakob Borg
51fa36d61f lib/db: Recover sequence number and metadata on startup (fixes #6335) (#6336)
lib/db: Recover sequence number and metadata on startup (fixes #6335)

If we crashed after writing new file entries but before updating
metadata in the database the sequence number and metadata will be wrong.
This fixes that.
2020-02-13 13:05:26 +01:00
Jakob Borg
d95a087829 lib/db: Don't leak snapshot when closing (#6331)
We could potentially get a snapshot and then fail to get a releaser,
leaking the snapshot. This takes the releaser first and makes sure to
release it on snapshot error.
2020-02-12 12:00:17 +01:00
Jakob Borg
a728743c86 lib/db: Use Commit() instead of commit() (#6330)
The readWriteTransaction offered both commit() (the one to use) and
Commit() (via embedding) where the latter didn't close the read
transaction. This removes the lower cased variant in order to prevent
the mistake.

The only place where the mistake was made was the new gc runner, where
it would leave a read snapshot open forever.
2020-02-12 11:59:55 +01:00
Simon Frei
ce27780a4c lib/model: Return empty summary on paused folders (ref #6272) (#6326) 2020-02-12 11:59:12 +01:00
Jakob Borg
0df39ddc72 lib/fs, lib/model: Rewrite RecvOnly tests (#6318)
During some other work I discovered these tests weren't great, so I've
rewritten them to be a little better. The real changes here are:

- Don't play games with not starting the folder and such, and don't
  construct a fake folder instance -- just use the one the model has. The
  folder starts and scans but the folder contents are empty at this point
  so that's fine.

- Use a fakefs instead of a temp dir.

- To support the above, implement a fakefs option `?content=true` to
  make the fakefs actually retain written content. Use sparingly,
  obviously, but it means the fakefs can usually be used instead of an
  on disk real directory.
2020-02-12 07:47:05 +01:00
Jakob Borg
b84aa114be gui, man, authors: Update docs, translations, and contributors 2020-02-12 07:45:30 +01:00
Simon Frei
a596e5e2f0 lib/model: Consistent error return values for folder methods on model (#6325) 2020-02-12 07:35:24 +01:00
Jakob Borg
04e648fee6 lib/db: Handle missing block lists as missing file (ref #6321) (#6322)
Also explicitly handle non-nil but empty block lists (if they should
ever pop up as an effect of unmarshalling changes or whatnot).
2020-02-11 15:37:22 +01:00
Simon Frei
29736b1e33 lib/db: Add closeWaitGroup to allow async operation (#6317) 2020-02-11 14:31:43 +01:00
153 changed files with 2243 additions and 1101 deletions

View File

@@ -18,6 +18,7 @@ Adam Piggott (ProactiveServices) <aD@simplypeachy.co.uk> <simplypeachy@users.nor
Adel Qalieh (adelq) <aqalieh95@gmail.com> <adelq@users.noreply.github.com>
Alan Pope <alan@popey.com>
Alessandro G. (alessandro.g89) <alessandro.g89@gmail.com>
Alex Xu <alex.hello71@gmail.com>
Alexander Graf (alex2108) <register-github@alex-graf.de>
Alexandre Viau (aviau) <alexandre@alexandreviau.net> <aviau@debian.org>
Aman Gupta <aman@tmm1.net>
@@ -203,6 +204,7 @@ Sacheendra Talluri (sacheendra) <sacheendra.t@gmail.com>
Scott Klupfel (kluppy) <kluppy@going2blue.com>
Sergey Mishin (ralder) <ralder@yandex.ru>
Simon Frei (imsodin) <freisim93@gmail.com>
Simon Mwepu <simonmwepu@gmail.com>
Sly_tom_cat <slytomcat@mail.ru>
Stefan Kuntz (Stefan-Code) <stefan.github@gmail.com> <Stefan.github@gmail.com>
Stefan Tatschner (rumpelsepp) <stefan@sevenbyte.org> <rumpelsepp@sevenbyte.org> <stefan@rumpelsepp.org>
@@ -214,11 +216,11 @@ Tim Howes (timhowes) <timhowes@berkeley.edu>
Tobias Nygren (tnn2) <tnn@nygren.pp.se>
Tobias Tom (tobiastom) <t.tom@succont.de>
Tom Jakubowski <tom@crystae.net>
Tomas Cerveny (kozec) <kozec@kozec.com>
Tomasz Wilczyński <5626656+tomasz1986@users.noreply.github.com>
Tommy Thorn <tommy-github-email@thorn.ws>
Tully Robinson (tojrobinson) <tully@tojr.org>
Tyler Brazier (tylerbrazier) <tyler@tylerbrazier.com>
Tyler Kropp <kropptyler@gmail.com>
Unrud (Unrud) <unrud@openaliasbox.org> <Unrud@users.noreply.github.com>
Veeti Paananen (veeti) <veeti.paananen@rojekti.fi>
Victor Buinsky (buinsky) <vix_booja@tut.by>

17
Dockerfile.builder Normal file
View File

@@ -0,0 +1,17 @@
# We will grab the Go compiler from the latest Go image.
FROM golang:1.13 as go
# Otherwise we base on the snapcraft container as that is by far the
# most complex and tricky thing to get installed and working...
FROM snapcore/snapcraft
# Go
COPY --from=go /usr/local/go /usr/local/go
ENV PATH="/usr/local/go/bin:$PATH"
# FPM to build Debian packages
RUN apt-get update && apt-get install -y --no-install-recommends \
locales rubygems ruby-dev build-essential git \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* \
&& gem install --no-ri --no-rdoc fpm

View File

@@ -654,7 +654,11 @@ func shouldBuildSyso(dir string) (string, error) {
}
jsonPath := filepath.Join(dir, "versioninfo.json")
ioutil.WriteFile(jsonPath, bs, 0644)
err = ioutil.WriteFile(jsonPath, bs, 0644)
if err != nil {
return "", errors.New("failed to create " + jsonPath + ": " + err.Error())
}
defer func() {
if err := os.Remove(jsonPath); err != nil {
log.Printf("Warning: unable to remove generated %s: %v. Please remove it manually.", jsonPath, err)
@@ -860,13 +864,22 @@ func getVersion() string {
return "unknown-dev"
}
func semanticVersion() (major, minor, patch, build string) {
func semanticVersion() (major, minor, patch, build int) {
r := regexp.MustCompile(`v(?P<Major>\d+)\.(?P<Minor>\d+).(?P<Patch>\d+).*\+(?P<CommitsAhead>\d+)`)
matches := r.FindStringSubmatch(getVersion())
if len(matches) != 5 {
return "0", "0", "0", "0"
return 0, 0, 0, 0
}
return matches[1], matches[2], matches[3], matches[4]
var ints [4]int
for i := 1; i < 5; i++ {
value, err := strconv.Atoi(matches[i])
if err != nil {
return 0, 0, 0, 0
}
ints[i-1] = value
}
return ints[0], ints[1], ints[2], ints[3]
}
func getBranchSuffix() string {

View File

@@ -10,6 +10,7 @@ import (
"bytes"
"context"
"crypto/tls"
"errors"
"fmt"
"net"
"net/http"
@@ -80,16 +81,16 @@ func (c *APIClient) Post(url, body string) (*http.Response, error) {
func checkResponse(response *http.Response) error {
if response.StatusCode == 404 {
return fmt.Errorf("Invalid endpoint or API call")
return errors.New("invalid endpoint or API call")
} else if response.StatusCode == 403 {
return fmt.Errorf("Invalid API key")
return errors.New("invalid API key")
} else if response.StatusCode != 200 {
data, err := responseToBArray(response)
if err != nil {
return err
}
body := strings.TrimSpace(string(data))
return fmt.Errorf("Unexpected HTTP status returned: %s\n%s", response.Status, body)
return fmt.Errorf("unexpected HTTP status returned: %s\n%s", response.Status, body)
}
return nil
}

View File

@@ -7,6 +7,7 @@
package main
import (
"errors"
"fmt"
"strings"
@@ -54,7 +55,7 @@ func errorsPush(c *cli.Context) error {
if body != "" {
errStr += "\nBody: " + body
}
return fmt.Errorf(errStr)
return errors.New(errStr)
}
return nil
}

View File

@@ -60,7 +60,7 @@ func compareDirectories(dirs ...string) error {
} else if res[i].name > res[0].name {
return fmt.Errorf("%s missing %v (present in %s)", dirs[i], res[0], dirs[0])
}
return fmt.Errorf("Mismatch; %v (%s) != %v (%s)", res[i], dirs[i], res[0], dirs[0])
return fmt.Errorf("mismatch; %v (%s) != %v (%s)", res[i], dirs[i], res[0], dirs[0])
}
}

View File

@@ -7,6 +7,7 @@
package main
import (
"context"
"crypto/tls"
"errors"
"flag"
@@ -95,7 +96,7 @@ func checkServer(deviceID protocol.DeviceID, server string) checkResult {
})
go func() {
addresses, err := disco.Lookup(deviceID)
addresses, err := disco.Lookup(context.Background(), deviceID)
res <- checkResult{addresses: addresses, error: err}
}()

View File

@@ -148,7 +148,7 @@ func idxck(ldb backend.Backend) (success bool) {
}
}
if fi.BlocksHash != nil {
if len(fi.Blocks) == 0 && len(fi.BlocksHash) != 0 {
key := string(fi.BlocksHash)
if _, ok := blocklists[key]; !ok {
fmt.Printf("Missing block list for file %q, block list hash %x\n", fi.Name, fi.BlocksHash)

View File

@@ -10,6 +10,7 @@ import (
"context"
"crypto/tls"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
@@ -494,7 +495,7 @@ func handleRelayTest(request request) {
if debug {
log.Println("Test for relay", request.relay, "failed")
}
request.result <- result{fmt.Errorf("connection test failed"), 0}
request.result <- result{errors.New("connection test failed"), 0}
return
}

View File

@@ -14,6 +14,7 @@ import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"flag"
"fmt"
"math/big"
@@ -191,7 +192,7 @@ func pemBlockForKey(priv interface{}) (*pem.Block, error) {
}
return &pem.Block{Type: "EC PRIVATE KEY", Bytes: b}, nil
default:
return nil, fmt.Errorf("unknown key type")
return nil, errors.New("unknown key type")
}
}

View File

@@ -119,8 +119,8 @@ are mostly useful for developers. Use with care.
"h", "m" and "s" abbreviations for hours minutes and seconds.
Valid values are like "720h", "30s", etc.
STGCBLOCKSEVERY Set to a time interval to override the default database
block GC interval of 13 hours. Same format as the
STGCINDIRECTEVERY Set to a time interval to override the default database
indirection GC interval of 13 hours. Same format as the
STRECHECKDBEVERY variable.
GOMAXPROCS Set the maximum number of CPU cores to use. Defaults to all
@@ -374,6 +374,7 @@ func main() {
l.Warnln("Resetting database:", err)
os.Exit(syncthing.ExitError.AsInt())
}
l.Infoln("Successfully reset database - it will be rebuilt after next start.")
return
}

View File

@@ -55,28 +55,32 @@ func monitorMain(runtimeOptions RuntimeOptions) {
logFile = expanded
}
var fileDst io.Writer
var err error
open := func(name string) (io.WriteCloser, error) {
return newAutoclosedFile(name, logFileAutoCloseDelay, logFileMaxOpenTime)
}
if runtimeOptions.logMaxSize > 0 {
open := func(name string) (io.WriteCloser, error) {
return newAutoclosedFile(name, logFileAutoCloseDelay, logFileMaxOpenTime), nil
}
fileDst = newRotatedFile(logFile, open, int64(runtimeOptions.logMaxSize), runtimeOptions.logMaxFiles)
fileDst, err = newRotatedFile(logFile, open, int64(runtimeOptions.logMaxSize), runtimeOptions.logMaxFiles)
} else {
fileDst = newAutoclosedFile(logFile, logFileAutoCloseDelay, logFileMaxOpenTime)
fileDst, err = open(logFile)
}
if runtime.GOOS == "windows" {
// Translate line breaks to Windows standard
fileDst = osutil.ReplacingWriter{
Writer: fileDst,
From: '\n',
To: []byte{'\r', '\n'},
if err != nil {
l.Warnln("Failed to setup logging to file, proceeding with logging to stdout only:", err)
} else {
if runtime.GOOS == "windows" {
// Translate line breaks to Windows standard
fileDst = osutil.ReplacingWriter{
Writer: fileDst,
From: '\n',
To: []byte{'\r', '\n'},
}
}
// Log to both stdout and file.
dst = io.MultiWriter(dst, fileDst)
l.Infof(`Log output saved to file "%s"`, logFile)
}
// Log to both stdout and file.
dst = io.MultiWriter(dst, fileDst)
l.Infof(`Log output saved to file "%s"`, logFile)
}
args := os.Args
@@ -353,16 +357,30 @@ type rotatedFile struct {
currentSize int64
}
// the createFn should act equivalently to os.Create
type createFn func(name string) (io.WriteCloser, error)
func newRotatedFile(name string, create createFn, maxSize int64, maxFiles int) *rotatedFile {
return &rotatedFile{
name: name,
create: create,
maxSize: maxSize,
maxFiles: maxFiles,
func newRotatedFile(name string, create createFn, maxSize int64, maxFiles int) (*rotatedFile, error) {
var size int64
if info, err := os.Lstat(name); err != nil {
if !os.IsNotExist(err) {
return nil, err
}
size = 0
} else {
size = info.Size()
}
writer, err := create(name)
if err != nil {
return nil, err
}
return &rotatedFile{
name: name,
create: create,
maxSize: maxSize,
maxFiles: maxFiles,
currentFile: writer,
currentSize: size,
}, nil
}
func (r *rotatedFile) Write(bs []byte) (int, error) {
@@ -370,19 +388,13 @@ func (r *rotatedFile) Write(bs []byte) (int, error) {
// file so we'll start on a new one.
if r.currentSize+int64(len(bs)) > r.maxSize {
r.currentFile.Close()
r.currentFile = nil
r.currentSize = 0
}
// If we have no current log, rotate old files out of the way and create
// a new one.
if r.currentFile == nil {
r.rotate()
fd, err := r.create(r.name)
f, err := r.create(r.name)
if err != nil {
return 0, err
}
r.currentFile = fd
r.currentFile = f
}
n, err := r.currentFile.Write(bs)
@@ -435,7 +447,7 @@ type autoclosedFile struct {
mut sync.Mutex
}
func newAutoclosedFile(name string, closeDelay, maxOpenTime time.Duration) *autoclosedFile {
func newAutoclosedFile(name string, closeDelay, maxOpenTime time.Duration) (*autoclosedFile, error) {
f := &autoclosedFile{
name: name,
closeDelay: closeDelay,
@@ -444,8 +456,13 @@ func newAutoclosedFile(name string, closeDelay, maxOpenTime time.Duration) *auto
closed: make(chan struct{}),
closeTimer: time.NewTimer(time.Minute),
}
f.mut.Lock()
defer f.mut.Unlock()
if err := f.ensureOpenLocked(); err != nil {
return nil, err
}
go f.closerLoop()
return f
return f, nil
}
func (f *autoclosedFile) Write(bs []byte) (int, error) {
@@ -453,7 +470,7 @@ func (f *autoclosedFile) Write(bs []byte) (int, error) {
defer f.mut.Unlock()
// Make sure the file is open for appending
if err := f.ensureOpen(); err != nil {
if err := f.ensureOpenLocked(); err != nil {
return 0, err
}
@@ -483,22 +500,14 @@ func (f *autoclosedFile) Close() error {
}
// Must be called with f.mut held!
func (f *autoclosedFile) ensureOpen() error {
func (f *autoclosedFile) ensureOpenLocked() error {
if f.fd != nil {
// File is already open
return nil
}
// We open the file for write only, and create it if it doesn't exist.
flags := os.O_WRONLY | os.O_CREATE
if f.opened.IsZero() {
// This is the first time we are opening the file. We should truncate
// it to better emulate an os.Create() call.
flags |= os.O_TRUNC
} else {
// The file was already opened once, so we should append to it.
flags |= os.O_APPEND
}
flags := os.O_WRONLY | os.O_CREATE | os.O_APPEND
fd, err := os.OpenFile(f.name, flags, 0644)
if err != nil {

View File

@@ -33,7 +33,10 @@ func TestRotatedFile(t *testing.T) {
maxSize := int64(len(testData) + len(testData)/2)
// We allow the log file plus two rotated copies.
rf := newRotatedFile(logName, open, maxSize, 2)
rf, err := newRotatedFile(logName, open, maxSize, 2)
if err != nil {
t.Fatal(err)
}
// Write some bytes.
if _, err := rf.Write(testData); err != nil {
@@ -140,7 +143,10 @@ func TestAutoClosedFile(t *testing.T) {
data := []byte("hello, world\n")
// An autoclosed file that closes very quickly
ac := newAutoclosedFile(file, time.Millisecond, time.Millisecond)
ac, err := newAutoclosedFile(file, time.Millisecond, time.Millisecond)
if err != nil {
t.Fatal(err)
}
// Write some data.
if _, err := ac.Write(data); err != nil {
@@ -182,21 +188,23 @@ func TestAutoClosedFile(t *testing.T) {
}
// Open the file again.
ac = newAutoclosedFile(file, time.Second, time.Second)
ac, err = newAutoclosedFile(file, time.Second, time.Second)
if err != nil {
t.Fatal(err)
}
// Write something
if _, err := ac.Write(data); err != nil {
t.Fatal(err)
}
// It should now contain only one write, because the first open
// should be a truncate.
// It should now contain three writes, as the file is always opened for appending
bs, err = ioutil.ReadFile(file)
if err != nil {
t.Fatal(err)
}
if len(bs) != len(data) {
t.Fatalf("Write failed, expected %d bytes, not %d", len(data), len(bs))
if len(bs) != 3*len(data) {
t.Fatalf("Write failed, expected %d bytes, not %d", 3*len(data), len(bs))
}
// Close.

View File

@@ -56,6 +56,7 @@ var (
{regexp.MustCompile("snap@build.syncthing.net"), "Snapcraft"},
{regexp.MustCompile("android-.*vagrant@basebox-stretch64"), "F-Droid"},
{regexp.MustCompile("builduser@svetlemodry"), "Arch (3rd party)"},
{regexp.MustCompile("synology@kastelo.net"), "Synology (Kastelo)"},
{regexp.MustCompile("@debian"), "Debian (3rd party)"},
{regexp.MustCompile("@fedora"), "Fedora (3rd party)"},
{regexp.MustCompile(`\bbrew@`), "Homebrew (3rd party)"},
@@ -266,10 +267,10 @@ type report struct {
func (r *report) Validate() error {
if r.UniqueID == "" || r.Version == "" || r.Platform == "" {
return fmt.Errorf("missing required field")
return errors.New("missing required field")
}
if len(r.Date) != 8 {
return fmt.Errorf("date not initialized")
return errors.New("date not initialized")
}
// Some fields may not be null.
@@ -1419,7 +1420,7 @@ func getReport(db *sql.DB) map[string]interface{} {
r["platforms"] = group(byPlatform, analyticsFor(platforms, 2000), 10)
r["compilers"] = group(byCompiler, analyticsFor(compilers, 2000), 5)
r["builders"] = analyticsFor(builders, 12)
r["distributions"] = analyticsFor(distributions, 10)
r["distributions"] = analyticsFor(distributions, len(knownDistributions))
r["featureOrder"] = featureOrder
r["locations"] = locations
r["contries"] = countryList

5
go.mod
View File

@@ -52,4 +52,7 @@ require (
gopkg.in/ldap.v2 v2.5.1
)
go 1.12
go 1.13
// https://github.com/spaolacci/murmur3/pull/30
replace github.com/spaolacci/murmur3 v1.1.0 => github.com/calmh/murmur3 v1.1.1-0.20200226160057-74e9af8f47ac

2
go.sum
View File

@@ -21,6 +21,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bkaradzic/go-lz4 v0.0.0-20160924222819-7224d8d8f27e h1:2augTYh6E+XoNrrivZJBadpThP/dsvYKj0nzqfQ8tM4=
github.com/bkaradzic/go-lz4 v0.0.0-20160924222819-7224d8d8f27e/go.mod h1:0YdlkowM3VswSROI7qDxhRvJ3sLhlFrRRwjwegp5jy4=
github.com/calmh/murmur3 v1.1.1-0.20200226160057-74e9af8f47ac h1:mc24tiVsBenJuhJFQzgvTo1ECJxCGXUgNktcfEhJHHo=
github.com/calmh/murmur3 v1.1.1-0.20200226160057-74e9af8f47ac/go.mod h1:nZyyz8Qrw2g3CakiZkVTsiwKlCgQSCf2ZCAFB3DHbaI=
github.com/calmh/xdr v1.1.0 h1:U/Dd4CXNLoo8EiQ4ulJUXkgO1/EyQLgDKLgpY1SOoJE=
github.com/calmh/xdr v1.1.0/go.mod h1:E8sz2ByAdXC8MbANf1LCRYzedSnnc+/sXXJs/PVqoeg=
github.com/ccding/go-stun v0.0.0-20180726100737-be486d185f3d h1:As4937T5NVbJ/DmZT9z33pyLEprMd6CUSfhbmMY57Io=

View File

@@ -106,6 +106,7 @@
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Въведете адреси разделени със запетая (\"tcp://ip:port\", \"tcp://host:port\") или \"dynamic\", за автоматично откриване на наличните адреси.",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Въведете адреси разделени със запетая (\"tcp://ip:port\", \"tcp://host:port\") или \"dynamic\", за автоматично откриване на наличните адреси.",
"Enter ignore patterns, one per line.": "Добавете шаблони за игнориране, по един на ред.",
"Enter up to three octal digits.": "Enter up to three octal digits.",
"Error": "Грешка",
"External File Versioning": "Външно управление на версиите",
"Failed Items": "Неуспешни",
@@ -348,6 +349,7 @@
"Time the item was last modified": "Часът на последна промяна на елемента",
"Trash Can File Versioning": "Само на файловете в кошчето",
"Type": "Тип",
"UNIX Permissions": "UNIX Permissions",
"Unavailable": "Не е на разположение",
"Unavailable/Disabled by administrator or maintainer": "Не е на разположение/Деактивриан от администраторът или поддръжника",
"Undecided (will prompt)": "Неизбрано (ще попита)",

View File

@@ -106,6 +106,7 @@
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Introdueix adreces separades per coma (\"tcp://ip:port\", \"tcp://host:port\") o \"dynamic\" per a realitzar el descobriment automàtic de l'adreça.",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Introduïr adreces separades per coma (\"tcp://ip:port\", \"tcp://host:port\") o dinàmiques per al descobriment automàtic de l'adreça.",
"Enter ignore patterns, one per line.": "Introduïr patrons a ignorar, un per línia.",
"Enter up to three octal digits.": "Enter up to three octal digits.",
"Error": "Error",
"External File Versioning": "Versionat extern de fitxers",
"Failed Items": "Objectes fallits",
@@ -348,6 +349,7 @@
"Time the item was last modified": "Hora a la que l'ítem fou modificat per última vegada",
"Trash Can File Versioning": "Versionat d'arxius de la paperera",
"Type": "Tipus",
"UNIX Permissions": "UNIX Permissions",
"Unavailable": "No disponible",
"Unavailable/Disabled by administrator or maintainer": "No disponible/Desactivar per l'administrador o mantenedor",
"Undecided (will prompt)": "No decidit (es preguntarà)",

View File

@@ -31,7 +31,7 @@
"Are you sure you want to remove device {%name%}?": "Opravdu chcete odebrat zařízení {{name}}?",
"Are you sure you want to remove folder {%label%}?": "Opravdu chcete odebrat složku {{label}}?",
"Are you sure you want to restore {%count%} files?": "Opravdu chcete obnovit {{count}} souborů?",
"Are you sure you want to upgrade?": "Are you sure you want to upgrade?",
"Are you sure you want to upgrade?": "Skutečně chcete provést aktualizaci?",
"Auto Accept": "Přijmout automaticky",
"Automatic Crash Reporting": "Automatické hlášení pádů",
"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 ně.",
@@ -60,13 +60,13 @@
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 následující přispěvatelé:",
"Copyright © 2014-2019 the following Contributors:": "Copyright © 2014-2019 následující přispěvatelé:",
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Vytvářejí se vzory ignorovaného a přepisuje se jimi existující soubor v {{path}}.",
"Currently Shared With Devices": "Currently Shared With Devices",
"Currently Shared With Devices": "Aktuálně sdíleno se zařízeními",
"Danger!": "Nebezpečí!",
"Debugging Facilities": "Nástroje pro ladění",
"Default Folder Path": "Popis umístění výchozí složky",
"Deleted": "Smazáno",
"Deselect All": "Zrušit výběr všeho",
"Deselect devices to stop sharing this folder with.": "Deselect devices to stop sharing this folder with.",
"Deselect devices to stop sharing this folder with.": "Zrušte výběr zařízení, se kterými již nemá být tato složka sdílena.",
"Device": "Zařízení",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Zařízení „{{name}}“ ({{device}} na {{address}}) se chce připojit. Přidat nové zařízení?",
"Device ID": "Identifikátor zařízení",
@@ -106,6 +106,7 @@
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Zadejte adresy oddělené čárkou („tcp://ip:port“, „tcp://host:port“) nebo „dynamic“ pro automatické zjišťování adres.",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Zadejte adresy oddělené čárkami („tcp://ip:port“, „tcp://host:port“) nebo „dynamic“ pro automatické zjištění adresy.",
"Enter ignore patterns, one per line.": "Zadejte vzory toho, co ignorovat každý na zvlášť řádek.",
"Enter up to three octal digits.": "Zadejte nanejvýš tři osmičkové číslice.",
"Error": "Chyba",
"External File Versioning": "Externí správa verzí souborů",
"Failed Items": "Nezdařené položky",
@@ -171,7 +172,7 @@
"Listeners": "Naslouchající",
"Loading data...": "Načítání dat…",
"Loading...": "Načítání…",
"Local Additions": "Local Additions",
"Local Additions": "Místní příbytky",
"Local Discovery": "Místní oznamování",
"Local State": "Místní status",
"Local State (Total)": "Místní status (Celkem)",
@@ -228,7 +229,7 @@
"Please wait": "Chvíli strpení",
"Prefix indicating that the file can be deleted if preventing directory removal": "Tato předpona značí, že pokud soubor brání odebrání složky, je možné ho smazat",
"Prefix indicating that the pattern should be matched without case sensitivity": "Tato předpona značí, že při porovnávání se vzorem nemají být rozlišována malá/velká písmena",
"Preparing to Sync": "Preparing to Sync",
"Preparing to Sync": "Probíhá příprava k synchronizaci",
"Preview": "Náhled",
"Preview Usage Report": "Náhled hlášení o využívání",
"Quick guide to supported patterns": "Rychlá nápověda k podporovaným vzorům",
@@ -265,7 +266,7 @@
"See external versioning help for supported templated command line parameters.": "Podporované šablonové parametry příkazové řádky jsou dostupné v nápovědě k externí správě verzí.",
"Select All": "Vybrat vše",
"Select a version": "Vyberte verzi",
"Select additional devices to share this folder with.": "Select additional devices to share this folder with.",
"Select additional devices to share this folder with.": "Vyberte další zařízení pro sdílení s touto složkou.",
"Select latest version": "Vybrat nejnovější verzi",
"Select oldest version": "Vybrat nejstarší verzi",
"Select the devices to share this folder with.": "Vybrat zařízení, se kterými sdílet tuto složku.",
@@ -338,7 +339,7 @@
"The path cannot be blank.": "Popis umístění nemůže zůstat nevyplněný.",
"The rate limit must be a non-negative number (0: no limit)": "Je třeba, aby limit rychlosti bylo kladné číslo (0: bez limitu)",
"The rescan interval must be a non-negative number of seconds.": "Je třeba, aby interval opakování skenování bylo kladné číslo.",
"There are no devices to share this folder with.": "There are no devices to share this folder with.",
"There are no devices to share this folder with.": "Nejsou žádná zařízení, se kterými lze sdílet tuto složku.",
"They are retried automatically and will be synced when the error is resolved.": "Nové pokusy o synchronizaci budou probíhat automaticky a položky budou synchronizovány jakmile bude chyba odstraněna.",
"This Device": "Toto zařízení",
"This can easily give hackers access to read and change any files on your computer.": "Toto může útočníkům jednoduše umožnit čtení a úpravy souborů na vašem počítači. ",
@@ -348,13 +349,14 @@
"Time the item was last modified": "Čas poslední modifikace položky",
"Trash Can File Versioning": "Ponechávat jednu předchozí verzi (jako Koš) ",
"Type": "Typ",
"UNIX Permissions": "UNIX oprávnění",
"Unavailable": "Nedostupné",
"Unavailable/Disabled by administrator or maintainer": "Není k dispozici / vypnuto správcem systému či balíčku",
"Undecided (will prompt)": "Nerozhodnuto (zeptá se)",
"Unignore": "Přestat ignorovat",
"Unknown": "Neznámý",
"Unshared": "Nesdílený",
"Unshared Devices": "Unshared Devices",
"Unshared Devices": "Nesdílená zařízení",
"Unused": "Nepoužitý",
"Up to Date": "Aktuální",
"Updated": "Aktualizováno",
@@ -372,8 +374,8 @@
"Versions": "Verze",
"Versions Path": "Popis umístění verzí",
"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.",
"Waiting to Scan": "Waiting to Scan",
"Waiting to Sync": "Waiting to Sync",
"Waiting to Scan": "Čekání na skenování",
"Waiting to Sync": "Čekání na synchronizaci",
"Waiting to scan": "Čekání na skenování",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Varování, tento popis umístění je nadřazenou složkou existující „{{otherFolder}}“.",
"Warning, this path is a parent directory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Varování, tento popis umístění je nadřazenou složkou existující „{{otherFolderLabel}}“ ({{otherFolder}}).",

View File

@@ -106,6 +106,7 @@
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Angiv kommaseparerede adresser (“tcp://ip:port”, “tcp://host:port”) eller “dynamic” for at benytte automatisk opdagelse af adressen.",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Angiv en kommaadskilt adresseliste (\"tcp://ip:port\", \"tcp://host:port\")  eller \"dynamic\" for automatisk at opdage adressen.",
"Enter ignore patterns, one per line.": "Indtast ignoreringsmønstre, ét per linje.",
"Enter up to three octal digits.": "Enter up to three octal digits.",
"Error": "Fejl",
"External File Versioning": "Ekstern filversionering",
"Failed Items": "Mislykkede filer",
@@ -348,6 +349,7 @@
"Time the item was last modified": "Tidspunkt for seneste ændring af filen",
"Trash Can File Versioning": "Versionering med papirkurv",
"Type": "Type",
"UNIX Permissions": "UNIX Permissions",
"Unavailable": "Ikke tilgængelig",
"Unavailable/Disabled by administrator or maintainer": "Ikke tilgængelig / deaktiveret af administrator eller vedligeholder",
"Undecided (will prompt)": "Ubestemt (du bliver spurgt)",

View File

@@ -106,6 +106,7 @@
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Kommagetrennte Adressen (\"tcp://ip:port\", \"tcp://host:port\") oder \"dynamic\" eingeben, um die Adresse automatisch zu ermitteln.",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Kommagetrennte Adressen (\"tcp://ip:port\", \"tcp://host:port\") oder \"dynamic\" eingeben, um die Adresse automatisch zu ermitteln.",
"Enter ignore patterns, one per line.": "Geben Sie Ignoriermuster ein, eines pro Zeile.",
"Enter up to three octal digits.": "Enter up to three octal digits.",
"Error": "Fehler",
"External File Versioning": "Externe Dateiversionierung",
"Failed Items": "Fehlgeschlagene Elemente",
@@ -348,6 +349,7 @@
"Time the item was last modified": "Zeit der letzten Änderung des Elements",
"Trash Can File Versioning": "Papierkorb Dateiversionierung",
"Type": "Typ",
"UNIX Permissions": "UNIX-Berechtigungen",
"Unavailable": " Nicht verfügbar",
"Unavailable/Disabled by administrator or maintainer": "Nicht verfügbar/durch Administrator oder Betreuer deaktiviert",
"Undecided (will prompt)": "Unentschlossen (wird nachgefragt)",

View File

@@ -106,6 +106,7 @@
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Εισάγετε τις διευθύνσεις χωρισμένες με κόμμα (\"tcp://ip:port\", \"tcp://host:port\") ή γράψτε \"dynamic\" για την αυτόματη ανεύρεση τους.",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.",
"Enter ignore patterns, one per line.": "Δώσε τα πρότυπα που θα αγνοηθούν, ένα σε κάθε γραμμή.",
"Enter up to three octal digits.": "Enter up to three octal digits.",
"Error": "Σφάλμα",
"External File Versioning": "Εξωτερική τήρηση εκδόσεων",
"Failed Items": "Αρχεία που απέτυχαν",
@@ -348,6 +349,7 @@
"Time the item was last modified": "Ώρα τελευταίας τροποποίησης του στοιχείου",
"Trash Can File Versioning": "Τήρηση εκδόσεων κάδου ανακύκλωσης",
"Type": "Τύπος",
"UNIX Permissions": "UNIX Permissions",
"Unavailable": "Μη διαθέσιμο",
"Unavailable/Disabled by administrator or maintainer": "Μη διαθέσιμο/απενεργοποιημένο από τον διαχειριστή ή υπεύθυνο διανομής",
"Undecided (will prompt)": "Μη καθορισμένη (θα γίνει ερώτηση)",

View File

@@ -106,6 +106,7 @@
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.",
"Enter ignore patterns, one per line.": "Enter ignore patterns, one per line.",
"Enter up to three octal digits.": "Enter up to three octal digits.",
"Error": "Error",
"External File Versioning": "External File Versioning",
"Failed Items": "Failed Items",
@@ -348,6 +349,7 @@
"Time the item was last modified": "Time the item was last modified",
"Trash Can File Versioning": "Rubbish Bin File Versioning",
"Type": "Type",
"UNIX Permissions": "UNIX Permissions",
"Unavailable": "Unavailable",
"Unavailable/Disabled by administrator or maintainer": "Unavailable/Disabled by administrator or maintainer",
"Undecided (will prompt)": "Undecided (will prompt)",

View File

@@ -106,6 +106,7 @@
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.",
"Enter ignore patterns, one per line.": "Enter ignore patterns, one per line.",
"Enter up to three octal digits.": "Enter up to three octal digits.",
"Error": "Error",
"External File Versioning": "External File Versioning",
"Failed Items": "Failed Items",
@@ -348,6 +349,7 @@
"Time the item was last modified": "Time the item was last modified",
"Trash Can File Versioning": "Trash Can File Versioning",
"Type": "Type",
"UNIX Permissions": "UNIX Permissions",
"Unavailable": "Unavailable",
"Unavailable/Disabled by administrator or maintainer": "Unavailable/Disabled by administrator or maintainer",
"Undecided (will prompt)": "Undecided (will prompt)",

View File

@@ -2,16 +2,16 @@
"A device with that ID is already added.": "Aparato kun samtia ID estis jam aldonita.",
"A negative number of days doesn't make sense.": "Negativa numero de tagoj ne havas sencon.",
"A new major version may not be compatible with previous versions.": "Nova ĉefa versio eble ne kongruanta kun antaŭaj versioj.",
"API Key": "API Ŝlosilo",
"API Key": "Ŝlosilo API",
"About": "Pri",
"Action": "Ago",
"Actions": "Agoj",
"Add": "Aldoni",
"Add Device": "Aldoni Aparaton",
"Add Folder": "Aldoni Dosierujon",
"Add Remote Device": "Aldoni Foran Aparaton",
"Add Device": "Aldoni aparaton",
"Add Folder": "Aldoni dosierujon",
"Add Remote Device": "Aldoni foran aparaton",
"Add devices from the introducer to our device list, for mutually shared folders.": "Aldoni aparatojn de la enkondukanto ĝis nia aparatlisto, por reciproke komunigitaj dosierujoj.",
"Add new folder?": "Aldoni novan dosierujon?",
"Add new folder?": "Ĉu aldoni novan dosierujon?",
"Additionally the full rescan interval will be increased (times 60, i.e. new default of 1h). You can also configure it manually for every folder later after choosing No.": "Aldone, plena reskana intervalo estos pliigita (60-oble, t.e. nova defaŭlto estas 1h). Vi povas ankaŭ agordi ĝin permane por ĉiu dosierujo poste post elekto de Ne.",
"Address": "Adreso",
"Addresses": "Adresoj",
@@ -31,9 +31,9 @@
"Are you sure you want to remove device {%name%}?": "Ĉu vi certas, ke vi volas forigi aparaton {{name}}?",
"Are you sure you want to remove folder {%label%}?": "Ĉu vi certas, ke vi volas forigi dosierujon {{label}}?",
"Are you sure you want to restore {%count%} files?": "Ĉu vi certas, ke vi volas restarigi {{count}} dosierojn?",
"Are you sure you want to upgrade?": "Are you sure you want to upgrade?",
"Are you sure you want to upgrade?": "Ĉu vi certe volas plinovigi?",
"Auto Accept": "Akcepti Aŭtomate",
"Automatic Crash Reporting": "Automatic Crash Reporting",
"Automatic Crash Reporting": "Aŭtomata raportado de kraŝoj",
"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",
"Automatic upgrades are always enabled for candidate releases.": "Aŭtomataj ĝisdatigoj ĉiam ŝaltitaj por kandidataj eldonoj.",
@@ -60,13 +60,13 @@
"Copyright © 2014-2017 the following Contributors:": "Kopirajto © 2014-2017 por la sekvantaj Kontribuantoj:",
"Copyright © 2014-2019 the following Contributors:": "Kopirajto © 2014-2019 por la sekvantaj Kontribuantoj:",
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Kreante ignorantajn ŝablonojn, anstataŭige ekzistantan dosieron ĉe {{path}}.",
"Currently Shared With Devices": "Currently Shared With Devices",
"Currently Shared With Devices": "Nune komunigita kun aparatoj",
"Danger!": "Danĝero!",
"Debugging Facilities": "Elpurigadiloj",
"Default Folder Path": "Defaŭlta Dosieruja Vojo",
"Deleted": "Forigita",
"Deselect All": "Malelekti Ĉiujn",
"Deselect devices to stop sharing this folder with.": "Deselect devices to stop sharing this folder with.",
"Deselect devices to stop sharing this folder with.": "Malelekti aparatojn por ĉesi komunigi tiun ĉi dosierujon kun ili.",
"Device": "Aparato",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Aparato \"{{name}}\" ({{device}} ĉe {{address}}) volas konekti. Aldoni la novan aparaton?",
"Device ID": "Aparato ID",
@@ -75,7 +75,7 @@
"Device rate limits": "Limoj de rapideco de aparato",
"Device that last modified the item": "Aparato kiu laste modifis la eron",
"Devices": "Aparatoj",
"Disable Crash Reporting": "Disable Crash Reporting",
"Disable Crash Reporting": "Malŝalti raportadon de kraŝoj",
"Disabled": "Malebligita",
"Disabled periodic scanning and disabled watching for changes": "Malebligita perioda skanado kaj malebligita rigardado je ŝanĝoj",
"Disabled periodic scanning and enabled watching for changes": "Malebligita perioda skanado kaj ebligita rigardado je ŝanĝoj",
@@ -97,7 +97,7 @@
"Edit Folder": "Redakti Dosierujon",
"Editing": "Redaktado",
"Editing {%path%}.": "Redaktado de {{path}}.",
"Enable Crash Reporting": "Enable Crash Reporting",
"Enable Crash Reporting": "Ŝalti raportadon de kraŝoj",
"Enable NAT traversal": "Ŝaltu trairan NAT",
"Enable Relaying": "Ŝaltu Relajsadon",
"Enabled": "Ebligita",
@@ -106,6 +106,7 @@
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enigu adresojn dividitajn per komoj (\"tcp://ip:port\", \"tcp://host:port\") aŭ \"dynamic\" por elfari aŭtomatan malkovradon de la adreso.",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enigu adresojn dividitajn per komoj (\"tcp://ip:port\", \"tcp://host:port\") aŭ \"dynamic\" por elfari aŭtomatan malkovradon de la adreso.",
"Enter ignore patterns, one per line.": "Enigu ignorantajn ŝablonojn, unu po linio.",
"Enter up to three octal digits.": "Entajpu ĝis tri okumajn ciferojn.",
"Error": "Eraro",
"External File Versioning": "Ekstera Versionado de Dosiero",
"Failed Items": "Malsukcesaj Eroj",
@@ -171,7 +172,7 @@
"Listeners": "Aŭskultantoj",
"Loading data...": "Ŝarĝas datumojn...",
"Loading...": "Ŝarĝas...",
"Local Additions": "Local Additions",
"Local Additions": "Lokaj aldonoj",
"Local Discovery": "Loka Malkovro",
"Local State": "Loka Stato",
"Local State (Total)": "Loka Stato (Tuta)",
@@ -228,7 +229,7 @@
"Please wait": "Bonvolu atendi",
"Prefix indicating that the file can be deleted if preventing directory removal": "Prefikso indikanta, ke la dosiero povas esti forigita, se ĝi malhelpas forigi dosierujon",
"Prefix indicating that the pattern should be matched without case sensitivity": "Prefikso indikanta, ke la ŝablono devus esti egalita usklecoblinde.",
"Preparing to Sync": "Preparing to Sync",
"Preparing to Sync": "Pretigante sinkronigadon",
"Preview": "Antaŭrigardo",
"Preview Usage Report": "Antaŭrigardo Uzada Raporto",
"Quick guide to supported patterns": "Rapida gvidilo pri subtenata ŝablonoj",
@@ -265,7 +266,7 @@
"See external versioning help for supported templated command line parameters.": "Vidu informlibron de ekstera versionado por subtenata ŝablona parametroj de komandlinio.",
"Select All": "Elekti Ĉiujn",
"Select a version": "Elekti version",
"Select additional devices to share this folder with.": "Select additional devices to share this folder with.",
"Select additional devices to share this folder with.": "Elektu pliajn aparatojn por komunigi tiun ĉi dosierujon kun ili.",
"Select latest version": "Elekti plej novan version",
"Select oldest version": "Elekti plej malnovan version",
"Select the devices to share this folder with.": "Elekti la aparatojn por komunigi ĉi tiun dosierujon.",
@@ -338,7 +339,7 @@
"The path cannot be blank.": "La vojo ne povas esti malplena.",
"The rate limit must be a non-negative number (0: no limit)": "La rapideca limo devas esti pozitiva nombro (0: senlimo)",
"The rescan interval must be a non-negative number of seconds.": "La intervalo de reskano devas esti pozitiva nombro da sekundoj.",
"There are no devices to share this folder with.": "There are no devices to share this folder with.",
"There are no devices to share this folder with.": "Estas neniu aparato kun kiu komunigi tiun ĉi dosierujon.",
"They are retried automatically and will be synced when the error is resolved.": "Ili estas reprovitaj aŭtomate kaj estos sinkronigitaj kiam la eraro estas solvita.",
"This Device": "Ĉi Tiu Aparato",
"This can easily give hackers access to read and change any files on your computer.": "Ĉi tio povas facile doni al kodumuloj atingon por legi kaj ŝanĝi ajnajn dosierojn en via komputilo.",
@@ -348,13 +349,14 @@
"Time the item was last modified": "Tempo de lasta modifo de la ero",
"Trash Can File Versioning": "Rubuja Dosiera Versionado",
"Type": "Tipo",
"UNIX Permissions": "Permesoj UNIX",
"Unavailable": "Ne disponebla",
"Unavailable/Disabled by administrator or maintainer": "Ne disponebla/Malebligita de administranto aŭ subtenanto",
"Undecided (will prompt)": "Hezitema (demandos)",
"Unignore": "Malignoru",
"Unknown": "Nekonata",
"Unshared": "Nekomunigita",
"Unshared Devices": "Unshared Devices",
"Unshared Devices": "Malkomunigitaj aparatoj",
"Unused": "Neuzita",
"Up to Date": "Ĝisdata",
"Updated": "Ĝisdatigita",
@@ -372,8 +374,8 @@
"Versions": "Versioj",
"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.",
"Waiting to Scan": "Waiting to Scan",
"Waiting to Sync": "Waiting to Sync",
"Waiting to Scan": "Atendante skanadon",
"Waiting to Sync": "Atendante sinkronigadon",
"Waiting to scan": "Atendante scanadon",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Averto, ĉi tiu vojo estas parenta dosierujo de ekzistanta dosierujo \"{{otherFolder}}\".",
"Warning, this path is a parent directory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Averto, ĉi tiu vojo estas parenta dosierujo de ekzistanta dosierujo \"{{otherFolderLabel}}\" ({{otherFolder}}).",

View File

@@ -106,6 +106,7 @@
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Introduzca las direcciones, separadas por comas (\"tcp://ip:port\", \"tcp://host:port\"), o \"dynamic\" para llevar a cabo el descubrimiento automático de la dirección.",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Introducir direcciones separadas por coma (\"tcp://ip:port\", \"tcp://host:port\") o dinámicas para realizar el descubrimiento automático de la dirección.",
"Enter ignore patterns, one per line.": "Introducir patrones a ignorar, uno por línea.",
"Enter up to three octal digits.": "Enter up to three octal digits.",
"Error": "Error",
"External File Versioning": "Versionado externo de fichero",
"Failed Items": "Elementos fallidos",
@@ -348,6 +349,7 @@
"Time the item was last modified": "Tiempo en el que se modificó el ítem por última vez",
"Trash Can File Versioning": "Versionado de archivos de la papelera",
"Type": "Tipo",
"UNIX Permissions": "UNIX Permissions",
"Unavailable": "No disponible",
"Unavailable/Disabled by administrator or maintainer": "No disponible/Desactivado por el administrador o el mantenedor",
"Undecided (will prompt)": "Aún no decidido (se preguntará al usuario)",

View File

@@ -106,6 +106,7 @@
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Introduzca las direcciones, separadas por comas (\"tcp://ip:port\", \"tcp://host:port\"), o \"dynamic\" para llevar a cabo el descubrimiento automático de la dirección.",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.",
"Enter ignore patterns, one per line.": "Introducir patrones a ignorar, uno por línea.",
"Enter up to three octal digits.": "Enter up to three octal digits.",
"Error": "Error",
"External File Versioning": "Versionado externo de fichero",
"Failed Items": "Elementos fallidos",
@@ -348,6 +349,7 @@
"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",
"UNIX Permissions": "UNIX Permissions",
"Unavailable": "No disponible",
"Unavailable/Disabled by administrator or maintainer": "No disponible/Deshabilitado por el administrador o mantenedor",
"Undecided (will prompt)": "No decidido (se preguntará)",

View File

@@ -106,6 +106,7 @@
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Syötä osoitteet pilkuilla erotettuina (\"tcp://ip:portti, tcp://nimi:portti\") tai \"dynamic\" käyttääksesi osoitteen automaattista selvitystä.",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.",
"Enter ignore patterns, one per line.": "Syötä ohituslausekkeet, yksi riviä kohden.",
"Enter up to three octal digits.": "Enter up to three octal digits.",
"Error": "Virhe",
"External File Versioning": "Ulkoinen tiedostoversionti",
"Failed Items": "Epäonnistuneet kohteet",
@@ -348,6 +349,7 @@
"Time the item was last modified": "Aika jolloin kohdetta viimeksi muokattiin",
"Trash Can File Versioning": "Roskakorin tiedostoversiointi",
"Type": "Tyyppi",
"UNIX Permissions": "UNIX Permissions",
"Unavailable": "Ei saatavilla",
"Unavailable/Disabled by administrator or maintainer": "Ei saatavilla / ylläpitäjän estämä.",
"Undecided (will prompt)": "Ei päätetty (kysytään myöhemmin)",

View File

@@ -106,6 +106,7 @@
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Entrer les adresses (\"tcp://ip:port\" ou \"tcp://hôte:port\") séparées par une virgule, ou \"dynamic\" afin d'activer la recherche automatique de l'adresse.",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Entrer les adresses (\"tcp://ip:port\" ou \"tcp://hôte:port\") séparées par une virgule, ou \"dynamic\" afin d'activer la recherche automatique de l'adresse.",
"Enter ignore patterns, one per line.": "Entrez les masques d'exclusion, un par ligne.",
"Enter up to three octal digits.": "Entrez jusqu'à 3 chiffres octaux",
"Error": "Erreur",
"External File Versioning": "Gestion externe des versions de fichiers",
"Failed Items": "Éléments en échec",
@@ -348,6 +349,7 @@
"Time the item was last modified": "Dernière modification de l'élément",
"Trash Can File Versioning": "Style poubelle",
"Type": "Type",
"UNIX Permissions": "Permissions UNIX",
"Unavailable": "Indisponible",
"Unavailable/Disabled by administrator or maintainer": "Indisponible/Désactivé par l'administrateur ou le mainteneur",
"Undecided (will prompt)": "Non défini (Choisir plus tard)",

View File

@@ -106,6 +106,7 @@
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Fier troch komma's skieden (\"tcp://ip:port\", \"tcp://host:port\") adressen yn of \"dynamic\" om automatyske ûntdekking fan it adres út te fieren.",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Fier troch komma's skieden (\"tcp://ip:port\", \"tcp://host:port\") adressen yn of \"dynamic\" om automatyske ûntdekking fan it adres út te fieren.",
"Enter ignore patterns, one per line.": "Fier negearpatroanen yn, ien per rigel.",
"Enter up to three octal digits.": "Enter up to three octal digits.",
"Error": "Flater",
"External File Versioning": "Ekstern ferzjebehear foar triemen",
"Failed Items": "Mislearre items",
@@ -348,6 +349,7 @@
"Time the item was last modified": "Tiidstip dat it ûnderdiel foar it lest oanpast waard.",
"Trash Can File Versioning": "Jiskefet-triemferzjebehear",
"Type": "Type",
"UNIX Permissions": "UNIX Permissions",
"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)",

View File

@@ -106,6 +106,7 @@
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Vesszővel elválasztva több cím is megadható (\"tcp://ip:port\", \"tcp://host:port\"), az automatikus felfedezéshez a 'dynamic' kulcsszó használatos. ",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Vesszővel elválasztva több cím is megadható („tcp://ip:port”, „tcp://kiszolgáló:port”), az automatikus felfedezéshez a „dynamic” kulcsszó használatos. ",
"Enter ignore patterns, one per line.": "A mellőzési mintákból soronként egyet kell megadni.",
"Enter up to three octal digits.": "Adjon meg legfeljebb három oktális számjegyet.",
"Error": "Hiba",
"External File Versioning": "Külső fájlverzió-követés",
"Failed Items": "Hibás elemek",
@@ -348,6 +349,7 @@
"Time the item was last modified": "Az idő, amikor utoljára módosítva lett az elem",
"Trash Can File Versioning": "Szemetes fájlverzió-követés",
"Type": "Típus",
"UNIX Permissions": "UNIX jogosultságok",
"Unavailable": "Nem elérhető",
"Unavailable/Disabled by administrator or maintainer": "Nem elérhető/letiltva egy adminisztrátor vagy karbantartó által",
"Undecided (will prompt)": "Bizonytalan (kérdezni fogja)",

View File

@@ -106,6 +106,7 @@
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Inserisci indirizzi separati da virgola (\"tcp://ip:porta\", \"tcp://host:porta\") oppure \"dynamic\" per effettuare il rilevamento automatico dell'indirizzo.",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Inserire gli indirizzi separati da virgola (\"tcp://ip:porta\", \"tcp://host:porta\") o \"dynamic\" per eseguire il rilevamento automatico dell'indirizzo.",
"Enter ignore patterns, one per line.": "Inserisci gli schemi di esclusione, uno per riga.",
"Enter up to three octal digits.": "Enter up to three octal digits.",
"Error": "Errore",
"External File Versioning": "Controllo Versione Esterno",
"Failed Items": "Elementi Errati",
@@ -348,6 +349,7 @@
"Time the item was last modified": "Ora dell'ultima modifica degli elementi",
"Trash Can File Versioning": "Controllo Versione con Cestino",
"Type": "Tipo",
"UNIX Permissions": "UNIX Permissions",
"Unavailable": "Non disponibile",
"Unavailable/Disabled by administrator or maintainer": "Non disponibile/Disabilitato dall'amministratore o dal manutentore",
"Undecided (will prompt)": "Non deciso (verrà richiesto)",

View File

@@ -31,7 +31,7 @@
"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?",
"Are you sure you want to upgrade?": "Are you sure you want to upgrade?",
"Are you sure you want to upgrade?": "アップグレードしてよろしいですか?",
"Auto Accept": "自動承諾",
"Automatic Crash Reporting": "Automatic Crash Reporting",
"Automatic upgrade now offers the choice between stable releases and release candidates.": "自動アップグレードは、安定版とリリース候補版のいずれかを選べるようになりました。",
@@ -106,6 +106,7 @@
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "アドレスを指定する場合は「tcp://IPアドレス:ポート, tcp://ホスト名:ポート」のようにコンマで区切って入力してください。自動探索を行う場合は「dynamic」と入力してください。",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.",
"Enter ignore patterns, one per line.": "無視するファイル名のパターンを、一行につき一条件で入力してください。",
"Enter up to three octal digits.": "Enter up to three octal digits.",
"Error": "エラー",
"External File Versioning": "外部バージョン管理",
"Failed Items": "失敗した項目",
@@ -266,8 +267,8 @@
"Select All": "すべて選択",
"Select a version": "バージョンを選択してください",
"Select additional devices to share this folder with.": "このフォルダの共有に追加したいデバイスがある場合は、当該デバイスを選択してください。",
"Select latest version": "Select latest version",
"Select oldest version": "Select oldest 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": "送受信",
@@ -348,6 +349,7 @@
"Time the item was last modified": "Time the item was last modified",
"Trash Can File Versioning": "ゴミ箱によるバージョン管理",
"Type": "タイプ",
"UNIX Permissions": "UNIX Permissions",
"Unavailable": "Unavailable",
"Unavailable/Disabled by administrator or maintainer": "Unavailable/Disabled by administrator or maintainer",
"Undecided (will prompt)": "未決定(再確認する)",
@@ -372,9 +374,9 @@
"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.": "古いバージョンは、最大保存日数もしくは期間ごとの最大保存数を超えた場合、自動的に削除されます。",
"Waiting to Scan": "Waiting to Scan",
"Waiting to Sync": "Waiting to Sync",
"Waiting to scan": "Waiting to scan",
"Waiting to Scan": "スキャンの待機中",
"Waiting to Sync": "同期の待機中",
"Waiting to scan": "スキャンの待機中",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "警告: 入力されたパスは、設定済みのフォルダー「{{otherFolder}}」の親ディレクトリです。",
"Warning, this path is a parent directory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "警告: 入力されたパスは、設定済みのフォルダー「{{otherFolderLabel}}」 ({{otherFolder}}) の親ディレクトリです。",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "警告: 入力されたパスは、設定済みのフォルダー「{{otherFolder}}」のサブディレクトリです。",

View File

@@ -106,6 +106,7 @@
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "주소 자동 검색을 하기 위해서는 \"ip:port\" 형식의 주소들을 쉼표로 구분해서 입력하거나 \"dynamic\"을 입력하세요.",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.",
"Enter ignore patterns, one per line.": "무시할 패턴을 한 줄에 하나씩 입력하세요.",
"Enter up to three octal digits.": "Enter up to three octal digits.",
"Error": "오류",
"External File Versioning": "외부 파일 버전 관리",
"Failed Items": "실패한 항목",
@@ -348,6 +349,7 @@
"Time the item was last modified": "항목이 마지막으로 수정 된 시간",
"Trash Can File Versioning": "휴지통을 통한 파일 버전 관리",
"Type": "종류",
"UNIX Permissions": "UNIX Permissions",
"Unavailable": "불가능",
"Unavailable/Disabled by administrator or maintainer": "운영자 또는 관리자에 의해 불가능/비활성화 됨",
"Undecided (will prompt)": "Undecided (will prompt)",

View File

@@ -106,6 +106,7 @@
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Įveskite kableliais atskirtus (\"tcp://ip:prievadas\", \"tcp://serveris:prievadas\") adresus arba \"dynamic\", kad atliktumėte automatinį adresų aptikimą.",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Įveskite kableliais atskirtus (\"tcp://ip:prievadas\", \"tcp://serveris:prievadas\") adresus arba \"dynamic\", kad atliktumėte automatinį adresų atlikimą.",
"Enter ignore patterns, one per line.": "Suveskite nepaisomus šablonus, kiekvieną naujoje eilutėje.",
"Enter up to three octal digits.": "Įveskite iki trijų aštuntainių skaitmenų.",
"Error": "Klaida",
"External File Versioning": "Išorinis versijų valdymas",
"Failed Items": "Nepavykę siuntimai",
@@ -348,6 +349,7 @@
"Time the item was last modified": "Laikas, kai elementas buvo paskutinį kartą modifikuotas",
"Trash Can File Versioning": "Šiukšliadėžės versijų valdymas",
"Type": "Tipas",
"UNIX Permissions": "UNIX leidimai",
"Unavailable": "Neprieinama",
"Unavailable/Disabled by administrator or maintainer": "Neprieinama/Išjungta administratoriaus ar prižiūrėtojo",
"Undecided (will prompt)": "Nenuspręsta (bus klausiama)",

View File

@@ -106,6 +106,7 @@
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Skriv inn kommaseparerte (\"tcp://ip:port\", \"tcp://host:port\") adresser, eller ordet \"dynamic\" for å gjøre automatisk oppslag i adressen.",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.",
"Enter ignore patterns, one per line.": "Skriv inn mønster som skal utelates, ett per linje.",
"Enter up to three octal digits.": "Enter up to three octal digits.",
"Error": "Feilmelding",
"External File Versioning": "Ekstern versjonskontroll",
"Failed Items": "Elementsynkronisering som har mislyktes",
@@ -348,6 +349,7 @@
"Time the item was last modified": "Tidspunktet elementet sist ble endret",
"Trash Can File Versioning": "Papirkurv versjonskontroll",
"Type": "Type",
"UNIX Permissions": "UNIX Permissions",
"Unavailable": "Utilgjengelig",
"Unavailable/Disabled by administrator or maintainer": "Utilgjengelig/avskrudd av administrator eller vedlikeholder",
"Undecided (will prompt)": "Ikke bestemt (vil spørre)",
@@ -372,7 +374,7 @@
"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.",
"Waiting to Scan": "Waiting to Scan",
"Waiting to Scan": "Venter på å starte gjennomsøkning",
"Waiting to Sync": "Waiting to Sync",
"Waiting to scan": "Venter på å starte gjennomsøkning",
"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

@@ -106,6 +106,7 @@
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Voer door komma's gescheiden (\"tcp://ip:port\", \"tcp://host:port\") adressen in of \"dynamic\" om automatische ontdekking van het adres uit te voeren.",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Voer door komma's gescheiden (\"tcp://ip:port\", \"tcp://host:port\") adressen in of \"dynamic\" om automatische ontdekking van het adres uit te voeren.",
"Enter ignore patterns, one per line.": "Negeerpatronen invoeren, één per regel.",
"Enter up to three octal digits.": "Voer tot drie octale cijfers in.",
"Error": "Fout",
"External File Versioning": "Extern versiebeheer",
"Failed Items": "Mislukte items",
@@ -348,6 +349,7 @@
"Time the item was last modified": "Tijdstip waarop het item laatst gewijzigd is",
"Trash Can File Versioning": "Versiebeheer prullenbak",
"Type": "Type",
"UNIX Permissions": "UNIX-machtigingen",
"Unavailable": "Niet beschikbaar",
"Unavailable/Disabled by administrator or maintainer": "Niet beschikbaar of uitgeschakeld door administrator of beheerder",
"Undecided (will prompt)": "Onbeslist (zal bevestiging vragen)",

View File

@@ -106,6 +106,7 @@
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Wpisz oddzielone przecinkiem adresy (\"tcp://ip:port\", \"tcp://host:port\") lub \"dynamic\" by przeprowadzić automatyczne odnalezienie adresu.",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Wprowadź adresy oddzielone przecinkiem (\"tcp://ip:port\", \"tcp://host:port\") lub \"dynamic\" celem automatycznego odkrycia adresu.",
"Enter ignore patterns, one per line.": "Wprowadź wzorce ignorowania, jeden w każdej linii.",
"Enter up to three octal digits.": "Enter up to three octal digits.",
"Error": "Błąd",
"External File Versioning": "Zewnętrzne wersjonowanie pliku",
"Failed Items": "Niepowodzenia",
@@ -348,6 +349,7 @@
"Time the item was last modified": "Czas ostatniej modyfikacji elementu",
"Trash Can File Versioning": "Kontrola werjsi plików w koszu",
"Type": "Typ",
"UNIX Permissions": "UNIX Permissions",
"Unavailable": "Niedostępne",
"Unavailable/Disabled by administrator or maintainer": "Niedostępne/Wyłączone przez administratora lub opiekuna",
"Undecided (will prompt)": "Jeszcze nie zdecydowałem (przypomnij później)",

View File

@@ -106,6 +106,7 @@
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Insira endereços (\"tcp://ip:porta\", \"tcp://host:porta\") separados por vírgula ou \"dynamic\" para executar a descoberta automática do endereço.",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.",
"Enter ignore patterns, one per line.": "Insira os filtros, um por linha.",
"Enter up to three octal digits.": "Enter up to three octal digits.",
"Error": "Erro",
"External File Versioning": "Externo",
"Failed Items": "Itens com falha",
@@ -348,6 +349,7 @@
"Time the item was last modified": "Momento em que o item foi modificado pela última vez",
"Trash Can File Versioning": "Lixeira",
"Type": "Tipo",
"UNIX Permissions": "UNIX Permissions",
"Unavailable": "Não disponível",
"Unavailable/Disabled by administrator or maintainer": "Não disponível ou desabilitado pelo administrador ou mantenedor",
"Undecided (will prompt)": "Não tenho certeza (perguntar sempre)",

View File

@@ -106,6 +106,7 @@
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Introduza endereços separados por vírgulas (\"tcp://ip:porto\", \"tcp://máquina:porto\") ou \"dynamic\" para detectar automaticamente os endereços.",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Introduza endereços separados por vírgulas (\"tcp://ip:porto\", \"tcp://máquina:porto\") ou \"dynamic\" para detectar automaticamente os endereços.",
"Enter ignore patterns, one per line.": "Escreva os padrões de exclusão, um por linha.",
"Enter up to three octal digits.": "Insira de um a três dígitos em octal.",
"Error": "Erro",
"External File Versioning": "Externa",
"Failed Items": "Itens que falharam",
@@ -348,6 +349,7 @@
"Time the item was last modified": "Quando o item foi modificado pela última vez",
"Trash Can File Versioning": "Reciclagem",
"Type": "Tipo",
"UNIX Permissions": "Permissões UNIX",
"Unavailable": "Indisponível",
"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)",

View File

@@ -106,6 +106,7 @@
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Введите через запятую («tcp://ip:port», «tcp://host:port») адреса, либо «dynamic», чтобы выполнить автоматическое обнаружение адреса.",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Введите через запятую («tcp://ip:port», «tcp://host:port») адреса, либо «dynamic», чтобы выполнить автоматическое обнаружение адреса.",
"Enter ignore patterns, one per line.": "Введите шаблоны игнорирования, по одному на строку.",
"Enter up to three octal digits.": "Enter up to three octal digits.",
"Error": "Ошибка",
"External File Versioning": "Внешний контроль версий файлов",
"Failed Items": "Сбои",
@@ -348,6 +349,7 @@
"Time the item was last modified": "Время последней модификации объекта",
"Trash Can File Versioning": "Использовать версионность для файлов в Корзине",
"Type": "Тип",
"UNIX Permissions": "UNIX Permissions",
"Unavailable": "Недоступно",
"Unavailable/Disabled by administrator or maintainer": "Недоступно или отключено администратором",
"Undecided (will prompt)": "Не определено (запрашивать каждый раз)",

View File

@@ -106,6 +106,7 @@
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Zadaj čiarkou oddelené (\"tcp://ip:port\", \"tcp://host:port\") adresy alebo \"dynamic\" na automatické zistenie adresy.",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.",
"Enter ignore patterns, one per line.": "Zadaj ignorované vzory, jeden na riadok.",
"Enter up to three octal digits.": "Enter up to three octal digits.",
"Error": "Chyba",
"External File Versioning": "Externé spracovanie verzií súborov",
"Failed Items": "Zlyhané položky",
@@ -348,6 +349,7 @@
"Time the item was last modified": "Čas poslednej zmeny položky",
"Trash Can File Versioning": "Verzie súborov v koši",
"Type": "Typ",
"UNIX Permissions": "UNIX Permissions",
"Unavailable": "Nedostupné",
"Unavailable/Disabled by administrator or maintainer": "Nedostupné/Zakázané administrátorom alebo správcom",
"Undecided (will prompt)": "Undecided (will prompt)",

View File

@@ -12,7 +12,7 @@
"Add Remote Device": "Lägg till fjärrenhet",
"Add devices from the introducer to our device list, for mutually shared folders.": "Lägg enheter från introduktören till vår enhetslista för ömsesidigt delade mappar.",
"Add new folder?": "Lägg till ny mapp?",
"Additionally the full rescan interval will be increased (times 60, i.e. new default of 1h). You can also configure it manually for every folder later after choosing No.": "Dessutom kommer det fullständiga återkommande uppdateringsintervallet att höjas (60 gånger, d.v.s. ny standard på 1h). Du kan också konfigurera det manuellt för varje mapp senare efter att du valt Nej.",
"Additionally the full rescan interval will be increased (times 60, i.e. new default of 1h). You can also configure it manually for every folder later after choosing No.": "Dessutom kommer det fullständiga återkommande skanningsintervallet att höjas (60 gånger, d.v.s. ny standard på 1h). Du kan också konfigurera det manuellt för varje mapp senare efter att du valt Nej.",
"Address": "Adress",
"Addresses": "Adresser",
"Advanced": "Avancerat",
@@ -53,7 +53,7 @@
"Connection Error": "Anslutningsproblem",
"Connection Type": "Anslutningstyp",
"Connections": "Anslutningar",
"Continuously watching for changes is now available within Syncthing. This will detect changes on disk and issue a scan on only the modified paths. The benefits are that changes are propagated quicker and that less full scans are required.": "Kontinuerligt utkik efter ändringar är nu tillgängligt för Syncthing. Detta kommer att upptäcka ändringar på disken och utfärda en uppdatering på endast de ändrade sökvägarna. Fördelarna är att förändringar sprids snabbare och att mindre fullständiga uppdateringar krävs.",
"Continuously watching for changes is now available within Syncthing. This will detect changes on disk and issue a scan on only the modified paths. The benefits are that changes are propagated quicker and that less full scans are required.": "Kontinuerligt utkik efter ändringar är nu tillgängligt för Syncthing. Detta kommer att upptäcka ändringar på disken och utfärda en skanning på endast de ändrade sökvägarna. Fördelarna är att förändringar sprids snabbare och att mindre fullständiga skanningar krävs.",
"Copied from elsewhere": "Kopierat från annanstans",
"Copied from original": "Kopierat från original",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 följande bidragare:",
@@ -77,9 +77,9 @@
"Devices": "Enheter",
"Disable Crash Reporting": "Inaktivera kraschrapportering",
"Disabled": "Inaktiverad",
"Disabled periodic scanning and disabled watching for changes": "Inaktiverad periodisk uppdatering och inaktiverad spaning efter ändringar",
"Disabled periodic scanning and enabled watching for changes": "Inaktiverad periodisk uppdatering och aktiverad spaning efter ändringar",
"Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:": "Inaktiverad periodisk uppdatering och misslyckad att ställa in spaning efter ändringar, försök igen varje 1m:",
"Disabled periodic scanning and disabled watching for changes": "Inaktiverad periodisk skanning och inaktiverad övervakning av ändringar",
"Disabled periodic scanning and enabled watching for changes": "Inaktiverad periodisk skanning och aktiverad övervakning av ändringar",
"Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:": "Inaktiverad periodisk skanning och misslyckades att ställa in övervakning av ändringar, försök igen varje 1m:",
"Discard": "Kassera",
"Disconnected": "Frånkopplad",
"Discovered": "Upptäckt",
@@ -106,6 +106,7 @@
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Ange kommaseparerade (\"tcp://ip:port\", \"tcp://host:port\")-adresser eller ordet \"dynamic\" för att använda automatisk uppslagning.",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Ange kommaseparerade adresser (\"tcp://ip:port\", \"tcp://host:port\") eller \"dynamic\" för att utföra automatisk upptäckt av adressen.",
"Enter ignore patterns, one per line.": "Ange mönster att ignorera, en per rad.",
"Enter up to three octal digits.": "Ange upp till tre oktala siffror.",
"Error": "Fel",
"External File Versioning": "Extern filversionshantering",
"Failed Items": "Misslyckade objekt",
@@ -132,7 +133,7 @@
"Folder Type": "Mapptyp",
"Folders": "Mappar",
"For the following folders an error occurred while starting to watch for changes. It will be retried every minute, so the errors might go away soon. If they persist, try to fix the underlying issue and ask for help if you can't.": "För följande mappar uppstod ett fel när du började bevaka ändringar. Det kommer att omförsökas varje minut, så felen kan försvinna snart. Om de fortsätter, försök att åtgärda det underliggande problemet och fråga om hjälp om du inte kan.",
"Full Rescan Interval (s)": "Fullständig(a) återkommande uppdateringsintervall(er)",
"Full Rescan Interval (s)": "Fullständig(a) återkommande skanningsintervall(er)",
"GUI": "Grafiskt gränssnitt",
"GUI Authentication Password": "Gränssnittets autentiseringslösenord",
"GUI Authentication User": "Gränssnittets autentiseringsanvändare",
@@ -162,7 +163,7 @@
"Keep Versions": "Behåll versioner",
"Largest First": "Största först",
"Last File Received": "Senaste fil mottagen",
"Last Scan": "Senaste uppdatering",
"Last Scan": "Senaste skanning",
"Last seen": "Senast sedd",
"Later": "Senare",
"Latest Change": "Senaste ändring",
@@ -190,7 +191,7 @@
"Mod. Device": "Enhet som utförde ändring",
"Mod. Time": "Tid för ändring",
"Move to top of queue": "Flytta till överst i kön",
"Multi level wildcard (matches multiple directory levels)": "Jokertecken som representerar noll eller fler godtyckliga tecken, även över kataloggränser.",
"Multi level wildcard (matches multiple directory levels)": "Flernivå jokertecken (matchar flera mappnivåer)",
"Never": "Aldrig",
"New Device": "Ny enhet",
"New Folder": "Ny mapp",
@@ -213,20 +214,20 @@
"Path": "Sökväg",
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Sökväg till mappen på din dator. Kommer att skapas om det inte finns. Tecknet tilde (~) kan användas som en genväg för",
"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%}.": "Sökvägen där nya automatiskt accepterade mappar kommer att skapas, liksom den föreslagna sökvägen när du lägger till nya mappar via gränssnittet. Tecknet tilde (~) expanderar till {{tilde}}.",
"Path where versions should be stored (leave empty for the default .stversions directory in the shared folder).": "Sökväg där versioner ska lagras (lämna tomt för standard .stversions-mappen i den delade katalogen).",
"Path where versions should be stored (leave empty for the default .stversions directory in the shared folder).": "Sökväg där versioner ska lagras (lämna tomt för standard .stversions-mappen i den delade mappen).",
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Sökväg där versioner sparas (lämna tomt för att använda standard .stversions-mappen i mappen).",
"Pause": "Paus",
"Pause": "Pausa",
"Pause All": "Pausa alla",
"Paused": "Pausad",
"Pending changes": "Väntar på ändringar",
"Periodic scanning at given interval and disabled watching for changes": "Periodisk uppdatering i givet intervall och inaktiverad spaning efter ändringar",
"Periodic scanning at given interval and enabled watching for changes": "Periodisk uppdatering i givet intervall och aktiverad spaning efter ändringar",
"Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:": "Periodisk uppdatering i givet intervall och misslyckades med att ställa in utkik efter ändringar, försök igen var 1m:",
"Periodic scanning at given interval and disabled watching for changes": "Periodisk skanning i givet intervall och inaktiverad övervakning av ändringar",
"Periodic scanning at given interval and enabled watching for changes": "Periodisk skanning i givet intervall och aktiverad övervakning av ändringar",
"Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:": "Periodisk skanning i givet intervall och misslyckades med att ställa in utkik efter ändringar, försök igen var 1m:",
"Permissions": "Behörigheter",
"Please consult the release notes before performing a major upgrade.": "Läs igenom versionsnyheterna innan den stora uppgraderingen.",
"Please set a GUI Authentication User and Password in the Settings dialog.": "Ställ in ett grafiska gränssnittets användarautentisering och lösenord i inställningsdialogrutan.",
"Please wait": "Var god vänta",
"Prefix indicating that the file can be deleted if preventing directory removal": "Prefix som indikerar att filen kan raderas om det förhindrar radering av katalog",
"Prefix indicating that the file can be deleted if preventing directory removal": "Prefix som indikerar att filen kan tas bort om det förhindrar mappborttagning",
"Prefix indicating that the pattern should be matched without case sensitivity": "Prefix som indikerar att mönstret ska matchas utan skiftlägeskänslighet",
"Preparing to Sync": "Förberedelser för synkronisering",
"Preview": "Förhandsgranska",
@@ -244,10 +245,10 @@
"Remove Device": "Ta bort enhet",
"Remove Folder": "Ta bort mapp",
"Required identifier for the folder. Must be the same on all cluster devices.": "Krävs identifierare för mappen. Måste vara densamma på alla kluster enheter.",
"Rescan": "Uppdatera igen",
"Rescan All": "Uppdatera alla igen",
"Rescan Interval": "Återkommande uppdateringsintervall",
"Rescans": "Återkommande uppdateringar",
"Rescan": "Skanna igen",
"Rescan All": "Skanna alla igen",
"Rescan Interval": "Återkommande skanningsintervall",
"Rescans": "Återkommande skanningar",
"Restart": "Starta om",
"Restart Needed": "Omstart behövs",
"Restarting": "Startar om",
@@ -259,8 +260,8 @@
"Revert Local Changes": "Återställ lokala ändringar",
"Running": "Körs",
"Save": "Spara",
"Scan Time Remaining": "Återstående uppdateringstid",
"Scanning": "Uppdaterar",
"Scan Time Remaining": "Återstående skanningstid",
"Scanning": "Skannar",
"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 All": "Markera alla",
@@ -321,7 +322,7 @@
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Enhets-ID som behövs här kan du hitta i \"Åtgärder > Visa ID\"-dialogrutan på den andra enheten. Mellanrum och bindestreck är valfria (ignoreras).",
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Den krypterade användarstatistiken skickas dagligen. Den används för att spåra vanliga plattformar, mappstorlekar och versioner. Om datat som rapporteras ändras så kommer du att bli tillfrågad igen.",
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Det inmatade enhets-ID:t verkar inte vara korrekt. Det ska vara en 52 eller 56 teckensträng bestående av siffror och bokstäver, eventuellt med mellanrum och bindestreck.",
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "Den första kommandoparametern är sökvägen till mappen och den andra parametern är den relativa sökvägen i katalogen.",
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "Den första kommandoparametern är sökvägen till mappen och den andra parametern är den relativa sökvägen i mappen.",
"The folder ID cannot be blank.": "Mapp-ID får inte vara tomt.",
"The folder ID must be unique.": "Mapp-ID måste vara unik.",
"The folder path cannot be blank.": "Mappsökvägen kan inte vara tom.",
@@ -348,6 +349,7 @@
"Time the item was last modified": "Tidpunkten objektet var senast ändrad",
"Trash Can File Versioning": "Papperskorgs filversionshantering",
"Type": "Typ",
"UNIX Permissions": "UNIX-behörigheter",
"Unavailable": "Inte tillgänglig",
"Unavailable/Disabled by administrator or maintainer": "Inte tillgänglig/inaktiverad av administratör eller underhållare",
"Undecided (will prompt)": "Obeslutad (kommer att skriva)",
@@ -372,17 +374,17 @@
"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 raderas automatiskt när de är äldre än den maximala åldersgränsen eller överstiger frekvensen i intervallet.",
"Waiting to Scan": "Waiting to Scan",
"Waiting to Sync": "Waiting to Sync",
"Waiting to scan": "Väntar på uppdatering",
"Waiting to Scan": "Väntar på att skanna",
"Waiting to Sync": "Väntar på att synkronisera",
"Waiting to scan": "Väntar på att skanna",
"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}}\".",
"Warning, this path is a parent directory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Varning, denna sökväg är en överordnad mapp av en befintlig mapp \"{{otherFolderLabel}}\" ({{otherFolder}}).",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Varning, denna sökväg är en underkatalog till en befintlig mapp \"{{otherFolder}}\".",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Varning, denna sökväg är en undermapp till en befintlig mapp \"{{otherFolder}}\".",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Varning, denna sökväg är en undermapp av en befintlig mapp \"{{otherFolderLabel}}\" ({{otherFolder}}).",
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "Varning: Om du använder en extern bevakare som {{syncthingInotify}}, bör du se till att den är inaktiverad.",
"Watch for Changes": "Håll utkik efter ändringar",
"Watching for Changes": "Håller utkik efter ändringar",
"Watching for changes discovers most changes without periodic scanning.": "Hålla utkik efter ändringar upptäcker de flesta förändringar utan periodisk uppdatering.",
"Watching for changes discovers most changes without periodic scanning.": "Hålla utkik efter ändringar upptäcker de flesta förändringar utan periodisk skanning.",
"When adding a new device, keep in mind that this device must be added on the other side too.": "När du lägger till en ny enhet, kom ihåg att den här enheten måste läggas till på den andra enheten också.",
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "När du lägger till ny mapp, tänk på att mapp-ID knyter ihop mappar mellan olika enheter. De skiftlägeskänsliga och måste matcha precis mellan alla enheter.",
"Yes": "Ja",
@@ -394,7 +396,7 @@
"You have unsaved changes. Do you really want to discard them?": "Du har ändringar som inte sparats. Vill du verkligen kassera dem?",
"You must keep at least one version.": "Du måste behålla åtminstone en version.",
"days": "dagar",
"directories": "kataloger",
"directories": "mappar",
"files": "filer",
"full documentation": "fullständig dokumentation",
"items": "objekt",

View File

@@ -106,6 +106,7 @@
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Введіть розділені комою (\"tcp://ip:port\", \"tcp://host:port\") адреси або \"dynamic\" для автоматичного визначення адреси.",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.",
"Enter ignore patterns, one per line.": "Введіть шаблони ігнорування, по одному на рядок.",
"Enter up to three octal digits.": "Enter up to three octal digits.",
"Error": "Помилка",
"External File Versioning": "Зовнішне керування версіями",
"Failed Items": "Невдалі",
@@ -348,6 +349,7 @@
"Time the item was last modified": "Час останньої зміни елемента:",
"Trash Can File Versioning": "Версіонування файлів у кошику ",
"Type": "Тип",
"UNIX Permissions": "UNIX Permissions",
"Unavailable": "Недоступно",
"Unavailable/Disabled by administrator or maintainer": "Недоступно/заборонено адміністратором або куратором",
"Undecided (will prompt)": "Невизначено (буде запитано)",

View File

@@ -10,7 +10,7 @@
"Add Device": "添加设备",
"Add Folder": "添加文件夹",
"Add Remote Device": "添加远程设备",
"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?": "添加新文件夹?",
"Additionally the full rescan interval will be increased (times 60, i.e. new default of 1h). You can also configure it manually for every folder later after choosing No.": "另外,完整重新扫描的间隔将增大(时间 60以新的默认 1 小时为例)。你也可以在选择“否”后手动配置每个文件夹的时间。",
"Address": "地址",
@@ -27,7 +27,7 @@
"An external command handles the versioning. It has to remove the file from the synced folder.": "外部命令接管了版本控制。该外部命令必须自行从同步文件夹中删除该文件。",
"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.": "在中介设备上添加的任何“远程设备”,也会被自动添加到本机的“远程设备”列表。",
"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}} 个文件吗?",
@@ -37,7 +37,7 @@
"Automatic upgrade now offers the choice between stable releases and release candidates.": "自动升级现在提供了稳定版本和候选发布版的选项。",
"Automatic upgrades": "自动升级",
"Automatic upgrades are always enabled for candidate releases.": "候选发布版会一直启用自动升级。",
"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.": "在本机默认文件夹中,自动地创建或共享这个设备共享出来的所有文件夹。",
"Available debug logging facilities:": "可用的调试日志功能:",
"Be careful!": "小心!",
"Bugs": "问题回报",
@@ -106,6 +106,7 @@
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "输入以半角逗号分隔的 (\"tcp://ip:port\", \"tcp://host:port\") 设备地址列表,或者输入 \"dynamic\" 以自动发现设备地址。",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "输入以半角逗号分隔的(\"tcp://ip:port\", \"tcp://host:port\"设备地址列表或者输入“dynamic”以自动发现设备地址。",
"Enter ignore patterns, one per line.": "请输入忽略模式,每行一条。",
"Enter up to three octal digits.": "最多输入三个8进制数字",
"Error": "错误",
"External File Versioning": "外部版本控制",
"Failed Items": "失败的项目",
@@ -119,7 +120,7 @@
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "当文件被 Syncthing 替换或删除时,将被移动到 .stversions 文件夹。",
"Files are moved to date stamped versions in a .stversions directory when replaced or deleted by Syncthing.": "当某个文件在其他设备被替换或删除时,本设备将在 .stversions 目录中保留该文件的备份,并在文件名中加入时间戳信息。",
"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.": "在其它设备中对该文件夹内文件的修改并不会被同步到本机,但是在本机上对其的修改,则会被同步到其它设备。",
"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 synchronized from the cluster, but any changes made locally will not be sent to other devices.": "文件将从集群同步,但本地所作的任何更改都不会被发送到其他设备。",
"Filesystem Notifications": "文件系统通知",
"Filesystem Watcher Errors": "文件系统监视器错误",
@@ -227,7 +228,7 @@
"Please set a GUI Authentication User and Password in the Settings dialog.": "请在设置对话框中设置 GUI 验证用户及其密码。",
"Please wait": "请稍候",
"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": "此前缀表示,后面的模式匹配时不区分大小写",
"Preparing to Sync": "准备同步",
"Preview": "预览",
"Preview Usage Report": "预览使用报告",
@@ -243,7 +244,7 @@
"Remove": "移除",
"Remove Device": "移除设备",
"Remove Folder": "移除文件夹",
"Required identifier for the folder. Must be the same on all cluster devices.": "需要给文件夹设置标识。所有设备上必须一致。",
"Required identifier for the folder. Must be the same on all cluster devices.": "必需的文件夹唯一标识。同一个文件夹在集群中的所有设备上ID必须相同。",
"Rescan": "重新扫描",
"Rescan All": "全部重新扫描",
"Rescan Interval": "扫描间隔",
@@ -283,8 +284,8 @@
"Show ID": "显示 ID",
"Show QR": "显示 QR 码",
"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.": "在设备丛中,显示该名称,而不是设备 ID。会作为一个可选的默认名称被发送到其他设备。",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "在设备丛中,将会显示名称,而不是设备 ID。如果设置为空则会使用目标设备提供的默认名称。",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "在集群状态中显示该名称,而不是设备 ID。会作为当前设备的可选的默认名称,报告给所有其他设备。",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "在集群状态中显示名称,而不是设备 ID。如果设置为空则会使用目标设备自报的默认名称。",
"Shutdown": "关闭 Syncthing",
"Shutdown Complete": "关闭完成",
"Simple File Versioning": "简易版本控制",
@@ -348,6 +349,7 @@
"Time the item was last modified": "该项最近修改的时间",
"Trash Can File Versioning": "回收站式版本控制",
"Type": "类型",
"UNIX Permissions": "UNIX权限",
"Unavailable": "无效",
"Unavailable/Disabled by administrator or maintainer": "无效/禁用(由管理员或维护者)",
"Undecided (will prompt)": "待定(将提示)",
@@ -372,8 +374,8 @@
"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.": "超过最长保留时间,或者不满足下列条件的历史版本,将会被删除。",
"Waiting to Scan": "Waiting to Scan",
"Waiting to Sync": "Waiting to Sync",
"Waiting to Scan": "等待扫描",
"Waiting to Sync": "等待同步",
"Waiting to scan": "等待扫描",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "警告,该路径是已有文件夹\"{{otherFolder}}\"的上级目录。",
"Warning, this path is a parent directory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "警告,该路径是已有文件夹\"{{otherFolderLabel}}\" ({{otherFolder}})的上级目录。",
@@ -383,7 +385,7 @@
"Watch for Changes": "监视更改",
"Watching for Changes": "正在监视更改",
"Watching for changes discovers most changes without periodic scanning.": "对更改的监视无需定期扫描就可以发现大多数更改。",
"When adding a new device, keep in mind that this device must be added on the other side too.": "若您在本机添加新设备,记住您也必须在这个设备上添加本机。",
"When adding a new device, keep in mind that this device must be added on the other side too.": "若您在本机添加新设备,记住您也必须在这个设备上添加本机。",
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "若你添加了新文件夹,记住文件夹 ID 是用以在不同设备间建立联系的。在不同设备间拥有相同 ID 的文件夹将会被同步。且文件夹 ID 区分大小写。",
"Yes": "是",
"You can also select one of these nearby devices:": "您也可以从这些附近的设备中选择:",

View File

@@ -106,6 +106,7 @@
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "輸入以半形逗號區隔的位址 (\"tcp://ip:port\", \"tcp://host:port\"),或輸入 \"dynamic\" 以進行位址的自動探索。",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.",
"Enter ignore patterns, one per line.": "輸入忽略樣式,每行一種。",
"Enter up to three octal digits.": "Enter up to three octal digits.",
"Error": "錯誤",
"External File Versioning": "外部的檔案版本控制",
"Failed Items": "失敗的項目",
@@ -348,6 +349,7 @@
"Time the item was last modified": "前次修改時間",
"Trash Can File Versioning": "垃圾筒式檔案版本控制",
"Type": "類型",
"UNIX Permissions": "UNIX Permissions",
"Unavailable": "無法使用",
"Unavailable/Disabled by administrator or maintainer": "無法使用 / 被系統管理員或維護者停用",
"Undecided (will prompt)": "未決定(將會提示)",

View File

@@ -604,14 +604,6 @@
</span>
</td>
</tr>
<tr>
<th><span class="fas fa-fw fa-microchip"></span>&nbsp;<span translate>RAM Utilization</span></th>
<td class="text-right">{{system.sys | binary}}B</td>
</tr>
<tr>
<th><span class="fas fa-fw fa-tachometer-alt"></span>&nbsp;<span translate>CPU Utilization</span></th>
<td class="text-right">{{system.cpuPercent | alwaysNumber | percent}}</td>
</tr>
<tr>
<th><span class="fas fa-fw fa-sitemap"></span>&nbsp;<span translate>Listeners</span></th>
<td class="text-right">

View File

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

View File

@@ -2491,4 +2491,11 @@ angular.module('syncthing.core')
$scope.config.options.crashReportingEnabled = enabled;
$scope.saveConfig();
};
$scope.isUnixAddress = function (address) {
return address != null &&
(address.startsWith('/') ||
address.startsWith('unix://') ||
address.startsWith('unixs://'));
}
});

View File

@@ -1,4 +1,4 @@
<modal id="editDevice" status="default" icon="{{editingExisting ? 'fas fa-pencil-alt' : 'fas fa-desktop'}}" heading="{{editingExisting ? 'Edit Device' : 'Add Device' | translate}}" large="yes" closeable="yes">
<modal id="editDevice" status="default" icon="{{editingExisting ? 'fas fa-pencil-alt' : 'fas fa-desktop'}}" heading="{{editingExisting ? 'Edit Device' : 'Add Device' | translate}} {{currentDevice.name}}" large="yes" closeable="yes">
<div class="modal-body">
<form role="form" name="deviceEditor">
<ul class="nav nav-tabs" ng-init="loadFormIntoScope(deviceEditor)">

View File

@@ -1,4 +1,4 @@
<modal id="editFolder" status="default" icon="{{editingExisting ? 'fas fa-pencil-alt' : 'fas fa-folder'}}" heading="{{editingExisting ? 'Edit Folder' : 'Add Folder' | translate}}" large="yes" closeable="yes">
<modal id="editFolder" status="default" icon="{{editingExisting ? 'fas fa-pencil-alt' : 'fas fa-folder'}}" heading="{{editingExisting ? 'Edit Folder' : 'Add Folder' | translate}} ({{folderLabel(currentFolder.id)}})" large="yes" closeable="yes">
<div class="modal-body">
<form role="form" name="folderEditor">
<ul class="nav nav-tabs" ng-init="loadFormIntoScope(folderEditor)">

View File

@@ -1,4 +1,4 @@
<modal id="restoreVersions" status="default" icon="fas fa-undo" heading="{{'Restore Versions' | translate}}" large="yes" closeable="yes">
<modal id="restoreVersions" status="default" icon="fas fa-undo" heading="{{'Restore Versions' | translate}} ({{folderLabel(restoreVersions.folder)}})" large="yes" closeable="yes">
<div class="modal-body">
<span translate ng-if="!restoreVersions.versions && !restoreVersions.errors">Loading data...</span>
<div ng-if="restoreVersions.versions">

View File

@@ -172,6 +172,13 @@
</div>
</div>
<div class="col-md-6">
<div ng-if="isUnixAddress(tmpGUI.address)" class="form-group" ng-class="{'has-error': settingsEditor.UnixSocketPermissions.$invalid && settingsEditor.UnixSocketPermissions.$dirty}">
<label translate>UNIX Permissions</label>
<input id="UnixSocketPermissions" name="UnixSocketPermissions" class="form-control" type="text" ng-model="tmpGUI.unixSocketPermissions" ng-pattern="/^0?[0-7]{0,3}$/" />
<p class="help-block" ng-show="settingsEditor.UnixSocketPermissions.$invalid" translate>
Enter up to three octal digits.
</p>
</div>
</div>
</div>
</div>

1
lib/api/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/testdata/config/csrftokens.txt

View File

@@ -81,7 +81,6 @@ type service struct {
fss model.FolderSummaryService
urService *ur.Service
systemConfigMut sync.Mutex // serializes posts to /rest/system/config
cpu Rater
contr Controller
noUpgrade bool
tlsDefaultCommonName string
@@ -95,10 +94,6 @@ type service struct {
systemLog logger.Recorder
}
type Rater interface {
Rate() float64
}
type Controller interface {
ExitUpgrading()
Restart()
@@ -111,7 +106,7 @@ type Service interface {
WaitForStart() error
}
func New(id protocol.DeviceID, cfg config.Wrapper, assetDir, tlsDefaultCommonName string, m model.Model, defaultSub, diskSub events.BufferedSubscription, evLogger events.Logger, discoverer discover.CachingMux, connectionsService connections.Service, urService *ur.Service, fss model.FolderSummaryService, errors, systemLog logger.Recorder, cpu Rater, contr Controller, noUpgrade bool) Service {
func New(id protocol.DeviceID, cfg config.Wrapper, assetDir, tlsDefaultCommonName string, m model.Model, defaultSub, diskSub events.BufferedSubscription, evLogger events.Logger, discoverer discover.CachingMux, connectionsService connections.Service, urService *ur.Service, fss model.FolderSummaryService, errors, systemLog logger.Recorder, contr Controller, noUpgrade bool) Service {
s := &service{
id: id,
cfg: cfg,
@@ -130,7 +125,6 @@ func New(id protocol.DeviceID, cfg config.Wrapper, assetDir, tlsDefaultCommonNam
systemConfigMut: sync.NewMutex(),
guiErrors: errors,
systemLog: systemLog,
cpu: cpu,
contr: contr,
noUpgrade: noUpgrade,
tlsDefaultCommonName: tlsDefaultCommonName,
@@ -187,6 +181,15 @@ func (s *service) getListener(guiCfg config.GUIConfiguration) (net.Listener, err
return nil, err
}
if guiCfg.Network() == "unix" && guiCfg.UnixSocketPermissions() != 0 {
// We should error if this fails under the assumption that these permissions are
// required for operation.
err = os.Chmod(guiCfg.Address(), guiCfg.UnixSocketPermissions())
if err != nil {
return nil, err
}
}
listener := &tlsutil.DowngradingListener{
Listener: rawListener,
TLSConfig: tlsCfg,
@@ -942,9 +945,7 @@ func (s *service) getSystemStatus(w http.ResponseWriter, r *http.Request) {
res["connectionServiceStatus"] = s.connectionsService.ListenerStatus()
res["lastDialStatus"] = s.connectionsService.ConnectionStatus()
// cpuUsage.Rate() is in milliseconds per second, so dividing by ten
// gives us percent
res["cpuPercent"] = s.cpu.Rate() / 10 / float64(runtime.NumCPU())
res["cpuPercent"] = 0 // deprecated from API
res["pathSeparator"] = string(filepath.Separator)
res["urVersionMax"] = ur.Version
res["uptime"] = s.urService.UptimeS()
@@ -1057,7 +1058,7 @@ func (s *service) getSupportBundle(w http.ResponseWriter, r *http.Request) {
}
// Report Data as a JSON
if usageReportingData, err := json.MarshalIndent(s.urService.ReportData(), "", " "); err != nil {
if usageReportingData, err := json.MarshalIndent(s.urService.ReportData(context.TODO()), "", " "); err != nil {
l.Warnln("Support bundle: failed to create versionPlatform.json:", err)
} else {
files = append(files, fileEntry{name: "usage-reporting.json.txt", data: usageReportingData})
@@ -1142,7 +1143,7 @@ func (s *service) getReport(w http.ResponseWriter, r *http.Request) {
if val, _ := strconv.Atoi(r.URL.Query().Get("version")); val > 0 {
version = val
}
sendJSON(w, s.urService.ReportDataPreview(version))
sendJSON(w, s.urService.ReportDataPreview(context.TODO(), version))
}
func (s *service) getRandomString(w http.ResponseWriter, r *http.Request) {

View File

@@ -107,7 +107,7 @@ func TestStopAfterBrokenConfig(t *testing.T) {
}
w := config.Wrap("/dev/null", cfg, events.NoopLogger)
srv := New(protocol.LocalDeviceID, w, "", "syncthing", nil, nil, nil, events.NoopLogger, nil, nil, nil, nil, nil, nil, nil, nil, false).(*service)
srv := New(protocol.LocalDeviceID, w, "", "syncthing", nil, nil, nil, events.NoopLogger, nil, nil, nil, nil, nil, nil, nil, false).(*service)
defer os.Remove(token)
srv.started = make(chan string)
@@ -522,12 +522,11 @@ func startHTTP(cfg *mockedConfig) (string, *suture.Supervisor, error) {
connections := new(mockedConnections)
errorLog := new(mockedLoggerRecorder)
systemLog := new(mockedLoggerRecorder)
cpu := new(mockedCPUService)
addrChan := make(chan string)
// Instantiate the API service
urService := ur.New(cfg, m, connections, false)
svc := New(protocol.LocalDeviceID, cfg, assetDir, "syncthing", m, eventSub, diskEventSub, events.NoopLogger, discoverer, connections, urService, &mockedFolderSummaryService{}, errorLog, systemLog, cpu, nil, false).(*service)
svc := New(protocol.LocalDeviceID, cfg, assetDir, "syncthing", m, eventSub, diskEventSub, events.NoopLogger, discoverer, connections, urService, &mockedFolderSummaryService{}, errorLog, systemLog, nil, false).(*service)
defer os.Remove(token)
svc.started = addrChan
@@ -543,7 +542,7 @@ func startHTTP(cfg *mockedConfig) (string, *suture.Supervisor, error) {
tcpAddr, err := net.ResolveTCPAddr("tcp", addr)
if err != nil {
supervisor.Stop()
return "", nil, fmt.Errorf("Weird address from API service: %v", err)
return "", nil, fmt.Errorf("weird address from API service: %w", err)
}
host, _, _ := net.SplitHostPort(cfg.gui.RawAddress)
@@ -1026,7 +1025,7 @@ func TestEventMasks(t *testing.T) {
cfg := new(mockedConfig)
defSub := new(mockedEventSub)
diskSub := new(mockedEventSub)
svc := New(protocol.LocalDeviceID, cfg, "", "syncthing", nil, defSub, diskSub, events.NoopLogger, nil, nil, nil, nil, nil, nil, nil, nil, false).(*service)
svc := New(protocol.LocalDeviceID, cfg, "", "syncthing", nil, defSub, diskSub, events.NoopLogger, nil, nil, nil, nil, nil, nil, nil, false).(*service)
defer os.Remove(token)
if mask := svc.getEventMask(""); mask != DefaultEventMask {

View File

@@ -1,13 +0,0 @@
// Copyright (C) 2017 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 api
type mockedCPUService struct{}
func (*mockedCPUService) Rate() float64 {
return 42
}

View File

@@ -7,6 +7,7 @@
package api
import (
"context"
"time"
"github.com/syncthing/syncthing/lib/discover"
@@ -26,7 +27,7 @@ func (m *mockedCachingMux) Stop() {
// from events.Finder
func (m *mockedCachingMux) Lookup(deviceID protocol.DeviceID) (direct []string, err error) {
func (m *mockedCachingMux) Lookup(ctx context.Context, deviceID protocol.DeviceID) (direct []string, err error) {
return nil, nil
}

View File

@@ -44,17 +44,29 @@ func writeBroadcasts(ctx context.Context, inbox <-chan []byte, port int) error {
return nil
}
addrs, err := net.InterfaceAddrs()
intfs, err := net.Interfaces()
if err != nil {
l.Debugln(err)
return err
}
var dsts []net.IP
for _, addr := range addrs {
if iaddr, ok := addr.(*net.IPNet); ok && len(iaddr.IP) >= 4 && iaddr.IP.IsGlobalUnicast() && iaddr.IP.To4() != nil {
baddr := bcast(iaddr)
dsts = append(dsts, baddr.IP)
for _, intf := range intfs {
if intf.Flags&net.FlagBroadcast == 0 {
continue
}
addrs, err := intf.Addrs()
if err != nil {
l.Debugln(err)
return err
}
for _, addr := range addrs {
if iaddr, ok := addr.(*net.IPNet); ok && len(iaddr.IP) >= 4 && iaddr.IP.IsGlobalUnicast() && iaddr.IP.To4() != nil {
baddr := bcast(iaddr)
dsts = append(dsts, baddr.IP)
}
}
}

View File

@@ -67,6 +67,10 @@ func writeMulticasts(ctx context.Context, inbox <-chan []byte, addr string) erro
success := 0
for _, intf := range intfs {
if intf.Flags&net.FlagMulticast == 0 {
continue
}
wcm.IfIndex = intf.Index
pconn.SetWriteDeadline(time.Now().Add(time.Second))
_, err = pconn.WriteTo(bs, wcm, gaddr)

View File

@@ -295,11 +295,11 @@ func (cfg *Configuration) clean() error {
}
if folder.Path == "" {
return fmt.Errorf("folder %q: %v", folder.ID, errFolderPathEmpty)
return fmt.Errorf("folder %q: %w", folder.ID, errFolderPathEmpty)
}
if _, ok := existingFolders[folder.ID]; ok {
return fmt.Errorf("folder %q: %v", folder.ID, errFolderIDDuplicate)
return fmt.Errorf("folder %q: %w", folder.ID, errFolderIDDuplicate)
}
existingFolders[folder.ID] = folder

View File

@@ -9,12 +9,14 @@ package config
import (
"net/url"
"os"
"strconv"
"strings"
)
type GUIConfiguration struct {
Enabled bool `xml:"enabled,attr" json:"enabled" default:"true"`
RawAddress string `xml:"address" json:"address" default:"127.0.0.1:8384"`
RawUnixSocketPermissions string `xml:"unixSocketPermissions,omitempty" json:"unixSocketPermissions"`
User string `xml:"user,omitempty" json:"user"`
Password string `xml:"password,omitempty" json:"password"`
AuthMode AuthMode `xml:"authMode,omitempty" json:"authMode"`
@@ -59,6 +61,15 @@ func (c GUIConfiguration) Address() string {
return c.RawAddress
}
func (c GUIConfiguration) UnixSocketPermissions() os.FileMode {
perm, err := strconv.ParseUint(c.RawUnixSocketPermissions, 8, 32)
if err != nil {
// ignore incorrectly formatted permissions
return 0
}
return os.FileMode(perm) & os.ModePerm
}
func (c GUIConfiguration) Network() string {
if override := os.Getenv("STGUIADDRESS"); strings.Contains(override, "/") {
url, err := url.Parse(override)

View File

@@ -7,7 +7,7 @@
package config
type LDAPConfiguration struct {
Address string `xml:"address,omitempty" json:"addresd"`
Address string `xml:"address,omitempty" json:"address"`
BindDN string `xml:"bindDN,omitempty" json:"bindDN"`
Transport LDAPTransport `xml:"transport,omitempty" json:"transport"`
InsecureSkipVerify bool `xml:"insecureSkipVerify,omitempty" json:"insecureSkipVerify" default:"false"`

View File

@@ -7,6 +7,8 @@
package connections
import (
"context"
"errors"
"net/url"
"testing"
@@ -167,3 +169,39 @@ func TestGetDialer(t *testing.T) {
}
}
}
func TestConnectionStatus(t *testing.T) {
s := newConnectionStatusHandler()
addr := "testAddr"
testErr := errors.New("testErr")
if stats := s.ConnectionStatus(); len(stats) != 0 {
t.Fatal("newly created connectionStatusHandler isn't empty:", len(stats))
}
check := func(in, out error) {
t.Helper()
s.setConnectionStatus(addr, in)
switch stat, ok := s.ConnectionStatus()[addr]; {
case !ok:
t.Fatal("entry missing")
case out == nil:
if stat.Error != nil {
t.Fatal("expected nil error, got", stat.Error)
}
case *stat.Error != out.Error():
t.Fatalf("expected %v error, got %v", out.Error(), *stat.Error)
}
}
check(nil, nil)
check(context.Canceled, nil)
check(testErr, testErr)
check(context.Canceled, testErr)
check(nil, nil)
}

View File

@@ -110,6 +110,8 @@ type ConnectionStatusEntry struct {
type service struct {
*suture.Supervisor
connectionStatusHandler
cfg config.Wrapper
myID protocol.DeviceID
model Model
@@ -127,9 +129,6 @@ type service struct {
listeners map[string]genericListener
listenerTokens map[string]suture.ServiceToken
listenerSupervisor *suture.Supervisor
connectionStatusMut sync.RWMutex
connectionStatus map[string]ConnectionStatusEntry // address -> latest error/status
}
func NewService(cfg config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *tls.Config, discoverer discover.Finder, bepProtocolName string, tlsDefaultCommonName string, evLogger events.Logger) Service {
@@ -140,6 +139,8 @@ func NewService(cfg config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *t
},
PassThroughPanics: true,
}),
connectionStatusHandler: newConnectionStatusHandler(),
cfg: cfg,
myID: myID,
model: mdl,
@@ -168,9 +169,6 @@ func NewService(cfg config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *t
FailureBackoff: 600 * time.Second,
PassThroughPanics: true,
}),
connectionStatusMut: sync.NewRWMutex(),
connectionStatus: make(map[string]ConnectionStatusEntry),
}
cfg.Subscribe(service)
@@ -360,6 +358,12 @@ func (s *service) connect(ctx context.Context) {
var seen []string
for _, deviceCfg := range cfg.Devices {
select {
case <-ctx.Done():
return
default:
}
deviceID := deviceCfg.DeviceID
if deviceID == s.myID {
continue
@@ -380,7 +384,7 @@ func (s *service) connect(ctx context.Context) {
for _, addr := range deviceCfg.Addresses {
if addr == "dynamic" {
if s.discoverer != nil {
if t, err := s.discoverer.Lookup(deviceID); err == nil {
if t, err := s.discoverer.Lookup(ctx, deviceID); err == nil {
addrs = append(addrs, t...)
}
}
@@ -696,7 +700,19 @@ func (s *service) ListenerStatus() map[string]ListenerStatusEntry {
return result
}
func (s *service) ConnectionStatus() map[string]ConnectionStatusEntry {
type connectionStatusHandler struct {
connectionStatusMut sync.RWMutex
connectionStatus map[string]ConnectionStatusEntry // address -> latest error/status
}
func newConnectionStatusHandler() connectionStatusHandler {
return connectionStatusHandler{
connectionStatusMut: sync.NewRWMutex(),
connectionStatus: make(map[string]ConnectionStatusEntry),
}
}
func (s *connectionStatusHandler) ConnectionStatus() map[string]ConnectionStatusEntry {
result := make(map[string]ConnectionStatusEntry)
s.connectionStatusMut.RLock()
for k, v := range s.connectionStatus {
@@ -706,8 +722,8 @@ func (s *service) ConnectionStatus() map[string]ConnectionStatusEntry {
return result
}
func (s *service) setConnectionStatus(address string, err error) {
if errors.Cause(err) != context.Canceled {
func (s *connectionStatusHandler) setConnectionStatus(address string, err error) {
if errors.Cause(err) == context.Canceled {
return
}
@@ -925,7 +941,7 @@ func (s *service) validateIdentity(c internalConn, expectedID protocol.DeviceID)
if remoteID == s.myID {
l.Infof("Connected to myself (%s) at %s - should not happen", remoteID, c)
c.Close()
return fmt.Errorf("connected to self")
return errors.New("connected to self")
}
// We should see the expected device ID

View File

@@ -48,10 +48,15 @@ type ReadTransaction interface {
// purposes of saving memory when transactions are in-RAM. Note that
// transactions may be checkpointed *anyway* even if this is not called, due to
// resource constraints, but this gives you a chance to decide when.
//
// Functions can be passed to Checkpoint. These are run if and only if the
// checkpoint will result in a flush, and will run before the flush. The
// transaction can be accessed via a closure. If an error is returned from
// these functions the flush will be aborted and the error bubbled.
type WriteTransaction interface {
ReadTransaction
Writer
Checkpoint() error
Checkpoint(...func() error) error
Commit() error
}
@@ -150,16 +155,18 @@ func IsNotFound(err error) bool {
// releaser manages counting on top of a waitgroup
type releaser struct {
wg *sync.WaitGroup
wg *closeWaitGroup
once *sync.Once
}
func newReleaser(wg *sync.WaitGroup) *releaser {
wg.Add(1)
func newReleaser(wg *closeWaitGroup) (*releaser, error) {
if err := wg.Add(1); err != nil {
return nil, err
}
return &releaser{
wg: wg,
once: new(sync.Once),
}
}, nil
}
func (r releaser) Release() {
@@ -169,3 +176,29 @@ func (r releaser) Release() {
r.wg.Done()
})
}
// closeWaitGroup behaves just like a sync.WaitGroup, but does not require
// a single routine to do the Add and Wait calls. If Add is called after
// CloseWait, it will return an error, and both are safe to be used concurrently.
type closeWaitGroup struct {
sync.WaitGroup
closed bool
closeMut sync.RWMutex
}
func (cg *closeWaitGroup) Add(i int) error {
cg.closeMut.RLock()
defer cg.closeMut.RUnlock()
if cg.closed {
return errClosed{}
}
cg.WaitGroup.Add(i)
return nil
}
func (cg *closeWaitGroup) CloseWait() {
cg.closeMut.Lock()
cg.closed = true
cg.closeMut.Unlock()
cg.WaitGroup.Wait()
}

View File

@@ -7,8 +7,6 @@
package backend
import (
"sync"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/iterator"
"github.com/syndtr/goleveldb/leveldb/util"
@@ -24,7 +22,14 @@ const (
// leveldbBackend implements Backend on top of a leveldb
type leveldbBackend struct {
ldb *leveldb.DB
closeWG sync.WaitGroup
closeWG *closeWaitGroup
}
func newLeveldbBackend(ldb *leveldb.DB) *leveldbBackend {
return &leveldbBackend{
ldb: ldb,
closeWG: &closeWaitGroup{},
}
}
func (b *leveldbBackend) NewReadTransaction() (ReadTransaction, error) {
@@ -32,31 +37,41 @@ func (b *leveldbBackend) NewReadTransaction() (ReadTransaction, error) {
}
func (b *leveldbBackend) newSnapshot() (leveldbSnapshot, error) {
rel, err := newReleaser(b.closeWG)
if err != nil {
return leveldbSnapshot{}, err
}
snap, err := b.ldb.GetSnapshot()
if err != nil {
rel.Release()
return leveldbSnapshot{}, wrapLeveldbErr(err)
}
return leveldbSnapshot{
snap: snap,
rel: newReleaser(&b.closeWG),
rel: rel,
}, nil
}
func (b *leveldbBackend) NewWriteTransaction() (WriteTransaction, error) {
rel, err := newReleaser(b.closeWG)
if err != nil {
return nil, err
}
snap, err := b.newSnapshot()
if err != nil {
rel.Release()
return nil, err // already wrapped
}
return &leveldbTransaction{
leveldbSnapshot: snap,
ldb: b.ldb,
batch: new(leveldb.Batch),
rel: newReleaser(&b.closeWG),
rel: rel,
}, nil
}
func (b *leveldbBackend) Close() error {
b.closeWG.Wait()
b.closeWG.CloseWait()
return wrapLeveldbErr(b.ldb.Close())
}
@@ -82,6 +97,13 @@ func (b *leveldbBackend) Delete(key []byte) error {
}
func (b *leveldbBackend) Compact() error {
// Race is detected during testing when db is closed while compaction
// is ongoing.
err := b.closeWG.Add(1)
if err != nil {
return err
}
defer b.closeWG.Done()
return wrapLeveldbErr(b.ldb.CompactRange(util.Range{}))
}
@@ -128,8 +150,8 @@ func (t *leveldbTransaction) Put(key, val []byte) error {
return t.checkFlush(dbFlushBatchMax)
}
func (t *leveldbTransaction) Checkpoint() error {
return t.checkFlush(dbFlushBatchMin)
func (t *leveldbTransaction) Checkpoint(preFlush ...func() error) error {
return t.checkFlush(dbFlushBatchMin, preFlush...)
}
func (t *leveldbTransaction) Commit() error {
@@ -145,10 +167,15 @@ func (t *leveldbTransaction) Release() {
}
// checkFlush flushes and resets the batch if its size exceeds the given size.
func (t *leveldbTransaction) checkFlush(size int) error {
func (t *leveldbTransaction) checkFlush(size int, preFlush ...func() error) error {
if len(t.batch.Dump()) < size {
return nil
}
for _, hook := range preFlush {
if err := hook(); err != nil {
return err
}
}
return t.flush()
}

View File

@@ -42,7 +42,7 @@ func OpenLevelDB(location string, tuning Tuning) (Backend, error) {
if err != nil {
return nil, err
}
return &leveldbBackend{ldb: ldb}, nil
return newLeveldbBackend(ldb), nil
}
// OpenRO attempts to open the database at the given location, read only.
@@ -55,13 +55,13 @@ func OpenLevelDBRO(location string) (Backend, error) {
if err != nil {
return nil, err
}
return &leveldbBackend{ldb: ldb}, nil
return newLeveldbBackend(ldb), nil
}
// OpenMemory returns a new Backend referencing an in-memory database.
func OpenLevelDBMemory() Backend {
ldb, _ := leveldb.Open(storage.NewMemStorage(), nil)
return &leveldbBackend{ldb: ldb}
return newLeveldbBackend(ldb)
}
// optsFor returns the database options to use when opening a database with

View File

@@ -76,7 +76,7 @@ func addToBlockMap(db *Lowlevel, folder []byte, fs []protocol.FileInfo) error {
}
}
}
return t.commit()
return t.Commit()
}
func discardFromBlockMap(db *Lowlevel, folder []byte, fs []protocol.FileInfo) error {
@@ -101,11 +101,12 @@ func discardFromBlockMap(db *Lowlevel, folder []byte, fs []protocol.FileInfo) er
}
}
}
return t.commit()
return t.Commit()
}
func TestBlockMapAddUpdateWipe(t *testing.T) {
db, f := setup()
defer db.Close()
if !dbEmpty(db) {
t.Fatal("db not empty")
@@ -193,6 +194,7 @@ func TestBlockMapAddUpdateWipe(t *testing.T) {
func TestBlockFinderLookup(t *testing.T) {
db, f := setup()
defer db.Close()
folder1 := []byte("folder1")
folder2 := []byte("folder2")

View File

@@ -7,6 +7,7 @@
package db
import (
"bytes"
"testing"
"github.com/syncthing/syncthing/lib/db/backend"
@@ -33,6 +34,7 @@ func TestIgnoredFiles(t *testing.T) {
t.Fatal(err)
}
db := NewLowlevel(ldb)
defer db.Close()
if err := UpdateSchema(db); err != nil {
t.Fatal(err)
}
@@ -161,6 +163,7 @@ func TestUpdate0to3(t *testing.T) {
}
db := NewLowlevel(ldb)
defer db.Close()
updater := schemaUpdater{db}
folder := []byte(update0to3Folder)
@@ -173,6 +176,7 @@ func TestUpdate0to3(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer trans.Release()
if _, ok, err := trans.getFile(folder, protocol.LocalDeviceID[:], []byte(slashPrefixed)); err != nil {
t.Fatal(err)
} else if ok {
@@ -196,6 +200,7 @@ func TestUpdate0to3(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer trans.Release()
_ = trans.withHaveSequence(folder, 0, func(fi FileIntf) bool {
f := fi.(protocol.FileInfo)
l.Infoln(f)
@@ -227,6 +232,7 @@ func TestUpdate0to3(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer trans.Release()
_ = trans.withNeed(folder, protocol.LocalDeviceID[:], false, func(fi FileIntf) bool {
e, ok := need[fi.FileName()]
if !ok {
@@ -244,8 +250,179 @@ func TestUpdate0to3(t *testing.T) {
}
}
// TestRepairSequence checks that a few hand-crafted messed-up sequence entries get fixed.
func TestRepairSequence(t *testing.T) {
db := NewLowlevel(backend.OpenMemory())
defer db.Close()
folderStr := "test"
folder := []byte(folderStr)
id := protocol.LocalDeviceID
short := protocol.LocalDeviceID.Short()
files := []protocol.FileInfo{
{Name: "fine", Blocks: genBlocks(1)},
{Name: "duplicate", Blocks: genBlocks(2)},
{Name: "missing", Blocks: genBlocks(3)},
{Name: "overwriting", Blocks: genBlocks(4)},
{Name: "inconsistent", Blocks: genBlocks(5)},
}
for i, f := range files {
files[i].Version = f.Version.Update(short)
}
trans, err := db.newReadWriteTransaction()
if err != nil {
t.Fatal(err)
}
defer trans.close()
addFile := func(f protocol.FileInfo, seq int64) {
dk, err := trans.keyer.GenerateDeviceFileKey(nil, folder, id[:], []byte(f.Name))
if err != nil {
t.Fatal(err)
}
if err := trans.putFile(dk, f, false); err != nil {
t.Fatal(err)
}
sk, err := trans.keyer.GenerateSequenceKey(nil, folder, seq)
if err != nil {
t.Fatal(err)
}
if err := trans.Put(sk, dk); err != nil {
t.Fatal(err)
}
}
// Plain normal entry
var seq int64 = 1
files[0].Sequence = 1
addFile(files[0], seq)
// Second entry once updated with original sequence still in place
f := files[1]
f.Sequence = int64(len(files) + 1)
addFile(f, f.Sequence)
// Original sequence entry
seq++
sk, err := trans.keyer.GenerateSequenceKey(nil, folder, seq)
if err != nil {
t.Fatal(err)
}
dk, err := trans.keyer.GenerateDeviceFileKey(nil, folder, id[:], []byte(f.Name))
if err != nil {
t.Fatal(err)
}
if err := trans.Put(sk, dk); err != nil {
t.Fatal(err)
}
// File later overwritten thus missing sequence entry
seq++
files[2].Sequence = seq
addFile(files[2], seq)
// File overwriting previous sequence entry (no seq bump)
seq++
files[3].Sequence = seq
addFile(files[3], seq)
// Inconistent file
seq++
files[4].Sequence = 101
addFile(files[4], seq)
// And a sequence entry pointing at nothing because why not
sk, err = trans.keyer.GenerateSequenceKey(nil, folder, 100001)
if err != nil {
t.Fatal(err)
}
dk, err = trans.keyer.GenerateDeviceFileKey(nil, folder, id[:], []byte("nonexisting"))
if err != nil {
t.Fatal(err)
}
if err := trans.Put(sk, dk); err != nil {
t.Fatal(err)
}
if err := trans.Commit(); err != nil {
t.Fatal(err)
}
// Loading the metadata for the first time means a "re"calculation happens,
// along which the sequences get repaired too.
db.gcMut.RLock()
_ = db.loadMetadataTracker(folderStr)
db.gcMut.RUnlock()
if err != nil {
t.Fatal(err)
}
// Check the db
ro, err := db.newReadOnlyTransaction()
if err != nil {
t.Fatal(err)
}
defer ro.close()
it, err := ro.NewPrefixIterator([]byte{KeyTypeDevice})
if err != nil {
t.Fatal(err)
}
defer it.Release()
for it.Next() {
fi, err := ro.unmarshalTrunc(it.Value(), true)
if err != nil {
t.Fatal(err)
}
if sk, err = ro.keyer.GenerateSequenceKey(sk, folder, fi.SequenceNo()); err != nil {
t.Fatal(err)
}
dk, err := ro.Get(sk)
if backend.IsNotFound(err) {
t.Error("Missing sequence entry for", fi.FileName())
} else if err != nil {
t.Fatal(err)
}
if !bytes.Equal(it.Key(), dk) {
t.Errorf("Wrong key for %v, expected %s, got %s", f.FileName(), it.Key(), dk)
}
}
if err := it.Error(); err != nil {
t.Fatal(err)
}
it.Release()
it, err = ro.NewPrefixIterator([]byte{KeyTypeSequence})
if err != nil {
t.Fatal(err)
}
defer it.Release()
for it.Next() {
intf, ok, err := ro.getFileTrunc(it.Value(), false)
if err != nil {
t.Fatal(err)
}
fi := intf.(protocol.FileInfo)
seq := ro.keyer.SequenceFromSequenceKey(it.Key())
if !ok {
t.Errorf("Sequence entry %v points at nothing", seq)
} else if fi.SequenceNo() != seq {
t.Errorf("Inconsistent sequence entry for %v: %v != %v", fi.FileName(), fi.SequenceNo(), seq)
}
if len(fi.Blocks) == 0 {
t.Error("Missing blocks in", fi.FileName())
}
}
if err := it.Error(); err != nil {
t.Fatal(err)
}
it.Release()
}
func TestDowngrade(t *testing.T) {
db := NewLowlevel(backend.OpenMemory())
defer db.Close()
// sets the min version etc
if err := UpdateSchema(db); err != nil {
t.Fatal(err)
@@ -267,3 +444,40 @@ func TestDowngrade(t *testing.T) {
t.Fatalf("Error has %v as min Syncthing version, expected %v", err.minSyncthingVersion, dbMinSyncthingVersion)
}
}
func TestCheckGlobals(t *testing.T) {
db := NewLowlevel(backend.OpenMemory())
defer db.Close()
fs := NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeFake, ""), db)
// Add any file
name := "foo"
fs.Update(protocol.LocalDeviceID, []protocol.FileInfo{
{
Name: name,
Type: protocol.FileInfoTypeFile,
Version: protocol.Vector{Counters: []protocol.Counter{{ID: 1, Value: 1001}}},
},
})
// Remove just the file entry
if err := db.dropPrefix([]byte{KeyTypeDevice}); err != nil {
t.Fatal(err)
}
// Clean up global entry of the now missing file
if err := db.checkGlobals([]byte(fs.folder), fs.meta); err != nil {
t.Fatal(err)
}
// Check that the global entry is gone
gk, err := db.keyer.GenerateGlobalVersionKey(nil, []byte(fs.folder), []byte(name))
if err != nil {
t.Fatal(err)
}
_, err = db.Get(gk)
if !backend.IsNotFound(err) {
t.Error("Expected key missing error, got", err)
}
}

View File

@@ -121,6 +121,10 @@ func (k deviceFileKey) WithoutNameAndDevice() []byte {
return k[:keyPrefixLen+keyFolderLen]
}
func (k deviceFileKey) WithoutName() []byte {
return k[:keyPrefixLen+keyFolderLen+keyDeviceLen]
}
func (k defaultKeyer) GenerateDeviceFileKey(key, folder, device, name []byte) (deviceFileKey, error) {
folderID, err := k.folderIdx.ID(folder)
if err != nil {

View File

@@ -19,6 +19,7 @@ func TestDeviceKey(t *testing.T) {
name := []byte("name")
db := NewLowlevel(backend.OpenMemory())
defer db.Close()
key, err := db.keyer.GenerateDeviceFileKey(nil, fld, dev, name)
if err != nil {
@@ -50,6 +51,7 @@ func TestGlobalKey(t *testing.T) {
name := []byte("name")
db := NewLowlevel(backend.OpenMemory())
defer db.Close()
key, err := db.keyer.GenerateGlobalVersionKey(nil, fld, name)
if err != nil {
@@ -78,6 +80,7 @@ func TestSequenceKey(t *testing.T) {
fld := []byte("folder6789012345678901234567890123456789012345678901234567890123")
db := NewLowlevel(backend.OpenMemory())
defer db.Close()
const seq = 1234567890
key, err := db.keyer.GenerateSequenceKey(nil, fld, seq)

View File

@@ -19,22 +19,25 @@ import (
)
const (
// We set the bloom filter capacity to handle 100k individual block lists
// with a false positive probability of 1% for the first pass. Once we know
// how many block lists we have we will use that number instead, if it's
// more than 100k. For fewer than 100k block lists we will just get better
// false positive rate instead.
blockGCBloomCapacity = 100000
blockGCBloomFalsePositiveRate = 0.01 // 1%
blockGCDefaultInterval = 13 * time.Hour
blockGCTimeKey = "lastBlockGCTime"
// We set the bloom filter capacity to handle 100k individual items with
// a false positive probability of 1% for the first pass. Once we know
// how many items we have we will use that number instead, if it's more
// than 100k. For fewer than 100k items we will just get better false
// positive rate instead.
indirectGCBloomCapacity = 100000
indirectGCBloomFalsePositiveRate = 0.01 // 1%
indirectGCDefaultInterval = 13 * time.Hour
indirectGCTimeKey = "lastIndirectGCTime"
// Use indirection for the block list when it exceeds this many entries
blocksIndirectionCutoff = 3
)
var blockGCInterval = blockGCDefaultInterval
var indirectGCInterval = indirectGCDefaultInterval
func init() {
if dur, err := time.ParseDuration(os.Getenv("STGCBLOCKSEVERY")); err == nil {
blockGCInterval = dur
if dur, err := time.ParseDuration(os.Getenv("STGCINDIRECTEVERY")); err == nil {
indirectGCInterval = dur
}
}
@@ -111,7 +114,7 @@ func (db *Lowlevel) updateRemoteFiles(folder, device []byte, fs []protocol.FileI
meta.addFile(devID, f)
l.Debugf("insert; folder=%q device=%v %v", folder, devID, f)
if err := t.putFile(dk, f); err != nil {
if err := t.putFile(dk, f, false); err != nil {
return err
}
@@ -124,12 +127,18 @@ func (db *Lowlevel) updateRemoteFiles(folder, device []byte, fs []protocol.FileI
return err
}
if err := t.Checkpoint(); err != nil {
if err := t.Checkpoint(func() error {
return meta.toDB(t, folder)
}); err != nil {
return err
}
}
return t.commit()
if err := meta.toDB(t, folder); err != nil {
return err
}
return t.Commit()
}
// updateLocalFiles adds fileinfos to the db, and updates the global versionlist,
@@ -192,7 +201,7 @@ func (db *Lowlevel) updateLocalFiles(folder []byte, fs []protocol.FileInfo, meta
meta.addFile(protocol.LocalDeviceID, f)
l.Debugf("insert (local); folder=%q %v", folder, f)
if err := t.putFile(dk, f); err != nil {
if err := t.putFile(dk, f, false); err != nil {
return err
}
@@ -227,12 +236,18 @@ func (db *Lowlevel) updateLocalFiles(folder []byte, fs []protocol.FileInfo, meta
}
}
if err := t.Checkpoint(); err != nil {
if err := t.Checkpoint(func() error {
return meta.toDB(t, folder)
}); err != nil {
return err
}
}
return t.commit()
if err := meta.toDB(t, folder); err != nil {
return err
}
return t.Commit()
}
func (db *Lowlevel) dropFolder(folder []byte) error {
@@ -290,7 +305,7 @@ func (db *Lowlevel) dropFolder(folder []byte) error {
return err
}
return t.commit()
return t.Commit()
}
func (db *Lowlevel) dropDeviceFolder(device, folder []byte, meta *metadataTracker) error {
@@ -343,7 +358,7 @@ func (db *Lowlevel) dropDeviceFolder(device, folder []byte, meta *metadataTracke
return err
}
}
return t.commit()
return t.Commit()
}
func (db *Lowlevel) checkGlobals(folder []byte, meta *metadataTracker) error {
@@ -365,8 +380,11 @@ func (db *Lowlevel) checkGlobals(folder []byte, meta *metadataTracker) error {
var dk []byte
for dbi.Next() {
vl, ok := unmarshalVersionList(dbi.Value())
if !ok {
var vl VersionList
if err := vl.Unmarshal(dbi.Value()); err != nil || len(vl.Versions) == 0 {
if err := t.Delete(dbi.Key()); err != nil {
return err
}
continue
}
@@ -392,7 +410,7 @@ func (db *Lowlevel) checkGlobals(folder []byte, meta *metadataTracker) error {
newVL.Versions = append(newVL.Versions, version)
if i == 0 {
if fi, ok, err := t.getFileByKey(dk); err != nil {
if fi, ok, err := t.getFileTrunc(dk, true); err != nil {
return err
} else if ok {
meta.addFile(protocol.GlobalDeviceID, fi)
@@ -400,7 +418,11 @@ func (db *Lowlevel) checkGlobals(folder []byte, meta *metadataTracker) error {
}
}
if len(newVL.Versions) != len(vl.Versions) {
if newLen := len(newVL.Versions); newLen == 0 {
if err := t.Delete(dbi.Key()); err != nil {
return err
}
} else if newLen != len(vl.Versions) {
if err := t.Put(dbi.Key(), mustMarshal(&newVL)); err != nil {
return err
}
@@ -411,7 +433,7 @@ func (db *Lowlevel) checkGlobals(folder []byte, meta *metadataTracker) error {
}
l.Debugf("db check completed for %q", folder)
return t.commit()
return t.Commit()
}
func (db *Lowlevel) getIndexID(device, folder []byte) (protocol.IndexID, error) {
@@ -469,22 +491,32 @@ func (db *Lowlevel) dropPrefix(prefix []byte) error {
if err := t.deleteKeyPrefix(prefix); err != nil {
return err
}
return t.commit()
return t.Commit()
}
func (db *Lowlevel) gcRunner() {
t := time.NewTimer(db.timeUntil(blockGCTimeKey, blockGCInterval))
// Calculate the time for the next GC run. Even if we should run GC
// directly, give the system a while to get up and running and do other
// stuff first. (We might have migrations and stuff which would be
// better off running before GC.)
next := db.timeUntil(indirectGCTimeKey, indirectGCInterval)
if next < time.Minute {
next = time.Minute
}
t := time.NewTimer(next)
defer t.Stop()
for {
select {
case <-db.gcStop:
return
case <-t.C:
if err := db.gcBlocks(); err != nil {
l.Warnln("Database block GC failed:", err)
if err := db.gcIndirect(); err != nil {
l.Warnln("Database indirection GC failed:", err)
}
db.recordTime(blockGCTimeKey)
t.Reset(db.timeUntil(blockGCTimeKey, blockGCInterval))
db.recordTime(indirectGCTimeKey)
t.Reset(db.timeUntil(indirectGCTimeKey, indirectGCInterval))
}
}
}
@@ -509,15 +541,16 @@ func (db *Lowlevel) timeUntil(key string, every time.Duration) time.Duration {
return sleepTime
}
func (db *Lowlevel) gcBlocks() error {
// The block GC uses a bloom filter to track used block lists. This means
// iterating over all items, adding their block lists to the filter, then
// iterating over the block lists and removing those that don't match the
// filter. The filter will give false positives so we will keep around one
// percent of block lists that we don't really need (at most).
func (db *Lowlevel) gcIndirect() error {
// The indirection GC uses bloom filters to track used block lists and
// versions. This means iterating over all items, adding their hashes to
// the filter, then iterating over the indirected items and removing
// those that don't match the filter. The filter will give false
// positives so we will keep around one percent of things that we don't
// really need (at most).
//
// Block GC needs to run when there are no modifications to the FileInfos or
// block lists.
// Indirection GC needs to run when there are no modifications to the
// FileInfos or indirected items.
db.gcMut.Lock()
defer db.gcMut.Unlock()
@@ -528,29 +561,33 @@ func (db *Lowlevel) gcBlocks() error {
}
defer t.Release()
// Set up the bloom filter with the initial capacity and false positive
// rate, or higher capacity if we've done this before and seen lots of block
// lists.
// Set up the bloom filters with the initial capacity and false positive
// rate, or higher capacity if we've done this before and seen lots of
// items. For simplicity's sake we track just one count, which is the
// highest of the various indirected items.
capacity := blockGCBloomCapacity
capacity := indirectGCBloomCapacity
if db.gcKeyCount > capacity {
capacity = db.gcKeyCount
}
filter := bloom.NewWithEstimates(uint(capacity), blockGCBloomFalsePositiveRate)
blockFilter := bloom.NewWithEstimates(uint(capacity), indirectGCBloomFalsePositiveRate)
// Iterate the FileInfos, unmarshal the blocks hashes and add them to
// the filter.
// Iterate the FileInfos, unmarshal the block and version hashes and
// add them to the filter.
it, err := db.NewPrefixIterator([]byte{KeyTypeDevice})
it, err := t.NewPrefixIterator([]byte{KeyTypeDevice})
if err != nil {
return err
}
defer it.Release()
for it.Next() {
var bl BlocksHashOnly
if err := bl.Unmarshal(it.Value()); err != nil {
return err
}
filter.Add(bl.BlocksHash)
if len(bl.BlocksHash) > 0 {
blockFilter.Add(bl.BlocksHash)
}
}
it.Release()
if err := it.Error(); err != nil {
@@ -560,15 +597,16 @@ func (db *Lowlevel) gcBlocks() error {
// Iterate over block lists, removing keys with hashes that don't match
// the filter.
it, err = db.NewPrefixIterator([]byte{KeyTypeBlockList})
it, err = t.NewPrefixIterator([]byte{KeyTypeBlockList})
if err != nil {
return err
}
matched := 0
defer it.Release()
matchedBlocks := 0
for it.Next() {
key := blockListKey(it.Key())
if filter.Test(key.BlocksHash()) {
matched++
if blockFilter.Test(key.BlocksHash()) {
matchedBlocks++
continue
}
if err := t.Delete(key); err != nil {
@@ -581,7 +619,7 @@ func (db *Lowlevel) gcBlocks() error {
}
// Remember the number of unique keys we kept until the next pass.
db.gcKeyCount = matched
db.gcKeyCount = matchedBlocks
if err := t.Commit(); err != nil {
return err
@@ -590,17 +628,227 @@ func (db *Lowlevel) gcBlocks() error {
return db.Compact()
}
func unmarshalVersionList(data []byte) (VersionList, bool) {
var vl VersionList
if err := vl.Unmarshal(data); err != nil {
l.Debugln("unmarshal error:", err)
return VersionList{}, false
// CheckRepair checks folder metadata and sequences for miscellaneous errors.
func (db *Lowlevel) CheckRepair() {
for _, folder := range db.ListFolders() {
_ = db.getMetaAndCheck(folder)
}
if len(vl.Versions) == 0 {
l.Debugln("empty version list")
return VersionList{}, false
}
func (db *Lowlevel) getMetaAndCheck(folder string) *metadataTracker {
db.gcMut.RLock()
defer db.gcMut.RUnlock()
meta, err := db.recalcMeta(folder)
if err == nil {
var fixed int
fixed, err = db.repairSequenceGCLocked(folder, meta)
if fixed != 0 {
l.Infof("Repaired %d sequence entries in database", fixed)
}
}
return vl, true
if backend.IsClosed(err) {
return nil
} else if err != nil {
panic(err)
}
return meta
}
func (db *Lowlevel) loadMetadataTracker(folder string) *metadataTracker {
meta := newMetadataTracker()
if err := meta.fromDB(db, []byte(folder)); err != nil {
l.Infof("No stored folder metadata for %q; recalculating", folder)
return db.getMetaAndCheck(folder)
}
curSeq := meta.Sequence(protocol.LocalDeviceID)
if metaOK := db.verifyLocalSequence(curSeq, folder); !metaOK {
l.Infof("Stored folder metadata for %q is out of date after crash; recalculating", folder)
return db.getMetaAndCheck(folder)
}
if age := time.Since(meta.Created()); age > databaseRecheckInterval {
l.Infof("Stored folder metadata for %q is %v old; recalculating", folder, age)
return db.getMetaAndCheck(folder)
}
return meta
}
func (db *Lowlevel) recalcMeta(folder string) (*metadataTracker, error) {
meta := newMetadataTracker()
if err := db.checkGlobals([]byte(folder), meta); err != nil {
return nil, err
}
t, err := db.newReadWriteTransaction()
if err != nil {
return nil, err
}
defer t.close()
var deviceID protocol.DeviceID
err = t.withAllFolderTruncated([]byte(folder), func(device []byte, f FileInfoTruncated) bool {
copy(deviceID[:], device)
meta.addFile(deviceID, f)
return true
})
if err != nil {
return nil, err
}
meta.SetCreated()
if err := meta.toDB(t, []byte(folder)); err != nil {
return nil, err
}
if err := t.Commit(); err != nil {
return nil, err
}
return meta, nil
}
// Verify the local sequence number from actual sequence entries. Returns
// true if it was all good, or false if a fixup was necessary.
func (db *Lowlevel) verifyLocalSequence(curSeq int64, folder string) bool {
// Walk the sequence index from the current (supposedly) highest
// sequence number and raise the alarm if we get anything. This recovers
// from the occasion where we have written sequence entries to disk but
// not yet written new metadata to disk.
//
// Note that we can have the same thing happen for remote devices but
// there it's not a problem -- we'll simply advertise a lower sequence
// number than we've actually seen and receive some duplicate updates
// and then be in sync again.
t, err := db.newReadOnlyTransaction()
if err != nil {
panic(err)
}
ok := true
if err := t.withHaveSequence([]byte(folder), curSeq+1, func(fi FileIntf) bool {
ok = false // we got something, which we should not have
return false
}); err != nil && !backend.IsClosed(err) {
panic(err)
}
t.close()
return ok
}
// repairSequenceGCLocked makes sure the sequence numbers in the sequence keys
// match those in the corresponding file entries. It returns the amount of fixed
// entries.
func (db *Lowlevel) repairSequenceGCLocked(folderStr string, meta *metadataTracker) (int, error) {
t, err := db.newReadWriteTransaction()
if err != nil {
return 0, err
}
defer t.close()
fixed := 0
folder := []byte(folderStr)
// First check that every file entry has a matching sequence entry
// (this was previously db schema upgrade to 9).
dk, err := t.keyer.GenerateDeviceFileKey(nil, folder, protocol.LocalDeviceID[:], nil)
if err != nil {
return 0, err
}
it, err := t.NewPrefixIterator(dk.WithoutName())
if err != nil {
return 0, err
}
defer it.Release()
var sk sequenceKey
for it.Next() {
intf, err := t.unmarshalTrunc(it.Value(), true)
if err != nil {
return 0, err
}
fi := intf.(FileInfoTruncated)
if sk, err = t.keyer.GenerateSequenceKey(sk, folder, fi.Sequence); err != nil {
return 0, err
}
switch dk, err = t.Get(sk); {
case err != nil:
if !backend.IsNotFound(err) {
return 0, err
}
fallthrough
case !bytes.Equal(it.Key(), dk):
fixed++
fi.Sequence = meta.nextLocalSeq()
if sk, err = t.keyer.GenerateSequenceKey(sk, folder, fi.Sequence); err != nil {
return 0, err
}
if err := t.Put(sk, it.Key()); err != nil {
return 0, err
}
if err := t.putFile(it.Key(), fi.copyToFileInfo(), true); err != nil {
return 0, err
}
}
if err := t.Checkpoint(func() error {
return meta.toDB(t, folder)
}); err != nil {
return 0, err
}
}
if err := it.Error(); err != nil {
return 0, err
}
it.Release()
// Secondly check there's no sequence entries pointing at incorrect things.
sk, err = t.keyer.GenerateSequenceKey(sk, folder, 0)
if err != nil {
return 0, err
}
it, err = t.NewPrefixIterator(sk.WithoutSequence())
if err != nil {
return 0, err
}
defer it.Release()
for it.Next() {
// Check that the sequence from the key matches the
// sequence in the file.
fi, ok, err := t.getFileTrunc(it.Value(), true)
if err != nil {
return 0, err
}
if ok {
if seq := t.keyer.SequenceFromSequenceKey(it.Key()); seq == fi.SequenceNo() {
continue
}
}
// Either the file is missing or has a different sequence number
fixed++
if err := t.Delete(it.Key()); err != nil {
return 0, err
}
}
if err := it.Error(); err != nil {
return 0, err
}
it.Release()
if err := meta.toDB(t, folder); err != nil {
return 0, err
}
return fixed, t.Commit()
}
// unchanged checks if two files are the same and thus don't need to be updated.

View File

@@ -28,8 +28,8 @@ type metadataTracker struct {
}
type metaKey struct {
dev protocol.DeviceID
flags uint32
dev protocol.DeviceID
flag uint32
}
func newMetadataTracker() *metadataTracker {
@@ -62,8 +62,8 @@ func (m *metadataTracker) Marshal() ([]byte, error) {
// toDB saves the marshalled metadataTracker to the given db, under the key
// corresponding to the given folder
func (m *metadataTracker) toDB(db *Lowlevel, folder []byte) error {
key, err := db.keyer.GenerateFolderMetaKey(nil, folder)
func (m *metadataTracker) toDB(t readWriteTransaction, folder []byte) error {
key, err := t.keyer.GenerateFolderMetaKey(nil, folder)
if err != nil {
return err
}
@@ -79,7 +79,7 @@ func (m *metadataTracker) toDB(db *Lowlevel, folder []byte) error {
if err != nil {
return err
}
err = db.Put(key, bs)
err = t.Put(key, bs)
if err == nil {
m.dirty = false
}
@@ -103,14 +103,18 @@ func (m *metadataTracker) fromDB(db *Lowlevel, folder []byte) error {
// countsPtr returns a pointer to the corresponding Counts struct, if
// necessary allocating one in the process
func (m *metadataTracker) countsPtr(dev protocol.DeviceID, flags uint32) *Counts {
func (m *metadataTracker) countsPtr(dev protocol.DeviceID, flag uint32) *Counts {
// must be called with the mutex held
key := metaKey{dev, flags}
if bits.OnesCount32(flag) > 1 {
panic("incorrect usage: set at most one bit in flag")
}
key := metaKey{dev, flag}
idx, ok := m.indexes[key]
if !ok {
idx = len(m.counts.Counts)
m.counts.Counts = append(m.counts.Counts, Counts{DeviceID: dev[:], LocalFlags: flags})
m.counts.Counts = append(m.counts.Counts, Counts{DeviceID: dev[:], LocalFlags: flag})
m.indexes[key] = idx
}
return &m.counts.Counts[idx]
@@ -157,8 +161,8 @@ func (m *metadataTracker) updateSeqLocked(dev protocol.DeviceID, f FileIntf) {
}
}
func (m *metadataTracker) addFileLocked(dev protocol.DeviceID, flags uint32, f FileIntf) {
cp := m.countsPtr(dev, flags)
func (m *metadataTracker) addFileLocked(dev protocol.DeviceID, flag uint32, f FileIntf) {
cp := m.countsPtr(dev, flag)
switch {
case f.IsDeleted():
@@ -196,8 +200,8 @@ func (m *metadataTracker) removeFile(dev protocol.DeviceID, f FileIntf) {
}
}
func (m *metadataTracker) removeFileLocked(dev protocol.DeviceID, flags uint32, f FileIntf) {
cp := m.countsPtr(dev, f.FileLocalFlags())
func (m *metadataTracker) removeFileLocked(dev protocol.DeviceID, flag uint32, f FileIntf) {
cp := m.countsPtr(dev, flag)
switch {
case f.IsDeleted():

View File

@@ -11,6 +11,8 @@ import (
"sort"
"testing"
"github.com/syncthing/syncthing/lib/db/backend"
"github.com/syncthing/syncthing/lib/fs"
"github.com/syncthing/syncthing/lib/protocol"
)
@@ -101,3 +103,75 @@ func TestMetaSequences(t *testing.T) {
t.Error("sequence of first device should be 4, not", seq)
}
}
func TestRecalcMeta(t *testing.T) {
ldb := NewLowlevel(backend.OpenMemory())
defer ldb.Close()
// Add some files
s1 := NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeFake, "fake"), ldb)
files := []protocol.FileInfo{
{Name: "a", Size: 1000},
{Name: "b", Size: 2000},
}
s1.Update(protocol.LocalDeviceID, files)
// Verify local/global size
snap := s1.Snapshot()
ls := snap.LocalSize()
gs := snap.GlobalSize()
snap.Release()
if ls.Bytes != 3000 {
t.Fatalf("Wrong initial local byte count, %d != 3000", ls.Bytes)
}
if gs.Bytes != 3000 {
t.Fatalf("Wrong initial global byte count, %d != 3000", gs.Bytes)
}
// Reach into the database to make the metadata tracker intentionally
// wrong and out of date
curSeq := s1.meta.Sequence(protocol.LocalDeviceID)
tran, err := ldb.newReadWriteTransaction()
if err != nil {
t.Fatal(err)
}
s1.meta.mut.Lock()
s1.meta.countsPtr(protocol.LocalDeviceID, 0).Sequence = curSeq - 1 // too low
s1.meta.countsPtr(protocol.LocalDeviceID, 0).Bytes = 1234 // wrong
s1.meta.countsPtr(protocol.GlobalDeviceID, 0).Bytes = 1234 // wrong
s1.meta.dirty = true
s1.meta.mut.Unlock()
if err := s1.meta.toDB(tran, []byte("test")); err != nil {
t.Fatal(err)
}
if err := tran.Commit(); err != nil {
t.Fatal(err)
}
// Verify that our bad data "took"
snap = s1.Snapshot()
ls = snap.LocalSize()
gs = snap.GlobalSize()
snap.Release()
if ls.Bytes != 1234 {
t.Fatalf("Wrong changed local byte count, %d != 1234", ls.Bytes)
}
if gs.Bytes != 1234 {
t.Fatalf("Wrong changed global byte count, %d != 1234", gs.Bytes)
}
// Create a new fileset, which will realize the inconsistency and recalculate
s2 := NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeFake, "fake"), ldb)
// Verify local/global size
snap = s2.Snapshot()
ls = snap.LocalSize()
gs = snap.GlobalSize()
snap.Release()
if ls.Bytes != 3000 {
t.Fatalf("Wrong fixed local byte count, %d != 3000", ls.Bytes)
}
if gs.Bytes != 3000 {
t.Fatalf("Wrong fixed global byte count, %d != 3000", gs.Bytes)
}
}

View File

@@ -15,6 +15,7 @@ import (
func TestNamespacedInt(t *testing.T) {
ldb := NewLowlevel(backend.OpenMemory())
defer ldb.Close()
n1 := NewNamespacedKV(ldb, "foo")
n2 := NewNamespacedKV(ldb, "bar")
@@ -62,6 +63,7 @@ func TestNamespacedInt(t *testing.T) {
func TestNamespacedTime(t *testing.T) {
ldb := NewLowlevel(backend.OpenMemory())
defer ldb.Close()
n1 := NewNamespacedKV(ldb, "foo")
@@ -85,6 +87,7 @@ func TestNamespacedTime(t *testing.T) {
func TestNamespacedString(t *testing.T) {
ldb := NewLowlevel(backend.OpenMemory())
defer ldb.Close()
n1 := NewNamespacedKV(ldb, "foo")
@@ -107,6 +110,7 @@ func TestNamespacedString(t *testing.T) {
func TestNamespacedReset(t *testing.T) {
ldb := NewLowlevel(backend.OpenMemory())
defer ldb.Close()
n1 := NewNamespacedKV(ldb, "foo")

View File

@@ -10,6 +10,7 @@ import (
"fmt"
"strings"
"github.com/syncthing/syncthing/lib/db/backend"
"github.com/syncthing/syncthing/lib/protocol"
)
@@ -17,14 +18,12 @@ import (
// 0: v0.14.0
// 1: v0.14.46
// 2: v0.14.48
// 3: v0.14.49
// 4: v0.14.49
// 5: v0.14.49
// 3-5: v0.14.49
// 6: v0.14.50
// 7: v0.14.53
// 8: v1.4.0
// 8-9: v1.4.0
const (
dbVersion = 8
dbVersion = 9
dbMinSyncthingVersion = "v1.4.0"
)
@@ -49,6 +48,11 @@ type schemaUpdater struct {
}
func (db *schemaUpdater) updateSchema() error {
// Updating the schema can touch any and all parts of the database. Make
// sure we do not run GC concurrently with schema migrations.
db.gcMut.Lock()
defer db.gcMut.Unlock()
miscDB := NewMiscDataNamespace(db.Lowlevel)
prevVersion, _, err := miscDB.Int64("dbVersion")
if err != nil {
@@ -80,7 +84,7 @@ func (db *schemaUpdater) updateSchema() error {
{5, db.updateSchemaTo5},
{6, db.updateSchema5to6},
{7, db.updateSchema6to7},
{8, db.updateSchema7to8},
{9, db.updateSchemato9},
}
for _, m := range migrations {
@@ -197,6 +201,9 @@ func (db *schemaUpdater) updateSchema0to1(_ int) error {
ignAdded++
}
}
if err := t.Checkpoint(); err != nil {
return err
}
}
for folder := range changedFolders {
@@ -204,7 +211,7 @@ func (db *schemaUpdater) updateSchema0to1(_ int) error {
return err
}
}
return t.commit()
return t.Commit()
}
// updateSchema1to2 introduces a sequenceKey->deviceKey bucket for local items
@@ -240,7 +247,7 @@ func (db *schemaUpdater) updateSchema1to2(_ int) error {
return err
}
}
return t.commit()
return t.Commit()
}
// updateSchema2to3 introduces a needKey->nil bucket for locally needed files.
@@ -288,7 +295,7 @@ func (db *schemaUpdater) updateSchema2to3(_ int) error {
return err
}
}
return t.commit()
return t.Commit()
}
// updateSchemaTo5 resets the need bucket due to bugs existing in the v0.14.49
@@ -314,7 +321,7 @@ func (db *schemaUpdater) updateSchemaTo5(prevVersion int) error {
return err
}
}
if err := t.commit(); err != nil {
if err := t.Commit(); err != nil {
return err
}
@@ -335,7 +342,7 @@ func (db *schemaUpdater) updateSchema5to6(_ int) error {
for _, folderStr := range db.ListFolders() {
folder := []byte(folderStr)
var putErr error
var iterErr error
err := t.withHave(folder, protocol.LocalDeviceID[:], nil, false, func(f FileIntf) bool {
if !f.IsInvalid() {
return true
@@ -346,22 +353,24 @@ func (db *schemaUpdater) updateSchema5to6(_ int) error {
fi.LocalFlags = protocol.FlagLocalIgnored
bs, _ := fi.Marshal()
dk, putErr = db.keyer.GenerateDeviceFileKey(dk, folder, protocol.LocalDeviceID[:], []byte(fi.Name))
if putErr != nil {
dk, iterErr = db.keyer.GenerateDeviceFileKey(dk, folder, protocol.LocalDeviceID[:], []byte(fi.Name))
if iterErr != nil {
return false
}
putErr = t.Put(dk, bs)
return putErr == nil
if iterErr = t.Put(dk, bs); iterErr != nil {
return false
}
iterErr = t.Checkpoint()
return iterErr == nil
})
if putErr != nil {
return putErr
if iterErr != nil {
return iterErr
}
if err != nil {
return err
}
}
return t.commit()
return t.Commit()
}
// updateSchema6to7 checks whether all currently locally needed files are really
@@ -417,12 +426,16 @@ func (db *schemaUpdater) updateSchema6to7(_ int) error {
if err != nil {
return err
}
if err := t.Checkpoint(); err != nil {
return err
}
}
return t.commit()
return t.Commit()
}
func (db *schemaUpdater) updateSchema7to8(_ int) error {
func (db *schemaUpdater) updateSchemato9(prev int) error {
// Loads and rewrites all files with blocks, to deduplicate block lists.
// Checks for missing or incorrect sequence entries and rewrites those.
t, err := db.newReadWriteTransaction()
if err != nil {
@@ -435,14 +448,27 @@ func (db *schemaUpdater) updateSchema7to8(_ int) error {
return err
}
for it.Next() {
var fi protocol.FileInfo
if err := fi.Unmarshal(it.Value()); err != nil {
intf, err := t.unmarshalTrunc(it.Value(), false)
if backend.IsNotFound(err) {
// Unmarshal error due to missing parts (block list), probably
// due to a bad migration in a previous RC. Drop this key, as
// getFile would anyway return this as a "not found" in the
// normal flow of things.
if err := t.Delete(it.Key()); err != nil {
return err
}
continue
} else if err != nil {
return err
}
fi := intf.(protocol.FileInfo)
if fi.Blocks == nil {
continue
}
if err := t.putFile(it.Key(), fi); err != nil {
if err := t.putFile(it.Key(), fi, false); err != nil {
return err
}
if err := t.Checkpoint(); err != nil {
return err
}
}
@@ -451,7 +477,7 @@ func (db *schemaUpdater) updateSchema7to8(_ int) error {
return err
}
db.recordTime(blockGCTimeKey)
db.recordTime(indirectGCTimeKey)
return t.commit()
return t.Commit()
}

View File

@@ -71,56 +71,13 @@ func init() {
}
func NewFileSet(folder string, fs fs.Filesystem, db *Lowlevel) *FileSet {
var s = FileSet{
return &FileSet{
folder: folder,
fs: fs,
db: db,
meta: newMetadataTracker(),
meta: db.loadMetadataTracker(folder),
updateMutex: sync.NewMutex(),
}
if err := s.meta.fromDB(db, []byte(folder)); err != nil {
l.Infof("No stored folder metadata for %q: recalculating", folder)
if err := s.recalcCounts(); backend.IsClosed(err) {
return nil
} else if err != nil {
panic(err)
}
} else if age := time.Since(s.meta.Created()); age > databaseRecheckInterval {
l.Infof("Stored folder metadata for %q is %v old; recalculating", folder, age)
if err := s.recalcCounts(); backend.IsClosed(err) {
return nil
} else if err != nil {
panic(err)
}
}
return &s
}
func (s *FileSet) recalcCounts() error {
s.meta = newMetadataTracker()
if err := s.db.checkGlobals([]byte(s.folder), s.meta); err != nil {
return err
}
t, err := s.db.newReadWriteTransaction()
if err != nil {
return err
}
var deviceID protocol.DeviceID
err = t.withAllFolderTruncated([]byte(s.folder), func(device []byte, f FileInfoTruncated) bool {
copy(deviceID[:], device)
s.meta.addFile(deviceID, f)
return true
})
if err != nil {
return err
}
s.meta.SetCreated()
return s.meta.toDB(s.db, []byte(s.folder))
}
func (s *FileSet) Drop(device protocol.DeviceID) {
@@ -150,7 +107,20 @@ func (s *FileSet) Drop(device protocol.DeviceID) {
s.meta.resetAll(device)
}
if err := s.meta.toDB(s.db, []byte(s.folder)); backend.IsClosed(err) {
t, err := s.db.newReadWriteTransaction()
if backend.IsClosed(err) {
return
} else if err != nil {
panic(err)
}
defer t.close()
if err := s.meta.toDB(t, []byte(s.folder)); backend.IsClosed(err) {
return
} else if err != nil {
panic(err)
}
if err := t.Commit(); backend.IsClosed(err) {
return
} else if err != nil {
panic(err)
@@ -168,12 +138,6 @@ func (s *FileSet) Update(device protocol.DeviceID, fs []protocol.FileInfo) {
s.updateMutex.Lock()
defer s.updateMutex.Unlock()
defer func() {
if err := s.meta.toDB(s.db, []byte(s.folder)); err != nil && !backend.IsClosed(err) {
panic(err)
}
}()
if device == protocol.LocalDeviceID {
// For the local device we have a bunch of metadata to track.
if err := s.db.updateLocalFiles([]byte(s.folder), fs, s.meta); err != nil && !backend.IsClosed(err) {
@@ -475,6 +439,18 @@ func (s *FileSet) ListDevices() []protocol.DeviceID {
return s.meta.devices()
}
func (s *FileSet) RepairSequence() (int, error) {
s.updateAndGCMutexLock() // Ensures consistent locking order
defer s.updateMutex.Unlock()
defer s.db.gcMut.RUnlock()
return s.db.repairSequenceGCLocked(s.folder, s.meta)
}
func (s *FileSet) updateAndGCMutexLock() {
s.updateMutex.Lock()
s.db.gcMut.RLock()
}
// DropFolder clears out all information related to the given folder from the
// database.
func DropFolder(db *Lowlevel, folder string) {

View File

@@ -127,18 +127,28 @@ func (l fileList) String() string {
return b.String()
}
func setSequence(seq int64, files fileList) int64 {
for i := range files {
seq++
files[i].Sequence = seq
}
return seq
}
func TestGlobalSet(t *testing.T) {
ldb := db.NewLowlevel(backend.OpenMemory())
defer ldb.Close()
m := db.NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
local0 := fileList{
protocol.FileInfo{Name: "a", Sequence: 1, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(1)},
protocol.FileInfo{Name: "b", Sequence: 2, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(2)},
protocol.FileInfo{Name: "c", Sequence: 3, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(3)},
protocol.FileInfo{Name: "d", Sequence: 4, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(4)},
protocol.FileInfo{Name: "z", Sequence: 5, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(8)},
protocol.FileInfo{Name: "a", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(1)},
protocol.FileInfo{Name: "b", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(2)},
protocol.FileInfo{Name: "c", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(3)},
protocol.FileInfo{Name: "d", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(4)},
protocol.FileInfo{Name: "z", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(8)},
}
localSeq := setSequence(0, local0)
local1 := fileList{
protocol.FileInfo{Name: "a", Sequence: 6, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(1)},
protocol.FileInfo{Name: "b", Sequence: 7, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(2)},
@@ -146,6 +156,7 @@ func TestGlobalSet(t *testing.T) {
protocol.FileInfo{Name: "d", Sequence: 9, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(4)},
protocol.FileInfo{Name: "z", Sequence: 10, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1001}}}, Deleted: true},
}
setSequence(localSeq, local1)
localTot := fileList{
local1[0],
local1[1],
@@ -159,10 +170,12 @@ func TestGlobalSet(t *testing.T) {
protocol.FileInfo{Name: "b", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(2)},
protocol.FileInfo{Name: "c", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1001}}}, Blocks: genBlocks(5)},
}
remoteSeq := setSequence(0, remote0)
remote1 := fileList{
protocol.FileInfo{Name: "b", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1001}}}, Blocks: genBlocks(6)},
protocol.FileInfo{Name: "e", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(7)},
}
setSequence(remoteSeq, remote1)
remoteTot := fileList{
remote0[0],
remote1[0],
@@ -194,140 +207,148 @@ func TestGlobalSet(t *testing.T) {
replace(m, remoteDevice0, remote0)
m.Update(remoteDevice0, remote1)
g := fileList(globalList(m))
sort.Sort(g)
check := func() {
t.Helper()
if fmt.Sprint(g) != fmt.Sprint(expectedGlobal) {
t.Errorf("Global incorrect;\n A: %v !=\n E: %v", g, expectedGlobal)
}
g := fileList(globalList(m))
sort.Sort(g)
globalFiles, globalDirectories, globalDeleted, globalBytes := int32(0), int32(0), int32(0), int64(0)
for _, f := range g {
if f.IsInvalid() {
continue
if fmt.Sprint(g) != fmt.Sprint(expectedGlobal) {
t.Errorf("Global incorrect;\n A: %v !=\n E: %v", g, expectedGlobal)
}
switch {
case f.IsDeleted():
globalDeleted++
case f.IsDirectory():
globalDirectories++
default:
globalFiles++
globalFiles, globalDirectories, globalDeleted, globalBytes := int32(0), int32(0), int32(0), int64(0)
for _, f := range g {
if f.IsInvalid() {
continue
}
switch {
case f.IsDeleted():
globalDeleted++
case f.IsDirectory():
globalDirectories++
default:
globalFiles++
}
globalBytes += f.FileSize()
}
globalBytes += f.FileSize()
}
gs := globalSize(m)
if gs.Files != globalFiles {
t.Errorf("Incorrect GlobalSize files; %d != %d", gs.Files, globalFiles)
}
if gs.Directories != globalDirectories {
t.Errorf("Incorrect GlobalSize directories; %d != %d", gs.Directories, globalDirectories)
}
if gs.Deleted != globalDeleted {
t.Errorf("Incorrect GlobalSize deleted; %d != %d", gs.Deleted, globalDeleted)
}
if gs.Bytes != globalBytes {
t.Errorf("Incorrect GlobalSize bytes; %d != %d", gs.Bytes, globalBytes)
}
h := fileList(haveList(m, protocol.LocalDeviceID))
sort.Sort(h)
if fmt.Sprint(h) != fmt.Sprint(localTot) {
t.Errorf("Have incorrect;\n A: %v !=\n E: %v", h, localTot)
}
haveFiles, haveDirectories, haveDeleted, haveBytes := int32(0), int32(0), int32(0), int64(0)
for _, f := range h {
if f.IsInvalid() {
continue
gs := globalSize(m)
if gs.Files != globalFiles {
t.Errorf("Incorrect GlobalSize files; %d != %d", gs.Files, globalFiles)
}
switch {
case f.IsDeleted():
haveDeleted++
case f.IsDirectory():
haveDirectories++
default:
haveFiles++
if gs.Directories != globalDirectories {
t.Errorf("Incorrect GlobalSize directories; %d != %d", gs.Directories, globalDirectories)
}
if gs.Deleted != globalDeleted {
t.Errorf("Incorrect GlobalSize deleted; %d != %d", gs.Deleted, globalDeleted)
}
if gs.Bytes != globalBytes {
t.Errorf("Incorrect GlobalSize bytes; %d != %d", gs.Bytes, globalBytes)
}
h := fileList(haveList(m, protocol.LocalDeviceID))
sort.Sort(h)
if fmt.Sprint(h) != fmt.Sprint(localTot) {
t.Errorf("Have incorrect (local);\n A: %v !=\n E: %v", h, localTot)
}
haveFiles, haveDirectories, haveDeleted, haveBytes := int32(0), int32(0), int32(0), int64(0)
for _, f := range h {
if f.IsInvalid() {
continue
}
switch {
case f.IsDeleted():
haveDeleted++
case f.IsDirectory():
haveDirectories++
default:
haveFiles++
}
haveBytes += f.FileSize()
}
ls := localSize(m)
if ls.Files != haveFiles {
t.Errorf("Incorrect LocalSize files; %d != %d", ls.Files, haveFiles)
}
if ls.Directories != haveDirectories {
t.Errorf("Incorrect LocalSize directories; %d != %d", ls.Directories, haveDirectories)
}
if ls.Deleted != haveDeleted {
t.Errorf("Incorrect LocalSize deleted; %d != %d", ls.Deleted, haveDeleted)
}
if ls.Bytes != haveBytes {
t.Errorf("Incorrect LocalSize bytes; %d != %d", ls.Bytes, haveBytes)
}
h = fileList(haveList(m, remoteDevice0))
sort.Sort(h)
if fmt.Sprint(h) != fmt.Sprint(remoteTot) {
t.Errorf("Have incorrect (remote);\n A: %v !=\n E: %v", h, remoteTot)
}
n := fileList(needList(m, protocol.LocalDeviceID))
sort.Sort(n)
if fmt.Sprint(n) != fmt.Sprint(expectedLocalNeed) {
t.Errorf("Need incorrect (local);\n A: %v !=\n E: %v", n, expectedLocalNeed)
}
n = fileList(needList(m, remoteDevice0))
sort.Sort(n)
if fmt.Sprint(n) != fmt.Sprint(expectedRemoteNeed) {
t.Errorf("Need incorrect (remote);\n A: %v !=\n E: %v", n, expectedRemoteNeed)
}
snap := m.Snapshot()
defer snap.Release()
f, ok := snap.Get(protocol.LocalDeviceID, "b")
if !ok {
t.Error("Unexpectedly not OK")
}
if fmt.Sprint(f) != fmt.Sprint(localTot[1]) {
t.Errorf("Get incorrect;\n A: %v !=\n E: %v", f, localTot[1])
}
f, ok = snap.Get(remoteDevice0, "b")
if !ok {
t.Error("Unexpectedly not OK")
}
if fmt.Sprint(f) != fmt.Sprint(remote1[0]) {
t.Errorf("Get incorrect (remote);\n A: %v !=\n E: %v", f, remote1[0])
}
f, ok = snap.GetGlobal("b")
if !ok {
t.Error("Unexpectedly not OK")
}
if fmt.Sprint(f) != fmt.Sprint(expectedGlobal[1]) {
t.Errorf("GetGlobal incorrect;\n A: %v !=\n E: %v", f, remote1[0])
}
f, ok = snap.Get(protocol.LocalDeviceID, "zz")
if ok {
t.Error("Unexpectedly OK")
}
if f.Name != "" {
t.Errorf("Get incorrect (local);\n A: %v !=\n E: %v", f, protocol.FileInfo{})
}
f, ok = snap.GetGlobal("zz")
if ok {
t.Error("Unexpectedly OK")
}
if f.Name != "" {
t.Errorf("GetGlobal incorrect;\n A: %v !=\n E: %v", f, protocol.FileInfo{})
}
haveBytes += f.FileSize()
}
ls := localSize(m)
if ls.Files != haveFiles {
t.Errorf("Incorrect LocalSize files; %d != %d", ls.Files, haveFiles)
}
if ls.Directories != haveDirectories {
t.Errorf("Incorrect LocalSize directories; %d != %d", ls.Directories, haveDirectories)
}
if ls.Deleted != haveDeleted {
t.Errorf("Incorrect LocalSize deleted; %d != %d", ls.Deleted, haveDeleted)
}
if ls.Bytes != haveBytes {
t.Errorf("Incorrect LocalSize bytes; %d != %d", ls.Bytes, haveBytes)
}
h = fileList(haveList(m, remoteDevice0))
sort.Sort(h)
if fmt.Sprint(h) != fmt.Sprint(remoteTot) {
t.Errorf("Have incorrect;\n A: %v !=\n E: %v", h, remoteTot)
}
n := fileList(needList(m, protocol.LocalDeviceID))
sort.Sort(n)
if fmt.Sprint(n) != fmt.Sprint(expectedLocalNeed) {
t.Errorf("Need incorrect;\n A: %v !=\n E: %v", n, expectedLocalNeed)
}
n = fileList(needList(m, remoteDevice0))
sort.Sort(n)
if fmt.Sprint(n) != fmt.Sprint(expectedRemoteNeed) {
t.Errorf("Need incorrect;\n A: %v !=\n E: %v", n, expectedRemoteNeed)
}
check()
snap := m.Snapshot()
defer snap.Release()
f, ok := snap.Get(protocol.LocalDeviceID, "b")
if !ok {
t.Error("Unexpectedly not OK")
}
if fmt.Sprint(f) != fmt.Sprint(localTot[1]) {
t.Errorf("Get incorrect;\n A: %v !=\n E: %v", f, localTot[1])
}
f, ok = snap.Get(remoteDevice0, "b")
if !ok {
t.Error("Unexpectedly not OK")
}
if fmt.Sprint(f) != fmt.Sprint(remote1[0]) {
t.Errorf("Get incorrect;\n A: %v !=\n E: %v", f, remote1[0])
}
f, ok = snap.GetGlobal("b")
if !ok {
t.Error("Unexpectedly not OK")
}
if fmt.Sprint(f) != fmt.Sprint(remote1[0]) {
t.Errorf("GetGlobal incorrect;\n A: %v !=\n E: %v", f, remote1[0])
}
f, ok = snap.Get(protocol.LocalDeviceID, "zz")
if ok {
t.Error("Unexpectedly OK")
}
if f.Name != "" {
t.Errorf("Get incorrect;\n A: %v !=\n E: %v", f, protocol.FileInfo{})
}
f, ok = snap.GetGlobal("zz")
if ok {
t.Error("Unexpectedly OK")
}
if f.Name != "" {
t.Errorf("GetGlobal incorrect;\n A: %v !=\n E: %v", f, protocol.FileInfo{})
}
av := []protocol.DeviceID{protocol.LocalDeviceID, remoteDevice0}
a := snap.Availability("a")
@@ -342,10 +363,75 @@ func TestGlobalSet(t *testing.T) {
if len(a) != 1 || a[0] != protocol.LocalDeviceID {
t.Errorf("Availability incorrect;\n A: %v !=\n E: %v", a, protocol.LocalDeviceID)
}
snap.Release()
// Now bring another remote into play
secRemote := fileList{
local1[0], // a
remote1[0], // b
local1[3], // d
remote1[1], // e
local1[4], // z
}
secRemote[0].Version = secRemote[0].Version.Update(remoteDevice1.Short())
secRemote[1].Version = secRemote[1].Version.Update(remoteDevice1.Short())
secRemote[4].Version = secRemote[4].Version.Update(remoteDevice1.Short())
secRemote[4].Deleted = false
secRemote[4].Blocks = genBlocks(1)
setSequence(0, secRemote)
expectedGlobal = fileList{
secRemote[0], // a
secRemote[1], // b
remote0[2], // c
localTot[3], // d
secRemote[3], // e
secRemote[4], // z
}
expectedLocalNeed = fileList{
secRemote[0], // a
secRemote[1], // b
remote0[2], // c
secRemote[3], // e
secRemote[4], // z
}
expectedRemoteNeed = fileList{
secRemote[0], // a
secRemote[1], // b
local0[3], // d
secRemote[4], // z
}
expectedSecRemoteNeed := fileList{
remote0[2], // c
}
m.Update(remoteDevice1, secRemote)
check()
h := fileList(haveList(m, remoteDevice1))
sort.Sort(h)
if fmt.Sprint(h) != fmt.Sprint(secRemote) {
t.Errorf("Have incorrect (secRemote);\n A: %v !=\n E: %v", h, secRemote)
}
n := fileList(needList(m, remoteDevice1))
sort.Sort(n)
if fmt.Sprint(n) != fmt.Sprint(expectedSecRemoteNeed) {
t.Errorf("Need incorrect (secRemote);\n A: %v !=\n E: %v", n, expectedSecRemoteNeed)
}
}
func TestNeedWithInvalid(t *testing.T) {
ldb := db.NewLowlevel(backend.OpenMemory())
defer ldb.Close()
s := db.NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
@@ -383,6 +469,7 @@ func TestNeedWithInvalid(t *testing.T) {
func TestUpdateToInvalid(t *testing.T) {
ldb := db.NewLowlevel(backend.OpenMemory())
defer ldb.Close()
folder := "test"
s := db.NewFileSet(folder, fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
@@ -439,6 +526,7 @@ func TestUpdateToInvalid(t *testing.T) {
func TestInvalidAvailability(t *testing.T) {
ldb := db.NewLowlevel(backend.OpenMemory())
defer ldb.Close()
s := db.NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
@@ -480,6 +568,7 @@ func TestInvalidAvailability(t *testing.T) {
func TestGlobalReset(t *testing.T) {
ldb := db.NewLowlevel(backend.OpenMemory())
defer ldb.Close()
m := db.NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
@@ -518,6 +607,7 @@ func TestGlobalReset(t *testing.T) {
func TestNeed(t *testing.T) {
ldb := db.NewLowlevel(backend.OpenMemory())
defer ldb.Close()
m := db.NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
@@ -556,6 +646,7 @@ func TestNeed(t *testing.T) {
func TestSequence(t *testing.T) {
ldb := db.NewLowlevel(backend.OpenMemory())
defer ldb.Close()
m := db.NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
@@ -586,6 +677,7 @@ func TestSequence(t *testing.T) {
func TestListDropFolder(t *testing.T) {
ldb := db.NewLowlevel(backend.OpenMemory())
defer ldb.Close()
s0 := db.NewFileSet("test0", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
local1 := []protocol.FileInfo{
@@ -636,6 +728,7 @@ func TestListDropFolder(t *testing.T) {
func TestGlobalNeedWithInvalid(t *testing.T) {
ldb := db.NewLowlevel(backend.OpenMemory())
defer ldb.Close()
s := db.NewFileSet("test1", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
@@ -677,6 +770,7 @@ func TestGlobalNeedWithInvalid(t *testing.T) {
func TestLongPath(t *testing.T) {
ldb := db.NewLowlevel(backend.OpenMemory())
defer ldb.Close()
s := db.NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
@@ -736,6 +830,7 @@ func BenchmarkUpdateOneFile(b *testing.B) {
func TestIndexID(t *testing.T) {
ldb := db.NewLowlevel(backend.OpenMemory())
defer ldb.Close()
s := db.NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
@@ -831,6 +926,7 @@ func TestDropFiles(t *testing.T) {
func TestIssue4701(t *testing.T) {
ldb := db.NewLowlevel(backend.OpenMemory())
defer ldb.Close()
s := db.NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
@@ -872,6 +968,7 @@ func TestIssue4701(t *testing.T) {
func TestWithHaveSequence(t *testing.T) {
ldb := db.NewLowlevel(backend.OpenMemory())
defer ldb.Close()
folder := "test"
s := db.NewFileSet(folder, fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
@@ -909,6 +1006,7 @@ func TestStressWithHaveSequence(t *testing.T) {
}
ldb := db.NewLowlevel(backend.OpenMemory())
defer ldb.Close()
folder := "test"
s := db.NewFileSet(folder, fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
@@ -953,6 +1051,7 @@ loop:
func TestIssue4925(t *testing.T) {
ldb := db.NewLowlevel(backend.OpenMemory())
defer ldb.Close()
folder := "test"
s := db.NewFileSet(folder, fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
@@ -979,6 +1078,7 @@ func TestIssue4925(t *testing.T) {
func TestMoveGlobalBack(t *testing.T) {
ldb := db.NewLowlevel(backend.OpenMemory())
defer ldb.Close()
folder := "test"
file := "foo"
@@ -1043,6 +1143,7 @@ func TestMoveGlobalBack(t *testing.T) {
// https://github.com/syncthing/syncthing/issues/5007
func TestIssue5007(t *testing.T) {
ldb := db.NewLowlevel(backend.OpenMemory())
defer ldb.Close()
folder := "test"
file := "foo"
@@ -1070,6 +1171,7 @@ func TestIssue5007(t *testing.T) {
// when the global file is deleted.
func TestNeedDeleted(t *testing.T) {
ldb := db.NewLowlevel(backend.OpenMemory())
defer ldb.Close()
folder := "test"
file := "foo"
@@ -1104,6 +1206,7 @@ func TestNeedDeleted(t *testing.T) {
func TestReceiveOnlyAccounting(t *testing.T) {
ldb := db.NewLowlevel(backend.OpenMemory())
defer ldb.Close()
folder := "test"
s := db.NewFileSet(folder, fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
@@ -1208,6 +1311,7 @@ func TestReceiveOnlyAccounting(t *testing.T) {
func TestNeedAfterUnignore(t *testing.T) {
ldb := db.NewLowlevel(backend.OpenMemory())
defer ldb.Close()
folder := "test"
file := "foo"
@@ -1240,6 +1344,7 @@ func TestRemoteInvalidNotAccounted(t *testing.T) {
// Remote files with the invalid bit should not count.
ldb := db.NewLowlevel(backend.OpenMemory())
defer ldb.Close()
s := db.NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
files := []protocol.FileInfo{
@@ -1259,6 +1364,7 @@ func TestRemoteInvalidNotAccounted(t *testing.T) {
func TestNeedWithNewerInvalid(t *testing.T) {
ldb := db.NewLowlevel(backend.OpenMemory())
defer ldb.Close()
s := db.NewFileSet("default", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
@@ -1297,6 +1403,7 @@ func TestNeedWithNewerInvalid(t *testing.T) {
func TestNeedAfterDeviceRemove(t *testing.T) {
ldb := db.NewLowlevel(backend.OpenMemory())
defer ldb.Close()
file := "foo"
s := db.NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
@@ -1324,6 +1431,7 @@ func TestCaseSensitive(t *testing.T) {
// Normal case sensitive lookup should work
ldb := db.NewLowlevel(backend.OpenMemory())
defer ldb.Close()
s := db.NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
local := []protocol.FileInfo{
@@ -1361,6 +1469,7 @@ func TestSequenceIndex(t *testing.T) {
// Set up a db and a few files that we will manipulate.
ldb := db.NewLowlevel(backend.OpenMemory())
defer ldb.Close()
s := db.NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
local := []protocol.FileInfo{
@@ -1454,6 +1563,7 @@ func TestSequenceIndex(t *testing.T) {
func TestIgnoreAfterReceiveOnly(t *testing.T) {
ldb := db.NewLowlevel(backend.OpenMemory())
defer ldb.Close()
file := "foo"
s := db.NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)

View File

@@ -129,27 +129,36 @@ func (f FileInfoTruncated) FileModifiedBy() protocol.ShortID {
}
func (f FileInfoTruncated) ConvertToIgnoredFileInfo(by protocol.ShortID) protocol.FileInfo {
return protocol.FileInfo{
Name: f.Name,
Type: f.Type,
ModifiedS: f.ModifiedS,
ModifiedNs: f.ModifiedNs,
ModifiedBy: by,
Version: f.Version,
RawBlockSize: f.RawBlockSize,
LocalFlags: protocol.FlagLocalIgnored,
}
file := f.copyToFileInfo()
file.SetIgnored(by)
return file
}
func (f FileInfoTruncated) ConvertToDeletedFileInfo(by protocol.ShortID, localFlags uint32) protocol.FileInfo {
func (f FileInfoTruncated) ConvertToDeletedFileInfo(by protocol.ShortID) protocol.FileInfo {
file := f.copyToFileInfo()
file.SetDeleted(by)
return file
}
// copyToFileInfo just copies all members of FileInfoTruncated to protocol.FileInfo
func (f FileInfoTruncated) copyToFileInfo() protocol.FileInfo {
return protocol.FileInfo{
Name: f.Name,
Type: f.Type,
ModifiedS: time.Now().Unix(),
ModifiedBy: by,
Deleted: true,
Version: f.Version.Update(by),
LocalFlags: localFlags,
Name: f.Name,
Size: f.Size,
ModifiedS: f.ModifiedS,
ModifiedBy: f.ModifiedBy,
Version: f.Version,
Sequence: f.Sequence,
SymlinkTarget: f.SymlinkTarget,
BlocksHash: f.BlocksHash,
Type: f.Type,
Permissions: f.Permissions,
ModifiedNs: f.ModifiedNs,
RawBlockSize: f.RawBlockSize,
LocalFlags: f.LocalFlags,
Deleted: f.Deleted,
RawInvalid: f.RawInvalid,
NoPermissions: f.NoPermissions,
}
}
@@ -248,14 +257,13 @@ func (vl VersionList) insertAt(i int, v FileVersion) VersionList {
// as the removed FileVersion and the position, where that FileVersion was.
// If there is no FileVersion for the given device, the position is -1.
func (vl VersionList) pop(device []byte) (VersionList, FileVersion, int) {
removedAt := -1
for i, v := range vl.Versions {
if bytes.Equal(v.Device, device) {
vl.Versions = append(vl.Versions[:i], vl.Versions[i+1:]...)
return vl, v, i
}
}
return vl, FileVersion{}, removedAt
return vl, FileVersion{}, -1
}
func (vl VersionList) Get(device []byte) (FileVersion, bool) {

View File

@@ -8,11 +8,14 @@ package db
import (
"bytes"
"errors"
"github.com/syncthing/syncthing/lib/db/backend"
"github.com/syncthing/syncthing/lib/protocol"
)
var errEntryFromGlobalMissing = errors.New("device present in global list but missing as device/fileinfo entry")
// A readOnlyTransaction represents a database snapshot.
type readOnlyTransaction struct {
backend.ReadTransaction
@@ -59,6 +62,9 @@ func (t readOnlyTransaction) getFileTrunc(key []byte, trunc bool) (FileIntf, boo
return nil, false, err
}
f, err := t.unmarshalTrunc(bs, trunc)
if backend.IsNotFound(err) {
return nil, false, nil
}
if err != nil {
return nil, false, err
}
@@ -75,30 +81,34 @@ func (t readOnlyTransaction) unmarshalTrunc(bs []byte, trunc bool) (FileIntf, er
return tf, nil
}
var tf protocol.FileInfo
if err := tf.Unmarshal(bs); err != nil {
var fi protocol.FileInfo
if err := fi.Unmarshal(bs); err != nil {
return nil, err
}
if err := t.fillBlockList(&tf); err != nil {
if err := t.fillFileInfo(&fi); err != nil {
return nil, err
}
return tf, nil
return fi, nil
}
func (t readOnlyTransaction) fillBlockList(fi *protocol.FileInfo) error {
if fi.BlocksHash == nil {
return nil
// fillFileInfo follows the (possible) indirection of blocks and fills it out.
func (t readOnlyTransaction) fillFileInfo(fi *protocol.FileInfo) error {
var key []byte
if len(fi.Blocks) == 0 && len(fi.BlocksHash) != 0 {
// The blocks list is indirected and we need to load it.
key = t.keyer.GenerateBlockListKey(key, fi.BlocksHash)
bs, err := t.Get(key)
if err != nil {
return err
}
var bl BlockList
if err := bl.Unmarshal(bs); err != nil {
return err
}
fi.Blocks = bl.Blocks
}
blocksKey := t.keyer.GenerateBlockListKey(nil, fi.BlocksHash)
bs, err := t.Get(blocksKey)
if err != nil {
return err
}
var bl BlockList
if err := bl.Unmarshal(bs); err != nil {
return err
}
fi.Blocks = bl.Blocks
return nil
}
@@ -117,9 +127,9 @@ func (t readOnlyTransaction) getGlobal(keyBuf, folder, file []byte, truncate boo
return nil, nil, false, err
}
vl, ok := unmarshalVersionList(bs)
if !ok {
return keyBuf, nil, false, nil
var vl VersionList
if err := vl.Unmarshal(bs); err != nil {
return nil, nil, false, err
}
keyBuf, err = t.keyer.GenerateDeviceFileKey(keyBuf, folder, vl.Versions[0].Device, file)
@@ -252,9 +262,9 @@ func (t *readOnlyTransaction) withGlobal(folder, prefix []byte, truncate bool, f
return nil
}
vl, ok := unmarshalVersionList(dbi.Value())
if !ok {
continue
var vl VersionList
if err := vl.Unmarshal(dbi.Value()); err != nil {
return err
}
dk, err = t.keyer.GenerateDeviceFileKey(dk, folder, vl.Versions[0].Device, name)
@@ -293,9 +303,9 @@ func (t *readOnlyTransaction) availability(folder, file []byte) ([]protocol.Devi
return nil, err
}
vl, ok := unmarshalVersionList(bs)
if !ok {
return nil, nil
var vl VersionList
if err := vl.Unmarshal(bs); err != nil {
return nil, err
}
var devices []protocol.DeviceID
@@ -331,59 +341,31 @@ func (t *readOnlyTransaction) withNeed(folder, device []byte, truncate bool, fn
var dk []byte
devID := protocol.DeviceIDFromBytes(device)
for dbi.Next() {
vl, ok := unmarshalVersionList(dbi.Value())
if !ok {
continue
var vl VersionList
if err := vl.Unmarshal(dbi.Value()); err != nil {
return err
}
haveFV, have := vl.Get(device)
// XXX: This marks Concurrent (i.e. conflicting) changes as
// needs. Maybe we should do that, but it needs special
// handling in the puller.
if have && haveFV.Version.GreaterEqual(vl.Versions[0].Version) {
continue
}
name := t.keyer.NameFromGlobalVersionKey(dbi.Key())
needVersion := vl.Versions[0].Version
needDevice := protocol.DeviceIDFromBytes(vl.Versions[0].Device)
for i := range vl.Versions {
if !vl.Versions[i].Version.Equal(needVersion) {
// We haven't found a valid copy of the file with the needed version.
break
}
if vl.Versions[i].Invalid {
// The file is marked invalid, don't use it.
continue
}
dk, err = t.keyer.GenerateDeviceFileKey(dk, folder, vl.Versions[i].Device, name)
if err != nil {
return err
}
gf, ok, err := t.getFileTrunc(dk, truncate)
if err != nil {
return err
}
if !ok {
continue
}
if gf.IsDeleted() && !have {
// We don't need deleted files that we don't have
break
}
l.Debugf("need folder=%q device=%v name=%q have=%v invalid=%v haveV=%v globalV=%v globalDev=%v", folder, devID, name, have, haveFV.Invalid, haveFV.Version, needVersion, needDevice)
if !fn(gf) {
return nil
}
// This file is handled, no need to look further in the version list
break
dk, err = t.keyer.GenerateDeviceFileKey(dk, folder, vl.Versions[0].Device, name)
if err != nil {
return err
}
gf, ok, err := t.getFileTrunc(dk, truncate)
if err != nil {
return err
}
if !ok {
return errEntryFromGlobalMissing
}
if !need(gf, have, haveFV.Version) {
continue
}
l.Debugf("need folder=%q device=%v name=%q have=%v invalid=%v haveV=%v globalV=%v globalDev=%v", folder, devID, name, have, haveFV.Invalid, haveFV.Version, vl.Versions[0].Version, vl.Versions[0].Device)
if !fn(gf) {
return dbi.Error()
}
}
return dbi.Error()
@@ -440,7 +422,7 @@ func (db *Lowlevel) newReadWriteTransaction() (readWriteTransaction, error) {
}, nil
}
func (t readWriteTransaction) commit() error {
func (t readWriteTransaction) Commit() error {
t.readOnlyTransaction.close()
return t.WriteTransaction.Commit()
}
@@ -450,26 +432,39 @@ func (t readWriteTransaction) close() {
t.WriteTransaction.Release()
}
func (t readWriteTransaction) putFile(key []byte, fi protocol.FileInfo) error {
if fi.Blocks != nil {
if fi.BlocksHash == nil {
fi.BlocksHash = protocol.BlocksHash(fi.Blocks)
}
blocksKey := t.keyer.GenerateBlockListKey(nil, fi.BlocksHash)
if _, err := t.Get(blocksKey); backend.IsNotFound(err) {
// putFile stores a file in the database, taking care of indirected fields.
// Set the truncated flag when putting a file that deliberatly can have an
// empty block list but a non-empty block list hash. This should normally be
// false.
func (t readWriteTransaction) putFile(fkey []byte, fi protocol.FileInfo, truncated bool) error {
var bkey []byte
// Always set the blocks hash when there are blocks. Leave the blocks
// hash alone when there are no blocks and we might be putting a
// "truncated" FileInfo (no blocks, but the hash reference is live).
if len(fi.Blocks) > 0 {
fi.BlocksHash = protocol.BlocksHash(fi.Blocks)
} else if !truncated {
fi.BlocksHash = nil
}
// Indirect the blocks if the block list is large enough.
if len(fi.Blocks) > blocksIndirectionCutoff {
bkey = t.keyer.GenerateBlockListKey(bkey, fi.BlocksHash)
if _, err := t.Get(bkey); backend.IsNotFound(err) {
// Marshal the block list and save it
blocksBs := mustMarshal(&BlockList{Blocks: fi.Blocks})
if err := t.Put(blocksKey, blocksBs); err != nil {
if err := t.Put(bkey, blocksBs); err != nil {
return err
}
} else if err != nil {
return err
}
fi.Blocks = nil
}
fi.Blocks = nil
fiBs := mustMarshal(&fi)
return t.Put(key, fiBs)
return t.Put(fkey, fiBs)
}
// updateGlobal adds this device+version to the version list for the given
@@ -490,14 +485,10 @@ func (t readWriteTransaction) updateGlobal(gk, keyBuf, folder, device []byte, fi
if err != nil {
return nil, false, err
}
if insertedAt == -1 {
l.Debugln("update global; same version, global unchanged")
return keyBuf, false, nil
}
name := []byte(file.Name)
var global protocol.FileInfo
var global FileIntf
if insertedAt == 0 {
// Inserted a new newest version
global = file
@@ -506,7 +497,7 @@ func (t readWriteTransaction) updateGlobal(gk, keyBuf, folder, device []byte, fi
if err != nil {
return nil, false, err
}
new, ok, err := t.getFileByKey(keyBuf)
new, ok, err := t.getFileTrunc(keyBuf, true)
if err != nil || !ok {
return keyBuf, false, err
}
@@ -539,7 +530,7 @@ func (t readWriteTransaction) updateGlobal(gk, keyBuf, folder, device []byte, fi
if err != nil {
return nil, false, err
}
oldFile, ok, err := t.getFileByKey(keyBuf)
oldFile, ok, err := t.getFileTrunc(keyBuf, true)
if err != nil {
return nil, false, err
}
@@ -563,7 +554,7 @@ func (t readWriteTransaction) updateGlobal(gk, keyBuf, folder, device []byte, fi
// updateLocalNeed checks whether the given file is still needed on the local
// device according to the version list and global FileInfo given and updates
// the db accordingly.
func (t readWriteTransaction) updateLocalNeed(keyBuf, folder, name []byte, fl VersionList, global protocol.FileInfo) ([]byte, error) {
func (t readWriteTransaction) updateLocalNeed(keyBuf, folder, name []byte, fl VersionList, global FileIntf) ([]byte, error) {
var err error
keyBuf, err = t.keyer.GenerateNeedFileKey(keyBuf, folder, name)
if err != nil {
@@ -640,8 +631,8 @@ func (t readWriteTransaction) removeFromGlobal(gk, keyBuf, folder, device []byte
if err != nil {
return nil, err
}
if f, ok, err := t.getFileByKey(keyBuf); err != nil {
return keyBuf, nil
if f, ok, err := t.getFileTrunc(keyBuf, true); err != nil {
return nil, err
} else if ok {
meta.removeFile(protocol.GlobalDeviceID, f)
}
@@ -666,9 +657,12 @@ func (t readWriteTransaction) removeFromGlobal(gk, keyBuf, folder, device []byte
if err != nil {
return nil, err
}
global, ok, err := t.getFileByKey(keyBuf)
if err != nil || !ok {
return keyBuf, err
global, ok, err := t.getFileTrunc(keyBuf, true)
if err != nil {
return nil, err
}
if !ok {
return nil, errEntryFromGlobalMissing
}
keyBuf, err = t.updateLocalNeed(keyBuf, folder, file, fl, global)
if err != nil {
@@ -720,15 +714,12 @@ func (t *readWriteTransaction) withAllFolderTruncated(folder []byte, fn func(dev
}
continue
}
var f FileInfoTruncated
// The iterator function may keep a reference to the unmarshalled
// struct, which in turn references the buffer it was unmarshalled
// from. dbi.Value() just returns an internal slice that it reuses, so
// we need to copy it.
err := f.Unmarshal(append([]byte{}, dbi.Value()...))
intf, err := t.unmarshalTrunc(dbi.Value(), true)
if err != nil {
return err
}
f := intf.(FileInfoTruncated)
switch f.Name {
case "", ".", "..", "/": // A few obviously invalid filenames
@@ -752,10 +743,7 @@ func (t *readWriteTransaction) withAllFolderTruncated(folder []byte, fn func(dev
return nil
}
}
if err := dbi.Error(); err != nil {
return err
}
return t.commit()
return dbi.Error()
}
type marshaller interface {

View File

@@ -7,6 +7,7 @@
package dialer
import (
"net"
"net/http"
"net/url"
"os"
@@ -58,3 +59,36 @@ func socksDialerFunction(u *url.URL, forward proxy.Dialer) (proxy.Dialer, error)
return proxy.SOCKS5("tcp", u.Host, auth, forward)
}
// dialerConn is needed because proxy dialed connections have RemoteAddr() pointing at the proxy,
// which then screws up various things such as IsLAN checks, and "let's populate the relay invitation address from
// existing connection" shenanigans.
type dialerConn struct {
net.Conn
addr net.Addr
}
func (c dialerConn) RemoteAddr() net.Addr {
return c.addr
}
func newDialerAddr(network, addr string) net.Addr {
netAddr, err := net.ResolveIPAddr(network, addr)
if err == nil {
return netAddr
}
return fallbackAddr{network, addr}
}
type fallbackAddr struct {
network string
addr string
}
func (a fallbackAddr) Network() string {
return a.network
}
func (a fallbackAddr) String() string {
return a.addr
}

View File

@@ -24,6 +24,8 @@ var errUnexpectedInterfaceType = errors.New("unexpected interface type")
// digging through dialerConn to extract the *net.TCPConn
func SetTCPOptions(conn net.Conn) error {
switch conn := conn.(type) {
case dialerConn:
return SetTCPOptions(conn.Conn)
case *net.TCPConn:
var err error
if err = conn.SetLinger(0); err != nil {
@@ -46,6 +48,8 @@ func SetTCPOptions(conn net.Conn) error {
func SetTrafficClass(conn net.Conn, class int) error {
switch conn := conn.(type) {
case dialerConn:
return SetTrafficClass(conn.Conn, class)
case *net.TCPConn:
e1 := ipv4.NewConn(conn).SetTOS(class)
e2 := ipv6.NewConn(conn).SetTrafficClass(class)
@@ -72,7 +76,10 @@ func dialContextWithFallback(ctx context.Context, fallback proxy.ContextDialer,
if noFallback {
conn, err := dialer.DialContext(ctx, network, addr)
l.Debugf("Dialing no fallback result %s %s: %v %v", network, addr, conn, err)
return conn, err
if err != nil {
return nil, err
}
return dialerConn{conn, newDialerAddr(network, addr)}, nil
}
ctx, cancel := context.WithCancel(ctx)
@@ -84,6 +91,9 @@ func dialContextWithFallback(ctx context.Context, fallback proxy.ContextDialer,
go func() {
proxyConn, proxyErr = dialer.DialContext(ctx, network, addr)
l.Debugf("Dialing proxy result %s %s: %v %v", network, addr, proxyConn, proxyErr)
if proxyErr == nil {
proxyConn = dialerConn{proxyConn, newDialerAddr(network, addr)}
}
close(proxyDone)
}()
go func() {
@@ -96,7 +106,7 @@ func dialContextWithFallback(ctx context.Context, fallback proxy.ContextDialer,
go func() {
<-fallbackDone
if fallbackErr == nil {
fallbackConn.Close()
_ = fallbackConn.Close()
}
}()
return proxyConn, nil

View File

@@ -7,6 +7,7 @@
package discover
import (
"context"
"sort"
stdsync "sync"
"time"
@@ -73,7 +74,7 @@ func (m *cachingMux) Add(finder Finder, cacheTime, negCacheTime time.Duration) {
// Lookup attempts to resolve the device ID using any of the added Finders,
// while obeying the cache settings.
func (m *cachingMux) Lookup(deviceID protocol.DeviceID) (addresses []string, err error) {
func (m *cachingMux) Lookup(ctx context.Context, deviceID protocol.DeviceID) (addresses []string, err error) {
m.mut.RLock()
for i, finder := range m.finders {
if cacheEntry, ok := m.caches[i].Get(deviceID); ok {
@@ -99,7 +100,7 @@ func (m *cachingMux) Lookup(deviceID protocol.DeviceID) (addresses []string, err
}
// Perform the actual lookup and cache the result.
if addrs, err := finder.Lookup(deviceID); err == nil {
if addrs, err := finder.Lookup(ctx, deviceID); err == nil {
l.Debugln("lookup for", deviceID, "at", finder)
l.Debugln(" addresses:", addrs)
addresses = append(addresses, addrs...)

View File

@@ -7,6 +7,7 @@
package discover
import (
"context"
"reflect"
"testing"
"time"
@@ -39,7 +40,9 @@ func TestCacheUnique(t *testing.T) {
f1 := &fakeDiscovery{addresses0}
c.Add(f1, time.Minute, 0)
addr, err := c.Lookup(protocol.LocalDeviceID)
ctx := context.Background()
addr, err := c.Lookup(ctx, protocol.LocalDeviceID)
if err != nil {
t.Fatal(err)
}
@@ -53,7 +56,7 @@ func TestCacheUnique(t *testing.T) {
f2 := &fakeDiscovery{addresses1}
c.Add(f2, time.Minute, 0)
addr, err = c.Lookup(protocol.LocalDeviceID)
addr, err = c.Lookup(ctx, protocol.LocalDeviceID)
if err != nil {
t.Fatal(err)
}
@@ -66,7 +69,7 @@ type fakeDiscovery struct {
addresses []string
}
func (f *fakeDiscovery) Lookup(deviceID protocol.DeviceID) (addresses []string, err error) {
func (f *fakeDiscovery) Lookup(_ context.Context, deviceID protocol.DeviceID) (addresses []string, err error) {
return f.addresses, nil
}
@@ -96,7 +99,7 @@ func TestCacheSlowLookup(t *testing.T) {
// Start a lookup, which will take at least a second
t0 := time.Now()
go c.Lookup(protocol.LocalDeviceID)
go c.Lookup(context.Background(), protocol.LocalDeviceID)
<-started // The slow lookup method has been called so we're inside the lock
// It should be possible to get ChildErrors while it's running
@@ -116,7 +119,7 @@ type slowDiscovery struct {
started chan struct{}
}
func (f *slowDiscovery) Lookup(deviceID protocol.DeviceID) (addresses []string, err error) {
func (f *slowDiscovery) Lookup(_ context.Context, deviceID protocol.DeviceID) (addresses []string, err error) {
close(f.started)
time.Sleep(f.delay)
return nil, nil

View File

@@ -7,6 +7,7 @@
package discover
import (
"context"
"time"
"github.com/syncthing/syncthing/lib/protocol"
@@ -15,7 +16,7 @@ import (
// A Finder provides lookup services of some kind.
type Finder interface {
Lookup(deviceID protocol.DeviceID) (address []string, err error)
Lookup(ctx context.Context, deviceID protocol.DeviceID) (address []string, err error)
Error() error
String() string
Cache() map[protocol.DeviceID]CacheEntry

View File

@@ -41,8 +41,8 @@ type globalClient struct {
}
type httpClient interface {
Get(url string) (*http.Response, error)
Post(url, ctype string, data io.Reader) (*http.Response, error)
Get(ctx context.Context, url string) (*http.Response, error)
Post(ctx context.Context, url, ctype string, data io.Reader) (*http.Response, error)
}
const (
@@ -89,7 +89,7 @@ func NewGlobal(server string, cert tls.Certificate, addrList AddressLister, evLo
// The http.Client used for announcements. It needs to have our
// certificate to prove our identity, and may or may not verify the server
// certificate depending on the insecure setting.
var announceClient httpClient = &http.Client{
var announceClient httpClient = &contextClient{&http.Client{
Timeout: requestTimeout,
Transport: &http.Transport{
DialContext: dialer.DialContext,
@@ -99,14 +99,14 @@ func NewGlobal(server string, cert tls.Certificate, addrList AddressLister, evLo
Certificates: []tls.Certificate{cert},
},
},
}
}}
if opts.id != "" {
announceClient = newIDCheckingHTTPClient(announceClient, devID)
}
// The http.Client used for queries. We don't need to present our
// certificate here, so lets not include it. May be insecure if requested.
var queryClient httpClient = &http.Client{
var queryClient httpClient = &contextClient{&http.Client{
Timeout: requestTimeout,
Transport: &http.Transport{
DialContext: dialer.DialContext,
@@ -115,7 +115,7 @@ func NewGlobal(server string, cert tls.Certificate, addrList AddressLister, evLo
InsecureSkipVerify: opts.insecure,
},
},
}
}}
if opts.id != "" {
queryClient = newIDCheckingHTTPClient(queryClient, devID)
}
@@ -139,7 +139,7 @@ func NewGlobal(server string, cert tls.Certificate, addrList AddressLister, evLo
}
// Lookup returns the list of addresses where the given device is available
func (c *globalClient) Lookup(device protocol.DeviceID) (addresses []string, err error) {
func (c *globalClient) Lookup(ctx context.Context, device protocol.DeviceID) (addresses []string, err error) {
if c.noLookup {
return nil, lookupError{
error: errors.New("lookups not supported"),
@@ -156,7 +156,7 @@ func (c *globalClient) Lookup(device protocol.DeviceID) (addresses []string, err
q.Set("device", device.String())
qURL.RawQuery = q.Encode()
resp, err := c.queryClient.Get(qURL.String())
resp, err := c.queryClient.Get(ctx, qURL.String())
if err != nil {
l.Debugln("globalClient.Lookup", qURL, err)
return nil, err
@@ -211,7 +211,7 @@ func (c *globalClient) serve(ctx context.Context) {
timer.Reset(2 * time.Second)
case <-timer.C:
c.sendAnnouncement(timer)
c.sendAnnouncement(ctx, timer)
case <-ctx.Done():
return
@@ -219,7 +219,7 @@ func (c *globalClient) serve(ctx context.Context) {
}
}
func (c *globalClient) sendAnnouncement(timer *time.Timer) {
func (c *globalClient) sendAnnouncement(ctx context.Context, timer *time.Timer) {
var ann announcement
if c.addrList != nil {
ann.Addresses = c.addrList.ExternalAddresses()
@@ -239,7 +239,7 @@ func (c *globalClient) sendAnnouncement(timer *time.Timer) {
l.Debugf("Announcement: %s", postData)
resp, err := c.announceClient.Post(c.server, "application/json", bytes.NewReader(postData))
resp, err := c.announceClient.Post(ctx, c.server, "application/json", bytes.NewReader(postData))
if err != nil {
l.Debugln("announce POST:", err)
c.setError(err)
@@ -362,8 +362,8 @@ func (c *idCheckingHTTPClient) check(resp *http.Response) error {
return nil
}
func (c *idCheckingHTTPClient) Get(url string) (*http.Response, error) {
resp, err := c.httpClient.Get(url)
func (c *idCheckingHTTPClient) Get(ctx context.Context, url string) (*http.Response, error) {
resp, err := c.httpClient.Get(ctx, url)
if err != nil {
return nil, err
}
@@ -374,8 +374,8 @@ func (c *idCheckingHTTPClient) Get(url string) (*http.Response, error) {
return resp, nil
}
func (c *idCheckingHTTPClient) Post(url, ctype string, data io.Reader) (*http.Response, error) {
resp, err := c.httpClient.Post(url, ctype, data)
func (c *idCheckingHTTPClient) Post(ctx context.Context, url, ctype string, data io.Reader) (*http.Response, error) {
resp, err := c.httpClient.Post(ctx, url, ctype, data)
if err != nil {
return nil, err
}
@@ -403,3 +403,32 @@ func (e *errorHolder) Error() error {
e.mut.Unlock()
return err
}
type contextClient struct {
*http.Client
}
func (c *contextClient) Get(ctx context.Context, url string) (*http.Response, error) {
// For <go1.13 compatibility. Use the following commented line once that
// isn't required anymore.
// req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
req.Cancel = ctx.Done()
return c.Client.Do(req)
}
func (c *contextClient) Post(ctx context.Context, url, ctype string, data io.Reader) (*http.Response, error) {
// For <go1.13 compatibility. Use the following commented line once that
// isn't required anymore.
// req, err := http.NewRequestWithContext(ctx, "POST", url, data)
req, err := http.NewRequest("POST", url, data)
if err != nil {
return nil, err
}
req.Cancel = ctx.Done()
req.Header.Set("Content-Type", ctype)
return c.Client.Do(req)
}

View File

@@ -7,6 +7,7 @@
package discover
import (
"context"
"crypto/tls"
"io/ioutil"
"net"
@@ -225,7 +226,7 @@ func testLookup(url string) ([]string, error) {
go disco.Serve()
defer disco.Stop()
return disco.Lookup(protocol.LocalDeviceID)
return disco.Lookup(context.Background(), protocol.LocalDeviceID)
}
type fakeDiscoveryServer struct {

View File

@@ -91,7 +91,7 @@ func NewLocal(id protocol.DeviceID, addr string, addrList AddressLister, evLogge
}
// Lookup returns a list of addresses the device is available at.
func (c *localClient) Lookup(device protocol.DeviceID) (addresses []string, err error) {
func (c *localClient) Lookup(_ context.Context, device protocol.DeviceID) (addresses []string, err error) {
if cache, ok := c.Get(device); ok {
if time.Since(cache.when) < CacheLifeTime {
addresses = cache.Addresses

View File

@@ -11,7 +11,6 @@ package fs
import (
"bytes"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
@@ -137,7 +136,7 @@ func (f *BasicFilesystem) Roots() ([]string, error) {
hr, _, _ := getLogicalDriveStringsHandle.Call(uintptr(unsafe.Pointer(&bufferSize)), uintptr(unsafe.Pointer(&buffer)))
if hr == 0 {
return nil, fmt.Errorf("Syscall failed")
return nil, errors.New("syscall failed")
}
var drives []string

View File

@@ -52,9 +52,10 @@ const randomBlockShift = 14 // 128k
// - Two fakefs:s pointing at the same root path see the same files.
//
type fakefs struct {
mut sync.Mutex
root *fakeEntry
insens bool
mut sync.Mutex
root *fakeEntry
insens bool
withContent bool
}
var (
@@ -93,9 +94,9 @@ func newFakeFilesystem(root string) *fakefs {
sizeavg, _ := strconv.Atoi(params.Get("sizeavg"))
seed, _ := strconv.Atoi(params.Get("seed"))
if params.Get("insens") == "true" {
fs.insens = true
}
fs.insens = params.Get("insens") == "true"
fs.withContent = params.Get("content") == "true"
if sizeavg == 0 {
sizeavg = 1 << 20
}
@@ -151,6 +152,7 @@ type fakeEntry struct {
gid int
mtime time.Time
children map[string]*fakeEntry
content []byte
}
func (fs *fakefs) entryForName(name string) *fakeEntry {
@@ -227,6 +229,10 @@ func (fs *fakefs) create(name string) (*fakeEntry, error) {
entry.size = 0
entry.mtime = time.Now()
entry.mode = 0666
entry.content = nil
if fs.withContent {
entry.content = make([]byte, 0)
}
return entry, nil
}
@@ -246,6 +252,10 @@ func (fs *fakefs) create(name string) (*fakeEntry, error) {
base = UnicodeLowercase(base)
}
if fs.withContent {
new.content = make([]byte, 0)
}
entry.children[base] = new
return new, nil
}
@@ -417,6 +427,9 @@ func (fs *fakefs) OpenFile(name string, flags int, mode FileMode) (File, error)
mode: mode,
mtime: time.Now(),
}
if fs.withContent {
newEntry.content = make([]byte, 0)
}
entry.children[key] = newEntry
return &fakeFile{fakeEntry: newEntry}, nil
@@ -660,6 +673,12 @@ func (f *fakeFile) readShortAt(p []byte, offs int64) (int, error) {
return 0, io.EOF
}
if f.content != nil {
n := copy(p, f.content[int(offs):])
f.offset = offs + int64(n)
return n, nil
}
// Lazily calculate our main seed, a simple 64 bit FNV hash our file
// name.
if f.seed == 0 {
@@ -746,6 +765,15 @@ func (f *fakeFile) WriteAt(p []byte, off int64) (int, error) {
return 0, errors.New("is a directory")
}
if f.content != nil {
if len(f.content) < int(off)+len(p) {
newc := make([]byte, int(off)+len(p))
copy(newc, f.content)
f.content = newc
}
copy(f.content[int(off):], p)
}
f.rng = nil
f.offset = off + int64(len(p))
if f.offset > f.size {
@@ -765,6 +793,9 @@ func (f *fakeFile) Truncate(size int64) error {
f.mut.Lock()
defer f.mut.Unlock()
if f.content != nil {
f.content = f.content[:int(size)]
}
f.rng = nil
f.size = size
if f.offset > size {

View File

@@ -896,6 +896,35 @@ func testFakeFSCreateInsens(t *testing.T, fs Filesystem) {
assertDir(t, fs, "/", []string{"FOO"})
}
func TestReadWriteContent(t *testing.T) {
fs := newFakeFilesystem("foo?content=true")
fd, err := fs.Create("file")
if err != nil {
t.Fatal(err)
}
if _, err := fd.Write([]byte("foo")); err != nil {
t.Fatal(err)
}
if _, err := fd.WriteAt([]byte("bar"), 5); err != nil {
t.Fatal(err)
}
expected := []byte("foo\x00\x00bar")
buf := make([]byte, len(expected)-1)
n, err := fd.ReadAt(buf, 1) // note offset one byte
if err != nil {
t.Fatal(err)
}
if n != len(expected)-1 {
t.Fatal("wrong number of bytes read")
}
if !bytes.Equal(buf[:n], expected[1:]) {
fmt.Printf("%d %q\n", n, buf[:n])
t.Error("wrong data in file")
}
}
func cleanup(fs Filesystem) error {
filenames, _ := fs.DirNames("/")
for _, filename := range filenames {

View File

@@ -508,13 +508,13 @@ func parseIgnoreFile(fs fs.Filesystem, fd io.Reader, currentFile string, cd Chan
case strings.HasPrefix(line, "#include"):
fields := strings.SplitN(line, " ", 2)
if len(fields) != 2 {
err = fmt.Errorf("failed to parse #include line: no file?")
err = errors.New("failed to parse #include line: no file?")
break
}
includeRel := strings.TrimSpace(fields[1])
if includeRel == "" {
err = fmt.Errorf("failed to parse #include line: no file?")
err = errors.New("failed to parse #include line: no file?")
break
}

View File

@@ -7,6 +7,7 @@
package model
import (
"context"
"sync"
)
@@ -29,19 +30,45 @@ func newByteSemaphore(max int) *byteSemaphore {
return &s
}
func (s *byteSemaphore) takeWithContext(ctx context.Context, bytes int) error {
done := make(chan struct{})
var err error
go func() {
err = s.takeInner(ctx, bytes)
close(done)
}()
select {
case <-done:
case <-ctx.Done():
s.cond.Broadcast()
<-done
}
return err
}
func (s *byteSemaphore) take(bytes int) {
_ = s.takeInner(context.Background(), bytes)
}
func (s *byteSemaphore) takeInner(ctx context.Context, bytes int) error {
s.mut.Lock()
defer s.mut.Unlock()
if bytes > s.max {
bytes = s.max
}
for bytes > s.available {
s.cond.Wait()
select {
case <-ctx.Done():
return ctx.Err()
default:
}
if bytes > s.max {
bytes = s.max
}
}
s.available -= bytes
s.mut.Unlock()
return nil
}
func (s *byteSemaphore) give(bytes int) {

View File

@@ -301,7 +301,9 @@ func (f *folder) pull() bool {
f.setState(FolderSyncWaiting)
defer f.setState(FolderIdle)
f.ioLimiter.take(1)
if err := f.ioLimiter.takeWithContext(f.ctx, 1); err != nil {
return true
}
defer f.ioLimiter.give(1)
return f.puller.pull()
@@ -340,7 +342,9 @@ func (f *folder) scanSubdirs(subDirs []string) error {
f.setError(nil)
f.setState(FolderScanWaiting)
f.ioLimiter.take(1)
if err := f.ioLimiter.takeWithContext(f.ctx, 1); err != nil {
return err
}
defer f.ioLimiter.give(1)
for i := range subDirs {
@@ -532,7 +536,8 @@ func (f *folder) scanSubdirs(subDirs []string) error {
}
return true
}
nf := file.ConvertToDeletedFileInfo(f.shortID, f.localFlags)
nf := file.ConvertToDeletedFileInfo(f.shortID)
nf.LocalFlags = f.localFlags
if file.ShouldConflict() {
// We do not want to override the global version with
// the deleted file. Setting to an empty version makes

View File

@@ -104,14 +104,8 @@ func (f *receiveOnlyFolder) Revert() {
return true // continue
}
fi = protocol.FileInfo{
Name: fi.Name,
Type: fi.Type,
ModifiedS: time.Now().Unix(),
ModifiedBy: f.shortID,
Deleted: true,
Version: protocol.Vector{}, // if this file ever resurfaces anywhere we want our delete to be strictly older
}
fi.SetDeleted(f.shortID)
fi.Version = protocol.Vector{} // if this file ever resurfaces anywhere we want our delete to be strictly older
} else {
// Revert means to throw away our local changes. We reset the
// version to the empty vector, which is strictly older than any

View File

@@ -9,8 +9,6 @@ package model
import (
"bytes"
"context"
"io/ioutil"
"path/filepath"
"testing"
"time"
@@ -28,18 +26,18 @@ func TestRecvOnlyRevertDeletes(t *testing.T) {
// Get us a model up and running
m, f := setupROFolder()
m, f := setupROFolder(t)
ffs := f.Filesystem()
defer cleanupModelAndRemoveDir(m, ffs.URI())
defer cleanupModel(m)
// Create some test data
for _, dir := range []string{".stfolder", "ignDir", "unknownDir"} {
must(t, ffs.MkdirAll(dir, 0755))
}
must(t, ioutil.WriteFile(filepath.Join(ffs.URI(), "ignDir/ignFile"), []byte("hello\n"), 0644))
must(t, ioutil.WriteFile(filepath.Join(ffs.URI(), "unknownDir/unknownFile"), []byte("hello\n"), 0644))
must(t, ioutil.WriteFile(filepath.Join(ffs.URI(), ".stignore"), []byte("ignDir\n"), 0644))
must(t, writeFile(ffs, "ignDir/ignFile", []byte("hello\n"), 0644))
must(t, writeFile(ffs, "unknownDir/unknownFile", []byte("hello\n"), 0644))
must(t, writeFile(ffs, ".stignore", []byte("ignDir\n"), 0644))
knownFiles := setupKnownFiles(t, ffs, []byte("hello\n"))
@@ -48,15 +46,18 @@ func TestRecvOnlyRevertDeletes(t *testing.T) {
m.Index(device1, "ro", knownFiles)
f.updateLocalsFromScanning(knownFiles)
size := globalSize(t, m, "ro")
m.fmut.RLock()
snap := m.folderFiles["ro"].Snapshot()
m.fmut.RUnlock()
size := snap.GlobalSize()
snap.Release()
if size.Files != 1 || size.Directories != 1 {
t.Fatalf("Global: expected 1 file and 1 directory: %+v", size)
}
// Start the folder. This will cause a scan, should discover the other stuff in the folder
// Scan, should discover the other stuff in the folder
m.startFolder("ro")
m.ScanFolder("ro")
must(t, m.ScanFolder("ro"))
// We should now have two files and two directories.
@@ -109,9 +110,9 @@ func TestRecvOnlyRevertNeeds(t *testing.T) {
// Get us a model up and running
m, f := setupROFolder()
m, f := setupROFolder(t)
ffs := f.Filesystem()
defer cleanupModelAndRemoveDir(m, ffs.URI())
defer cleanupModel(m)
// Create some test data
@@ -124,10 +125,9 @@ func TestRecvOnlyRevertNeeds(t *testing.T) {
m.Index(device1, "ro", knownFiles)
f.updateLocalsFromScanning(knownFiles)
// Start the folder. This will cause a scan.
// Scan the folder.
m.startFolder("ro")
m.ScanFolder("ro")
must(t, m.ScanFolder("ro"))
// Everything should be in sync.
@@ -151,7 +151,7 @@ func TestRecvOnlyRevertNeeds(t *testing.T) {
// Update the file.
newData := []byte("totally different data\n")
must(t, ioutil.WriteFile(filepath.Join(ffs.URI(), "knownDir/knownFile"), newData, 0644))
must(t, writeFile(ffs, "knownDir/knownFile", newData, 0644))
// Rescan.
@@ -196,13 +196,11 @@ func TestRecvOnlyRevertNeeds(t *testing.T) {
}
func TestRecvOnlyUndoChanges(t *testing.T) {
testOs := &fatalOs{t}
// Get us a model up and running
m, f := setupROFolder()
m, f := setupROFolder(t)
ffs := f.Filesystem()
defer cleanupModelAndRemoveDir(m, ffs.URI())
defer cleanupModel(m)
// Create some test data
@@ -210,20 +208,14 @@ func TestRecvOnlyUndoChanges(t *testing.T) {
oldData := []byte("hello\n")
knownFiles := setupKnownFiles(t, ffs, oldData)
m.fmut.Lock()
fset := m.folderFiles["ro"]
m.fmut.Unlock()
folderFs := fset.MtimeFS()
// Send and index update for the known stuff
// Send an index update for the known stuff
m.Index(device1, "ro", knownFiles)
f.updateLocalsFromScanning(knownFiles)
// Start the folder. This will cause a scan.
// Scan the folder.
m.startFolder("ro")
m.ScanFolder("ro")
must(t, m.ScanFolder("ro"))
// Everything should be in sync.
@@ -246,12 +238,11 @@ func TestRecvOnlyUndoChanges(t *testing.T) {
// Create a file and modify another
file := filepath.Join(ffs.URI(), "foo")
must(t, ioutil.WriteFile(file, []byte("hello\n"), 0644))
const file = "foo"
must(t, writeFile(ffs, file, []byte("hello\n"), 0644))
must(t, writeFile(ffs, "knownDir/knownFile", []byte("bye\n"), 0644))
must(t, ioutil.WriteFile(filepath.Join(ffs.URI(), "knownDir/knownFile"), []byte("bye\n"), 0644))
m.ScanFolder("ro")
must(t, m.ScanFolder("ro"))
size = receiveOnlyChangedSize(t, m, "ro")
if size.Files != 2 {
@@ -260,11 +251,11 @@ func TestRecvOnlyUndoChanges(t *testing.T) {
// Remove the file again and undo the modification
testOs.Remove(file)
must(t, ioutil.WriteFile(filepath.Join(ffs.URI(), "knownDir/knownFile"), oldData, 0644))
folderFs.Chtimes("knownDir/knownFile", knownFiles[1].ModTime(), knownFiles[1].ModTime())
must(t, ffs.Remove(file))
must(t, writeFile(ffs, "knownDir/knownFile", oldData, 0644))
must(t, ffs.Chtimes("knownDir/knownFile", knownFiles[1].ModTime(), knownFiles[1].ModTime()))
m.ScanFolder("ro")
must(t, m.ScanFolder("ro"))
size = receiveOnlyChangedSize(t, m, "ro")
if size.Files+size.Directories+size.Deleted != 0 {
@@ -276,7 +267,7 @@ func setupKnownFiles(t *testing.T, ffs fs.Filesystem, data []byte) []protocol.Fi
t.Helper()
must(t, ffs.MkdirAll("knownDir", 0755))
must(t, ioutil.WriteFile(filepath.Join(ffs.URI(), "knownDir/knownFile"), data, 0644))
must(t, writeFile(ffs, "knownDir/knownFile", data, 0644))
t0 := time.Now().Add(-1 * time.Minute)
must(t, ffs.Chtimes("knownDir/knownFile", t0, t0))
@@ -310,30 +301,38 @@ func setupKnownFiles(t *testing.T, ffs fs.Filesystem, data []byte) []protocol.Fi
return knownFiles
}
func setupROFolder() (*model, *sendOnlyFolder) {
func setupROFolder(t *testing.T) (*model, *receiveOnlyFolder) {
t.Helper()
w := createTmpWrapper(defaultCfg)
fcfg := testFolderConfigTmp()
fcfg := testFolderConfigFake()
fcfg.ID = "ro"
fcfg.Label = "ro"
fcfg.Type = config.FolderTypeReceiveOnly
w.SetFolder(fcfg)
m := newModel(w, myID, "syncthing", "dev", db.NewLowlevel(backend.OpenMemory()), nil)
m.ServeBackground()
// Folder should only be added, not started.
m.removeFolder(fcfg)
m.addFolder(fcfg)
must(t, m.ScanFolder("ro"))
m.fmut.RLock()
f := &sendOnlyFolder{
folder: folder{
stateTracker: newStateTracker(fcfg.ID, m.evLogger),
fset: m.folderFiles[fcfg.ID],
FolderConfiguration: fcfg,
},
}
m.fmut.RUnlock()
defer m.fmut.RUnlock()
f := m.folderRunners["ro"].(*receiveOnlyFolder)
return m, f
}
func writeFile(fs fs.Filesystem, filename string, data []byte, perm fs.FileMode) error {
fd, err := fs.Create(filename)
if err != nil {
return err
}
_, err = fd.Write(data)
if err != nil {
return err
}
if err := fd.Close(); err != nil {
return err
}
return fs.Chmod(filename, perm)
}

View File

@@ -118,10 +118,7 @@ func (f *sendOnlyFolder) Override() {
}
if !ok || have.Name != need.Name {
// We are missing the file
need.Deleted = true
need.Blocks = nil
need.Version = need.Version.Update(f.shortID)
need.Size = 0
need.SetDeleted(f.shortID)
} else {
// We have the file, replace with our version
have.Version = have.Version.Merge(need.Version).Update(f.shortID)

View File

@@ -364,7 +364,7 @@ func (f *sendReceiveFolder) processNeeded(snap *db.Snapshot, dbUpdateChan chan<-
case file.Type == protocol.FileInfoTypeFile:
curFile, hasCurFile := snap.Get(protocol.LocalDeviceID, file.Name)
if _, need := blockDiff(curFile.Blocks, file.Blocks); hasCurFile && len(need) == 0 {
if hasCurFile && file.BlocksEqual(curFile) {
// We are supposed to copy the entire file, and then fetch nothing. We
// are only updating metadata, so we don't actually *need* to make the
// copy.
@@ -460,7 +460,7 @@ nextFile:
// we can just do a rename instead.
key := string(fi.Blocks[0].Hash)
for i, candidate := range buckets[key] {
if protocol.BlocksEqual(candidate.Blocks, fi.Blocks) {
if candidate.BlocksEqual(fi) {
// Remove the candidate from the bucket
lidx := len(buckets[key]) - 1
buckets[key][i] = buckets[key][lidx]
@@ -1392,7 +1392,10 @@ func (f *sendReceiveFolder) pullerRoutine(in <-chan pullBlockState, out chan<- *
state := state
bytes := int(state.block.Size)
requestLimiter.take(bytes)
if err := requestLimiter.takeWithContext(f.ctx, bytes); err != nil {
break
}
wg.Add(1)
go func() {

View File

@@ -15,6 +15,7 @@ import (
"github.com/thejerf/suture"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/db"
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/sync"
@@ -77,28 +78,37 @@ func (c *folderSummaryService) String() string {
func (c *folderSummaryService) Summary(folder string) (map[string]interface{}, error) {
var res = make(map[string]interface{})
snap, err := c.model.DBSnapshot(folder)
if err != nil {
var local, global, need, ro db.Counts
var ourSeq, remoteSeq int64
errors, err := c.model.FolderErrors(folder)
if err == nil {
var snap *db.Snapshot
if snap, err = c.model.DBSnapshot(folder); err == nil {
global = snap.GlobalSize()
local = snap.LocalSize()
need = snap.NeedSize()
ro = snap.ReceiveOnlyChangedSize()
ourSeq = snap.Sequence(protocol.LocalDeviceID)
remoteSeq = snap.Sequence(protocol.GlobalDeviceID)
snap.Release()
}
}
// For API backwards compatibility (SyncTrayzor needs it) an empty folder
// summary is returned for not running folders, an error might actually be
// more appropriate
if err != nil && err != ErrFolderPaused && err != errFolderNotRunning {
return nil, err
}
errors, err := c.model.FolderErrors(folder)
if err != nil && err != ErrFolderPaused && err != errFolderNotRunning {
// Stats from the db can still be obtained if the folder is just paused/being started
return nil, err
}
res["errors"] = len(errors)
res["pullErrors"] = len(errors) // deprecated
res["invalid"] = "" // Deprecated, retains external API for now
global := snap.GlobalSize()
res["globalFiles"], res["globalDirectories"], res["globalSymlinks"], res["globalDeleted"], res["globalBytes"], res["globalTotalItems"] = global.Files, global.Directories, global.Symlinks, global.Deleted, global.Bytes, global.TotalItems()
local := snap.LocalSize()
res["localFiles"], res["localDirectories"], res["localSymlinks"], res["localDeleted"], res["localBytes"], res["localTotalItems"] = local.Files, local.Directories, local.Symlinks, local.Deleted, local.Bytes, local.TotalItems()
need := snap.NeedSize()
need.Bytes -= c.model.FolderProgressBytesCompleted(folder)
// This may happen if we are in progress of pulling files that were
// deleted globally after the pull started.
@@ -116,7 +126,6 @@ func (c *folderSummaryService) Summary(folder string) (map[string]interface{}, e
if ok && fcfg.Type == config.FolderTypeReceiveOnly {
// Add statistics for things that have changed locally in a receive
// only folder.
ro := snap.ReceiveOnlyChangedSize()
res["receiveOnlyChangedFiles"] = ro.Files
res["receiveOnlyChangedDirectories"] = ro.Directories
res["receiveOnlyChangedSymlinks"] = ro.Symlinks
@@ -132,9 +141,6 @@ func (c *folderSummaryService) Summary(folder string) (map[string]interface{}, e
res["error"] = err.Error()
}
ourSeq := snap.Sequence(protocol.LocalDeviceID)
remoteSeq := snap.Sequence(protocol.GlobalDeviceID)
res["version"] = ourSeq + remoteSeq // legacy
res["sequence"] = ourSeq + remoteSeq // new name
@@ -264,7 +270,12 @@ func (c *folderSummaryService) calculateSummaries(ctx context.Context) {
case <-pump.C:
t0 := time.Now()
for _, folder := range c.foldersToHandle() {
c.sendSummary(folder)
select {
case <-ctx.Done():
return
default:
}
c.sendSummary(ctx, folder)
}
// We don't want to spend all our time calculating summaries. Lets
@@ -274,7 +285,7 @@ func (c *folderSummaryService) calculateSummaries(ctx context.Context) {
pump.Reset(wait)
case folder := <-c.immediate:
c.sendSummary(folder)
c.sendSummary(ctx, folder)
case <-ctx.Done():
return
@@ -307,7 +318,7 @@ func (c *folderSummaryService) foldersToHandle() []string {
}
// sendSummary send the summary events for a single folder
func (c *folderSummaryService) sendSummary(folder string) {
func (c *folderSummaryService) sendSummary(ctx context.Context, folder string) {
// The folder summary contains how many bytes, files etc
// are in the folder and how in sync we are.
data, err := c.Summary(folder)
@@ -320,6 +331,12 @@ func (c *folderSummaryService) sendSummary(folder string) {
})
for _, devCfg := range c.cfg.Folders()[folder].Devices {
select {
case <-ctx.Done():
return
default:
}
if devCfg.DeviceID.Equals(c.id) {
// We already know about ourselves.
continue

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