Compare commits

..

85 Commits

Author SHA1 Message Date
Jakob Borg
6d64daaba3 chore(db): process "unchanged" files anyway (#9755)
Skipping these makes the sequence numbering inconcistent; we've received
a file and suppsedly added it to the database, but if you check the
sequence number afterwards it didn't increase, i.e., we trigger [this
failure
condition](47f48faed7/lib/model/indexhandler.go (L447-L459))
and, similarly, a future update will look like there was a hole in the
numbering.

I propose to at least temporarily remove this optimisation in order for
things to make more sense. Is there a reason to keep this beyond saving
some database operations?
2024-10-04 19:47:57 +00:00
Jakob Borg
47f48faed7 fix(upgrades): avoid clobbering cache when filtering (#9752)
The slice is shared, can't overwrite elements of it. (Upgrade server
only thing.)
2024-10-02 18:56:39 +00:00
Jakob Borg
cfa834177b build(deps): update dependencies (#9751) 2024-10-02 12:53:14 +00:00
tomasz1986
c454fc8baa chore(build): use conventional commit title in update script (#9747) 2024-09-30 15:59:14 -05:00
Jakob Borg
dbe7fa9155 Merge branch 'infrastructure'
* infrastructure:
  feat(ursrv): new metrics based approach
2024-09-30 14:17:02 -05:00
Jakob Borg
4d842f7d3b feat(ursrv): new metrics based approach 2024-09-30 14:16:27 -05:00
Syncthing Release Automation
0e68221c91 gui, man, authors: Update docs, translations, and contributors 2024-09-30 03:48:06 +00:00
Jakob Borg
19f63c7ea3 chore(model): improve tracking sentPrevSeq for index debugging (#9740) 2024-09-29 22:18:24 +00:00
Emil Lundberg
fb939ec496 fix(model): prevent division by zero in numHashers (#9744)
This should prevent the panic that occurred in this test run:
https://github.com/syncthing/syncthing/actions/runs/11095876010/job/30825046810

```
2024-09-29T21:01:53.5425372Z === RUN   TestIssue4357
2024-09-29T21:01:53.5505943Z panic: runtime error: integer divide by zero [recovered]
2024-09-29T21:01:53.5512200Z 	panic: runtime error: integer divide by zero
2024-09-29T21:01:53.5516633Z
2024-09-29T21:01:53.5523018Z goroutine 2655 [running]:
2024-09-29T21:01:53.5524157Z github.com/thejerf/suture/v4.(*Supervisor).runService.func2.2()
2024-09-29T21:01:53.5527176Z 	/home/runner/go/pkg/mod/github.com/thejerf/suture/v4@v4.0.5/supervisor.go:563 +0xd0
2024-09-29T21:01:53.5530556Z panic({0x1080d20?, 0x1851290?})
2024-09-29T21:01:53.5564723Z 	/home/runner/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.1.linux-amd64/src/runtime/panic.go:785 +0x132
2024-09-29T21:01:53.5566616Z github.com/syncthing/syncthing/lib/model.(*model).numHashers(0xc0006f6180, {0x117dc1a, 0x7})
2024-09-29T21:01:53.5568061Z 	/home/runner/work/syncthing/syncthing/lib/model/model.go:2581 +0x210
2024-09-29T21:01:53.5569912Z github.com/syncthing/syncthing/lib/model.(*folder).scanSubdirsChangedAndNew(0xc00c38c808, {0x0, 0x0, 0x0}, 0xc0003fc060)
2024-09-29T21:01:53.5571612Z 	/home/runner/work/syncthing/syncthing/lib/model/folder.go:653 +0x250
2024-09-29T21:01:53.5573010Z github.com/syncthing/syncthing/lib/model.(*folder).scanSubdirs(0xc00c38c808, {0x0, 0x0, 0x0})
2024-09-29T21:01:53.5574447Z 	/home/runner/work/syncthing/syncthing/lib/model/folder.go:512 +0xd0f
2024-09-29T21:01:53.5576011Z github.com/syncthing/syncthing/lib/model.(*folder).scanTimerFired(0xc00c38c808)
2024-09-29T21:01:53.5577367Z 	/home/runner/work/syncthing/syncthing/lib/model/folder.go:916 +0x46
2024-09-29T21:01:53.5579010Z github.com/syncthing/syncthing/lib/model.(*folder).Serve(0xc00c38c808, {0x1307650, 0xc0006a0910})
2024-09-29T21:01:53.5580428Z 	/home/runner/work/syncthing/syncthing/lib/model/folder.go:205 +0xd7e
2024-09-29T21:01:53.5581624Z github.com/thejerf/suture/v4.(*Supervisor).runService.func2()
2024-09-29T21:01:53.5582978Z 	/home/runner/go/pkg/mod/github.com/thejerf/suture/v4@v4.0.5/supervisor.go:567 +0x249
2024-09-29T21:01:53.5584400Z created by github.com/thejerf/suture/v4.(*Supervisor).runService in goroutine 2651
2024-09-29T21:01:53.5585872Z 	/home/runner/go/pkg/mod/github.com/thejerf/suture/v4@v4.0.5/supervisor.go:541 +0x32a
2024-09-29T21:01:53.5661413Z FAIL	github.com/syncthing/syncthing/lib/model	5.510s
```

### Testing

I have not been able to reproduce the panic throughout a few minutes of
continuously running the test without this fix, but judging by the
traceback it seems to only happen if the test happens to delete the
folder from config at the same time `scanTimerFired` triggers.
2024-09-30 00:01:52 +02:00
Jakob Borg
39df3173d4 chore(model): log sequence anomaly when update appears not to "take" (#9741)
I hope this doesn't fire, but 👻  I'm Seeing Things I Can't Explain 👻
2024-09-29 15:04:06 +00:00
maxice8
429672e0b4 docs(docker): add healthcheck to docker-compose (#9742)
### Purpose

Syncthing had a healthcheck API for a while, and the example Dockerfile
for it has it in the form of:

HEALTHCHECK --interval=1m --timeout=10s \
CMD curl -fkLsS -m 2 127.0.0.1:8384/rest/noauth/health | grep -o
--color=never OK || exit 1

Let's add it to the docker-compose as well

### Testing

I use this docker-compose.yml file to deploy via ansible (using
community.docker.docker_compose_v2) to my machine with success, using
`wait: true` in ansible for it to use `docker compose up --wait`.

```yml
- name: Enable syncthing docker
  community.docker.docker_compose_v2:
    project_src: /srv/syncthing
    wait: true
    wait_timeout: 90
```
2024-09-29 09:53:13 -05:00
Simon Frei
605fd6d726 fix(ignore): ensure normalization of patterns and paths match (fixes #9597) (#9717)
In ignores, normalize the input when parsing it.
When scanning, normalize earlier such that the path is already
normalized when checking ignores. This requires splitting normalization
of the string from normalization of the file, as we don't want to
attempt the latter if the file is ignored.

Closes #9598

---------

Co-authored-by: Jakob Borg <jakob@kastelo.net>
2024-09-28 17:16:44 +02:00
Jakob Borg
3c476542d2 fix(ur): actually send usage report directly when enabled (#9736)
There was a bug that the unique ID was not set when reporting was
enabled, and thus the reports where rejected by the server. The unique
ID got set only on startup, so next time Syncthing restarted.

This makes sure to set the unique ID when blank.
2024-09-28 17:02:05 +02:00
Jakob Borg
31874f3ebb chore(model): remove GUI/log warning on sequence anomaly (#9738)
I can see already in our Sentry data that there are a fair amount of
these warnings, and mostly the shape of it. Asking users to report them
will likely cause a lot of reporting effort to fairly little additional
value. We can do that when/if we have something more targeted to ask
for.
2024-09-28 16:38:07 +02:00
Jakob Borg
77942747db Merge branch 'infrastructure'
* infrastructure:
  feat(stupgrades): filter returned releases per compatibility
2024-09-26 10:25:02 +02:00
Jakob Borg
fe01b396ba feat(stupgrades): filter returned releases per compatibility 2024-09-26 10:22:23 +02:00
Jakob Borg
3583949706 refactor(upgrade): rename insecureGet which is no longer insecure (#9735) 2024-09-25 15:50:22 +00:00
Jakob Borg
23fc22ebc5 chore: add more advanced policy configuration (#9726)
This codifies a review policy which is closer to what I always
envisioned, but which isn't expressible using the normal checks in the
GitHub GUI. It would move the commit approval check from GitHub into the
policy-bot check which is already present to enforce the
conventional-commits standard. Approvals in general would still work the
same -- it's just that the bot picks it up and toggles the status
accordingly. From a GitHub side when this is enabled we'd remove the
requires-review check from there and let the bot decide that part. We
would still require builds and tests to pass of course.

There are a couple of relexations from the current policy, details in
the code but briefly:

- Changes to translations or dependencies by a trusted person don't
require review
- Trivial changes by a trusted person, explicitly marked as such, don't
require review

This enables less bureaucracy for things like adding new translated
languages and updating dependencies, and enables the trivial-change
workflow to a larger audience than, like, me, who could always just
bypass the rules by way of being admin.
2024-09-25 17:41:56 +02:00
Jakob Borg
cba163a1fd chore: enable TLS client cache for HTTPS where appropriate (#9721)
https://forum.syncthing.net/t/infrastructure-report-discovery-stuff/22819/4
2024-09-24 08:55:04 +02:00
Jakob Borg
a8e2c8edb6 fix(connections): announce PtP links again (fixes #9730) (#9731) 2024-09-23 14:32:19 +02:00
Syncthing Release Automation
3e501d9036 gui, man, authors: Update docs, translations, and contributors 2024-09-23 03:46:01 +00:00
bt90
9ca101756d chore(ursrv): add Nix detection (#9729)
Classify the builder `nix@nix` as [Nix](https://nixos.org/)

![369684243-172cab09-df6f-449a-a638-1f0a0c080ab3](https://github.com/user-attachments/assets/37a6e0a5-bdcb-4b31-8b36-eaaa42423382)
2024-09-22 14:03:40 +02:00
bt90
a873d12c65 chore(ursrv): extend F-Droid detection (#9728)
Our f-droid apps are currently built using `vagrant@bookworm`:

![grafik](https://github.com/user-attachments/assets/172cab09-df6f-449a-a638-1f0a0c080ab3)
2024-09-22 13:48:38 +02:00
Emil Lundberg
8ff670c564 fix(gui): get version from header when not authenticated (#9724)
### Purpose

Since #8757, the Syncthing GUI now has an unauthenticated state. One
consequence of this is that `$scope.versionBase()` is not initialized
while unauthenticated, which causes the `docsURL` function to truncate
links to just `https://docs.syncthing.net`/, discarding the section
path. This currently affects at least the "Help > Introduction" link
reachable both while logged in and not. The issue is exacerbated in
https://github.com/syncthing/syncthing/pull/9175 where we sometimes want
to show additional contextual help links from the login page to
particular sections of the docs.

I don't think it's any worse to try to preserve the section path even
without an explicit version tag, than to fall back to just the host and
lose all context the link was attempting to provide.

### Testing

- On commit b1ed2802fb (before):
  - Open the GUI, set a username and log out.
- Open the "Help" drop-down. The "Introduction" item links to:
https://docs.syncthing.net/
  - Log in.
- Open the "Help" drop-down. The "Introduction" item links to:
https://docs.syncthing.net/v1.27.10/intro/gui
- On commit 44fef31780 (after):
  - Open the GUI, set a username and log out.
- Open the "Help" drop-down. The "Introduction" item links to:
https://docs.syncthing.net/intro/gui
  - Log in.
- Open the "Help" drop-down. The "Introduction" item links to:
https://docs.syncthing.net/v1.27.10/intro/gui

### Screenshots

This is a GUI change, but affecting only URLs in the markup with no
visual changes.


### Drawbacks

If a `docsURL` call generates a versionless link to a docs page that
doesn't exist on https://docs.syncthing.net - presumably because
Syncthing is not the latest version and links to a deleted page? - then
this will lead to GitHub's generic 404 page with no link to the
Syncthing docs root. Before, any versionless link would also be a
pathless link, leading to the Syncthing docs root instead of a 404 page.
2024-09-22 09:47:02 +02:00
Jakob Borg
b1ed2802fb fix(connections): skip point-to-point interfaces when listing LANs (fixes #9719) (#9720)
Point-to-point interfaces are typically VPNs and similar which, for our
purposes, do not qualify as LANs.
2024-09-21 09:27:23 +02:00
Jakob Borg
b70cb580c8 build(deps): update all dependencies (#9723) 2024-09-21 09:25:27 +02:00
Sonu Kumar Saw
28be3ba788 chore(connections): lower log level from INFO to DEBUG for "already connected to this device" messages (fixes #9715) (#9722)
### Purpose

The primary aim of this change is to minimize log clutter in production
environments. There are many lines in the logs coming from an expected
race condition when two devices connect `already connected to this
device`. These messages do not indicate errors and can overwhelm the log
files with unnecessary noise.

By lowering the logging level, we enhance the usability of the logs,
making it easier for users and developers to identify actual issues
without being distracted

### Testing
1. Build syncthing locally
2. Start two Syncthing instances
```bash
./syncthing -no-browser -home=~/.config/syncthing1
./syncthing -no-browser -home=~/.config/syncthing2
```
3. Enable the DEBUG logs from UI for `connections` package
4. Connect the synching instances by adding remote devices from the UI
5. Observe the logs for the message `XXXX already connected to this
device`

### Screenshots


![image](https://github.com/user-attachments/assets/882ccb4c-d39d-463a-8f66-2aad97010700)

## Authorship

Your name and email will be added automatically to the AUTHORS file
based on the commit metadata.
2024-09-21 07:19:58 +00:00
Jakob Borg
d4770ddc77 chore(cmd): clean up commands (#9705)
Move infrastructure related commands to under `cmd/infra` and
development stuff to `cmd/dev`. The default build command builds the
regular user facing binaries: syncthing, stdiscosrv, and strelaysrv.
2024-09-21 09:04:22 +02:00
Simon Frei
cbe1220680 chore(fs): put the caseFS as the outermost layer again (#9716)
Reasoning in comments. The main motivation is to avoid all the case
checks when walking the filesystem.

"again" as we already tried once, but it caused a major issue ragarding
mtimefs layer. The root of this problem has been fixed in the meantime
in ac8b3342a
2024-09-18 20:31:19 +02:00
André Colomb
0b95c5fa76 fix(meta): return read error in forbidden_words_test (#9706)
When reading a file fails, the error is currently swallowed / hidden.
Probably just a typo.
2024-09-18 19:12:24 +02:00
Jakob Borg
0343bca257 Merge branch 'infrastructure'
* infrastructure:
  chore(stdiscosrv): ensure incoming addresses are sorted and unique
  chore(stdiscosrv): use zero-allocation merge in the common case
  chore(stdiscosrv): properly clean out old addresses from memory
  chore(stdiscosrv): calculate IPv6 GUA
2024-09-16 09:33:15 +02:00
Syncthing Release Automation
878016db39 gui, man, authors: Update docs, translations, and contributors 2024-09-16 03:47:01 +00:00
Simon Frei
1f4fde9525 chore(protocol): prioritize closing a connection (#9711)
The read/write loops may keep going for a while on a closing connection
with lots of read/write activity, as it's random which select case is
chosen. And if the connection is slow or even broken, a single
read/write
can take a long time/until timeout. Add initial non-blocking selects
with only the cases relevant to closing, to prioritize those.
2024-09-15 21:13:56 +02:00
Jakob Borg
5b9d8a838f chore(stdiscosrv): ensure incoming addresses are sorted and unique 2024-09-15 17:01:16 +02:00
Jakob Borg
8b19cb1e11 chore(stdiscosrv): use zero-allocation merge in the common case 2024-09-15 15:26:40 +02:00
Jakob Borg
ce1e259bb4 chore(stdiscosrv): properly clean out old addresses from memory 2024-09-15 14:20:59 +02:00
Jakob Borg
2238a288d9 fix(model): shut down index sender faster (#9704) 2024-09-15 11:37:49 +02:00
Jakob Borg
c8ee2a5cf6 chore(stdiscosrv): calculate IPv6 GUA 2024-09-15 10:48:16 +02:00
Ross Smith II
1704827d04 chore(gui): update HumanDuration.js (#9710)
Relevant changes:

ko: Use correct names for month and hour in Korean (465eaed)
Hide unit count if 2 in Arabic (f90d847)
2024-09-15 10:21:18 +02:00
Jakob Borg
a156e88eef Merge branch 'infrastructure'
* infrastructure:
  chore(stdiscosrv): hide internal/undocumented flags
  chore(stdiscosrv): remove legacy replication
  chore(stdiscosrv): clean up s3 handling
  chore(stdiscosrv): less garbage in statistics
  chore(stdiscosrv): improve expire, logging
  chore(stdiscosrv): sched in loop
  chore(stdiscosrv): database writing logging
  chore(stdiscosrv): use order-preserving expire
  chore(stdiscosrv): simplify sorting
  chore(stdiscosrv): reduce allocations in cert handling
  chore(stdiscosrv): reduce unnecessary allocations in merge
  feat(stdiscosrv): enable HTTP profiler
  feat(discosrv): in-memory storage with S3 backing
  feat(stdiscosrv): make compression optional (and faster)
2024-09-13 08:50:24 +02:00
Jakob Borg
94d0195b63 chore(stdiscosrv): hide internal/undocumented flags 2024-09-13 08:49:13 +02:00
Jakob Borg
1616edcee3 chore(stdiscosrv): remove legacy replication 2024-09-13 08:49:13 +02:00
Jakob Borg
6505e123bb chore(stdiscosrv): clean up s3 handling 2024-09-13 08:48:04 +02:00
Jakob Borg
63e4659282 chore(stdiscosrv): less garbage in statistics 2024-09-13 08:48:04 +02:00
Jakob Borg
f3f5557c8e chore(stdiscosrv): improve expire, logging 2024-09-13 08:48:04 +02:00
Jakob Borg
b794726e1f chore(stdiscosrv): sched in loop 2024-09-13 08:48:04 +02:00
Jakob Borg
3d59740a0a chore(stdiscosrv): database writing logging 2024-09-13 08:48:04 +02:00
Jakob Borg
66fb65b01f chore(stdiscosrv): use order-preserving expire 2024-09-13 08:48:04 +02:00
Jakob Borg
5c2fcbfd19 chore(stdiscosrv): simplify sorting 2024-09-13 08:48:03 +02:00
Jakob Borg
f9b72330a8 chore(stdiscosrv): reduce allocations in cert handling 2024-09-13 08:48:03 +02:00
Jakob Borg
822b6ac36b chore(stdiscosrv): reduce unnecessary allocations in merge 2024-09-13 08:48:03 +02:00
Jakob Borg
77f7778292 feat(stdiscosrv): enable HTTP profiler 2024-09-13 08:48:03 +02:00
Jakob Borg
aed2c66e52 feat(discosrv): in-memory storage with S3 backing 2024-09-13 08:48:03 +02:00
Jakob Borg
68a1fd010f feat(stdiscosrv): make compression optional (and faster) 2024-09-13 08:33:03 +02:00
Simon Frei
ac8b3342ac chore(fs): only cache the cache for case FS, not the entire FS (#9701)
This would have addressed a recent issue that arose when re-ordering our
"filesystem layers". Specifically moving the caseFilesystem to the
outermost layer. The previous cache included the filesystem, and as such
all the layers below. This isn't desirable (to put it mildly), as you
can create different variants of filesystems with different layers for
the same path and options. Concretely this did happen with the mtime
layer, which isn't always present. A test for the mtime related breakage
was added in #9687, and I intend to redo the caseFilesystem reordering
after this.

Ref: #9677
Followup to: #9687
2024-09-12 20:35:21 +02:00
Jakob Borg
0ea90dd932 build: add generating compat.json (#9700)
This is to add the generation of `compat.json` as a release artifact. It
describes the runtime requirements of the release in question. The next
step is to have the upgrade server use this information to filter
releases provided to clients. This is per the discussion in #9656

---------

Co-authored-by: Ross Smith II <ross@smithii.com>
2024-09-11 09:29:49 +02:00
Jakob Borg
718b1ce2b7 chore(discovery,upgrade): use regular TLS certificate verification (#9673)
This changes the two remaining instances where we use insecure HTTPS to
use standard HTTPS certificate verification.

When we introduced these things, almost a decade ago, HTTPS certificates
were expensive and annoying to get, much of the web was still HTTP, and
many devices seemed to not have up-to-date CA bundles.

Nowadays _all_ of the web is HTTPS and I'm skeptical that any device can
work well without understanding LetsEncrypt certificates in particular.

Our current discovery servers use hardcoded certificates which has
several issues:
- Not great for security if it leaks as there is no way to rotate it
- Not great for infrastructure flexibility as we can't use many load
balancer or TLS termination services
- The certificate is a very oddball ECDSA-SHA384 type certificate which
has higher CPU cost than a more regular certificate, which has real
effects on our infrastructure

Using normal TLS certificates here improves these things.

I expect there will be some very few devices out there for which this
doesn't work. For the foreseeable future they can simply change the
config to use the old URLs and parameters -- it'll be years before we
can retire those entirely.

For the upgrade client this simply seems like better hygiene. While our
releases are signed anyway, protecting the metadata exchange is _better_
and, again, I doubt many clients will fail this today.
2024-09-11 09:29:19 +02:00
Simon Frei
29f7510f5a lib/fs: Add test reproducing missing mtimefs issue (ref #9677) (#9687)
The test is quite odd and specific, but it does reproduce the issue that
caused #9677, so I'd propose to add it to have a simple regression test
for the basic scenario. Also the option to the fakefs might come handy
for other scenarios where you want to quickly test some behaviour on a
filesystem without nanosecond precision, without actually needing access
to one.
2024-09-10 13:36:17 +02:00
Syncthing Release Automation
a7f9ed4a80 gui, man, authors: Update docs, translations, and contributors 2024-09-09 03:45:15 +00:00
tomasz1986
1baefea410 gui: Actually filter scope ID out of IPv6 address when using Remote GUI (ref #8084) (#9688)
gui: Actually filter scope ID out of IPv6 address when using Remote GUI
(ref #8084)

The current code does not work, because it uses a string in the
replace() method instead of regex. Thus, change it to a proper regex.

Signed-off-by: Tomasz Wilczyński <twilczynski@naver.com>
2024-09-08 12:42:32 +02:00
Jakob Borg
563cec8923 Merge branch 'release'
* release:
  Revert "lib/fs: Put the caseFS as the outermost layer (#9648)"
2024-09-06 09:39:09 +02:00
Jakob Borg
a3c340ece9 Revert "lib/fs: Put the caseFS as the outermost layer (#9648)"
This reverts commit 7517d18fbb.

Fixes #9677
2024-09-06 09:15:45 +02:00
André Colomb
cb24638ec9 lib/api: Correct ordering of Accept-Language codes by weight (fixes #9670) (#9671)
The preference for languages in the Accept-Language header field
should not be deduced from the listed order, but from the passed
"quality values", according to the HTTP specification:
https://httpwg.org/specs/rfc9110.html#field.accept-language

This implements the parsing of q=values and ordering within the API
backend, to not complicate things further in the GUI code.  Entries
with invalid (unparseable) quality values are discarded completely.

* gui: Fix API endpoint in comment.
2024-09-02 10:15:04 +02:00
Syncthing Release Automation
2fb24dc2cc gui, man, authors: Update docs, translations, and contributors 2024-09-02 03:45:15 +00:00
tomasz1986
9aa2d2c92f gui: Fix incorrect UI language auto detection (fixes #9668) (#9669)
gui: Fix incorrect UI language auto detection (fixes #9668)

Currently, the code only checks whether the detected language partially
matches one of the available languages. This means that if the detected
language is "fi" but among the available languages there is only "fil"
and no "fi", then it will match "fi" with "fil", even though the two are
completely different languages.

With this change, the matching is only done when there is a hyphen in
the language code, e.g. "en" will match with "en-US".

Signed-off-by: Tomasz Wilczyński <twilczynski@naver.com>
2024-08-31 22:47:10 +02:00
Jakob Borg
d1c5100c98 Merge branch 'release'
* release:
  lib/upgrade: Send OS version header to upgrade server (#9663)
2024-08-30 11:32:49 +02:00
Jakob Borg
42e677c055 lib/model, lib/protocol: Index sending/receiving debugging (#9657)
This adds guardrails to the index sending and receiving, to verify that
what we thinks is happening is what actually happens.
2024-08-28 15:00:19 +02:00
Jakob Borg
27bba2c0c2 lib/upgrade: Send OS version header to upgrade server (#9663)
This adds a header with the operating system version, verbatim in
whatever format the operating system reports it, to the upgrade check.
The intention is that the upgrade server can use this information to
filter out (or maybe just mark) potentially unsupported upgrades.
2024-08-28 08:32:03 +02:00
Jakob Borg
feff334547 lib/upgrade: Send OS version header to upgrade server (#9663)
This adds a header with the operating system version, verbatim in
whatever format the operating system reports it, to the upgrade check.
The intention is that the upgrade server can use this information to
filter out (or maybe just mark) potentially unsupported upgrades.
2024-08-28 08:31:10 +02:00
Syncthing Release Automation
713cf357ce gui, man, authors: Update docs, translations, and contributors 2024-08-26 03:45:23 +00:00
Jakob Borg
5342bec1b7 lib/protocol: Further interface refactor (#9396)
This is a symmetric change to #9375 -- where that PR changed the
protocol->model interface, this changes the model->protocol one.
2024-08-24 12:45:10 +02:00
Emil Lundberg
7df75e681d gui: Replace global "Panel padding decrease" style with targeted class (#9659)
Transplanted from https://github.com/emlun/syncthing/pull/8 (meta-PR
into https://github.com/syncthing/syncthing/pull/9175) by request of
@acolomb (see:
https://github.com/emlun/syncthing/pull/8#discussion_r1724470574).

This padding decrease currently applies to _all_ collapsible panels, but
this padding decrease may not be appropriate for all collapsible panels.
In particular, it will not be appropriate for the collapsible panels
introduced in https://github.com/emlun/syncthing/pull/8.
2024-08-21 15:02:45 +02:00
Jakob Borg
8dc826b234 build: use Go 1.23, require minimum 1.22 (#9651)
🥳

---------

Co-authored-by: Ross Smith II <ross@smithii.com>
2024-08-19 20:26:08 +02:00
Syncthing Release Automation
9ef37e1485 gui, man, authors: Update docs, translations, and contributors 2024-08-19 03:45:18 +00:00
Simon Frei
7517d18fbb lib/fs: Put the caseFS as the outermost layer (#9648)
Reasoning in comments. The main motivation is to avoid all the case
checks when walking the filesystem.
2024-08-13 10:59:31 +02:00
André Colomb
42d0fee536 gui: Add Irish (ga) translation template (#9646)
Based on user request from Weblate, user `@aindriu80`.

Looks promising based on the profile:
https://hosted.weblate.org/user/aindriu80/ Not sure whether almost
30.000 translations in about one month is realistic for a human though.
2024-08-12 13:32:29 +02:00
Syncthing Release Automation
2ca9d3b5c5 gui, man, authors: Update docs, translations, and contributors 2024-08-12 03:45:16 +00:00
Tommy van der Vorst
9cde068f2a lib/syncthing: Add wrapper for access to model (#9627)
### Purpose

Wrap access to Model for users that use the syncthing Go package. See
discussion:
https://github.com/syncthing/syncthing/pull/9619#pullrequestreview-2212484910

### Testing

It works with the iOS app. Other than that, there are no current users
of this API (to my knowledge) as Model was only exposed recently form
the iOS app.
2024-08-11 20:20:43 +02:00
Gusted
1243083831 cli: Remove go-shlex dependency (#9644) 2024-08-11 11:37:18 +02:00
Gusted
356c5055ad lib/sha256: Remove it (#9643)
### Purpose

Remove the `lib/sha256` package, because it's no longer necessary. Go's
standard library now has the same performance and is on par with
`sha256-simd` since [Since Go
1.21](1a64574f42).
Therefore using `sha256-simd` has no benefits anymore.

ARM already has optimized sha256 assembly code since
7b8a7f8272,
`sha256-simd` published their results before that optimized assembly was
implemented,
f941fedda8.
The assembly looks very similar and the benchmarks in the Go commit
match that of `sha256-simd`.

This patch removes all of the related code of `lib/sha256` and makes
`crypto/sha256` the 'default'.

Benchmark of `sha256-simd` and `crypto/sha256`:
<details>

```
cpu: AMD Ryzen 5 3600X 6-Core Processor
                │  simd.txt   │               go.txt                │
                │   sec/op    │    sec/op     vs base               │
Hash/8Bytes-12    63.25n ± 1%    73.38n ± 1%  +16.02% (p=0.002 n=6)
Hash/64Bytes-12   98.73n ± 1%   105.30n ± 1%   +6.65% (p=0.002 n=6)
Hash/1K-12        567.2n ± 1%    572.8n ± 1%   +0.99% (p=0.002 n=6)
Hash/8K-12        4.062µ ± 1%    4.062µ ± 1%        ~ (p=0.396 n=6)
Hash/1M-12        512.1µ ± 0%    510.6µ ± 1%        ~ (p=0.485 n=6)
Hash/5M-12        2.556m ± 1%    2.564m ± 0%        ~ (p=0.093 n=6)
Hash/10M-12       5.112m ± 0%    5.127m ± 0%        ~ (p=0.093 n=6)
geomean           13.82µ         14.27µ        +3.28%

                │   simd.txt   │               go.txt                │
                │     B/s      │     B/s       vs base               │
Hash/8Bytes-12    120.6Mi ± 1%   104.0Mi ± 1%  -13.81% (p=0.002 n=6)
Hash/64Bytes-12   618.2Mi ± 1%   579.8Mi ± 1%   -6.22% (p=0.002 n=6)
Hash/1K-12        1.682Gi ± 1%   1.665Gi ± 1%   -0.98% (p=0.002 n=6)
Hash/8K-12        1.878Gi ± 1%   1.878Gi ± 1%        ~ (p=0.310 n=6)
Hash/1M-12        1.907Gi ± 0%   1.913Gi ± 1%        ~ (p=0.485 n=6)
Hash/5M-12        1.911Gi ± 1%   1.904Gi ± 0%        ~ (p=0.093 n=6)
Hash/10M-12       1.910Gi ± 0%   1.905Gi ± 0%        ~ (p=0.093 n=6)
geomean           1.066Gi        1.032Gi        -3.18%
```

</details>


### Testing

Compiled and tested on Linux.

### Documentation

https://github.com/syncthing/docs/pull/874
2024-08-10 12:58:20 +01:00
Jakob Borg
19693734a3 build: Update dependencies (#9640) 2024-08-09 16:04:51 +02:00
Ross Smith II
17e60b9e0c Chmod -x non-executable files (fixes #9629) (#9630)
Fixed via
```
git ls-files -s | grep 100755 | grep -E -v '(run|sh)$' | cut -f 2 | xargs git update-index --chmod -x
```
See #9629.
2024-08-05 18:32:23 +02:00
Syncthing Release Automation
ac22b2d00a gui, man, authors: Update docs, translations, and contributors 2024-08-05 03:45:25 +00:00
Tommy van der Vorst
de0b4270df all: minimal set of changes for iOS app (#9619)
### Purpose

This PR contains the set of changes needed to make Syncthing work on iOS
for [my iOS app for
Syncthing](https://github.com/pixelspark/sushitrain).

Most changes originate from [the Mobius Sync
fork](http://github.com/MobiusSync/syncthing/tree/ios). I have removed
the changes from their fork that are not strictly needed for my app
(i.e. their changes to the GUI and command line utilities, for instance)
and squashed it all in a single commit.

In summary, the changes are:

* Resolve non-absolute paths to the 'Documents' folder (basically the
only one an app can/should write user data to by default on iOS)
* Tweaking of build flags/conditions for iOS (i.e. determine which
basicfs_watch, ignoreresult variant to build for iOS)
* Disable upgrade mechanism on iOS
* Make `RequestGlobal` and `PullerProgress` public symbols
* Expose syncthing.app's Model instance (app.M)
* Add no-op stub for SetLowPriority on iOS

I would very much appreciate these changes to be (eventually) merged to
mainline syncthing, as this would allow my iOS app to track the mainline
source code directly and removes the need (for me at least) for
maintaining a separate fork. Perhaps the Mobius folks can also benefit
from this (although as noted this branch does not contain their changes
to e.g. the GUI).

### Testing

This branch has been tested with the iOS app and appears to work fine.
The full set of MobiusSync changes has been used before with success.

### Screenshots

n/a

### Documentation

There should be no visible changes for users due to this set of changes.

---------

Co-authored-by: Simon Pickup <simon@pickupinfinity.com>
2024-07-31 07:31:14 +02:00
Syncthing Release Automation
e738af7c56 gui, man, authors: Update docs, translations, and contributors 2024-07-29 03:45:25 +00:00
192 changed files with 5074 additions and 5326 deletions

2
.github/CODEOWNERS vendored
View File

@@ -1,2 +0,0 @@
/AUTHORS @calmh
/*.md @calmh

View File

@@ -7,7 +7,7 @@ on:
- infra-*
env:
GO_VERSION: "~1.22.3"
GO_VERSION: "~1.23.0"
CGO_ENABLED: "0"
BUILD_USER: docker
BUILD_HOST: github.syncthing.net

View File

@@ -12,7 +12,7 @@ env:
# The go version to use for builds. We set check-latest to true when
# installing, so we get the latest patch version that matches the
# expression.
GO_VERSION: "~1.22.3"
GO_VERSION: "~1.23.0"
# Optimize compatibility on the slow archictures.
GO386: softfloat
@@ -48,7 +48,7 @@ jobs:
runner: ["windows-latest", "ubuntu-latest", "macos-latest"]
# The oldest version in this list should match what we have in our go.mod.
# Variables don't seem to be supported here, or we could have done something nice.
go: ["~1.21.7", "~1.22.3"]
go: ["~1.22.6", "~1.23.0"]
runs-on: ${{ matrix.runner }}
steps:
- name: Set git to use LF
@@ -238,7 +238,9 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: packages-linux
path: syncthing-linux-*.tar.gz
path: |
syncthing-linux-*.tar.gz
compat.json
#
# macOS
@@ -514,7 +516,7 @@ jobs:
- name: Install signing tool
run: |
go install ./cmd/stsigtool
go install ./cmd/dev/stsigtool
- name: Sign archives
run: |

1
.gitignore vendored
View File

@@ -18,3 +18,4 @@ deb
/repos
/proto/scripts/protoc-gen-gosyncthing
/gui/next-gen-gui
/compat.json

93
.policy.yml Normal file
View File

@@ -0,0 +1,93 @@
# This is the policy-bot configuration for this repository. It controls
# which approvals are required for any given pull request. The format is
# described at https://github.com/palantir/policy-bot. The syntax of the
# policy can be verified by the bot:
# curl https://pb.syncthing.net/api/validate -X PUT -T .policy.yml
# The policy below is what is required for any pull request.
policy:
approval:
- subject is conventional commit
- project metadata requires maintainer approval
- or:
- is approved by a syncthing contributor
- is a translation or dependency update by a contributor
- is a trivial change by a contributor
# Additionally, contributors can disapprove of a PR
disapproval:
requires:
teams:
- syncthing/contributors
# The rules for the policy are described below.
approval_rules:
# All commits (PRs before squashing) should have a valid conventional
# commit type subject.
- name: subject is conventional commit
requires:
conditions:
title:
matches:
- '^(feat|fix|docs|chore|refactor|build): [a-z].+'
- '^(feat|fix|docs|chore|refactor|build)\(\w+(, \w+)*\): [a-z].+'
# Changes to important project metadata and documentation, including this
# policy, require signoff by a maintainer
- name: project metadata requires maintainer approval
if:
changed_files:
paths:
- ^[^/]+\.md
- ^\.policy\.yml
- ^\.github/
- ^LICENSE
requires:
count: 1
teams:
- syncthing/maintainers
# Regular pull requests require approval by an active contributor
- name: is approved by a syncthing contributor
requires:
count: 1
teams:
- syncthing/contributors
# Changes to some files (translations, dependencies, compatibility) do not
# require approval if they were proposed by a contributor and have a
# matching commit subject
- name: is a translation or dependency update by a contributor
if:
only_changed_files:
paths:
- ^gui/default/assets/lang/
- ^go\.mod$
- ^go\.sum$
- ^compat\.yaml$
title:
matches:
- '^chore\(gui\):'
- '^build\(deps\):'
- '^build\(compat\):'
has_author_in:
teams:
- syncthing/contributors
# If the change is small and the label "trivial" is added, we accept that
# on trust. These PRs can be audited after the fact as appropriate.
# Features are not trivial.
- name: is a trivial change by a contributor
if:
modified_lines:
total: "< 25"
title:
not_matches:
- '^feat'
has_labels:
- trivial
has_author_in:
teams:
- syncthing/contributors

View File

@@ -141,6 +141,7 @@ greatroar <61184462+greatroar@users.noreply.github.com>
Greg <gco@jazzhaiku.com>
guangwu <guoguangwu@magic-shield.com>
gudvinr <gudvinr@gmail.com>
Gusted <postmaster@gusted.xyz> <williamzijl7@hotmail.com>
Han Boetes <han@boetes.org>
HansK-p <42314815+HansK-p@users.noreply.github.com>
Harrison Jones (harrisonhjones) <harrisonhjones@users.noreply.github.com>
@@ -232,6 +233,7 @@ Matteo Ruina <matteo.ruina@gmail.com>
Maurizio Tomasi <ziotom78@gmail.com>
Max <github@germancoding.com>
Max Schulze (kralo) <max.schulze@online.de> <kralo@users.noreply.github.com>
maxice8 <30738253+maxice8@users.noreply.github.com>
MaximAL <almaximal@ya.ru>
Maxime Thirouin <m@moox.io>
Maximilian <maxi.rostock@outlook.de> <public@complexvector.space>
@@ -305,7 +307,9 @@ Severin von Wnuck-Lipinski <ss7@live.de>
Shaarad Dalvi <60266155+shaaraddalvi@users.noreply.github.com> <shdalv@microsoft.com>
Simon Frei (imsodin) <freisim93@gmail.com>
Simon Mwepu <simonmwepu@gmail.com>
Simon Pickup <simon@pickupinfinity.com>
Sly_tom_cat <slytomcat@mail.ru>
Sonu Kumar Saw <31889738+dev-saw99@users.noreply.github.com>
Stefan Kuntz (Stefan-Code) <stefan.github@gmail.com> <Stefan.github@gmail.com>
Stefan Tatschner (rumpelsepp) <stefan@sevenbyte.org> <rumpelsepp@sevenbyte.org> <stefan@rumpelsepp.org>
Steven Eckhoff <steven.eckhoff.opensource@gmail.com>
@@ -325,6 +329,7 @@ Tobias Tom (tobiastom) <t.tom@succont.de>
Tom Jakubowski <tom@crystae.net>
Tomasz Wilczyński <5626656+tomasz1986@users.noreply.github.com> <twilczynski@naver.com>
Tommy Thorn <tommy-github-email@thorn.ws>
Tommy van der Vorst <tommy-github@pixelspark.nl> <tommy@pixelspark.nl>
Tully Robinson (tojrobinson) <tully@tojr.org>
Tyler Brazier (tylerbrazier) <tyler@tylerbrazier.com>
Tyler Kropp <kropptyler@gmail.com>

View File

@@ -49,6 +49,11 @@ services:
- 22000:22000/udp # QUIC file transfers
- 21027:21027/udp # Receive local discovery broadcasts
restart: unless-stopped
healthcheck:
test: curl -fkLsS -m 2 127.0.0.1:8384/rest/noauth/health | grep -o --color=never OK || exit 1
interval: 1m
timeout: 10s
retries: 3
```
## Discovery
@@ -84,6 +89,11 @@ services:
- /wherever/st-sync:/var/syncthing
network_mode: host
restart: unless-stopped
healthcheck:
test: curl -fkLsS -m 2 127.0.0.1:8384/rest/noauth/health | grep -o --color=never OK || exit 1
interval: 1m
timeout: 10s
retries: 3
```
Be aware that syncthing alone is now in control of what interfaces and ports it

View File

@@ -4,8 +4,8 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
//go:build ignore
// +build ignore
//go:build tools
// +build tools
package main
@@ -34,6 +34,8 @@ import (
"time"
buildpkg "github.com/syncthing/syncthing/lib/build"
"github.com/syncthing/syncthing/lib/upgrade"
"sigs.k8s.io/yaml"
)
var (
@@ -191,37 +193,37 @@ var targets = map[string]target{
debname: "syncthing-relaypoolsrv",
debdeps: []string{"libc6"},
description: "Syncthing Relay Pool Server",
buildPkgs: []string{"github.com/syncthing/syncthing/cmd/strelaypoolsrv"},
buildPkgs: []string{"github.com/syncthing/syncthing/cmd/infra/strelaypoolsrv"},
binaryName: "strelaypoolsrv", // .exe will be added automatically for Windows builds
archiveFiles: []archiveFile{
{src: "{{binary}}", dst: "{{binary}}", perm: 0755},
{src: "cmd/strelaypoolsrv/README.md", dst: "README.txt", perm: 0644},
{src: "cmd/strelaypoolsrv/LICENSE", dst: "LICENSE.txt", perm: 0644},
{src: "cmd/infra/strelaypoolsrv/README.md", dst: "README.txt", perm: 0644},
{src: "cmd/infra/strelaypoolsrv/LICENSE", dst: "LICENSE.txt", perm: 0644},
{src: "AUTHORS", dst: "AUTHORS.txt", perm: 0644},
},
installationFiles: []archiveFile{
{src: "{{binary}}", dst: "deb/usr/bin/{{binary}}", perm: 0755},
{src: "cmd/strelaypoolsrv/README.md", dst: "deb/usr/share/doc/syncthing-relaypoolsrv/README.txt", perm: 0644},
{src: "cmd/strelaypoolsrv/LICENSE", dst: "deb/usr/share/doc/syncthing-relaypoolsrv/LICENSE.txt", perm: 0644},
{src: "cmd/infra/strelaypoolsrv/README.md", dst: "deb/usr/share/doc/syncthing-relaypoolsrv/README.txt", perm: 0644},
{src: "cmd/infra/strelaypoolsrv/LICENSE", dst: "deb/usr/share/doc/syncthing-relaypoolsrv/LICENSE.txt", perm: 0644},
{src: "AUTHORS", dst: "deb/usr/share/doc/syncthing-relaypoolsrv/AUTHORS.txt", perm: 0644},
},
},
"stupgrades": {
name: "stupgrades",
description: "Syncthing Upgrade Check Server",
buildPkgs: []string{"github.com/syncthing/syncthing/cmd/stupgrades"},
buildPkgs: []string{"github.com/syncthing/syncthing/cmd/infra/stupgrades"},
binaryName: "stupgrades",
},
"stcrashreceiver": {
name: "stcrashreceiver",
description: "Syncthing Crash Server",
buildPkgs: []string{"github.com/syncthing/syncthing/cmd/stcrashreceiver"},
buildPkgs: []string{"github.com/syncthing/syncthing/cmd/infra/stcrashreceiver"},
binaryName: "stcrashreceiver",
},
"ursrv": {
name: "ursrv",
description: "Syncthing Usage Reporting Server",
buildPkgs: []string{"github.com/syncthing/syncthing/cmd/ursrv"},
buildPkgs: []string{"github.com/syncthing/syncthing/cmd/infra/ursrv"},
binaryName: "ursrv",
},
}
@@ -230,15 +232,11 @@ func initTargets() {
all := targets["all"]
pkgs, _ := filepath.Glob("cmd/*")
for _, pkg := range pkgs {
pkg = filepath.Base(pkg)
if strings.HasPrefix(pkg, ".") {
// ignore dotfiles
if files, err := filepath.Glob(pkg + "/*.go"); err != nil || len(files) == 0 {
// No go files in the directory
continue
}
if noupgrade && pkg == "stupgrades" {
continue
}
all.buildPkgs = append(all.buildPkgs, fmt.Sprintf("github.com/syncthing/syncthing/cmd/%s", pkg))
all.buildPkgs = append(all.buildPkgs, fmt.Sprintf("github.com/syncthing/syncthing/%s", pkg))
}
targets["all"] = all
@@ -342,9 +340,11 @@ func runCommand(cmd string, target target) {
case "tar":
buildTar(target, tags)
writeCompatJSON()
case "zip":
buildZip(target, tags)
writeCompatJSON()
case "deb":
buildDeb(target)
@@ -834,12 +834,12 @@ func listFiles(dir string) []string {
func rebuildAssets() {
os.Setenv("SOURCE_DATE_EPOCH", fmt.Sprint(buildStamp()))
runPrint(goCmd, "generate", "github.com/syncthing/syncthing/lib/api/auto", "github.com/syncthing/syncthing/cmd/strelaypoolsrv/auto")
runPrint(goCmd, "generate", "github.com/syncthing/syncthing/lib/api/auto", "github.com/syncthing/syncthing/cmd/infra/strelaypoolsrv/auto")
}
func lazyRebuildAssets() {
shouldRebuild := shouldRebuildAssets("lib/api/auto/gui.files.go", "gui") ||
shouldRebuildAssets("cmd/strelaypoolsrv/auto/gui.files.go", "cmd/strelaypoolsrv/gui")
shouldRebuildAssets("cmd/infra/strelaypoolsrv/auto/gui.files.go", "cmd/infra/strelaypoolsrv/gui")
if withNextGenGUI {
shouldRebuild = buildNextGenGUI() || shouldRebuild
@@ -1557,3 +1557,29 @@ func nextPatchVersion(ver string) string {
digits[len(digits)-1] = strconv.Itoa(n + 1)
return strings.Join(digits, ".")
}
func writeCompatJSON() {
bs, err := os.ReadFile("compat.yaml")
if err != nil {
log.Fatal("Reading compat.yaml:", err)
}
var entries []upgrade.ReleaseCompatibility
if err := yaml.Unmarshal(bs, &entries); err != nil {
log.Fatal("Parsing compat.yaml:", err)
}
rt := runtime.Version()
for _, e := range entries {
if !strings.HasPrefix(rt, e.Runtime) {
continue
}
bs, _ := json.MarshalIndent(e, "", " ")
if err := os.WriteFile("compat.json", bs, 0o644); err != nil {
log.Fatal("Writing compat.json:", err)
}
return
}
log.Fatalf("runtime %v not found in compat.yaml", rt)
}

View File

@@ -26,7 +26,7 @@ case "${1:-default}" in
build weblate
pushd man ; ./refresh.sh ; popd
git add -A gui man AUTHORS
git commit -m 'gui, man, authors: Update docs, translations, and contributors'
git commit -m 'chore(gui, man, authors): update docs, translations, and contributors'
;;
*)

View File

@@ -7,6 +7,7 @@
package main
import (
"crypto/sha256"
"errors"
"flag"
"fmt"
@@ -16,7 +17,6 @@ import (
"path/filepath"
_ "github.com/syncthing/syncthing/lib/automaxprocs"
"github.com/syncthing/syncthing/lib/sha256"
)
func main() {

View File

@@ -7,6 +7,7 @@
package main
import (
"crypto/sha256"
"flag"
"fmt"
"io"
@@ -14,7 +15,6 @@ import (
"time"
_ "github.com/syncthing/syncthing/lib/automaxprocs"
"github.com/syncthing/syncthing/lib/sha256"
)
func main() {

View File

@@ -14,6 +14,7 @@ package main
import (
"context"
"crypto/sha256"
"encoding/json"
"fmt"
"io"
@@ -29,7 +30,6 @@ import (
"github.com/prometheus/client_golang/prometheus/promhttp"
_ "github.com/syncthing/syncthing/lib/automaxprocs"
"github.com/syncthing/syncthing/lib/build"
"github.com/syncthing/syncthing/lib/sha256"
"github.com/syncthing/syncthing/lib/ur"
)

View File

@@ -9,14 +9,13 @@ package main
import (
"bytes"
"compress/gzip"
"crypto/sha256"
"fmt"
"net"
"net/http"
"os"
"path/filepath"
"time"
"github.com/syncthing/syncthing/lib/sha256"
)
// userIDFor returns a string we can use as the user ID for the purpose of

View File

@@ -21,4 +21,3 @@ See `relaypoolsrv -help` for configuration options.
[oschwald/geoip2-golang](https://github.com/oschwald/geoip2-golang), [oschwald/maxminddb-golang](https://github.com/oschwald/maxminddb-golang), Copyright (C) 2015 [Gregory J. Oschwald](mailto:oschwald@gmail.com).
[lib/pq](https://github.com/lib/pq)</a>, Copyright (C) 2011-2013 'pq' Contributors Portions Copyright (C) 2011 Blake Mizerany.

View File

@@ -4,7 +4,7 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
//go:generate go run ../../../script/genassets.go -o gui.files.go ../gui
//go:generate go run ../../../../script/genassets.go -o gui.files.go ../gui
// Package auto contains auto generated files for web assets.
package auto

View File

@@ -23,7 +23,7 @@ import (
lru "github.com/hashicorp/golang-lru/v2"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/syncthing/syncthing/cmd/strelaypoolsrv/auto"
"github.com/syncthing/syncthing/cmd/infra/strelaypoolsrv/auto"
"github.com/syncthing/syncthing/lib/assets"
_ "github.com/syncthing/syncthing/lib/automaxprocs"
"github.com/syncthing/syncthing/lib/geoip"

View File

@@ -0,0 +1,351 @@
// Copyright (C) 2019 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
package main
import (
"context"
"encoding/json"
"fmt"
"io"
"log/slog"
"net"
"net/http"
"os"
"regexp"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/alecthomas/kong"
"github.com/prometheus/client_golang/prometheus/promhttp"
_ "github.com/syncthing/syncthing/lib/automaxprocs"
"github.com/syncthing/syncthing/lib/httpcache"
"github.com/syncthing/syncthing/lib/upgrade"
)
type cli struct {
Listen string `default:":8080" help:"Listen address"`
MetricsListen string `default:":8082" help:"Listen address for metrics"`
URL string `short:"u" default:"https://api.github.com/repos/syncthing/syncthing/releases?per_page=25" help:"GitHub releases url"`
Forward []string `short:"f" help:"Forwarded pages, format: /path->https://example/com/url"`
CacheTime time.Duration `default:"15m" help:"Cache time"`
}
func main() {
var params cli
kong.Parse(&params)
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
})))
if err := server(&params); err != nil {
fmt.Printf("Error: %v\n", err)
os.Exit(1)
}
}
func server(params *cli) error {
if params.MetricsListen != "" {
mux := http.NewServeMux()
mux.Handle("/metrics", promhttp.Handler())
metricsListen, err := net.Listen("tcp", params.MetricsListen)
if err != nil {
return fmt.Errorf("metrics: %w", err)
}
slog.Info("Metrics listener started", "addr", params.MetricsListen)
go func() {
if err := http.Serve(metricsListen, mux); err != nil {
slog.Warn("Metrics server returned", "error", err)
}
}()
}
cache := &cachedReleases{url: params.URL}
if err := cache.Update(context.Background()); err != nil {
return fmt.Errorf("initial cache update: %w", err)
} else {
slog.Info("Initial cache update done")
}
go func() {
for range time.NewTicker(params.CacheTime).C {
slog.Info("Refreshing cached releases", "url", params.URL)
if err := cache.Update(context.Background()); err != nil {
slog.Error("Failed to refresh cached releases", "url", params.URL, "error", err)
}
}
}()
ghRels := &githubReleases{cache: cache}
mux := http.NewServeMux()
mux.HandleFunc("/ping", ghRels.servePing)
mux.HandleFunc("/meta.json", ghRels.serveReleases)
for _, fwd := range params.Forward {
path, url, ok := strings.Cut(fwd, "->")
if !ok {
return fmt.Errorf("invalid forward: %q", fwd)
}
slog.Info("Forwarding", "from", path, "to", url)
name := strings.ReplaceAll(path, "/", "_")
mux.Handle(path, httpcache.SinglePath(&proxy{name: name, url: url}, params.CacheTime))
}
srv := &http.Server{
Addr: params.Listen,
Handler: mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
srv.SetKeepAlivesEnabled(false)
srvListener, err := net.Listen("tcp", params.Listen)
if err != nil {
return fmt.Errorf("listen: %w", err)
}
slog.Info("Main listener started", "addr", params.Listen)
return srv.Serve(srvListener)
}
type githubReleases struct {
cache *cachedReleases
}
func (p *githubReleases) servePing(w http.ResponseWriter, req *http.Request) {
rels := p.cache.Releases()
if len(rels) == 0 {
http.Error(w, "No releases available", http.StatusServiceUnavailable)
return
}
w.Header().Set("Syncthing-Num-Releases", strconv.Itoa(len(rels)))
w.WriteHeader(http.StatusOK)
}
func (p *githubReleases) serveReleases(w http.ResponseWriter, req *http.Request) {
rels := p.cache.Releases()
ua := req.Header.Get("User-Agent")
osv := req.Header.Get("Syncthing-Os-Version")
if ua != "" && osv != "" {
// We should determine the compatibility of the releases.
rels = filterForCompabitility(rels, ua, osv)
} else {
metricFilterCalls.WithLabelValues("no-ua-or-osversion").Inc()
}
rels = filterForLatest(rels)
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET")
w.Header().Set("Cache-Control", "public, max-age=900")
w.Header().Set("Vary", "User-Agent, Syncthing-Os-Version")
_ = json.NewEncoder(w).Encode(rels)
metricUpgradeChecks.Inc()
}
type proxy struct {
name string
url string
}
func (p *proxy) ServeHTTP(w http.ResponseWriter, req *http.Request) {
req, err := http.NewRequestWithContext(req.Context(), http.MethodGet, p.url, nil)
if err != nil {
metricHTTPRequests.WithLabelValues(p.name, "error").Inc()
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
metricHTTPRequests.WithLabelValues(p.name, "error").Inc()
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer resp.Body.Close()
metricHTTPRequests.WithLabelValues(p.name, "success").Inc()
ct := resp.Header.Get("Content-Type")
w.Header().Set("Content-Type", ct)
if resp.StatusCode == http.StatusOK {
w.Header().Set("Cache-Control", "public, max-age=900")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET")
}
w.WriteHeader(resp.StatusCode)
if strings.HasPrefix(ct, "application/json") {
// Special JSON handling; clean it up a bit.
var v interface{}
if err := json.NewDecoder(resp.Body).Decode(&v); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(v)
} else {
_, _ = io.Copy(w, resp.Body)
}
}
// filterForLatest returns the latest stable and prerelease only. If the
// stable version is newer (comes first in the list) there is no need to go
// looking for a prerelease at all.
func filterForLatest(rels []upgrade.Release) []upgrade.Release {
var filtered []upgrade.Release
var havePre bool
for _, rel := range rels {
if !rel.Prerelease {
// We found a stable version, we're good now.
filtered = append(filtered, rel)
break
}
if rel.Prerelease && !havePre {
// We remember the first prerelease we find.
filtered = append(filtered, rel)
havePre = true
}
}
return filtered
}
var userAgentOSArchExp = regexp.MustCompile(`^syncthing.*\(.+ (\w+)-(\w+)\)$`)
func filterForCompabitility(rels []upgrade.Release, ua, osv string) []upgrade.Release {
osArch := userAgentOSArchExp.FindStringSubmatch(ua)
if len(osArch) != 3 {
metricFilterCalls.WithLabelValues("bad-os-arch").Inc()
return rels
}
os := osArch[1]
var filtered []upgrade.Release
for _, rel := range rels {
if rel.Compatibility == nil {
// No requirements means it's compatible with everything.
filtered = append(filtered, rel)
continue
}
req, ok := rel.Compatibility.Requirements[os]
if !ok {
// No entry for the current OS means it's compatible.
filtered = append(filtered, rel)
continue
}
if upgrade.CompareVersions(osv, req) >= 0 {
filtered = append(filtered, rel)
continue
}
}
if len(filtered) != len(rels) {
metricFilterCalls.WithLabelValues("filtered").Inc()
} else {
metricFilterCalls.WithLabelValues("unchanged").Inc()
}
return filtered
}
type cachedReleases struct {
url string
mut sync.RWMutex
current []upgrade.Release
}
func (c *cachedReleases) Releases() []upgrade.Release {
c.mut.RLock()
defer c.mut.RUnlock()
return c.current
}
func (c *cachedReleases) Update(ctx context.Context) error {
rels, err := fetchGithubReleases(ctx, c.url)
if err != nil {
return err
}
c.mut.Lock()
c.current = rels
c.mut.Unlock()
return nil
}
func fetchGithubReleases(ctx context.Context, url string) ([]upgrade.Release, error) {
req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, url, nil)
if err != nil {
metricHTTPRequests.WithLabelValues("github-releases", "error").Inc()
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
metricHTTPRequests.WithLabelValues("github-releases", "error").Inc()
return nil, err
}
defer resp.Body.Close()
var rels []upgrade.Release
if err := json.NewDecoder(resp.Body).Decode(&rels); err != nil {
metricHTTPRequests.WithLabelValues("github-releases", "error").Inc()
return nil, err
}
metricHTTPRequests.WithLabelValues("github-releases", "success").Inc()
// Move the URL used for browser downloads to the URL field, and remove
// the browser URL field. This avoids going via the GitHub API for
// downloads, since Syncthing uses the URL field.
for _, rel := range rels {
for j, asset := range rel.Assets {
rel.Assets[j].URL = asset.BrowserURL
rel.Assets[j].BrowserURL = ""
}
}
addReleaseCompatibility(ctx, rels)
sort.Sort(upgrade.SortByRelease(rels))
return rels, nil
}
func addReleaseCompatibility(ctx context.Context, rels []upgrade.Release) {
for i := range rels {
rel := &rels[i]
for i, asset := range rel.Assets {
if asset.Name != "compat.json" {
continue
}
// Load compat.json into the Compatibility field
req, err := http.NewRequestWithContext(ctx, http.MethodGet, asset.URL, nil)
if err != nil {
metricHTTPRequests.WithLabelValues("compat-json", "error").Inc()
break
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
metricHTTPRequests.WithLabelValues("compat-json", "error").Inc()
break
}
if resp.StatusCode != http.StatusOK {
metricHTTPRequests.WithLabelValues("compat-json", "error").Inc()
resp.Body.Close()
break
}
_ = json.NewDecoder(io.LimitReader(resp.Body, 10<<10)).Decode(&rel.Compatibility)
metricHTTPRequests.WithLabelValues("compat-json", "success").Inc()
resp.Body.Close()
// Remove compat.json from the asset list since it's been processed
rel.Assets = append(rel.Assets[:i], rel.Assets[i+1:]...)
break
}
}
}

View File

@@ -0,0 +1,30 @@
// Copyright (C) 2024 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
package main
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var (
metricUpgradeChecks = promauto.NewCounter(prometheus.CounterOpts{
Namespace: "syncthing",
Subsystem: "upgrade",
Name: "metadata_requests",
})
metricFilterCalls = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: "syncthing",
Subsystem: "upgrade",
Name: "filter_calls",
}, []string{"result"})
metricHTTPRequests = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: "syncthing",
Subsystem: "upgrade",
Name: "http_requests",
}, []string{"target", "result"})
)

View File

@@ -8,22 +8,22 @@ package main
import (
"log"
"log/slog"
"os"
"github.com/alecthomas/kong"
"github.com/syncthing/syncthing/cmd/ursrv/aggregate"
"github.com/syncthing/syncthing/cmd/ursrv/serve"
"github.com/syncthing/syncthing/cmd/infra/ursrv/serve"
_ "github.com/syncthing/syncthing/lib/automaxprocs"
)
type CLI struct {
Serve serve.CLI `cmd:"" default:""`
Aggregate aggregate.CLI `cmd:""`
Serve serve.CLI `cmd:"" default:""`
}
func main() {
log.SetFlags(log.Ltime | log.Ldate | log.Lshortfile)
log.SetOutput(os.Stdout)
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
})))
var cli CLI
ctx := kong.Parse(&cli)

View File

@@ -0,0 +1,46 @@
// Copyright (C) 2023 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 serve
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var (
metricReportsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: "syncthing",
Subsystem: "ursrv_v2",
Name: "incoming_reports_total",
}, []string{"result"})
metricsCollectsTotal = promauto.NewCounter(prometheus.CounterOpts{
Namespace: "syncthing",
Subsystem: "ursrv_v2",
Name: "collects_total",
})
metricsCollectSecondsTotal = promauto.NewCounter(prometheus.CounterOpts{
Namespace: "syncthing",
Subsystem: "ursrv_v2",
Name: "collect_seconds_total",
})
metricsCollectSecondsLast = promauto.NewGauge(prometheus.GaugeOpts{
Namespace: "syncthing",
Subsystem: "ursrv_v2",
Name: "collect_seconds_last",
})
metricsWriteSecondsLast = promauto.NewGauge(prometheus.GaugeOpts{
Namespace: "syncthing",
Subsystem: "ursrv_v2",
Name: "write_seconds_last",
})
)
func init() {
metricReportsTotal.WithLabelValues("fail")
metricReportsTotal.WithLabelValues("replace")
metricReportsTotal.WithLabelValues("accept")
}

View File

@@ -0,0 +1,314 @@
// Copyright (C) 2024 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 serve
import (
"reflect"
"slices"
"strconv"
"strings"
"sync"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/syncthing/syncthing/lib/ur/contract"
)
const namePrefix = "syncthing_usage_"
type metricsSet struct {
srv *server
gauges map[string]prometheus.Gauge
gaugeVecs map[string]*prometheus.GaugeVec
gaugeVecLabels map[string][]string
summaries map[string]*metricSummary
collectMut sync.Mutex
collectCutoff time.Duration
}
func newMetricsSet(srv *server) *metricsSet {
s := &metricsSet{
srv: srv,
gauges: make(map[string]prometheus.Gauge),
gaugeVecs: make(map[string]*prometheus.GaugeVec),
gaugeVecLabels: make(map[string][]string),
summaries: make(map[string]*metricSummary),
collectCutoff: -24 * time.Hour,
}
var initForType func(reflect.Type)
initForType = func(t reflect.Type) {
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
if field.Type.Kind() == reflect.Struct {
initForType(field.Type)
continue
}
name, typ, label := fieldNameTypeLabel(field)
sname, labels := nameConstLabels(name)
switch typ {
case "gauge":
s.gauges[name] = prometheus.NewGauge(prometheus.GaugeOpts{
Name: namePrefix + sname,
ConstLabels: labels,
})
case "summary":
s.summaries[name] = newMetricSummary(namePrefix+sname, nil, labels)
case "gaugeVec":
s.gaugeVecLabels[name] = append(s.gaugeVecLabels[name], label)
case "summaryVec":
s.summaries[name] = newMetricSummary(namePrefix+sname, []string{label}, labels)
}
}
}
initForType(reflect.ValueOf(contract.Report{}).Type())
for name, labels := range s.gaugeVecLabels {
s.gaugeVecs[name] = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Name: namePrefix + name,
}, labels)
}
return s
}
func fieldNameTypeLabel(rf reflect.StructField) (string, string, string) {
metric := rf.Tag.Get("metric")
name, typ, ok := strings.Cut(metric, ",")
if !ok {
return "", "", ""
}
gv, label, ok := strings.Cut(typ, ":")
if ok {
typ = gv
}
return name, typ, label
}
func nameConstLabels(name string) (string, prometheus.Labels) {
if name == "-" {
return "", nil
}
name, labels, ok := strings.Cut(name, "{")
if !ok {
return name, nil
}
lls := strings.Split(labels[:len(labels)-1], ",")
m := make(map[string]string)
for _, l := range lls {
k, v, _ := strings.Cut(l, "=")
m[k] = v
}
return name, m
}
func (s *metricsSet) addReport(r *contract.Report) {
gaugeVecs := make(map[string][]string)
s.addReportStruct(reflect.ValueOf(r).Elem(), gaugeVecs)
for name, lv := range gaugeVecs {
s.gaugeVecs[name].WithLabelValues(lv...).Add(1)
}
}
func (s *metricsSet) addReportStruct(v reflect.Value, gaugeVecs map[string][]string) {
t := v.Type()
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
if field.Kind() == reflect.Struct {
s.addReportStruct(field, gaugeVecs)
continue
}
name, typ, label := fieldNameTypeLabel(t.Field(i))
switch typ {
case "gauge":
switch v := field.Interface().(type) {
case int:
s.gauges[name].Add(float64(v))
case string:
s.gaugeVecs[name].WithLabelValues(v).Add(1)
case bool:
if v {
s.gauges[name].Add(1)
}
}
case "gaugeVec":
var labelValue string
switch v := field.Interface().(type) {
case string:
labelValue = v
case int:
labelValue = strconv.Itoa(v)
case map[string]int:
for k, v := range v {
labelValue = k
field.SetInt(int64(v))
break
}
}
if _, ok := gaugeVecs[name]; !ok {
gaugeVecs[name] = make([]string, len(s.gaugeVecLabels[name]))
}
for i, l := range s.gaugeVecLabels[name] {
if l == label {
gaugeVecs[name][i] = labelValue
break
}
}
case "summary", "summaryVec":
switch v := field.Interface().(type) {
case int:
s.summaries[name].Observe("", float64(v))
case float64:
s.summaries[name].Observe("", v)
case []int:
for _, v := range v {
s.summaries[name].Observe("", float64(v))
}
case map[string]int:
for k, v := range v {
if k == "" {
// avoid empty string labels as those are the sign
// of a non-vec summary
k = "unknown"
}
s.summaries[name].Observe(k, float64(v))
}
}
}
}
}
func (s *metricsSet) Describe(c chan<- *prometheus.Desc) {
for _, g := range s.gauges {
g.Describe(c)
}
for _, g := range s.gaugeVecs {
g.Describe(c)
}
for _, g := range s.summaries {
g.Describe(c)
}
}
func (s *metricsSet) Collect(c chan<- prometheus.Metric) {
s.collectMut.Lock()
defer s.collectMut.Unlock()
t0 := time.Now()
defer func() {
dur := time.Since(t0).Seconds()
metricsCollectSecondsLast.Set(dur)
metricsCollectSecondsTotal.Add(dur)
metricsCollectsTotal.Inc()
}()
for _, g := range s.gauges {
g.Set(0)
}
for _, g := range s.gaugeVecs {
g.Reset()
}
for _, g := range s.summaries {
g.Reset()
}
cutoff := time.Now().Add(s.collectCutoff)
s.srv.reports.Range(func(key string, r *contract.Report) bool {
if s.collectCutoff < 0 && r.Received.Before(cutoff) {
s.srv.reports.Delete(key)
return true
}
s.addReport(r)
return true
})
for _, g := range s.gauges {
c <- g
}
for _, g := range s.gaugeVecs {
g.Collect(c)
}
for _, g := range s.summaries {
g.Collect(c)
}
}
type metricSummary struct {
name string
values map[string][]float64
zeroes map[string]int
qDesc *prometheus.Desc
countDesc *prometheus.Desc
sumDesc *prometheus.Desc
zDesc *prometheus.Desc
}
func newMetricSummary(name string, labels []string, constLabels prometheus.Labels) *metricSummary {
return &metricSummary{
name: name,
values: make(map[string][]float64),
zeroes: make(map[string]int),
qDesc: prometheus.NewDesc(name, "", append(labels, "quantile"), constLabels),
countDesc: prometheus.NewDesc(name+"_nonzero_count", "", labels, constLabels),
sumDesc: prometheus.NewDesc(name+"_sum", "", labels, constLabels),
zDesc: prometheus.NewDesc(name+"_zero_count", "", labels, constLabels),
}
}
func (q *metricSummary) Observe(labelValue string, v float64) {
if v == 0 {
q.zeroes[labelValue]++
return
}
q.values[labelValue] = append(q.values[labelValue], v)
}
func (q *metricSummary) Describe(c chan<- *prometheus.Desc) {
c <- q.qDesc
c <- q.countDesc
c <- q.sumDesc
c <- q.zDesc
}
func (q *metricSummary) Collect(c chan<- prometheus.Metric) {
for lv, vs := range q.values {
var labelVals []string
if lv != "" {
labelVals = []string{lv}
}
c <- prometheus.MustNewConstMetric(q.countDesc, prometheus.GaugeValue, float64(len(vs)), labelVals...)
c <- prometheus.MustNewConstMetric(q.zDesc, prometheus.GaugeValue, float64(q.zeroes[lv]), labelVals...)
var sum float64
for _, v := range vs {
sum += v
}
c <- prometheus.MustNewConstMetric(q.sumDesc, prometheus.GaugeValue, sum, labelVals...)
if len(vs) == 0 {
return
}
slices.Sort(vs)
c <- prometheus.MustNewConstMetric(q.qDesc, prometheus.GaugeValue, vs[0], append(labelVals, "0")...)
c <- prometheus.MustNewConstMetric(q.qDesc, prometheus.GaugeValue, vs[len(vs)*5/100], append(labelVals, "0.05")...)
c <- prometheus.MustNewConstMetric(q.qDesc, prometheus.GaugeValue, vs[len(vs)/2], append(labelVals, "0.5")...)
c <- prometheus.MustNewConstMetric(q.qDesc, prometheus.GaugeValue, vs[len(vs)*9/10], append(labelVals, "0.9")...)
c <- prometheus.MustNewConstMetric(q.qDesc, prometheus.GaugeValue, vs[len(vs)*95/100], append(labelVals, "0.95")...)
c <- prometheus.MustNewConstMetric(q.qDesc, prometheus.GaugeValue, vs[len(vs)-1], append(labelVals, "1")...)
}
}
func (q *metricSummary) Reset() {
clear(q.values)
clear(q.zeroes)
}

View File

@@ -0,0 +1,408 @@
// Copyright (C) 2018 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
package serve
import (
"bufio"
"compress/gzip"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log/slog"
"net"
"net/http"
"os"
"regexp"
"strings"
"time"
_ "net/http/pprof"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/puzpuzpuz/xsync/v3"
"github.com/syncthing/syncthing/lib/build"
"github.com/syncthing/syncthing/lib/geoip"
"github.com/syncthing/syncthing/lib/s3"
"github.com/syncthing/syncthing/lib/ur/contract"
)
type CLI struct {
Listen string `env:"UR_LISTEN" help:"Usage reporting & metrics endpoint listen address" default:"0.0.0.0:8080"`
ListenInternal string `env:"UR_LISTEN_INTERNAL" help:"Internal metrics endpoint listen address" default:"0.0.0.0:8082"`
GeoIPLicenseKey string `env:"UR_GEOIP_LICENSE_KEY"`
GeoIPAccountID int `env:"UR_GEOIP_ACCOUNT_ID"`
DumpFile string `env:"UR_DUMP_FILE" default:"reports.jsons.gz"`
DumpInterval time.Duration `env:"UR_DUMP_INTERVAL" default:"5m"`
S3Endpoint string `name:"s3-endpoint" hidden:"true" env:"UR_S3_ENDPOINT"`
S3Region string `name:"s3-region" hidden:"true" env:"UR_S3_REGION"`
S3Bucket string `name:"s3-bucket" hidden:"true" env:"UR_S3_BUCKET"`
S3AccessKeyID string `name:"s3-access-key-id" hidden:"true" env:"UR_S3_ACCESS_KEY_ID"`
S3SecretKey string `name:"s3-secret-key" hidden:"true" env:"UR_S3_SECRET_KEY"`
}
var (
compilerRe = regexp.MustCompile(`\(([A-Za-z0-9()., -]+) \w+-\w+(?:| android| default)\) ([\w@.-]+)`)
knownDistributions = []distributionMatch{
// Maps well known builders to the official distribution method that
// they represent
{regexp.MustCompile(`\steamcity@build\.syncthing\.net`), "GitHub"},
{regexp.MustCompile(`\sjenkins@build\.syncthing\.net`), "GitHub"},
{regexp.MustCompile(`\sbuilder@github\.syncthing\.net`), "GitHub"},
{regexp.MustCompile(`\sdeb@build\.syncthing\.net`), "APT"},
{regexp.MustCompile(`\sdebian@github\.syncthing\.net`), "APT"},
{regexp.MustCompile(`\sdocker@syncthing\.net`), "Docker Hub"},
{regexp.MustCompile(`\sdocker@build.syncthing\.net`), "Docker Hub"},
{regexp.MustCompile(`\sdocker@github.syncthing\.net`), "Docker Hub"},
{regexp.MustCompile(`\sandroid-builder@github\.syncthing\.net`), "Google Play"},
{regexp.MustCompile(`\sandroid-.*teamcity@build\.syncthing\.net`), "Google Play"},
{regexp.MustCompile(`\sandroid-.*vagrant@basebox-stretch64`), "F-Droid"},
{regexp.MustCompile(`\svagrant@bullseye`), "F-Droid"},
{regexp.MustCompile(`\svagrant@bookworm`), "F-Droid"},
{regexp.MustCompile(`Anwender@NET2017`), "Syncthing-Fork (3rd party)"},
{regexp.MustCompile(`\sbuilduser@(archlinux|svetlemodry)`), "Arch (3rd party)"},
{regexp.MustCompile(`\ssyncthing@archlinux`), "Arch (3rd party)"},
{regexp.MustCompile(`@debian`), "Debian (3rd party)"},
{regexp.MustCompile(`@fedora`), "Fedora (3rd party)"},
{regexp.MustCompile(`\sbrew@`), "Homebrew (3rd party)"},
{regexp.MustCompile(`\sroot@buildkitsandbox`), "LinuxServer.io (3rd party)"},
{regexp.MustCompile(`\sports@freebsd`), "FreeBSD (3rd party)"},
{regexp.MustCompile(`\snix@nix`), "Nix (3rd party)"},
{regexp.MustCompile(`.`), "Others"},
}
)
type distributionMatch struct {
matcher *regexp.Regexp
distribution string
}
func (cli *CLI) Run() error {
slog.Info("Starting", "version", build.Version)
// Listening
urListener, err := net.Listen("tcp", cli.Listen)
if err != nil {
slog.Error("Failed to listen (usage reports)", "error", err)
return err
}
slog.Info("Listening (usage reports)", "address", urListener.Addr())
internalListener, err := net.Listen("tcp", cli.ListenInternal)
if err != nil {
slog.Error("Failed to listen (internal)", "error", err)
return err
}
slog.Info("Listening (internal)", "address", internalListener.Addr())
var geo *geoip.Provider
if cli.GeoIPAccountID != 0 && cli.GeoIPLicenseKey != "" {
geo, err = geoip.NewGeoLite2CityProvider(context.Background(), cli.GeoIPAccountID, cli.GeoIPLicenseKey, os.TempDir())
if err != nil {
slog.Error("Failed to load GeoIP", "error", err)
return err
}
go geo.Serve(context.TODO())
}
// s3
var s3sess *s3.Session
if cli.S3Endpoint != "" {
s3sess, err = s3.NewSession(cli.S3Endpoint, cli.S3Region, cli.S3Bucket, cli.S3AccessKeyID, cli.S3SecretKey)
if err != nil {
slog.Error("Failed to create S3 session", "error", err)
return err
}
}
if _, err := os.Stat(cli.DumpFile); err != nil && s3sess != nil {
if err := cli.downloadDumpFile(s3sess); err != nil {
slog.Error("Failed to download dump file", "error", err)
}
}
// server
srv := &server{
geo: geo,
reports: xsync.NewMapOf[string, *contract.Report](),
}
if fd, err := os.Open(cli.DumpFile); err == nil {
gr, err := gzip.NewReader(fd)
if err == nil {
srv.load(gr)
}
fd.Close()
}
go func() {
for range time.Tick(cli.DumpInterval) {
if err := cli.saveDumpFile(srv, s3sess); err != nil {
slog.Error("Failed to write dump file", "error", err)
}
}
}()
// The internal metrics endpoint just serves metrics about what the
// server is doing.
http.Handle("/metrics", promhttp.Handler())
internalSrv := http.Server{
ReadTimeout: 5 * time.Second,
WriteTimeout: 15 * time.Second,
}
go internalSrv.Serve(internalListener)
// New external metrics endpoint accepts reports from clients and serves
// aggregated usage reporting metrics.
ms := newMetricsSet(srv)
reg := prometheus.NewRegistry()
reg.MustRegister(ms)
mux := http.NewServeMux()
mux.Handle("/metrics", promhttp.HandlerFor(reg, promhttp.HandlerOpts{}))
mux.HandleFunc("/newdata", srv.handleNewData)
mux.HandleFunc("/ping", srv.handlePing)
metricsSrv := http.Server{
ReadTimeout: 5 * time.Second,
WriteTimeout: 15 * time.Second,
Handler: mux,
}
slog.Info("Ready to serve")
return metricsSrv.Serve(urListener)
}
func (cli *CLI) downloadDumpFile(s3sess *s3.Session) error {
latestKey, err := s3sess.LatestKey()
if err != nil {
return fmt.Errorf("list latest S3 key: %w", err)
}
fd, err := os.Create(cli.DumpFile)
if err != nil {
return fmt.Errorf("create dump file: %w", err)
}
if err := s3sess.Download(fd, latestKey); err != nil {
_ = fd.Close()
return fmt.Errorf("download dump file: %w", err)
}
if err := fd.Close(); err != nil {
return fmt.Errorf("close dump file: %w", err)
}
slog.Info("Dump file downloaded", "key", latestKey)
return nil
}
func (cli *CLI) saveDumpFile(srv *server, s3sess *s3.Session) error {
fd, err := os.Create(cli.DumpFile + ".tmp")
if err != nil {
return fmt.Errorf("creating dump file: %w", err)
}
gw := gzip.NewWriter(fd)
if err := srv.save(gw); err != nil {
return fmt.Errorf("saving dump file: %w", err)
}
if err := gw.Close(); err != nil {
fd.Close()
return fmt.Errorf("closing gzip writer: %w", err)
}
if err := fd.Close(); err != nil {
return fmt.Errorf("closing dump file: %w", err)
}
if err := os.Rename(cli.DumpFile+".tmp", cli.DumpFile); err != nil {
return fmt.Errorf("renaming dump file: %w", err)
}
slog.Info("Dump file saved")
if s3sess != nil {
key := fmt.Sprintf("reports-%s.jsons.gz", time.Now().UTC().Format("2006-01-02"))
fd, err := os.Open(cli.DumpFile)
if err != nil {
return fmt.Errorf("opening dump file: %w", err)
}
if err := s3sess.Upload(fd, key); err != nil {
return fmt.Errorf("uploading dump file: %w", err)
}
_ = fd.Close()
slog.Info("Dump file uploaded")
}
return nil
}
type server struct {
geo *geoip.Provider
reports *xsync.MapOf[string, *contract.Report]
}
func (s *server) handlePing(w http.ResponseWriter, r *http.Request) {
}
func (s *server) handleNewData(w http.ResponseWriter, r *http.Request) {
result := "fail"
defer func() {
// result is "accept" (new report), "replace" (existing report) or
// "fail"
metricReportsTotal.WithLabelValues(result).Inc()
}()
defer r.Body.Close()
if r.Method != http.MethodPost {
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
return
}
addr := r.Header.Get("X-Forwarded-For")
if addr != "" {
addr = strings.Split(addr, ", ")[0]
} else {
addr = r.RemoteAddr
}
if host, _, err := net.SplitHostPort(addr); err == nil {
addr = host
}
log := slog.With("addr", addr)
if net.ParseIP(addr) == nil {
addr = ""
}
var rep contract.Report
lr := &io.LimitedReader{R: r.Body, N: 40 * 1024}
bs, _ := io.ReadAll(lr)
if err := json.Unmarshal(bs, &rep); err != nil {
log.Error("Failed to decode JSON", "error", err)
http.Error(w, "JSON Decode Error", http.StatusInternalServerError)
return
}
rep.Received = time.Now()
rep.Date = rep.Received.UTC().Format("20060102")
rep.Address = addr
if err := rep.Validate(); err != nil {
log.Error("Failed to validate report", "error", err)
http.Error(w, "Validation Error", http.StatusInternalServerError)
return
}
if s.addReport(&rep) {
result = "replace"
} else {
result = "accept"
}
}
func (s *server) addReport(rep *contract.Report) bool {
if s.geo != nil {
if ip := net.ParseIP(rep.Address); ip != nil {
if city, err := s.geo.City(ip); err == nil {
rep.Country = city.Country.Names["en"]
rep.CountryCode = city.Country.IsoCode
}
}
}
if rep.Country == "" {
rep.Country = "Unknown"
}
if rep.CountryCode == "" {
rep.CountryCode = "ZZ"
}
rep.Version = transformVersion(rep.Version)
if strings.Contains(rep.Version, ".") {
split := strings.SplitN(rep.Version, ".", 3)
if len(split) == 3 {
rep.MajorVersion = strings.Join(split[:2], ".")
}
}
rep.OS, rep.Arch, _ = strings.Cut(rep.Platform, "-")
if m := compilerRe.FindStringSubmatch(rep.LongVersion); len(m) == 3 {
rep.Compiler = m[1]
rep.Builder = m[2]
}
for _, d := range knownDistributions {
if d.matcher.MatchString(rep.LongVersion) {
rep.Distribution = d.distribution
break
}
}
_, loaded := s.reports.LoadAndStore(rep.UniqueID, rep)
return loaded
}
func (s *server) save(w io.Writer) error {
bw := bufio.NewWriter(w)
enc := json.NewEncoder(bw)
var err error
s.reports.Range(func(k string, v *contract.Report) bool {
err = enc.Encode(v)
return err == nil
})
if err != nil {
return err
}
return bw.Flush()
}
func (s *server) load(r io.Reader) {
dec := json.NewDecoder(r)
s.reports.Clear()
for {
var rep contract.Report
if err := dec.Decode(&rep); errors.Is(err, io.EOF) {
break
} else if err != nil {
slog.Error("Failed to load record", "error", err)
break
}
s.addReport(&rep)
}
slog.Info("Loaded reports", "count", s.reports.Size())
}
var (
plusRe = regexp.MustCompile(`(\+.*|[.-]dev\..*)$`)
plusStr = "-dev"
)
// transformVersion returns a version number formatted correctly, with all
// development versions aggregated into one.
func transformVersion(v string) string {
if v == "unknown-dev" {
return v
}
if !strings.HasPrefix(v, "v") {
v = "v" + v
}
v = plusRe.ReplaceAllString(v, plusStr)
return v
}

View File

@@ -10,8 +10,10 @@ import (
"context"
"fmt"
"io"
"log"
amqp "github.com/rabbitmq/amqp091-go"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/thejerf/suture/v4"
)
@@ -49,7 +51,7 @@ func newAMQPReplicator(broker, clientID string, db database) *amqpReplicator {
}
}
func (s *amqpReplicator) send(key string, ps []DatabaseAddress, seen int64) {
func (s *amqpReplicator) send(key *protocol.DeviceID, ps []DatabaseAddress, seen int64) {
s.sender.send(key, ps, seen)
}
@@ -109,9 +111,9 @@ func (s *amqpSender) String() string {
return fmt.Sprintf("amqpSender(%q)", s.broker)
}
func (s *amqpSender) send(key string, ps []DatabaseAddress, seen int64) {
func (s *amqpSender) send(key *protocol.DeviceID, ps []DatabaseAddress, seen int64) {
item := ReplicationRecord{
Key: key,
Key: key[:],
Addresses: ps,
Seen: seen,
}
@@ -161,8 +163,17 @@ func (s *amqpReceiver) Serve(ctx context.Context) error {
replicationRecvsTotal.WithLabelValues("error").Inc()
return fmt.Errorf("replication unmarshal: %w", err)
}
id, err := protocol.DeviceIDFromBytes(rec.Key)
if err != nil {
id, err = protocol.DeviceIDFromString(string(rec.Key))
}
if err != nil {
log.Println("Replication device ID:", err)
replicationRecvsTotal.WithLabelValues("error").Inc()
continue
}
if err := s.db.merge(rec.Key, rec.Addresses, rec.Seen); err != nil {
if err := s.db.merge(&id, rec.Addresses, rec.Seen); err != nil {
return fmt.Errorf("replication database merge: %w", err)
}

View File

@@ -22,7 +22,7 @@ import (
"net"
"net/http"
"net/url"
"sort"
"slices"
"strconv"
"strings"
"sync"
@@ -45,10 +45,14 @@ type apiSrv struct {
listener net.Listener
repl replicator // optional
useHTTP bool
missesIncrease int
compression bool
gzipWriters sync.Pool
seenTracker *retryAfterTracker
notSeenTracker *retryAfterTracker
}
mapsMut sync.Mutex
misses map[string]int32
type replicator interface {
send(key *protocol.DeviceID, addrs []DatabaseAddress, seen int64)
}
type requestID int64
@@ -61,19 +65,30 @@ type contextKey int
const idKey contextKey = iota
func newAPISrv(addr string, cert tls.Certificate, db database, repl replicator, useHTTP bool, missesIncrease int) *apiSrv {
func newAPISrv(addr string, cert tls.Certificate, db database, repl replicator, useHTTP, compression bool) *apiSrv {
return &apiSrv{
addr: addr,
cert: cert,
db: db,
repl: repl,
useHTTP: useHTTP,
misses: make(map[string]int32),
missesIncrease: missesIncrease,
addr: addr,
cert: cert,
db: db,
repl: repl,
useHTTP: useHTTP,
compression: compression,
seenTracker: &retryAfterTracker{
name: "seenTracker",
bucketStarts: time.Now(),
desiredRate: 250,
currentDelay: notFoundRetryUnknownMinSeconds,
},
notSeenTracker: &retryAfterTracker{
name: "notSeenTracker",
bucketStarts: time.Now(),
desiredRate: 250,
currentDelay: notFoundRetryUnknownMaxSeconds / 2,
},
}
}
func (s *apiSrv) Serve(_ context.Context) error {
func (s *apiSrv) Serve(ctx context.Context) error {
if s.useHTTP {
listener, err := net.Listen("tcp", s.addr)
if err != nil {
@@ -107,6 +122,11 @@ func (s *apiSrv) Serve(_ context.Context) error {
ErrorLog: log.New(io.Discard, "", 0),
}
go func() {
<-ctx.Done()
srv.Shutdown(context.Background())
}()
err := srv.Serve(s.listener)
if err != nil {
log.Println("Serve:", err)
@@ -183,8 +203,7 @@ func (s *apiSrv) handleGET(w http.ResponseWriter, req *http.Request) {
return
}
key := deviceID.String()
rec, err := s.db.get(key)
rec, err := s.db.get(&deviceID)
if err != nil {
// some sort of internal error
lookupRequestsTotal.WithLabelValues("internal_error").Inc()
@@ -194,27 +213,14 @@ func (s *apiSrv) handleGET(w http.ResponseWriter, req *http.Request) {
}
if len(rec.Addresses) == 0 {
lookupRequestsTotal.WithLabelValues("not_found").Inc()
s.mapsMut.Lock()
misses := s.misses[key]
if misses < rec.Misses {
misses = rec.Misses
var afterS int
if rec.Seen == 0 {
afterS = s.notSeenTracker.retryAfterS()
lookupRequestsTotal.WithLabelValues("not_found_ever").Inc()
} else {
afterS = s.seenTracker.retryAfterS()
lookupRequestsTotal.WithLabelValues("not_found_recent").Inc()
}
misses += int32(s.missesIncrease)
s.misses[key] = misses
s.mapsMut.Unlock()
if misses >= notFoundMissesWriteInterval {
rec.Misses = misses
rec.Missed = time.Now().UnixNano()
rec.Addresses = nil
// rec.Seen retained from get
s.db.put(key, rec)
}
afterS := notFoundRetryAfterSeconds(int(misses))
retryAfterHistogram.Observe(float64(afterS))
w.Header().Set("Retry-After", strconv.Itoa(afterS))
http.Error(w, "Not Found", http.StatusNotFound)
return
@@ -226,10 +232,16 @@ func (s *apiSrv) handleGET(w http.ResponseWriter, req *http.Request) {
var bw io.Writer = w
// Use compression if the client asks for it
if strings.Contains(req.Header.Get("Accept-Encoding"), "gzip") {
if s.compression && strings.Contains(req.Header.Get("Accept-Encoding"), "gzip") {
gw, ok := s.gzipWriters.Get().(*gzip.Writer)
if ok {
gw.Reset(w)
} else {
gw = gzip.NewWriter(w)
}
w.Header().Set("Content-Encoding", "gzip")
gw := gzip.NewWriter(bw)
defer gw.Close()
defer s.gzipWriters.Put(gw)
bw = gw
}
@@ -292,25 +304,25 @@ func (s *apiSrv) Stop() {
}
func (s *apiSrv) handleAnnounce(deviceID protocol.DeviceID, addresses []string) error {
key := deviceID.String()
now := time.Now()
expire := now.Add(addressExpiryTime).UnixNano()
// The address slice must always be sorted for database merges to work
// properly.
slices.Sort(addresses)
addresses = slices.Compact(addresses)
dbAddrs := make([]DatabaseAddress, len(addresses))
for i := range addresses {
dbAddrs[i].Address = addresses[i]
dbAddrs[i].Expires = expire
}
// The address slice must always be sorted for database merges to work
// properly.
sort.Sort(databaseAddressOrder(dbAddrs))
seen := now.UnixNano()
if s.repl != nil {
s.repl.send(key, dbAddrs, seen)
s.repl.send(&deviceID, dbAddrs, seen)
}
return s.db.merge(key, dbAddrs, seen)
return s.db.merge(&deviceID, dbAddrs, seen)
}
func handlePing(w http.ResponseWriter, _ *http.Request) {
@@ -360,7 +372,7 @@ func certificateBytes(req *http.Request) ([]byte, error) {
}
bs = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: hdr})
} else if hdr := req.Header.Get("X-Forwarded-Tls-Client-Cert"); hdr != "" {
} else if cert := req.Header.Get("X-Forwarded-Tls-Client-Cert"); cert != "" {
// Traefik 2 passtlsclientcert
//
// The certificate is in PEM format, maybe with URL encoding
@@ -368,19 +380,36 @@ func certificateBytes(req *http.Request) ([]byte, error) {
// statements. We need to decode, reinstate the newlines every 64
// character and add statements for the PEM decoder
if strings.Contains(hdr, "%") {
if unesc, err := url.QueryUnescape(hdr); err == nil {
hdr = unesc
if strings.Contains(cert, "%") {
if unesc, err := url.QueryUnescape(cert); err == nil {
cert = unesc
}
}
for i := 64; i < len(hdr); i += 65 {
hdr = hdr[:i] + "\n" + hdr[i:]
const (
header = "-----BEGIN CERTIFICATE-----"
footer = "-----END CERTIFICATE-----"
)
var b bytes.Buffer
b.Grow(len(header) + 1 + len(cert) + len(cert)/64 + 1 + len(footer) + 1)
b.WriteString(header)
b.WriteByte('\n')
for i := 0; i < len(cert); i += 64 {
end := i + 64
if end > len(cert) {
end = len(cert)
}
b.WriteString(cert[i:end])
b.WriteByte('\n')
}
hdr = "-----BEGIN CERTIFICATE-----\n" + hdr
hdr += "\n-----END CERTIFICATE-----\n"
bs = []byte(hdr)
b.WriteString(footer)
b.WriteByte('\n')
bs = b.Bytes()
}
if bs == nil {
@@ -494,15 +523,44 @@ func errorRetryAfterString() string {
return strconv.Itoa(errorRetryAfterSeconds + rand.Intn(errorRetryFuzzSeconds))
}
func notFoundRetryAfterSeconds(misses int) int {
retryAfterS := notFoundRetryMinSeconds + notFoundRetryIncSeconds*misses
if retryAfterS > notFoundRetryMaxSeconds {
retryAfterS = notFoundRetryMaxSeconds
}
retryAfterS += rand.Intn(notFoundRetryFuzzSeconds)
return retryAfterS
}
func reannounceAfterString() string {
return strconv.Itoa(reannounceAfterSeconds + rand.Intn(reannounzeFuzzSeconds))
}
type retryAfterTracker struct {
name string
desiredRate float64 // requests per second
mut sync.Mutex
lastCount int // requests in the last bucket
curCount int // requests in the current bucket
bucketStarts time.Time // start of the current bucket
currentDelay int // current delay in seconds
}
func (t *retryAfterTracker) retryAfterS() int {
now := time.Now()
t.mut.Lock()
if durS := now.Sub(t.bucketStarts).Seconds(); durS > float64(t.currentDelay) {
t.bucketStarts = now
t.lastCount = t.curCount
lastRate := float64(t.lastCount) / durS
switch {
case t.currentDelay > notFoundRetryUnknownMinSeconds &&
lastRate < 0.75*t.desiredRate:
t.currentDelay = max(8*t.currentDelay/10, notFoundRetryUnknownMinSeconds)
case t.currentDelay < notFoundRetryUnknownMaxSeconds &&
lastRate > 1.25*t.desiredRate:
t.currentDelay = min(3*t.currentDelay/2, notFoundRetryUnknownMaxSeconds)
}
t.curCount = 0
}
if t.curCount == 0 {
retryAfterLevel.WithLabelValues(t.name).Set(float64(t.currentDelay))
}
t.curCount++
t.mut.Unlock()
return t.currentDelay + rand.Intn(t.currentDelay/4)
}

View File

@@ -7,9 +7,20 @@
package main
import (
"context"
"crypto/tls"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
"os"
"regexp"
"strings"
"testing"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/tlsutil"
)
func TestFixupAddresses(t *testing.T) {
@@ -94,3 +105,79 @@ func addr(host string, port int) *net.TCPAddr {
Port: port,
}
}
func BenchmarkAPIRequests(b *testing.B) {
db := newInMemoryStore(b.TempDir(), 0, nil)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go db.Serve(ctx)
api := newAPISrv("127.0.0.1:0", tls.Certificate{}, db, nil, true, true)
srv := httptest.NewServer(http.HandlerFunc(api.handler))
kf := b.TempDir() + "/cert"
crt, err := tlsutil.NewCertificate(kf+".crt", kf+".key", "localhost", 7)
if err != nil {
b.Fatal(err)
}
certBs, err := os.ReadFile(kf + ".crt")
if err != nil {
b.Fatal(err)
}
certBs = regexp.MustCompile(`---[^\n]+---\n`).ReplaceAll(certBs, nil)
certString := string(strings.ReplaceAll(string(certBs), "\n", " "))
devID := protocol.NewDeviceID(crt.Certificate[0])
devIDString := devID.String()
b.Run("Announce", func(b *testing.B) {
b.ReportAllocs()
url := srv.URL + "/v2/?device=" + devIDString
for i := 0; i < b.N; i++ {
req, _ := http.NewRequest(http.MethodPost, url, strings.NewReader(`{"addresses":["tcp://10.10.10.10:42000"]}`))
req.Header.Set("X-Forwarded-Tls-Client-Cert", certString)
resp, err := http.DefaultClient.Do(req)
if err != nil {
b.Fatal(err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusNoContent {
b.Fatalf("unexpected status %s", resp.Status)
}
}
})
b.Run("Lookup", func(b *testing.B) {
b.ReportAllocs()
url := srv.URL + "/v2/?device=" + devIDString
for i := 0; i < b.N; i++ {
req, _ := http.NewRequest(http.MethodGet, url, nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
b.Fatal(err)
}
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
b.Fatalf("unexpected status %s", resp.Status)
}
}
})
b.Run("LookupNoCompression", func(b *testing.B) {
b.ReportAllocs()
url := srv.URL + "/v2/?device=" + devIDString
for i := 0; i < b.N; i++ {
req, _ := http.NewRequest(http.MethodGet, url, nil)
req.Header.Set("Accept-Encoding", "identity") // disable compression
resp, err := http.DefaultClient.Do(req)
if err != nil {
b.Fatal(err)
}
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
b.Fatalf("unexpected status %s", resp.Status)
}
}
})
}

View File

@@ -10,17 +10,24 @@
package main
import (
"bufio"
"cmp"
"context"
"encoding/binary"
"errors"
"io"
"log"
"net"
"net/url"
"sort"
"os"
"path"
"runtime"
"slices"
"strings"
"time"
"github.com/syncthing/syncthing/lib/sliceutil"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/storage"
"github.com/syndtr/goleveldb/leveldb/util"
"github.com/puzpuzpuz/xsync/v3"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/rand"
"github.com/syncthing/syncthing/lib/s3"
)
type clock interface {
@@ -34,380 +41,400 @@ func (defaultClock) Now() time.Time {
}
type database interface {
put(key string, rec DatabaseRecord) error
merge(key string, addrs []DatabaseAddress, seen int64) error
get(key string) (DatabaseRecord, error)
put(key *protocol.DeviceID, rec DatabaseRecord) error
merge(key *protocol.DeviceID, addrs []DatabaseAddress, seen int64) error
get(key *protocol.DeviceID) (DatabaseRecord, error)
}
type levelDBStore struct {
db *leveldb.DB
inbox chan func()
clock clock
marshalBuf []byte
type inMemoryStore struct {
m *xsync.MapOf[protocol.DeviceID, DatabaseRecord]
dir string
flushInterval time.Duration
s3 *s3.Session
objKey string
clock clock
}
func newLevelDBStore(dir string) (*levelDBStore, error) {
db, err := leveldb.OpenFile(dir, levelDBOptions)
func newInMemoryStore(dir string, flushInterval time.Duration, s3sess *s3.Session) *inMemoryStore {
hn, err := os.Hostname()
if err != nil {
return nil, err
hn = rand.String(8)
}
return &levelDBStore{
db: db,
inbox: make(chan func(), 16),
clock: defaultClock{},
}, nil
}
func newMemoryLevelDBStore() (*levelDBStore, error) {
db, err := leveldb.Open(storage.NewMemStorage(), nil)
if err != nil {
return nil, err
s := &inMemoryStore{
m: xsync.NewMapOf[protocol.DeviceID, DatabaseRecord](),
dir: dir,
flushInterval: flushInterval,
s3: s3sess,
objKey: hn + ".db",
clock: defaultClock{},
}
return &levelDBStore{
db: db,
inbox: make(chan func(), 16),
clock: defaultClock{},
}, nil
}
func (s *levelDBStore) put(key string, rec DatabaseRecord) error {
t0 := time.Now()
defer func() {
databaseOperationSeconds.WithLabelValues(dbOpPut).Observe(time.Since(t0).Seconds())
}()
rc := make(chan error)
s.inbox <- func() {
size := rec.Size()
if len(s.marshalBuf) < size {
s.marshalBuf = make([]byte, size)
nr, err := s.read()
if os.IsNotExist(err) && s3sess != nil {
// Try to read from AWS
latestKey, cerr := s3sess.LatestKey()
if cerr != nil {
log.Println("Error reading database from S3:", err)
return s
}
n, _ := rec.MarshalTo(s.marshalBuf)
rc <- s.db.Put([]byte(key), s.marshalBuf[:n], nil)
fd, cerr := os.Create(path.Join(s.dir, "records.db"))
if cerr != nil {
log.Println("Error creating database file:", err)
return s
}
if cerr := s3sess.Download(fd, latestKey); cerr != nil {
log.Printf("Error reading database from S3: %v", err)
}
_ = fd.Close()
nr, err = s.read()
}
err := <-rc
if err != nil {
databaseOperations.WithLabelValues(dbOpPut, dbResError).Inc()
} else {
databaseOperations.WithLabelValues(dbOpPut, dbResSuccess).Inc()
log.Println("Error reading database:", err)
}
return err
log.Printf("Read %d records from database", nr)
s.expireAndCalculateStatistics()
return s
}
func (s *levelDBStore) merge(key string, addrs []DatabaseAddress, seen int64) error {
func (s *inMemoryStore) put(key *protocol.DeviceID, rec DatabaseRecord) error {
t0 := time.Now()
s.m.Store(*key, rec)
databaseOperations.WithLabelValues(dbOpPut, dbResSuccess).Inc()
databaseOperationSeconds.WithLabelValues(dbOpPut).Observe(time.Since(t0).Seconds())
return nil
}
func (s *inMemoryStore) merge(key *protocol.DeviceID, addrs []DatabaseAddress, seen int64) error {
t0 := time.Now()
defer func() {
databaseOperationSeconds.WithLabelValues(dbOpMerge).Observe(time.Since(t0).Seconds())
}()
rc := make(chan error)
newRec := DatabaseRecord{
Addresses: addrs,
Seen: seen,
}
s.inbox <- func() {
// grab the existing record
oldRec, err := s.get(key)
if err != nil {
// "not found" is not an error from get, so this is serious
// stuff only
rc <- err
return
}
newRec = merge(newRec, oldRec)
oldRec, _ := s.m.Load(*key)
newRec = merge(oldRec, newRec)
s.m.Store(*key, newRec)
// We replicate s.put() functionality here ourselves instead of
// calling it because we want to serialize our get above together
// with the put in the same function.
size := newRec.Size()
if len(s.marshalBuf) < size {
s.marshalBuf = make([]byte, size)
}
n, _ := newRec.MarshalTo(s.marshalBuf)
rc <- s.db.Put([]byte(key), s.marshalBuf[:n], nil)
}
databaseOperations.WithLabelValues(dbOpMerge, dbResSuccess).Inc()
databaseOperationSeconds.WithLabelValues(dbOpMerge).Observe(time.Since(t0).Seconds())
err := <-rc
if err != nil {
databaseOperations.WithLabelValues(dbOpMerge, dbResError).Inc()
} else {
databaseOperations.WithLabelValues(dbOpMerge, dbResSuccess).Inc()
}
return err
return nil
}
func (s *levelDBStore) get(key string) (DatabaseRecord, error) {
func (s *inMemoryStore) get(key *protocol.DeviceID) (DatabaseRecord, error) {
t0 := time.Now()
defer func() {
databaseOperationSeconds.WithLabelValues(dbOpGet).Observe(time.Since(t0).Seconds())
}()
keyBs := []byte(key)
val, err := s.db.Get(keyBs, nil)
if err == leveldb.ErrNotFound {
rec, ok := s.m.Load(*key)
if !ok {
databaseOperations.WithLabelValues(dbOpGet, dbResNotFound).Inc()
return DatabaseRecord{}, nil
}
if err != nil {
databaseOperations.WithLabelValues(dbOpGet, dbResError).Inc()
return DatabaseRecord{}, err
}
var rec DatabaseRecord
if err := rec.Unmarshal(val); err != nil {
databaseOperations.WithLabelValues(dbOpGet, dbResUnmarshalError).Inc()
return DatabaseRecord{}, nil
}
rec.Addresses = expire(rec.Addresses, s.clock.Now().UnixNano())
rec.Addresses = expire(rec.Addresses, s.clock.Now())
databaseOperations.WithLabelValues(dbOpGet, dbResSuccess).Inc()
return rec, nil
}
func (s *levelDBStore) Serve(ctx context.Context) error {
t := time.NewTimer(0)
defer t.Stop()
defer s.db.Close()
func (s *inMemoryStore) Serve(ctx context.Context) error {
if s.flushInterval <= 0 {
<-ctx.Done()
return nil
}
// Start the statistics serve routine. It will exit with us when
// statisticsTrigger is closed.
statisticsTrigger := make(chan struct{})
statisticsDone := make(chan struct{})
go s.statisticsServe(statisticsTrigger, statisticsDone)
t := time.NewTimer(s.flushInterval)
defer t.Stop()
loop:
for {
select {
case fn := <-s.inbox:
// Run function in serialized order.
fn()
case <-t.C:
// Trigger the statistics routine to do its thing in the
// background.
statisticsTrigger <- struct{}{}
case <-statisticsDone:
// The statistics routine is done with one iteratation, schedule
// the next.
t.Reset(databaseStatisticsInterval)
log.Println("Calculating statistics")
s.expireAndCalculateStatistics()
log.Println("Flushing database")
if err := s.write(); err != nil {
log.Println("Error writing database:", err)
}
log.Println("Finished flushing database")
t.Reset(s.flushInterval)
case <-ctx.Done():
// We're done.
close(statisticsTrigger)
break loop
}
}
// Also wait for statisticsServe to return
<-statisticsDone
return s.write()
}
func (s *inMemoryStore) expireAndCalculateStatistics() {
now := s.clock.Now()
cutoff24h := now.Add(-24 * time.Hour).UnixNano()
cutoff1w := now.Add(-7 * 24 * time.Hour).UnixNano()
current, currentIPv4, currentIPv6, currentIPv6GUA, last24h, last1w := 0, 0, 0, 0, 0, 0
n := 0
s.m.Range(func(key protocol.DeviceID, rec DatabaseRecord) bool {
if n%1000 == 0 {
runtime.Gosched()
}
n++
addresses := expire(rec.Addresses, now)
if len(addresses) == 0 {
rec.Addresses = nil
s.m.Store(key, rec)
} else if len(addresses) != len(rec.Addresses) {
rec.Addresses = addresses
s.m.Store(key, rec)
}
switch {
case len(rec.Addresses) > 0:
current++
seenIPv4, seenIPv6, seenIPv6GUA := false, false, false
for _, addr := range rec.Addresses {
// We do fast and loose matching on strings here instead of
// parsing the address and the IP and doing "proper" checks,
// to keep things fast and generate less garbage.
if strings.Contains(addr.Address, "[") {
seenIPv6 = true
if strings.Contains(addr.Address, "[2") {
seenIPv6GUA = true
}
} else {
seenIPv4 = true
}
if seenIPv4 && seenIPv6 && seenIPv6GUA {
break
}
}
if seenIPv4 {
currentIPv4++
}
if seenIPv6 {
currentIPv6++
}
if seenIPv6GUA {
currentIPv6GUA++
}
case rec.Seen > cutoff24h:
last24h++
case rec.Seen > cutoff1w:
last1w++
default:
// drop the record if it's older than a week
s.m.Delete(key)
}
return true
})
databaseKeys.WithLabelValues("current").Set(float64(current))
databaseKeys.WithLabelValues("currentIPv4").Set(float64(currentIPv4))
databaseKeys.WithLabelValues("currentIPv6").Set(float64(currentIPv6))
databaseKeys.WithLabelValues("currentIPv6GUA").Set(float64(currentIPv6GUA))
databaseKeys.WithLabelValues("last24h").Set(float64(last24h))
databaseKeys.WithLabelValues("last1w").Set(float64(last1w))
databaseStatisticsSeconds.Set(time.Since(now).Seconds())
}
func (s *inMemoryStore) write() (err error) {
t0 := time.Now()
defer func() {
if err == nil {
databaseWriteSeconds.Set(time.Since(t0).Seconds())
databaseLastWritten.Set(float64(t0.Unix()))
}
}()
dbf := path.Join(s.dir, "records.db")
fd, err := os.Create(dbf + ".tmp")
if err != nil {
return err
}
bw := bufio.NewWriter(fd)
var buf []byte
var rangeErr error
now := s.clock.Now()
cutoff1w := now.Add(-7 * 24 * time.Hour).UnixNano()
n := 0
s.m.Range(func(key protocol.DeviceID, value DatabaseRecord) bool {
if n%1000 == 0 {
runtime.Gosched()
}
n++
if value.Seen < cutoff1w {
// drop the record if it's older than a week
return true
}
rec := ReplicationRecord{
Key: key[:],
Addresses: value.Addresses,
Seen: value.Seen,
}
s := rec.Size()
if s+4 > len(buf) {
buf = make([]byte, s+4)
}
n, err := rec.MarshalTo(buf[4:])
if err != nil {
rangeErr = err
return false
}
binary.BigEndian.PutUint32(buf, uint32(n))
if _, err := bw.Write(buf[:n+4]); err != nil {
rangeErr = err
return false
}
return true
})
if rangeErr != nil {
_ = fd.Close()
return rangeErr
}
if err := bw.Flush(); err != nil {
_ = fd.Close
return err
}
if err := fd.Close(); err != nil {
return err
}
if err := os.Rename(dbf+".tmp", dbf); err != nil {
return err
}
// Upload to S3
if s.s3 != nil {
fd, err = os.Open(dbf)
if err != nil {
log.Printf("Error uploading database to S3: %v", err)
return nil
}
defer fd.Close()
if err := s.s3.Upload(fd, s.objKey); err != nil {
log.Printf("Error uploading database to S3: %v", err)
}
log.Println("Finished uploading database")
}
return nil
}
func (s *levelDBStore) statisticsServe(trigger <-chan struct{}, done chan<- struct{}) {
defer close(done)
func (s *inMemoryStore) read() (int, error) {
fd, err := os.Open(path.Join(s.dir, "records.db"))
if err != nil {
return 0, err
}
defer fd.Close()
for range trigger {
t0 := time.Now()
nowNanos := t0.UnixNano()
cutoff24h := t0.Add(-24 * time.Hour).UnixNano()
cutoff1w := t0.Add(-7 * 24 * time.Hour).UnixNano()
cutoff2Mon := t0.Add(-60 * 24 * time.Hour).UnixNano()
current, currentIPv4, currentIPv6, last24h, last1w, inactive, errors := 0, 0, 0, 0, 0, 0, 0
iter := s.db.NewIterator(&util.Range{}, nil)
for iter.Next() {
// Attempt to unmarshal the record and count the
// failure if there's something wrong with it.
var rec DatabaseRecord
if err := rec.Unmarshal(iter.Value()); err != nil {
errors++
continue
}
// If there are addresses that have not expired it's a current
// record, otherwise account it based on when it was last seen
// (last 24 hours or last week) or finally as inactice.
addrs := expire(rec.Addresses, nowNanos)
switch {
case len(addrs) > 0:
current++
seenIPv4, seenIPv6 := false, false
for _, addr := range addrs {
uri, err := url.Parse(addr.Address)
if err != nil {
continue
}
host, _, err := net.SplitHostPort(uri.Host)
if err != nil {
continue
}
if ip := net.ParseIP(host); ip != nil && ip.To4() != nil {
seenIPv4 = true
} else if ip != nil {
seenIPv6 = true
}
if seenIPv4 && seenIPv6 {
break
}
}
if seenIPv4 {
currentIPv4++
}
if seenIPv6 {
currentIPv6++
}
case rec.Seen > cutoff24h:
last24h++
case rec.Seen > cutoff1w:
last1w++
case rec.Seen > cutoff2Mon:
inactive++
case rec.Missed < cutoff2Mon:
// It hasn't been seen lately and we haven't recorded
// someone asking for this device in a long time either;
// delete the record.
if err := s.db.Delete(iter.Key(), nil); err != nil {
databaseOperations.WithLabelValues(dbOpDelete, dbResError).Inc()
} else {
databaseOperations.WithLabelValues(dbOpDelete, dbResSuccess).Inc()
}
default:
inactive++
br := bufio.NewReader(fd)
var buf []byte
nr := 0
for {
var n uint32
if err := binary.Read(br, binary.BigEndian, &n); err != nil {
if errors.Is(err, io.EOF) {
break
}
return nr, err
}
if int(n) > len(buf) {
buf = make([]byte, n)
}
if _, err := io.ReadFull(br, buf[:n]); err != nil {
return nr, err
}
rec := ReplicationRecord{}
if err := rec.Unmarshal(buf[:n]); err != nil {
return nr, err
}
key, err := protocol.DeviceIDFromBytes(rec.Key)
if err != nil {
key, err = protocol.DeviceIDFromString(string(rec.Key))
}
if err != nil {
log.Println("Bad device ID:", err)
continue
}
iter.Release()
databaseKeys.WithLabelValues("current").Set(float64(current))
databaseKeys.WithLabelValues("currentIPv4").Set(float64(currentIPv4))
databaseKeys.WithLabelValues("currentIPv6").Set(float64(currentIPv6))
databaseKeys.WithLabelValues("last24h").Set(float64(last24h))
databaseKeys.WithLabelValues("last1w").Set(float64(last1w))
databaseKeys.WithLabelValues("inactive").Set(float64(inactive))
databaseKeys.WithLabelValues("error").Set(float64(errors))
databaseStatisticsSeconds.Set(time.Since(t0).Seconds())
// Signal that we are done and can be scheduled again.
done <- struct{}{}
slices.SortFunc(rec.Addresses, DatabaseAddress.Cmp)
rec.Addresses = slices.CompactFunc(rec.Addresses, DatabaseAddress.Equal)
s.m.Store(key, DatabaseRecord{
Addresses: expire(rec.Addresses, s.clock.Now()),
Seen: rec.Seen,
})
nr++
}
return nr, nil
}
// merge returns the merged result of the two database records a and b. The
// result is the union of the two address sets, with the newer expiry time
// chosen for any duplicates.
// chosen for any duplicates. The address list in a is overwritten and
// reused for the result.
func merge(a, b DatabaseRecord) DatabaseRecord {
// Both lists must be sorted for this to work.
if !sort.IsSorted(databaseAddressOrder(a.Addresses)) {
log.Println("Warning: bug: addresses not correctly sorted in merge")
a.Addresses = sortedAddressCopy(a.Addresses)
}
if !sort.IsSorted(databaseAddressOrder(b.Addresses)) {
// no warning because this is the side we read from disk and it may
// legitimately predate correct sorting.
b.Addresses = sortedAddressCopy(b.Addresses)
}
res := DatabaseRecord{
Addresses: make([]DatabaseAddress, 0, len(a.Addresses)+len(b.Addresses)),
Seen: a.Seen,
}
if b.Seen > a.Seen {
res.Seen = b.Seen
}
a.Seen = max(a.Seen, b.Seen)
aIdx := 0
bIdx := 0
aAddrs := a.Addresses
bAddrs := b.Addresses
loop:
for {
switch {
case aIdx == len(aAddrs) && bIdx == len(bAddrs):
// both lists are exhausted, we are done
break loop
case aIdx == len(aAddrs):
// a is exhausted, pick from b and continue
res.Addresses = append(res.Addresses, bAddrs[bIdx])
bIdx++
continue
case bIdx == len(bAddrs):
// b is exhausted, pick from a and continue
res.Addresses = append(res.Addresses, aAddrs[aIdx])
aIdx++
continue
}
// We have values left on both sides.
aVal := aAddrs[aIdx]
bVal := bAddrs[bIdx]
switch {
case aVal.Address == bVal.Address:
// update for same address, pick newer
if aVal.Expires > bVal.Expires {
res.Addresses = append(res.Addresses, aVal)
} else {
res.Addresses = append(res.Addresses, bVal)
}
for aIdx < len(a.Addresses) && bIdx < len(b.Addresses) {
switch cmp.Compare(a.Addresses[aIdx].Address, b.Addresses[bIdx].Address) {
case 0:
// a == b, choose the newer expiry time
a.Addresses[aIdx].Expires = max(a.Addresses[aIdx].Expires, b.Addresses[bIdx].Expires)
aIdx++
bIdx++
case aVal.Address < bVal.Address:
// a is smallest, pick it and continue
res.Addresses = append(res.Addresses, aVal)
case -1:
// a < b, keep a and move on
aIdx++
default:
// b is smallest, pick it and continue
res.Addresses = append(res.Addresses, bVal)
case 1:
// a > b, insert b before a
a.Addresses = append(a.Addresses[:aIdx], append([]DatabaseAddress{b.Addresses[bIdx]}, a.Addresses[aIdx:]...)...)
bIdx++
}
}
return res
if bIdx < len(b.Addresses) {
a.Addresses = append(a.Addresses, b.Addresses[bIdx:]...)
}
return a
}
// expire returns the list of addresses after removing expired entries.
// Expiration happen in place, so the slice given as the parameter is
// destroyed. Internal order is not preserved.
func expire(addrs []DatabaseAddress, now int64) []DatabaseAddress {
i := 0
for i < len(addrs) {
if addrs[i].Expires < now {
addrs = sliceutil.RemoveAndZero(addrs, i)
// destroyed. Internal order is preserved.
func expire(addrs []DatabaseAddress, now time.Time) []DatabaseAddress {
cutoff := now.UnixNano()
naddrs := addrs[:0]
for i := range addrs {
if i > 0 && addrs[i].Address == addrs[i-1].Address {
// Skip duplicates
continue
}
i++
if addrs[i].Expires >= cutoff {
naddrs = append(naddrs, addrs[i])
}
}
return addrs
if len(naddrs) == 0 {
return nil
}
return naddrs
}
func sortedAddressCopy(addrs []DatabaseAddress) []DatabaseAddress {
sorted := make([]DatabaseAddress, len(addrs))
copy(sorted, addrs)
sort.Sort(databaseAddressOrder(sorted))
return sorted
func (d DatabaseAddress) Cmp(other DatabaseAddress) (n int) {
if c := cmp.Compare(d.Address, other.Address); c != 0 {
return c
}
return cmp.Compare(d.Expires, other.Expires)
}
type databaseAddressOrder []DatabaseAddress
func (s databaseAddressOrder) Less(a, b int) bool {
return s[a].Address < s[b].Address
}
func (s databaseAddressOrder) Swap(a, b int) {
s[a], s[b] = s[b], s[a]
}
func (s databaseAddressOrder) Len() int {
return len(s)
func (d DatabaseAddress) Equal(other DatabaseAddress) bool {
return d.Address == other.Address
}

View File

@@ -25,9 +25,7 @@ const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package
type DatabaseRecord struct {
Addresses []DatabaseAddress `protobuf:"bytes,1,rep,name=addresses,proto3" json:"addresses"`
Misses int32 `protobuf:"varint,2,opt,name=misses,proto3" json:"misses,omitempty"`
Seen int64 `protobuf:"varint,3,opt,name=seen,proto3" json:"seen,omitempty"`
Missed int64 `protobuf:"varint,4,opt,name=missed,proto3" json:"missed,omitempty"`
}
func (m *DatabaseRecord) Reset() { *m = DatabaseRecord{} }
@@ -64,7 +62,7 @@ func (m *DatabaseRecord) XXX_DiscardUnknown() {
var xxx_messageInfo_DatabaseRecord proto.InternalMessageInfo
type ReplicationRecord struct {
Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
Addresses []DatabaseAddress `protobuf:"bytes,2,rep,name=addresses,proto3" json:"addresses"`
Seen int64 `protobuf:"varint,3,opt,name=seen,proto3" json:"seen,omitempty"`
}
@@ -149,24 +147,23 @@ func init() {
func init() { proto.RegisterFile("database.proto", fileDescriptor_b90fe3356ea5df07) }
var fileDescriptor_b90fe3356ea5df07 = []byte{
// 270 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x90, 0x41, 0x4a, 0xc4, 0x30,
0x18, 0x85, 0x9b, 0x49, 0x1d, 0x99, 0x08, 0xa3, 0x06, 0x94, 0x20, 0x12, 0x4b, 0xdd, 0x74, 0xd5,
0x01, 0x5d, 0xb9, 0x74, 0xd0, 0x0b, 0xe4, 0x06, 0xe9, 0xe4, 0x77, 0x08, 0x3a, 0x4d, 0x49, 0x2a,
0xe8, 0x29, 0xf4, 0x58, 0x5d, 0xce, 0xd2, 0x95, 0x68, 0x7b, 0x11, 0x69, 0x26, 0x55, 0x14, 0x37,
0xb3, 0x7b, 0xdf, 0xff, 0xbf, 0x97, 0xbc, 0x84, 0x4c, 0x95, 0xac, 0x65, 0x21, 0x1d, 0xe4, 0x95,
0x35, 0xb5, 0xa1, 0xf1, 0x4a, 0xea, 0xf2, 0xe4, 0xdc, 0x42, 0x65, 0xdc, 0xcc, 0x8f, 0x8a, 0xc7,
0xbb, 0xd9, 0xd2, 0x2c, 0x8d, 0x07, 0xaf, 0x36, 0xd6, 0xf4, 0x05, 0x91, 0xe9, 0x4d, 0x48, 0x0b,
0x58, 0x18, 0xab, 0xe8, 0x15, 0x99, 0x48, 0xa5, 0x2c, 0x38, 0x07, 0x8e, 0xa1, 0x04, 0x67, 0x7b,
0x17, 0x47, 0x79, 0x7f, 0x62, 0x3e, 0x18, 0xaf, 0x37, 0xeb, 0x79, 0xdc, 0xbc, 0x9f, 0x45, 0xe2,
0xc7, 0x4d, 0x8f, 0xc9, 0x78, 0xa5, 0x7d, 0x6e, 0x94, 0xa0, 0x6c, 0x47, 0x04, 0xa2, 0x94, 0xc4,
0x0e, 0xa0, 0x64, 0x38, 0x41, 0x19, 0x16, 0x5e, 0x7f, 0x7b, 0x15, 0x8b, 0xfd, 0x34, 0x50, 0x5a,
0x93, 0x43, 0x01, 0xd5, 0x83, 0x5e, 0xc8, 0x5a, 0x9b, 0x32, 0x74, 0x3a, 0x20, 0xf8, 0x1e, 0x9e,
0x19, 0x4a, 0x50, 0x36, 0x11, 0xbd, 0xfc, 0xdd, 0x72, 0xb4, 0x55, 0xcb, 0x7f, 0xda, 0xa4, 0xb7,
0x64, 0xff, 0x4f, 0x8e, 0x32, 0xb2, 0x1b, 0x32, 0xe1, 0xde, 0x01, 0xfb, 0x0d, 0x3c, 0x55, 0xda,
0x86, 0x77, 0x62, 0x31, 0xe0, 0xfc, 0xb4, 0xf9, 0xe4, 0x51, 0xd3, 0x72, 0xb4, 0x6e, 0x39, 0xfa,
0x68, 0x39, 0x7a, 0xed, 0x78, 0xb4, 0xee, 0x78, 0xf4, 0xd6, 0xf1, 0xa8, 0x18, 0xfb, 0x3f, 0xbf,
0xfc, 0x0a, 0x00, 0x00, 0xff, 0xff, 0x7a, 0xa2, 0xf6, 0x1e, 0xb0, 0x01, 0x00, 0x00,
// 243 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x4b, 0x49, 0x2c, 0x49,
0x4c, 0x4a, 0x2c, 0x4e, 0xd5, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0xc9, 0x4d, 0xcc, 0xcc,
0x93, 0x52, 0x2e, 0x4a, 0x2d, 0xc8, 0x2f, 0xd6, 0x07, 0x0b, 0x25, 0x95, 0xa6, 0xe9, 0xa7, 0xe7,
0xa7, 0xe7, 0x83, 0x39, 0x60, 0x16, 0x44, 0xa9, 0x52, 0x3c, 0x17, 0x9f, 0x0b, 0x54, 0x73, 0x50,
0x6a, 0x72, 0x7e, 0x51, 0x8a, 0x90, 0x25, 0x17, 0x67, 0x62, 0x4a, 0x4a, 0x51, 0x6a, 0x71, 0x71,
0x6a, 0xb1, 0x04, 0xa3, 0x02, 0xb3, 0x06, 0xb7, 0x91, 0xa8, 0x1e, 0xc8, 0x40, 0x3d, 0x98, 0x42,
0x47, 0x88, 0xb4, 0x13, 0xcb, 0x89, 0x7b, 0xf2, 0x0c, 0x41, 0x08, 0xd5, 0x42, 0x42, 0x5c, 0x2c,
0xc5, 0xa9, 0xa9, 0x79, 0x12, 0xcc, 0x0a, 0x8c, 0x1a, 0xcc, 0x41, 0x60, 0xb6, 0x52, 0x09, 0x97,
0x60, 0x50, 0x6a, 0x41, 0x4e, 0x66, 0x72, 0x62, 0x49, 0x66, 0x7e, 0x1e, 0xd4, 0x0e, 0x01, 0x2e,
0xe6, 0xec, 0xd4, 0x4a, 0x09, 0x46, 0x05, 0x46, 0x0d, 0x9e, 0x20, 0x10, 0x13, 0xd5, 0x56, 0x26,
0x8a, 0x6d, 0x75, 0xe5, 0xe2, 0x47, 0xd3, 0x27, 0x24, 0xc1, 0xc5, 0x0e, 0xd5, 0x03, 0xb6, 0x97,
0x33, 0x08, 0xc6, 0x05, 0xc9, 0xa4, 0x56, 0x14, 0x64, 0x16, 0x81, 0x6d, 0x06, 0x99, 0x01, 0xe3,
0x3a, 0xc9, 0x9c, 0x78, 0x28, 0xc7, 0x70, 0xe2, 0x91, 0x1c, 0xe3, 0x85, 0x47, 0x72, 0x8c, 0x0f,
0x1e, 0xc9, 0x31, 0x4e, 0x78, 0x2c, 0xc7, 0x70, 0xe1, 0xb1, 0x1c, 0xc3, 0x8d, 0xc7, 0x72, 0x0c,
0x49, 0x6c, 0xe0, 0x20, 0x34, 0x06, 0x04, 0x00, 0x00, 0xff, 0xff, 0xc6, 0x0b, 0x9b, 0x77, 0x7f,
0x01, 0x00, 0x00,
}
func (m *DatabaseRecord) Marshal() (dAtA []byte, err error) {
@@ -189,21 +186,11 @@ func (m *DatabaseRecord) MarshalToSizedBuffer(dAtA []byte) (int, error) {
_ = i
var l int
_ = l
if m.Missed != 0 {
i = encodeVarintDatabase(dAtA, i, uint64(m.Missed))
i--
dAtA[i] = 0x20
}
if m.Seen != 0 {
i = encodeVarintDatabase(dAtA, i, uint64(m.Seen))
i--
dAtA[i] = 0x18
}
if m.Misses != 0 {
i = encodeVarintDatabase(dAtA, i, uint64(m.Misses))
i--
dAtA[i] = 0x10
}
if len(m.Addresses) > 0 {
for iNdEx := len(m.Addresses) - 1; iNdEx >= 0; iNdEx-- {
{
@@ -328,15 +315,9 @@ func (m *DatabaseRecord) Size() (n int) {
n += 1 + l + sovDatabase(uint64(l))
}
}
if m.Misses != 0 {
n += 1 + sovDatabase(uint64(m.Misses))
}
if m.Seen != 0 {
n += 1 + sovDatabase(uint64(m.Seen))
}
if m.Missed != 0 {
n += 1 + sovDatabase(uint64(m.Missed))
}
return n
}
@@ -447,25 +428,6 @@ func (m *DatabaseRecord) Unmarshal(dAtA []byte) error {
return err
}
iNdEx = postIndex
case 2:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field Misses", wireType)
}
m.Misses = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowDatabase
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
m.Misses |= int32(b&0x7F) << shift
if b < 0x80 {
break
}
}
case 3:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field Seen", wireType)
@@ -485,25 +447,6 @@ func (m *DatabaseRecord) Unmarshal(dAtA []byte) error {
break
}
}
case 4:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field Missed", wireType)
}
m.Missed = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowDatabase
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
m.Missed |= int64(b&0x7F) << shift
if b < 0x80 {
break
}
}
default:
iNdEx = preIndex
skippy, err := skipDatabase(dAtA[iNdEx:])
@@ -558,7 +501,7 @@ func (m *ReplicationRecord) Unmarshal(dAtA []byte) error {
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Key", wireType)
}
var stringLen uint64
var byteLen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowDatabase
@@ -568,23 +511,25 @@ func (m *ReplicationRecord) Unmarshal(dAtA []byte) error {
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= uint64(b&0x7F) << shift
byteLen |= int(b&0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
if byteLen < 0 {
return ErrInvalidLengthDatabase
}
postIndex := iNdEx + intStringLen
postIndex := iNdEx + byteLen
if postIndex < 0 {
return ErrInvalidLengthDatabase
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.Key = string(dAtA[iNdEx:postIndex])
m.Key = append(m.Key[:0], dAtA[iNdEx:postIndex]...)
if m.Key == nil {
m.Key = []byte{}
}
iNdEx = postIndex
case 2:
if wireType != 2 {

View File

@@ -17,15 +17,11 @@ option (gogoproto.goproto_sizecache_all) = false;
message DatabaseRecord {
repeated DatabaseAddress addresses = 1 [(gogoproto.nullable) = false];
int32 misses = 2; // Number of lookups* without hits
int64 seen = 3; // Unix nanos, last device announce
int64 missed = 4; // Unix nanos, last* failed lookup
}
// *) Not every lookup results in a write, so may not be completely accurate
message ReplicationRecord {
string key = 1;
bytes key = 1; // raw 32 byte device ID
repeated DatabaseAddress addresses = 2 [(gogoproto.nullable) = false];
int64 seen = 3; // Unix nanos, last device announce
}

View File

@@ -11,29 +11,25 @@ import (
"fmt"
"testing"
"time"
"github.com/syncthing/syncthing/lib/protocol"
)
func TestDatabaseGetSet(t *testing.T) {
db, err := newMemoryLevelDBStore()
if err != nil {
t.Fatal(err)
}
db := newInMemoryStore(t.TempDir(), 0, nil)
ctx, cancel := context.WithCancel(context.Background())
go db.Serve(ctx)
defer cancel()
// Check missing record
rec, err := db.get("abcd")
rec, err := db.get(&protocol.EmptyDeviceID)
if err != nil {
t.Error("not found should not be an error")
}
if len(rec.Addresses) != 0 {
t.Error("addresses should be empty")
}
if rec.Misses != 0 {
t.Error("missing should be zero")
}
// Set up a clock
@@ -46,13 +42,13 @@ func TestDatabaseGetSet(t *testing.T) {
rec.Addresses = []DatabaseAddress{
{Address: "tcp://1.2.3.4:5", Expires: tc.Now().Add(time.Minute).UnixNano()},
}
if err := db.put("abcd", rec); err != nil {
if err := db.put(&protocol.EmptyDeviceID, rec); err != nil {
t.Fatal(err)
}
// Verify it
rec, err = db.get("abcd")
rec, err = db.get(&protocol.EmptyDeviceID)
if err != nil {
t.Fatal(err)
}
@@ -72,13 +68,13 @@ func TestDatabaseGetSet(t *testing.T) {
addrs := []DatabaseAddress{
{Address: "tcp://6.7.8.9:0", Expires: tc.Now().Add(time.Minute).UnixNano()},
}
if err := db.merge("abcd", addrs, tc.Now().UnixNano()); err != nil {
if err := db.merge(&protocol.EmptyDeviceID, addrs, tc.Now().UnixNano()); err != nil {
t.Fatal(err)
}
// Verify it
rec, err = db.get("abcd")
rec, err = db.get(&protocol.EmptyDeviceID)
if err != nil {
t.Fatal(err)
}
@@ -101,7 +97,7 @@ func TestDatabaseGetSet(t *testing.T) {
// Verify it
rec, err = db.get("abcd")
rec, err = db.get(&protocol.EmptyDeviceID)
if err != nil {
t.Fatal(err)
}
@@ -114,40 +110,18 @@ func TestDatabaseGetSet(t *testing.T) {
t.Error("incorrect address")
}
// Put a record with misses
rec = DatabaseRecord{Misses: 42, Missed: tc.Now().UnixNano()}
if err := db.put("efgh", rec); err != nil {
t.Fatal(err)
}
// Verify it
rec, err = db.get("efgh")
if err != nil {
t.Fatal(err)
}
if len(rec.Addresses) != 0 {
t.Log(rec.Addresses)
t.Fatal("should have no addresses")
}
if rec.Misses != 42 {
t.Log(rec.Misses)
t.Error("incorrect misses")
}
// Set an address
addrs = []DatabaseAddress{
{Address: "tcp://6.7.8.9:0", Expires: tc.Now().Add(time.Minute).UnixNano()},
}
if err := db.merge("efgh", addrs, tc.Now().UnixNano()); err != nil {
if err := db.merge(&protocol.GlobalDeviceID, addrs, tc.Now().UnixNano()); err != nil {
t.Fatal(err)
}
// Verify it
rec, err = db.get("efgh")
rec, err = db.get(&protocol.GlobalDeviceID)
if err != nil {
t.Fatal(err)
}
@@ -155,10 +129,6 @@ func TestDatabaseGetSet(t *testing.T) {
t.Log(rec.Addresses)
t.Fatal("should have one address")
}
if rec.Misses != 0 {
t.Log(rec.Misses)
t.Error("should have no misses")
}
}
func TestFilter(t *testing.T) {
@@ -190,13 +160,95 @@ func TestFilter(t *testing.T) {
}
for _, tc := range cases {
res := expire(tc.a, 10)
res := expire(tc.a, time.Unix(0, 10))
if fmt.Sprint(res) != fmt.Sprint(tc.b) {
t.Errorf("Incorrect result %v, expected %v", res, tc.b)
}
}
}
func TestMerge(t *testing.T) {
cases := []struct {
a, b, res []DatabaseAddress
}{
{nil, nil, nil},
{
nil,
[]DatabaseAddress{{Address: "a", Expires: 10}},
[]DatabaseAddress{{Address: "a", Expires: 10}},
},
{
nil,
[]DatabaseAddress{{Address: "a", Expires: 10}, {Address: "b", Expires: 10}, {Address: "c", Expires: 10}},
[]DatabaseAddress{{Address: "a", Expires: 10}, {Address: "b", Expires: 10}, {Address: "c", Expires: 10}},
},
{
[]DatabaseAddress{{Address: "a", Expires: 10}},
[]DatabaseAddress{{Address: "a", Expires: 15}},
[]DatabaseAddress{{Address: "a", Expires: 15}},
},
{
[]DatabaseAddress{{Address: "a", Expires: 10}},
[]DatabaseAddress{{Address: "b", Expires: 15}},
[]DatabaseAddress{{Address: "a", Expires: 10}, {Address: "b", Expires: 15}},
},
{
[]DatabaseAddress{{Address: "a", Expires: 10}, {Address: "b", Expires: 15}},
[]DatabaseAddress{{Address: "a", Expires: 15}, {Address: "b", Expires: 15}},
[]DatabaseAddress{{Address: "a", Expires: 15}, {Address: "b", Expires: 15}},
},
{
[]DatabaseAddress{{Address: "a", Expires: 10}, {Address: "b", Expires: 15}},
[]DatabaseAddress{{Address: "b", Expires: 15}, {Address: "c", Expires: 20}},
[]DatabaseAddress{{Address: "a", Expires: 10}, {Address: "b", Expires: 15}, {Address: "c", Expires: 20}},
},
{
[]DatabaseAddress{{Address: "a", Expires: 10}, {Address: "b", Expires: 15}},
[]DatabaseAddress{{Address: "b", Expires: 5}, {Address: "c", Expires: 20}},
[]DatabaseAddress{{Address: "a", Expires: 10}, {Address: "b", Expires: 15}, {Address: "c", Expires: 20}},
},
{
[]DatabaseAddress{{Address: "y", Expires: 10}, {Address: "z", Expires: 10}},
[]DatabaseAddress{{Address: "a", Expires: 5}, {Address: "b", Expires: 15}},
[]DatabaseAddress{{Address: "a", Expires: 5}, {Address: "b", Expires: 15}, {Address: "y", Expires: 10}, {Address: "z", Expires: 10}},
},
{
[]DatabaseAddress{{Address: "a", Expires: 10}, {Address: "b", Expires: 15}, {Address: "d", Expires: 10}},
[]DatabaseAddress{{Address: "b", Expires: 5}, {Address: "c", Expires: 20}},
[]DatabaseAddress{{Address: "a", Expires: 10}, {Address: "b", Expires: 15}, {Address: "c", Expires: 20}, {Address: "d", Expires: 10}},
},
}
for _, tc := range cases {
rec := merge(DatabaseRecord{Addresses: tc.a}, DatabaseRecord{Addresses: tc.b})
if fmt.Sprint(rec.Addresses) != fmt.Sprint(tc.res) {
t.Errorf("Incorrect result %v, expected %v", rec.Addresses, tc.res)
}
rec = merge(DatabaseRecord{Addresses: tc.b}, DatabaseRecord{Addresses: tc.a})
if fmt.Sprint(rec.Addresses) != fmt.Sprint(tc.res) {
t.Errorf("Incorrect result %v, expected %v", rec.Addresses, tc.res)
}
}
}
func BenchmarkMergeEqual(b *testing.B) {
for i := 0; i < b.N; i++ {
ar := []DatabaseAddress{{Address: "a", Expires: 10}, {Address: "b", Expires: 15}}
br := []DatabaseAddress{{Address: "a", Expires: 15}, {Address: "b", Expires: 10}}
res := merge(DatabaseRecord{Addresses: ar}, DatabaseRecord{Addresses: br})
if len(res.Addresses) != 2 {
b.Fatal("wrong length")
}
if res.Addresses[0].Address != "a" || res.Addresses[1].Address != "b" {
b.Fatal("wrong address")
}
if res.Addresses[0].Expires != 15 || res.Addresses[1].Expires != 15 {
b.Fatal("wrong expiry")
}
}
b.ReportAllocs() // should be zero per operation
}
type testClock struct {
now time.Time
}

View File

@@ -9,22 +9,23 @@ package main
import (
"context"
"crypto/tls"
"flag"
"log"
"net"
"net/http"
"os"
"os/signal"
"runtime"
"strings"
"time"
_ "net/http/pprof"
"github.com/alecthomas/kong"
"github.com/prometheus/client_golang/prometheus/promhttp"
_ "github.com/syncthing/syncthing/lib/automaxprocs"
"github.com/syncthing/syncthing/lib/build"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/rand"
"github.com/syncthing/syncthing/lib/s3"
"github.com/syncthing/syncthing/lib/tlsutil"
"github.com/syndtr/goleveldb/leveldb/opt"
"github.com/thejerf/suture/v4"
)
@@ -39,17 +40,12 @@ const (
errorRetryAfterSeconds = 1500
errorRetryFuzzSeconds = 300
// Retry for not found is minSeconds + failures * incSeconds +
// random(fuzz), where failures is the number of consecutive lookups
// with no answer, up to maxSeconds. The fuzz is applied after capping
// to maxSeconds.
notFoundRetryMinSeconds = 60
notFoundRetryMaxSeconds = 3540
notFoundRetryIncSeconds = 10
notFoundRetryFuzzSeconds = 60
// How often (in requests) we serialize the missed counter to database.
notFoundMissesWriteInterval = 10
// Retry for not found is notFoundRetrySeenSeconds for records we have
// seen an announcement for (but it's not active right now) and
// notFoundRetryUnknownSeconds for records we have never seen (or not
// seen within the last week).
notFoundRetryUnknownMinSeconds = 60
notFoundRetryUnknownMaxSeconds = 3600
httpReadTimeout = 5 * time.Second
httpWriteTimeout = 5 * time.Second
@@ -59,184 +55,116 @@ const (
replicationOutboxSize = 10000
)
// These options make the database a little more optimized for writes, at
// the expense of some memory usage and risk of losing writes in a (system)
// crash.
var levelDBOptions = &opt.Options{
NoSync: true,
WriteBuffer: 32 << 20, // default 4<<20
}
var debug = false
type CLI struct {
Cert string `group:"Listen" help:"Certificate file" default:"./cert.pem" env:"DISCOVERY_CERT_FILE"`
Key string `group:"Listen" help:"Key file" default:"./key.pem" env:"DISCOVERY_KEY_FILE"`
HTTP bool `group:"Listen" help:"Listen on HTTP (behind an HTTPS proxy)" env:"DISCOVERY_HTTP"`
Compression bool `group:"Listen" help:"Enable GZIP compression of responses" env:"DISCOVERY_COMPRESSION"`
Listen string `group:"Listen" help:"Listen address" default:":8443" env:"DISCOVERY_LISTEN"`
MetricsListen string `group:"Listen" help:"Metrics listen address" env:"DISCOVERY_METRICS_LISTEN"`
DBDir string `group:"Database" help:"Database directory" default:"." env:"DISCOVERY_DB_DIR"`
DBFlushInterval time.Duration `group:"Database" help:"Interval between database flushes" default:"5m" env:"DISCOVERY_DB_FLUSH_INTERVAL"`
DBS3Endpoint string `name:"db-s3-endpoint" group:"Database (S3 backup)" hidden:"true" help:"S3 endpoint for database" env:"DISCOVERY_DB_S3_ENDPOINT"`
DBS3Region string `name:"db-s3-region" group:"Database (S3 backup)" hidden:"true" help:"S3 region for database" env:"DISCOVERY_DB_S3_REGION"`
DBS3Bucket string `name:"db-s3-bucket" group:"Database (S3 backup)" hidden:"true" help:"S3 bucket for database" env:"DISCOVERY_DB_S3_BUCKET"`
DBS3AccessKeyID string `name:"db-s3-access-key-id" group:"Database (S3 backup)" hidden:"true" help:"S3 access key ID for database" env:"DISCOVERY_DB_S3_ACCESS_KEY_ID"`
DBS3SecretKey string `name:"db-s3-secret-key" group:"Database (S3 backup)" hidden:"true" help:"S3 secret key for database" env:"DISCOVERY_DB_S3_SECRET_KEY"`
AMQPAddress string `group:"AMQP replication" hidden:"true" help:"Address to AMQP broker" env:"DISCOVERY_AMQP_ADDRESS"`
Debug bool `short:"d" help:"Print debug output" env:"DISCOVERY_DEBUG"`
Version bool `short:"v" help:"Print version and exit"`
}
func main() {
var listen string
var dir string
var metricsListen string
var replicationListen string
var replicationPeers string
var certFile string
var keyFile string
var replCertFile string
var replKeyFile string
var useHTTP bool
var largeDB bool
var amqpAddress string
missesIncrease := 1
log.SetOutput(os.Stdout)
log.SetFlags(0)
flag.StringVar(&certFile, "cert", "./cert.pem", "Certificate file")
flag.StringVar(&keyFile, "key", "./key.pem", "Key file")
flag.StringVar(&dir, "db-dir", "./discovery.db", "Database directory")
flag.BoolVar(&debug, "debug", false, "Print debug output")
flag.BoolVar(&useHTTP, "http", false, "Listen on HTTP (behind an HTTPS proxy)")
flag.StringVar(&listen, "listen", ":8443", "Listen address")
flag.StringVar(&metricsListen, "metrics-listen", "", "Metrics listen address")
flag.StringVar(&replicationPeers, "replicate", "", "Replication peers, id@address, comma separated")
flag.StringVar(&replicationListen, "replication-listen", ":19200", "Replication listen address")
flag.StringVar(&replCertFile, "replication-cert", "", "Certificate file for replication")
flag.StringVar(&replKeyFile, "replication-key", "", "Key file for replication")
flag.BoolVar(&largeDB, "large-db", false, "Use larger database settings")
flag.StringVar(&amqpAddress, "amqp-address", "", "Address to AMQP broker")
flag.IntVar(&missesIncrease, "misses-increase", 1, "How many times to increase the misses counter on each miss")
showVersion := flag.Bool("version", false, "Show version")
flag.Parse()
var cli CLI
kong.Parse(&cli)
debug = cli.Debug
log.Println(build.LongVersionFor("stdiscosrv"))
if *showVersion {
if cli.Version {
return
}
buildInfo.WithLabelValues(build.Version, runtime.Version(), build.User, build.Date.UTC().Format("2006-01-02T15:04:05Z")).Set(1)
if largeDB {
levelDBOptions.BlockCacheCapacity = 64 << 20
levelDBOptions.BlockSize = 64 << 10
levelDBOptions.CompactionTableSize = 16 << 20
levelDBOptions.CompactionTableSizeMultiplier = 2.0
levelDBOptions.WriteBuffer = 64 << 20
levelDBOptions.CompactionL0Trigger = 8
}
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if os.IsNotExist(err) {
log.Println("Failed to load keypair. Generating one, this might take a while...")
cert, err = tlsutil.NewCertificate(certFile, keyFile, "stdiscosrv", 20*365)
if err != nil {
log.Fatalln("Failed to generate X509 key pair:", err)
}
} else if err != nil {
log.Fatalln("Failed to load keypair:", err)
}
devID := protocol.NewDeviceID(cert.Certificate[0])
log.Println("Server device ID is", devID)
replCert := cert
if replCertFile != "" && replKeyFile != "" {
replCert, err = tls.LoadX509KeyPair(replCertFile, replKeyFile)
if err != nil {
log.Fatalln("Failed to load replication keypair:", err)
}
}
replDevID := protocol.NewDeviceID(replCert.Certificate[0])
log.Println("Replication device ID is", replDevID)
// Parse the replication specs, if any.
var allowedReplicationPeers []protocol.DeviceID
var replicationDestinations []string
parts := strings.Split(replicationPeers, ",")
for _, part := range parts {
if part == "" {
continue
}
fields := strings.Split(part, "@")
switch len(fields) {
case 2:
// This is an id@address specification. Grab the address for the
// destination list. Try to resolve it once to catch obvious
// syntax errors here rather than having the sender service fail
// repeatedly later.
_, err := net.ResolveTCPAddr("tcp", fields[1])
var cert tls.Certificate
if !cli.HTTP {
var err error
cert, err = tls.LoadX509KeyPair(cli.Cert, cli.Key)
if os.IsNotExist(err) {
log.Println("Failed to load keypair. Generating one, this might take a while...")
cert, err = tlsutil.NewCertificate(cli.Cert, cli.Key, "stdiscosrv", 20*365)
if err != nil {
log.Fatalln("Resolving address:", err)
log.Fatalln("Failed to generate X509 key pair:", err)
}
replicationDestinations = append(replicationDestinations, fields[1])
fallthrough // N.B.
case 1:
// The first part is always a device ID.
id, err := protocol.DeviceIDFromString(fields[0])
if err != nil {
log.Fatalln("Parsing device ID:", err)
}
if id == protocol.EmptyDeviceID {
log.Fatalf("Missing device ID for peer in %q", part)
}
allowedReplicationPeers = append(allowedReplicationPeers, id)
default:
log.Fatalln("Unrecognized replication spec:", part)
} else if err != nil {
log.Fatalln("Failed to load keypair:", err)
}
devID := protocol.NewDeviceID(cert.Certificate[0])
log.Println("Server device ID is", devID)
}
// Root of the service tree.
main := suture.New("main", suture.Spec{
PassThroughPanics: true,
Timeout: 2 * time.Minute,
})
// Start the database.
db, err := newLevelDBStore(dir)
if err != nil {
log.Fatalln("Open database:", err)
// If configured, use S3 for database backups.
var s3c *s3.Session
if cli.DBS3Endpoint != "" {
var err error
s3c, err = s3.NewSession(cli.DBS3Endpoint, cli.DBS3Region, cli.DBS3Bucket, cli.DBS3AccessKeyID, cli.DBS3SecretKey)
if err != nil {
log.Fatalf("Failed to create S3 session: %v", err)
}
}
// Start the database.
db := newInMemoryStore(cli.DBDir, cli.DBFlushInterval, s3c)
main.Add(db)
// Start any replication senders.
var repl replicationMultiplexer
for _, dst := range replicationDestinations {
rs := newReplicationSender(dst, replCert, allowedReplicationPeers)
main.Add(rs)
repl = append(repl, rs)
}
// If we have replication configured, start the replication listener.
if len(allowedReplicationPeers) > 0 {
rl := newReplicationListener(replicationListen, replCert, allowedReplicationPeers, db)
main.Add(rl)
}
// If we have an AMQP broker, start that
if amqpAddress != "" {
// If we have an AMQP broker for replication, start that
var repl replicator
if cli.AMQPAddress != "" {
clientID := rand.String(10)
kr := newAMQPReplicator(amqpAddress, clientID, db)
repl = append(repl, kr)
kr := newAMQPReplicator(cli.AMQPAddress, clientID, db)
main.Add(kr)
repl = kr
}
go func() {
for range time.NewTicker(time.Second).C {
for _, r := range repl {
r.send("<heartbeat>", nil, time.Now().UnixNano())
}
}
}()
// Start the main API server.
qs := newAPISrv(listen, cert, db, repl, useHTTP, missesIncrease)
qs := newAPISrv(cli.Listen, cert, db, repl, cli.HTTP, cli.Compression)
main.Add(qs)
// If we have a metrics port configured, start a metrics handler.
if metricsListen != "" {
if cli.MetricsListen != "" {
go func() {
mux := http.NewServeMux()
mux.Handle("/metrics", promhttp.Handler())
log.Fatal(http.ListenAndServe(metricsListen, mux))
log.Fatal(http.ListenAndServe(cli.MetricsListen, mux))
}()
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Cancel on signal
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, os.Interrupt)
go func() {
sig := <-signalChan
log.Printf("Received signal %s; shutting down", sig)
cancel()
}()
// Engage!
main.Serve(context.Background())
main.Serve(ctx)
}

View File

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

View File

@@ -96,13 +96,28 @@ var (
Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
}, []string{"operation"})
retryAfterHistogram = prometheus.NewHistogram(prometheus.HistogramOpts{
Namespace: "syncthing",
Subsystem: "discovery",
Name: "retry_after_seconds",
Help: "Retry-After header value in seconds.",
Buckets: prometheus.ExponentialBuckets(60, 2, 7), // 60, 120, 240, 480, 960, 1920, 3840
})
databaseWriteSeconds = prometheus.NewGauge(
prometheus.GaugeOpts{
Namespace: "syncthing",
Subsystem: "discovery",
Name: "database_write_seconds",
Help: "Time spent writing the database.",
})
databaseLastWritten = prometheus.NewGauge(
prometheus.GaugeOpts{
Namespace: "syncthing",
Subsystem: "discovery",
Name: "database_last_written",
Help: "Timestamp of the last successful database write.",
})
retryAfterLevel = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: "syncthing",
Subsystem: "discovery",
Name: "retry_after_seconds",
Help: "Retry-After header value in seconds.",
}, []string{"name"})
)
const (
@@ -123,5 +138,6 @@ func init() {
replicationSendsTotal, replicationRecvsTotal,
databaseKeys, databaseStatisticsSeconds,
databaseOperations, databaseOperationSeconds,
retryAfterHistogram)
databaseWriteSeconds, databaseLastWritten,
retryAfterLevel)
}

View File

@@ -1,171 +0,0 @@
// Copyright (C) 2019 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"sort"
"strings"
"time"
"github.com/alecthomas/kong"
"github.com/prometheus/client_golang/prometheus/promhttp"
_ "github.com/syncthing/syncthing/lib/automaxprocs"
"github.com/syncthing/syncthing/lib/httpcache"
"github.com/syncthing/syncthing/lib/upgrade"
)
type cli struct {
Listen string `default:":8080" help:"Listen address"`
MetricsListen string `default:":8081" help:"Listen address for metrics"`
URL string `short:"u" default:"https://api.github.com/repos/syncthing/syncthing/releases?per_page=25" help:"GitHub releases url"`
Forward []string `short:"f" help:"Forwarded pages, format: /path->https://example/com/url"`
CacheTime time.Duration `default:"15m" help:"Cache time"`
}
func main() {
var params cli
kong.Parse(&params)
if err := server(&params); err != nil {
fmt.Printf("Error: %v\n", err)
os.Exit(1)
}
}
func server(params *cli) error {
if params.MetricsListen != "" {
mux := http.NewServeMux()
mux.Handle("/metrics", promhttp.Handler())
go func() {
log.Println("Listening for metrics on", params.MetricsListen)
if err := http.ListenAndServe(params.MetricsListen, mux); err != nil {
log.Fatalf("Failed to start metrics server: %v", err)
}
}()
}
mux := http.NewServeMux()
mux.Handle("/meta.json", httpcache.SinglePath(&githubReleases{url: params.URL}, params.CacheTime))
for _, fwd := range params.Forward {
path, url, ok := strings.Cut(fwd, "->")
if !ok {
return fmt.Errorf("invalid forward: %q", fwd)
}
log.Println("Forwarding", path, "to", url)
mux.Handle(path, httpcache.SinglePath(&proxy{url: url}, params.CacheTime))
}
srv := &http.Server{
Addr: params.Listen,
Handler: mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
srv.SetKeepAlivesEnabled(false)
return srv.ListenAndServe()
}
type githubReleases struct {
url string
}
func (p *githubReleases) ServeHTTP(w http.ResponseWriter, _ *http.Request) {
log.Println("Fetching", p.url)
rels := upgrade.FetchLatestReleases(p.url, "")
if rels == nil {
http.Error(w, "no releases", http.StatusInternalServerError)
return
}
sort.Sort(upgrade.SortByRelease(rels))
rels = filterForLatest(rels)
// Move the URL used for browser downloads to the URL field, and remove
// the browser URL field. This avoids going via the GitHub API for
// downloads, since Syncthing uses the URL field.
for _, rel := range rels {
for j, asset := range rel.Assets {
rel.Assets[j].URL = asset.BrowserURL
rel.Assets[j].BrowserURL = ""
}
}
buf := new(bytes.Buffer)
_ = json.NewEncoder(buf).Encode(rels)
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET")
w.Write(buf.Bytes())
}
type proxy struct {
url string
}
func (p *proxy) ServeHTTP(w http.ResponseWriter, req *http.Request) {
log.Println("Fetching", p.url)
req, err := http.NewRequestWithContext(req.Context(), http.MethodGet, p.url, nil)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer resp.Body.Close()
ct := resp.Header.Get("Content-Type")
w.Header().Set("Content-Type", ct)
if resp.StatusCode == http.StatusOK {
w.Header().Set("Cache-Control", "public, max-age=900")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET")
}
w.WriteHeader(resp.StatusCode)
if strings.HasPrefix(ct, "application/json") {
// Special JSON handling; clean it up a bit.
var v interface{}
if err := json.NewDecoder(resp.Body).Decode(&v); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(v)
} else {
_, _ = io.Copy(w, resp.Body)
}
}
// filterForLatest returns the latest stable and prerelease only. If the
// stable version is newer (comes first in the list) there is no need to go
// looking for a prerelease at all.
func filterForLatest(rels []upgrade.Release) []upgrade.Release {
var filtered []upgrade.Release
var havePre bool
for _, rel := range rels {
if !rel.Prerelease {
// We found a stable version, we're good now.
filtered = append(filtered, rel)
break
}
if rel.Prerelease && !havePre {
// We remember the first prerelease we find.
filtered = append(filtered, rel)
havePre = true
}
}
return filtered
}

View File

@@ -12,7 +12,7 @@ import (
"os"
"github.com/alecthomas/kong"
"github.com/flynn-archive/go-shlex"
"github.com/kballard/go-shellquote"
"github.com/syncthing/syncthing/cmd/syncthing/cmdutil"
"github.com/syncthing/syncthing/lib/config"
@@ -67,7 +67,7 @@ func (*stdinCommand) Run() error {
fmt.Println("Reading commands from stdin...", args)
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
input, err := shlex.Split(scanner.Text())
input, err := shellquote.Split(scanner.Text())
if err != nil {
return fmt.Errorf("parsing input: %w", err)
}

View File

@@ -9,6 +9,7 @@ package main
import (
"bytes"
"context"
"crypto/sha256"
"fmt"
"net/http"
"os"
@@ -16,8 +17,6 @@ import (
"sort"
"strings"
"time"
"github.com/syncthing/syncthing/lib/sha256"
)
const (

View File

@@ -92,11 +92,6 @@ above.
STLOCKTHRESHOLD Used for debugging internal deadlocks; sets debug
sensitivity. Use only under direction of a developer.
STHASHING Select the SHA256 hashing package to use. Possible values
are "standard" for the Go standard library implementation,
"minio" for the github.com/minio/sha256-simd implementation,
and blank (the default) for auto detection.
STVERSIONEXTRA Add extra information to the version string in logs and the
version line in the GUI. Can be set to the name of a wrapper
or tool controlling syncthing to communicate this to the end

View File

@@ -241,22 +241,6 @@ func copyStderr(stderr io.Reader, dst io.Writer) {
if panicFd == nil {
dst.Write([]byte(line))
if strings.Contains(line, "SIGILL") {
l.Warnln(`
*******************************************************************************
* Crash due to illegal instruction detected. This is most likely due to a CPU *
* incompatibility with the high performance hashing package. Switching to the *
* standard hashing package instead. Please report this issue at: *
* *
* https://github.com/syncthing/syncthing/issues *
* *
* Include the details of your CPU. *
*******************************************************************************
`)
os.Setenv("STHASHING", "standard")
return
}
if strings.HasPrefix(line, "panic:") || strings.HasPrefix(line, "fatal error:") {
panicFd, err = os.Create(locations.GetTimestamped(locations.PanicLog))
if err != nil {

View File

@@ -1,226 +0,0 @@
// Copyright (C) 2018 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
package aggregate
import (
"database/sql"
"fmt"
"log"
"os"
"time"
_ "github.com/lib/pq"
)
type CLI struct {
DBConn string `env:"UR_DB_URL" default:"postgres://user:password@localhost/ur?sslmode=disable"`
}
func (cli *CLI) Run() error {
log.SetFlags(log.Ltime | log.Ldate)
log.SetOutput(os.Stdout)
db, err := sql.Open("postgres", cli.DBConn)
if err != nil {
return fmt.Errorf("database: %w", err)
}
err = setupDB(db)
if err != nil {
return fmt.Errorf("database: %w", err)
}
for {
runAggregation(db)
// Sleep until one minute past next midnight
sleepUntilNext(24*time.Hour, 1*time.Minute)
}
}
func runAggregation(db *sql.DB) {
since := maxIndexedDay(db, "VersionSummary")
log.Println("Aggregating VersionSummary data since", since)
rows, err := aggregateVersionSummary(db, since.Add(24*time.Hour))
if err != nil {
log.Println("aggregate:", err)
}
log.Println("Inserted", rows, "rows")
since = maxIndexedDay(db, "Performance")
log.Println("Aggregating Performance data since", since)
rows, err = aggregatePerformance(db, since.Add(24*time.Hour))
if err != nil {
log.Println("aggregate:", err)
}
log.Println("Inserted", rows, "rows")
since = maxIndexedDay(db, "BlockStats")
log.Println("Aggregating BlockStats data since", since)
rows, err = aggregateBlockStats(db, since.Add(24*time.Hour))
if err != nil {
log.Println("aggregate:", err)
}
log.Println("Inserted", rows, "rows")
}
func sleepUntilNext(intv, margin time.Duration) {
now := time.Now().UTC()
next := now.Truncate(intv).Add(intv).Add(margin)
log.Println("Sleeping until", next)
time.Sleep(next.Sub(now))
}
func setupDB(db *sql.DB) error {
_, err := db.Exec(`CREATE TABLE IF NOT EXISTS VersionSummary (
Day TIMESTAMP NOT NULL,
Version VARCHAR(8) NOT NULL,
Count INTEGER NOT NULL
)`)
if err != nil {
return err
}
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS Performance (
Day TIMESTAMP NOT NULL,
TotFiles INTEGER NOT NULL,
TotMiB INTEGER NOT NULL,
SHA256Perf DOUBLE PRECISION NOT NULL,
MemorySize INTEGER NOT NULL,
MemoryUsageMiB INTEGER NOT NULL
)`)
if err != nil {
return err
}
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS BlockStats (
Day TIMESTAMP NOT NULL,
Reports INTEGER NOT NULL,
Total BIGINT NOT NULL,
Renamed BIGINT NOT NULL,
Reused BIGINT NOT NULL,
Pulled BIGINT NOT NULL,
CopyOrigin BIGINT NOT NULL,
CopyOriginShifted BIGINT NOT NULL,
CopyElsewhere BIGINT NOT NULL
)`)
if err != nil {
return err
}
var t string
row := db.QueryRow(`SELECT 'UniqueDayVersionIndex'::regclass`)
if err := row.Scan(&t); err != nil {
_, _ = db.Exec(`CREATE UNIQUE INDEX UniqueDayVersionIndex ON VersionSummary (Day, Version)`)
}
row = db.QueryRow(`SELECT 'VersionDayIndex'::regclass`)
if err := row.Scan(&t); err != nil {
_, _ = db.Exec(`CREATE INDEX VersionDayIndex ON VersionSummary (Day)`)
}
row = db.QueryRow(`SELECT 'PerformanceDayIndex'::regclass`)
if err := row.Scan(&t); err != nil {
_, _ = db.Exec(`CREATE INDEX PerformanceDayIndex ON Performance (Day)`)
}
row = db.QueryRow(`SELECT 'BlockStatsDayIndex'::regclass`)
if err := row.Scan(&t); err != nil {
_, _ = db.Exec(`CREATE INDEX BlockStatsDayIndex ON BlockStats (Day)`)
}
return nil
}
func maxIndexedDay(db *sql.DB, table string) time.Time {
var t time.Time
row := db.QueryRow("SELECT MAX(DATE_TRUNC('day', Day)) FROM " + table)
err := row.Scan(&t)
if err != nil {
return time.Time{}
}
return t
}
func aggregateVersionSummary(db *sql.DB, since time.Time) (int64, error) {
res, err := db.Exec(`INSERT INTO VersionSummary (
SELECT
DATE_TRUNC('day', Received) AS Day,
SUBSTRING(Report->>'version' FROM '^v\d.\d+') AS Ver,
COUNT(*) AS Count
FROM ReportsJson
WHERE
Received > $1
AND Received < DATE_TRUNC('day', NOW())
AND Report->>'version' like 'v_.%'
GROUP BY Day, Ver
);
`, since)
if err != nil {
return 0, err
}
return res.RowsAffected()
}
func aggregatePerformance(db *sql.DB, since time.Time) (int64, error) {
res, err := db.Exec(`INSERT INTO Performance (
SELECT
DATE_TRUNC('day', Received) AS Day,
AVG((Report->>'totFiles')::numeric) As TotFiles,
AVG((Report->>'totMiB')::numeric) As TotMiB,
AVG((Report->>'sha256Perf')::numeric) As SHA256Perf,
AVG((Report->>'memorySize')::numeric) As MemorySize,
AVG((Report->>'memoryUsageMiB')::numeric) As MemoryUsageMiB
FROM ReportsJson
WHERE
Received > $1
AND Received < DATE_TRUNC('day', NOW())
AND Report->>'version' like 'v_.%'
/* Some custom implementation reported bytes when we expect megabytes, cap at petabyte */
AND (Report->>'memorySize')::numeric < 1073741824
GROUP BY Day
);
`, since)
if err != nil {
return 0, err
}
return res.RowsAffected()
}
func aggregateBlockStats(db *sql.DB, since time.Time) (int64, error) {
// Filter out anything prior 0.14.41 as that has sum aggregations which
// made no sense.
res, err := db.Exec(`INSERT INTO BlockStats (
SELECT
DATE_TRUNC('day', Received) AS Day,
COUNT(1) As Reports,
SUM((Report->'blockStats'->>'total')::numeric)::bigint AS Total,
SUM((Report->'blockStats'->>'renamed')::numeric)::bigint AS Renamed,
SUM((Report->'blockStats'->>'reused')::numeric)::bigint AS Reused,
SUM((Report->'blockStats'->>'pulled')::numeric)::bigint AS Pulled,
SUM((Report->'blockStats'->>'copyOrigin')::numeric)::bigint AS CopyOrigin,
SUM((Report->'blockStats'->>'copyOriginShifted')::numeric)::bigint AS CopyOriginShifted,
SUM((Report->'blockStats'->>'copyElsewhere')::numeric)::bigint AS CopyElsewhere
FROM ReportsJson
WHERE
Received > $1
AND Received < DATE_TRUNC('day', NOW())
AND (Report->>'urVersion')::numeric >= 3
AND Report->>'version' like 'v_.%'
AND Report->>'version' NOT LIKE 'v0.14.40%'
AND Report->>'version' NOT LIKE 'v0.14.39%'
AND Report->>'version' NOT LIKE 'v0.14.38%'
GROUP BY Day
);
`, since)
if err != nil {
return 0, err
}
return res.RowsAffected()
}

View File

@@ -1,276 +0,0 @@
// Copyright (C) 2018 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
package serve
import (
"regexp"
"sort"
"strconv"
"strings"
)
type analytic struct {
Key string
Count int
Percentage float64
Items []analytic `json:",omitempty"`
}
type analyticList []analytic
func (l analyticList) Less(a, b int) bool {
if l[a].Key == "Others" {
return false
}
if l[b].Key == "Others" {
return true
}
return l[b].Count < l[a].Count // inverse
}
func (l analyticList) Swap(a, b int) {
l[a], l[b] = l[b], l[a]
}
func (l analyticList) Len() int {
return len(l)
}
// Returns a list of frequency analytics for a given list of strings.
func analyticsFor(ss []string, cutoff int) []analytic {
m := make(map[string]int)
t := 0
for _, s := range ss {
m[s]++
t++
}
l := make([]analytic, 0, len(m))
for k, c := range m {
l = append(l, analytic{
Key: k,
Count: c,
Percentage: 100 * float64(c) / float64(t),
})
}
sort.Sort(analyticList(l))
if cutoff > 0 && len(l) > cutoff {
c := 0
for _, i := range l[cutoff:] {
c += i.Count
}
l = append(l[:cutoff], analytic{
Key: "Others",
Count: c,
Percentage: 100 * float64(c) / float64(t),
})
}
return l
}
// Find the points at which certain penetration levels are met
func penetrationLevels(as []analytic, points []float64) []analytic {
sort.Slice(as, func(a, b int) bool {
return versionLess(as[b].Key, as[a].Key)
})
var res []analytic
idx := 0
sum := 0.0
for _, a := range as {
sum += a.Percentage
if sum >= points[idx] {
a.Count = int(points[idx])
a.Percentage = sum
res = append(res, a)
idx++
if idx == len(points) {
break
}
}
}
return res
}
func statsForInts(data []int) [4]float64 {
var res [4]float64
if len(data) == 0 {
return res
}
sort.Ints(data)
res[0] = float64(data[int(float64(len(data))*0.05)])
res[1] = float64(data[len(data)/2])
res[2] = float64(data[int(float64(len(data))*0.95)])
res[3] = float64(data[len(data)-1])
return res
}
func statsForInt64s(data []int64) [4]float64 {
var res [4]float64
if len(data) == 0 {
return res
}
sort.Slice(data, func(a, b int) bool {
return data[a] < data[b]
})
res[0] = float64(data[int(float64(len(data))*0.05)])
res[1] = float64(data[len(data)/2])
res[2] = float64(data[int(float64(len(data))*0.95)])
res[3] = float64(data[len(data)-1])
return res
}
func statsForFloats(data []float64) [4]float64 {
var res [4]float64
if len(data) == 0 {
return res
}
sort.Float64s(data)
res[0] = data[int(float64(len(data))*0.05)]
res[1] = data[len(data)/2]
res[2] = data[int(float64(len(data))*0.95)]
res[3] = data[len(data)-1]
return res
}
func group(by func(string) string, as []analytic, perGroup int, otherPct float64) []analytic {
var res []analytic
next:
for _, a := range as {
group := by(a.Key)
for i := range res {
if res[i].Key == group {
res[i].Count += a.Count
res[i].Percentage += a.Percentage
if len(res[i].Items) < perGroup {
res[i].Items = append(res[i].Items, a)
}
continue next
}
}
res = append(res, analytic{
Key: group,
Count: a.Count,
Percentage: a.Percentage,
Items: []analytic{a},
})
}
sort.Sort(analyticList(res))
if otherPct > 0 {
// Groups with less than otherPCt go into "Other"
other := analytic{
Key: "Other",
}
for i := 0; i < len(res); i++ {
if res[i].Percentage < otherPct || res[i].Key == "Other" {
other.Count += res[i].Count
other.Percentage += res[i].Percentage
res = append(res[:i], res[i+1:]...)
i--
}
}
if other.Count > 0 {
res = append(res, other)
}
}
return res
}
func byVersion(s string) string {
parts := strings.Split(s, ".")
if len(parts) >= 2 {
return strings.Join(parts[:2], ".")
}
return s
}
func byPlatform(s string) string {
parts := strings.Split(s, "-")
if len(parts) >= 2 {
return parts[0]
}
return s
}
var numericGoVersion = regexp.MustCompile(`^go[0-9]\.[0-9]+`)
func byCompiler(s string) string {
if m := numericGoVersion.FindString(s); m != "" {
return m
}
return "Other"
}
func versionLess(a, b string) bool {
arel, apre := versionParts(a)
brel, bpre := versionParts(b)
minlen := len(arel)
if l := len(brel); l < minlen {
minlen = l
}
for i := 0; i < minlen; i++ {
if arel[i] != brel[i] {
return arel[i] < brel[i]
}
}
// Longer version is newer, when the preceding parts are equal
if len(arel) != len(brel) {
return len(arel) < len(brel)
}
if apre != bpre {
// "(+dev)" versions are ahead
if apre == plusStr {
return false
}
if bpre == plusStr {
return true
}
return apre < bpre
}
// don't actually care how the prerelease stuff compares for our purposes
return false
}
// Split a version as returned from transformVersion into parts.
// "1.2.3-beta.2" -> []int{1, 2, 3}, "beta.2"}
func versionParts(v string) ([]int, string) {
parts := strings.SplitN(v[1:], " ", 2) // " (+dev)" versions
if len(parts) == 1 {
parts = strings.SplitN(parts[0], "-", 2) // "-rc.1" type versions
}
fields := strings.Split(parts[0], ".")
release := make([]int, len(fields))
for i, s := range fields {
v, _ := strconv.Atoi(s)
release[i] = v
}
var prerelease string
if len(parts) > 1 {
prerelease = parts[1]
}
return release, prerelease
}

View File

@@ -1,131 +0,0 @@
// Copyright (C) 2018 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
package serve
import (
"bytes"
"fmt"
"strings"
)
type NumberType int
const (
NumberMetric NumberType = iota
NumberBinary
NumberDuration
)
func number(ntype NumberType, v float64) string {
switch ntype {
case NumberMetric:
return metric(v)
case NumberDuration:
return duration(v)
case NumberBinary:
return binary(v)
default:
return metric(v)
}
}
type suffix struct {
Suffix string
Multiplier float64
}
var metricSuffixes = []suffix{
{"G", 1e9},
{"M", 1e6},
{"k", 1e3},
}
var binarySuffixes = []suffix{
{"Gi", 1 << 30},
{"Mi", 1 << 20},
{"Ki", 1 << 10},
}
var durationSuffix = []suffix{
{"year", 365 * 24 * 60 * 60},
{"month", 30 * 24 * 60 * 60},
{"day", 24 * 60 * 60},
{"hour", 60 * 60},
{"minute", 60},
{"second", 1},
}
func metric(v float64) string {
return withSuffix(v, metricSuffixes, false)
}
func binary(v float64) string {
return withSuffix(v, binarySuffixes, false)
}
func duration(v float64) string {
return withSuffix(v, durationSuffix, true)
}
func withSuffix(v float64, ps []suffix, pluralize bool) string {
for _, p := range ps {
if v >= p.Multiplier {
suffix := p.Suffix
if pluralize && v/p.Multiplier != 1.0 {
suffix += "s"
}
// If the number only has decimal zeroes, strip em off.
num := strings.TrimRight(strings.TrimRight(fmt.Sprintf("%.1f", v/p.Multiplier), "0"), ".")
return fmt.Sprintf("%s %s", num, suffix)
}
}
return strings.TrimRight(strings.TrimRight(fmt.Sprintf("%.1f", v), "0"), ".")
}
// commatize returns a number with sep as thousands separators. Handles
// integers and plain floats.
func commatize(sep, s string) string {
// If no dot, don't do anything.
if !strings.ContainsRune(s, '.') {
return s
}
var b bytes.Buffer
fs := strings.SplitN(s, ".", 2)
l := len(fs[0])
for i := range fs[0] {
b.Write([]byte{s[i]})
if i < l-1 && (l-i)%3 == 1 {
b.WriteString(sep)
}
}
if len(fs) > 1 && len(fs[1]) > 0 {
b.WriteString(".")
b.WriteString(fs[1])
}
return b.String()
}
func proportion(m map[string]int, count int) float64 {
total := 0
isMax := true
for _, n := range m {
total += n
if n > count {
isMax = false
}
}
pct := (100 * float64(count)) / float64(total)
// To avoid rounding errors in the template, surpassing 100% and breaking
// the progress bars.
if isMax && len(m) > 1 && count != total {
pct -= 0.01
}
return pct
}

View File

@@ -1,26 +0,0 @@
// Copyright (C) 2023 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 serve
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var metricReportsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: "syncthing",
Subsystem: "ursrv",
Name: "reports_total",
}, []string{"version"})
func init() {
metricReportsTotal.WithLabelValues("fail")
metricReportsTotal.WithLabelValues("duplicate")
metricReportsTotal.WithLabelValues("v1")
metricReportsTotal.WithLabelValues("v2")
metricReportsTotal.WithLabelValues("v3")
}

View File

File diff suppressed because it is too large Load Diff

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because one or more lines are too long

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

View File

@@ -1,623 +0,0 @@
<!DOCTYPE html>
<!--
Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
Use of this source code is governed by an MIT-style license that can be
found in the LICENSE file.
-->
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="">
<meta name="author" content="">
<link rel="shortcut icon" href="static/assets/img/favicon.png">
<title>Syncthing Usage Reports</title>
<link href="static/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script type="text/javascript" src="static/bootstrap/js/bootstrap.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/leaflet.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/leaflet.js"></script>
<script src="https://cdn.jsdelivr.net/npm/heatmapjs@2.0.2/heatmap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/leaflet-heatmap@1.0.0/leaflet-heatmap.js"></script>
<style type="text/css">
body {
margin: 40px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
}
tr.main td {
font-weight: bold;
}
tr.child td.first {
padding-left: 2em;
}
.progress-bar {
overflow:hidden;
white-space:nowrap;
text-overflow: ellipsis;
}
</style>
<script type="text/javascript"
src='https://www.google.com/jsapi?autoload={
"modules":[{
"name":"visualization",
"version":"1",
"packages":["corechart"]
}]
}'></script>
<script type="text/javascript">
google.setOnLoadCallback(drawVersionChart);
google.setOnLoadCallback(drawBlockStatsChart);
google.setOnLoadCallback(drawPerformanceCharts);
function drawVersionChart() {
// Summary version chart for versions that at some point in the chart
// reaches 250 devices. This filters out versions that are old and
// uninteresting yet linger forever with like four users.
var jsonData = $.ajax({url: "summary.json?min=250", dataType:"json", async: false}).responseText;
var rows = JSON.parse(jsonData);
var data = new google.visualization.DataTable();
data.addColumn('date', 'Day');
for (var i = 1; i < rows[0].length; i++){
data.addColumn('number', rows[0][i]);
}
for (var i = 1; i < rows.length; i++){
rows[i][0] = new Date(rows[i][0]);
data.addRow(rows[i]);
};
var options = {
legend: { position: 'bottom', alignment: 'center' },
isStacked: true,
colors: ['rgb(102,194,165)','rgb(252,141,98)','rgb(141,160,203)','rgb(231,138,195)','rgb(166,216,84)','rgb(255,217,47)'],
chartArea: {left: 80, top: 20, width: '1020', height: '300'},
};
var chart = new google.visualization.AreaChart(document.getElementById('versionChart'));
chart.draw(data, options);
}
function formatGibibytes(gibibytes, decimals) {
if(gibibytes == 0) return '0 GiB';
var k = 1024,
dm = decimals || 2,
sizes = ['GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'],
i = Math.floor(Math.log(gibibytes) / Math.log(k));
if (i < 0) {
sizes = 'MiB';
} else {
sizes = sizes[i];
}
return parseFloat((gibibytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes;
}
function drawBlockStatsChart() {
var jsonData = $.ajax({url: "blockstats.json", dataType:"json", async: false}).responseText;
var rows = JSON.parse(jsonData);
var data = new google.visualization.DataTable();
data.addColumn('date', 'Day');
for (var i = 1; i < rows[0].length; i++){
data.addColumn('number', rows[0][i]);
}
var totals = [0, 0, 0, 0, 0, 0];
for (var i = 1; i < rows.length; i++){
rows[i][0] = new Date(rows[i][0]);
for (var j = 2; j < rows[i].length; j++) {
totals[j-2] += rows[i][j];
}
data.addRow(rows[i]);
};
var totalTotals = totals.reduce(function(a, b) { return a + b; }, 0);
if (totalTotals > 0) {
var content = "<table class='table'>\n"
for (var j = 2; j < rows[0].length; j++) {
content += "<tr><td><b>" + rows[0][j].replace(' (GiB)', '') + "</b></td><td>" + formatGibibytes(totals[j-2].toFixed(2)) + " (" + ((100*totals[j-2])/totalTotals).toFixed(2) +"%)</td></tr>\n";
}
content += "</table>";
document.getElementById("data-to-date").innerHTML = content;
} else {
// No data, hide it.
document.getElementById("block-stats").outerHTML = "";
return;
}
var options = {
focusTarget: 'category',
vAxes: {0: {}, 1: {}},
series: {0: {type: 'line', targetAxisIndex:1}},
isStacked: true,
legend: {position: 'none'},
colors: ['rgb(102,194,165)','rgb(252,141,98)','rgb(141,160,203)','rgb(231,138,195)','rgb(166,216,84)','rgb(255,217,47)'],
chartArea: {left: 80, top: 20, width: '1020', height: '300'},
};
var chart = new google.visualization.AreaChart(document.getElementById('blockStatsChart'));
chart.draw(data, options);
}
function drawPerformanceCharts() {
var jsonData = $.ajax({url: "/performance.json", dataType:"json", async: false}).responseText;
var rows = JSON.parse(jsonData);
for (var i = 1; i < rows.length; i++){
rows[i][0] = new Date(rows[i][0]);
}
drawChart(rows, 1, 'Total Number of Files', 'totFilesChart', 1e6, 1);
drawChart(rows, 2, 'Total Folder Size (GiB)', 'totMiBChart', 1e6, 1024);
drawChart(rows, 3, 'Hash Performance (MiB/s)', 'hashPerfChart', 1000, 1);
drawChart(rows, 4, 'System RAM Size (GiB)', 'memSizeChart', 1e6, 1024);
drawChart(rows, 5, 'Memory Usage (MiB)', 'memUsageChart', 250, 1);
}
function drawChart(rows, index, title, id, cutoff, divisor) {
var data = new google.visualization.DataTable();
data.addColumn('date', 'Day');
data.addColumn('number', title);
var row;
for (var i = 1; i < rows.length; i++){
row = [rows[i][0], rows[i][index] / divisor];
if (row[1] > cutoff) {
row[1] = null;
}
data.addRow(row);
}
var options = {
legend: { position: 'bottom', alignment: 'center' },
colors: ['rgb(102,194,165)','rgb(252,141,98)','rgb(141,160,203)','rgb(231,138,195)','rgb(166,216,84)','rgb(255,217,47)'],
chartArea: {left: 80, top: 20, width: '1020', height: '300'},
vAxes: {0: {minValue: 0}},
};
var chart = new google.visualization.LineChart(document.getElementById(id));
chart.draw(data, options);
}
var locations = [];
{{range $location, $weight := .locations}}
locations.push({lat:{{- $location.Latitude -}},lng:{{- $location.Longitude -}},count:Math.min(100, {{- $weight -}})});
{{- end}}
function drawHeatMap() {
if (locations.length == 0) {
return;
}
var testData = {
data: locations
};
var baseLayer = L.tileLayer(
'https://tile.openstreetmap.org/{z}/{x}/{y}.png',{
attribution: '...',
maxZoom: 18
}
);
var cfg = {
"radius": 1,
"minOpacity": .25,
"maxOpacity": .8,
"scaleRadius": true,
"useLocalExtrema": true,
latField: 'lat',
lngField: 'lng',
valueField: 'count',
gradient: {
'.1': 'cyan',
'.8': 'blue',
'.95': 'red'
}
};
var heatmapLayer = new HeatmapOverlay(cfg);
var map = new L.Map('map', {
center: new L.LatLng(25, 0),
zoom: 1,
layers: [baseLayer, heatmapLayer]
});
heatmapLayer.setData(testData);
}
</script>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-12">
<h1>Syncthing Usage Data</h1>
<h4 id="active-users">Active Users per Day and Version</h4>
<p>
This is the total number of unique users with reporting enabled, per day. Area color represents the major version.
</p>
<div class="img-thumbnail" id="versionChart" style="width: 1130px; height: 400px; padding: 10px;"></div>
<div id="block-stats">
<h4>Data Transfers per Day</h4>
<p>
This is total data transferred per day. Also shows how much data was saved (not transferred) by each of the methods syncthing uses.
</p>
<div class="img-thumbnail" id="blockStatsChart" style="width: 1130px; height: 400px; padding: 10px;"></div>
<h4 id="totals-to-date">Totals to date</h4>
<p id="data-to-date">
No data
</p>
</div>
<h4 id="metrics">Usage Metrics</h4>
<p>
This is the aggregated usage report data for the last 24 hours. Data based on <b>{{.nodes}}</b> devices that have reported in.
</p>
{{if .locations}}
<div class="img-thumbnail" id="map" style="width: 1130px; height: 400px; padding: 10px;"></div>
<p class="text-muted">
Heatmap max intensity is capped at 100 reports within a location.
</p>
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a data-toggle="collapse" href="#collapseTwo">Break down per country</a>
</h4>
</div>
<div id="collapseTwo" class="panel-collapse collapse">
<div class="panel-body">
<div class="row">
<div class="col-md-6">
<table class="table table-striped">
<tbody>
{{range .countries | slice 2 1}}
<tr>
<td style="width: 45%">{{.Key}}</td>
<td style="width: 5%" class="text-right">{{if ge .Pct 10.0}}{{.Pct | printf "%.0f"}}{{else if ge .Pct 1.0}}{{.Pct | printf "%.01f"}}{{else}}{{.Pct | printf "%.02f"}}{{end}}%</td>
<td style="width: 5%" class="text-right">{{.Count}}</td>
<td>
<div class="progress-bar" role="progressbar" aria-valuenow="{{.Pct | printf "%.02f"}}" aria-valuemin="0" aria-valuemax="100" style="width: {{.Pct | printf "%.02f"}}%; height:20px"></div>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
<div class="col-md-6">
<table class="table table-striped">
<tbody>
{{range .countries | slice 2 2}}
<tr>
<td style="width: 45%">{{.Key}}</td>
<td style="width: 5%" class="text-right">{{if ge .Pct 10.0}}{{.Pct | printf "%.0f"}}{{else if ge .Pct 1.0}}{{.Pct | printf "%.01f"}}{{else}}{{.Pct | printf "%.02f"}}{{end}}%</td>
<td style="width: 5%" class="text-right">{{.Count}}</td>
<td>
<div class="progress-bar" role="progressbar" aria-valuenow="{{.Pct | printf "%.02f"}}" aria-valuemin="0" aria-valuemax="100" style="width: {{.Pct | printf "%.02f"}}%; height:20px"></div>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{{end}}
<table class="table table-striped">
<thead>
<tr>
<th></th>
<th colspan="4" class="text-center">
<a href="https://en.wikipedia.org/wiki/Percentile">Percentile</a>
</th>
</tr>
<tr>
<th></th>
<th class="text-right">5%</th>
<th class="text-right">50%</th>
<th class="text-right">95%</th>
<th class="text-right">100%</th>
</tr>
</thead>
<tbody>
{{range .categories}}
<tr>
<td>{{.Descr}}</td>
<td class="text-right">{{index .Values 0 | number .Type | commatize " "}}{{.Unit}}</td>
<td class="text-right">{{index .Values 1 | number .Type | commatize " "}}{{.Unit}}</td>
<td class="text-right">{{index .Values 2 | number .Type | commatize " "}}{{.Unit}}</td>
<td class="text-right">{{index .Values 3 | number .Type | commatize " "}}{{.Unit}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</div>
<div class="row">
<div class="col-md-6">
<table class="table table-striped">
<thead>
<tr>
<th>Version</th><th class="text-right">Devices</th><th class="text-right">Share</th>
</tr>
</thead>
<tbody>
{{range .versions}}
{{if gt .Percentage 0.1}}
<tr class="main">
<td>{{.Key}}</td>
<td class="text-right">{{.Count}}</td>
<td class="text-right">{{.Percentage | printf "%.01f"}}%</td>
</tr>
{{range .Items}}
{{if gt .Percentage 0.1}}
<tr class="child">
<td class="first">{{.Key}}</td>
<td class="text-right">{{.Count}}</td>
<td class="text-right">{{.Percentage | printf "%.01f"}}%</td>
</tr>
{{end}}
{{end}}
{{end}}
{{end}}
</tbody>
</table>
<table class="table table-striped">
<thead>
<tr>
<th>Penetration Level</th>
<th>Version</th>
<th class="text-right">Actual</th>
</tr>
</thead>
<tbody>
{{range .versionPenetrations}}
<tr>
<td>{{.Count}}%</td>
<td>&ge; {{.Key}}</td>
<td class="text-right">{{.Percentage | printf "%.01f"}}%</td>
</tr>
{{end}}
</tbody>
</table>
</div>
<div class="col-md-6">
<table class="table table-striped">
<thead>
<tr>
<th>Platform</th>
<th class="text-right">Devices</th>
<th class="text-right">Share</th>
</tr>
</thead>
<tbody>
{{range .platforms}}
<tr class="main">
<td>{{.Key}}</td>
<td class="text-right">{{.Count}}</td>
<td class="text-right">{{.Percentage | printf "%.01f"}}%</td>
</tr>
{{range .Items}}
<tr class="child">
<td class="first">{{.Key}}</td>
<td class="text-right">{{.Count}}</td>
<td class="text-right">{{.Percentage | printf "%.01f"}}%</td>
</tr>
{{end}}
{{end}}
</tbody>
</table>
</div>
</div>
<div class="row">
<div class="col-md-6">
<table class="table table-striped">
<thead>
<tr>
<th>Compiler</th>
<th class="text-right">Devices</th>
<th class="text-right">Share</th>
</tr>
</thead>
<tbody>
{{range .compilers}}
<tr class="main">
<td>{{.Key}}</td>
<td class="text-right">{{.Count}}</td>
<td class="text-right">{{.Percentage | printf "%.01f"}}%</td>
</tr>
{{range .Items}}
{{if or (gt .Percentage 0.1) (eq .Key "Others")}}
<tr class="child">
<td class="first">{{.Key}}</td>
<td class="text-right">{{.Count}}</td>
<td class="text-right">{{.Percentage | printf "%.01f"}}%</td>
</tr>
{{end}}
{{end}}
{{end}}
</tbody>
</table>
</div>
<div class="col-md-6">
<table class="table table-striped">
<thead>
<tr>
<th>Distribution Channel</th>
<th class="text-right">Devices</th>
<th class="text-right">Share</th>
</tr>
</thead>
<tbody>
{{range .distributions}}
<tr>
<td>{{.Key}}</td>
<td class="text-right">{{.Count}}</td>
<td class="text-right">{{.Percentage | printf "%.01f"}}%</td>
</tr>
{{end}}
</tbody>
</table>
</div>
<div class="col-md-6">
<table class="table table-striped">
<thead>
<tr>
<th>Builder</th>
<th class="text-right">Devices</th>
<th class="text-right">Share</th>
</tr>
</thead>
<tbody>
{{range .builders}}
<tr>
<td>{{.Key}}</td>
<td class="text-right">{{.Count}}</td>
<td class="text-right">{{.Percentage | printf "%.01f"}}%</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</div>
<div class="row">
<div class="col-md-12">
<h4 id="features">Feature Usage</h4>
<p>
The following lists feature usage. Some features are reported per report, some are per sum of units within report (eg. devices with static addresses among all known devices per report).
Currently there are <b>{{.versionNodes.v2}}</b> devices reporting for version 2 and <b>{{.versionNodes.v3}}</b> for version 3.
</p>
</div>
</div>
<div class="row">
{{$i := counter}}
{{range $featureName := .featureOrder}}
{{$featureValues := index $.features $featureName }}
{{if $i.DrawTwoDivider}}
</div>
<div class="row">
{{end}}
{{ $i.Increment }}
<div class="col-md-6">
<table class="table table-striped">
<thead><tr>
<th>{{$featureName}} Features</th><th colspan="2" class="text-center">Usage</th>
</tr></thead>
<tbody>
{{range $featureValues}}
<tr>
<td style="width: 50%">{{.Key}} ({{.Version}})</td>
<td style="width: 10%" class="text-right">{{if ge .Pct 10.0}}{{.Pct | printf "%.0f"}}{{else if ge .Pct 1.0}}{{.Pct | printf "%.01f"}}{{else}}{{.Pct | printf "%.02f"}}{{end}}%</td>
<td style="width: 40%" {{if lt .Pct 5.0}}data-toggle="tooltip" title='{{.Count}}'{{end}}>
<div class="progress-bar" role="progressbar" aria-valuenow="{{.Pct | printf "%.02f"}}" aria-valuemin="0" aria-valuemax="100" style="width: {{.Pct | printf "%.02f"}}%; height:20px" {{if ge .Pct 5.0}}data-toggle="tooltip" title='{{.Count}}'{{end}}></div>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{end}}
</div>
<div class="row">
<div class="col-md-12">
<h4 id="features">Feature Group Usage</h4>
<p>
The following lists feature usage groups, which might include multiple occourances of a feature use per report.
</p>
</div>
</div>
<div class="row">
{{$i := counter}}
{{range $featureName := .featureOrder}}
{{$featureValues := index $.featureGroups $featureName }}
{{if $i.DrawTwoDivider}}
</div>
<div class="row">
{{end}}
{{ $i.Increment }}
<div class="col-md-6">
<table class="table table-striped">
<thead><tr>
<th>{{$featureName}} Group Features</th><th colspan="2" class="text-center">Usage</th>
</tr></thead>
<tbody>
{{range $featureValues}}
{{$counts := .Counts}}
<tr>
<td style="width: 50%">
<div data-toggle="tooltip" title='{{range $key, $value := .Counts}}{{$key}} ({{$value | proportion $counts | printf "%.02f"}}% - {{$value}})</br>{{end}}'>
{{.Key}} ({{.Version}})
</div>
</td>
<td style="width: 50%">
<div class="progress" role="progressbar" style="width: 100%">
{{$j := counter}}
{{range $key, $value := .Counts}}
{{with $valuePct := $value | proportion $counts}}
<div class="progress-bar {{ $j.Current | progressBarClassByIndex }}" style='width: {{$valuePct | printf "%.02f"}}%' data-toggle="tooltip" title='{{$key}} ({{$valuePct | printf "%.02f"}}% - {{$value}})'>
{{if ge $valuePct 30.0}}{{$key}}{{end}}
</div>
{{end}}
{{ $j.Increment }}
{{end}}
</div>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{end}}
</div>
<div class="row">
<div class="col-md-12">
<h1 id="performance-charts">Historical Performance Data</h1>
<p>These charts are all the average of the corresponding metric, for the entire population of a given day.</p>
<h4 id="hash-performance">Hash Performance (MiB/s)</h4>
<div class="img-thumbnail" id="hashPerfChart" style="width: 1130px; height: 400px; padding: 10px;"></div>
<h4 id="memory-usage">Memory Usage (MiB)</h4>
<div class="img-thumbnail" id="memUsageChart" style="width: 1130px; height: 400px; padding: 10px;"></div>
<h4 id="total-files">Total Number of Files</h4>
<div class="img-thumbnail" id="totFilesChart" style="width: 1130px; height: 400px; padding: 10px;"></div>
<h4 id="total-size">Total Folder Size (GiB)</h4>
<div class="img-thumbnail" id="totMiBChart" style="width: 1130px; height: 400px; padding: 10px;"></div>
<h4 id="system-ram">System RAM Size (GiB)</h4>
<div class="img-thumbnail" id="memSizeChart" style="width: 1130px; height: 400px; padding: 10px;"></div>
</div>
</div>
</div>
<hr>
<p>
<a href="https://github.com/syncthing/syncthing/tree/main/cmd/ursrv">Source code</a>.
This product includes GeoLite2 data created by MaxMind, available from
<a href="http://www.maxmind.com">http://www.maxmind.com</a>.
</p>
<script type="text/javascript">
$('[data-toggle="tooltip"]').tooltip({html:true});
drawHeatMap();
</script>
</body>
</html>

28
compat.yaml Normal file
View File

@@ -0,0 +1,28 @@
- runtime: go1.21
requirements:
# See https://en.wikipedia.org/wiki/MacOS_version_history#Releases
#
# macOS 10.15 (Catalina) per https://go.dev/doc/go1.22#darwin
darwin: "19"
# Per https://go.dev/doc/go1.23#linux
linux: "2.6.32"
# Windows 10's initial release was 10.0.10240.16405, per
# https://learn.microsoft.com/en-us/windows/release-health/release-information
# and Windows 11's initial release was 10.0.22000.194 per
# https://learn.microsoft.com/en-us/windows/release-health/windows11-release-information
#
# Windows 10/Windows Server 2016 per https://go.dev/doc/go1.21#windows
windows: "10.0"
- runtime: go1.22
requirements:
darwin: "19"
linux: "2.6.32"
windows: "10.0"
- runtime: go1.23
requirements:
# macOS 11 (Big Sur) per https://tip.golang.org/doc/go1.23#darwin
darwin: "20"
linux: "2.6.32"
windows: "10.0"

62
go.mod
View File

@@ -1,16 +1,16 @@
module github.com/syncthing/syncthing
go 1.21.0
go 1.22.0
require (
github.com/AudriusButkevicius/recli v0.0.7-0.20220911121932-d000ce8fbf0f
github.com/alecthomas/kong v0.9.0
github.com/alecthomas/kong v1.2.1
github.com/aws/aws-sdk-go v1.55.5
github.com/calmh/incontainer v1.0.0
github.com/calmh/xdr v1.1.0
github.com/ccding/go-stun v0.1.4
github.com/calmh/xdr v1.2.0
github.com/ccding/go-stun v0.1.5
github.com/chmduquesne/rollinghash v4.0.0+incompatible
github.com/d4l3k/messagediff v1.2.1
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568
github.com/getsentry/raven-go v0.2.0
github.com/go-ldap/ldap/v3 v3.4.8
github.com/gobwas/glob v0.2.3
@@ -21,33 +21,33 @@ require (
github.com/jackpal/go-nat-pmp v1.0.2
github.com/julienschmidt/httprouter v1.3.0
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/lib/pq v1.10.9
github.com/maruel/panicparse/v2 v2.3.1
github.com/maxbrunsfeld/counterfeiter/v6 v6.8.1
github.com/maxmind/geoipupdate/v6 v6.1.0
github.com/minio/sha256-simd v1.0.1
github.com/miscreant/miscreant.go v0.0.0-20200214223636-26d376326b75
github.com/oschwald/geoip2-golang v1.11.0
github.com/pierrec/lz4/v4 v4.1.21
github.com/prometheus/client_golang v1.19.1
github.com/quic-go/quic-go v0.44.0
github.com/prometheus/client_golang v1.20.4
github.com/puzpuzpuz/xsync/v3 v3.4.0
github.com/quic-go/quic-go v0.47.0
github.com/rabbitmq/amqp091-go v1.10.0
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475
github.com/shirou/gopsutil/v3 v3.24.5
github.com/shirou/gopsutil/v4 v4.24.9
github.com/syncthing/notify v0.0.0-20210616190510-c6b7342338d2
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d
github.com/thejerf/suture/v4 v4.0.5
github.com/urfave/cli v1.22.15
github.com/vitrun/qart v0.0.0-20160531060029-bf64b92db6b0
github.com/willabides/kongplete v0.4.0
go.uber.org/automaxprocs v1.5.3
golang.org/x/crypto v0.23.0
golang.org/x/net v0.25.0
golang.org/x/sys v0.20.0
golang.org/x/text v0.15.0
golang.org/x/time v0.5.0
golang.org/x/tools v0.21.0
google.golang.org/protobuf v1.34.1
go.uber.org/automaxprocs v1.6.0
golang.org/x/crypto v0.27.0
golang.org/x/net v0.29.0
golang.org/x/sys v0.25.0
golang.org/x/text v0.18.0
golang.org/x/time v0.6.0
golang.org/x/tools v0.25.0
google.golang.org/protobuf v1.34.2
sigs.k8s.io/yaml v1.4.0
)
require (
@@ -56,38 +56,44 @@ require (
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/ebitengine/purego v0.8.0 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/gofrs/flock v0.8.1 // indirect
github.com/gofrs/flock v0.12.1 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/pprof v0.0.0-20240528025155-186aa0362fba // indirect
github.com/google/pprof v0.0.0-20241001023024-f4c0cfd0cf1d // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/klauspost/compress v1.17.10 // indirect
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nxadm/tail v1.4.11 // indirect
github.com/onsi/ginkgo/v2 v2.19.0 // indirect
github.com/oschwald/maxminddb-golang v1.13.0 // indirect
github.com/onsi/ginkgo/v2 v2.20.2 // indirect
github.com/oschwald/maxminddb-golang v1.13.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/posener/complete v1.2.3 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.54.0 // indirect
github.com/prometheus/common v0.60.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/riywo/loginshell v0.0.0-20200815045211-7d26008be1ab // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.9.0 // indirect
github.com/tklauser/go-sysconf v0.3.14 // indirect
github.com/tklauser/numcpus v0.8.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.uber.org/mock v0.4.0 // indirect
golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect
golang.org/x/mod v0.21.0 // indirect
golang.org/x/sync v0.8.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

138
go.sum
View File

@@ -3,24 +3,26 @@ github.com/AudriusButkevicius/recli v0.0.7-0.20220911121932-d000ce8fbf0f/go.mod
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU=
github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/kong v0.9.0 h1:G5diXxc85KvoV2f0ZRVuMsi45IrBgx9zDNGNj165aPA=
github.com/alecthomas/kong v0.9.0/go.mod h1:Y47y5gKfHp1hDc7CH7OeXgLIpp+Q2m1Ni0L5s3bI8Os=
github.com/alecthomas/assert/v2 v2.10.0 h1:jjRCHsj6hBJhkmhznrCzoNpbA3zqy0fYiUcYZP/GkPY=
github.com/alecthomas/assert/v2 v2.10.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/kong v1.2.1 h1:E8jH4Tsgv6wCRX2nGrdPyHDUCSG83WH2qE4XLACD33Q=
github.com/alecthomas/kong v1.2.1/go.mod h1:rKTSFhbdp3Ryefn8x5MOEprnRFQ7nlmMC01GKhehhBM=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU=
github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
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/calmh/glob v0.0.0-20220615080505-1d823af5017b h1:Fjm4GuJ+TGMgqfGHN42IQArJb77CfD/mAwLbDUoJe6g=
github.com/calmh/glob v0.0.0-20220615080505-1d823af5017b/go.mod h1:91K7jfEsgJSyfSrX+gmrRfZMtntx6JsHolWubGXDopg=
github.com/calmh/incontainer v1.0.0 h1:g2cTUtZuFGmMGX8GoykPkN1Judj2uw8/3/aEtq4Z/rg=
github.com/calmh/incontainer v1.0.0/go.mod h1:eOhqnw15c9X+4RNBe0W3HlUZFfX16O0EDsCOInTndHY=
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.1.4 h1:lC0co3Q3vjAuu2Jz098WivVPBPbemYFqbwE1syoka4M=
github.com/ccding/go-stun v0.1.4/go.mod h1:cCZjJ1J3WFSJV6Wj8Y9Di8JMTsEXh6uv2eNmLzKaUeM=
github.com/calmh/xdr v1.2.0 h1:GaGSNH4ZDw9kNdYqle6+RcAENiaQ8/611Ok+jQbBEeU=
github.com/calmh/xdr v1.2.0/go.mod h1:vO5+lCx/8xz7Ekd/ZLf+xuy7c1x6FMO1pBJyjDebwyg=
github.com/ccding/go-stun v0.1.5 h1:qEM367nnezmj7dv+SdT52prv5x6HUTG3nlrjX5aitlo=
github.com/ccding/go-stun v0.1.5/go.mod h1:cCZjJ1J3WFSJV6Wj8Y9Di8JMTsEXh6uv2eNmLzKaUeM=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d h1:S2NE3iHSwP0XV47EEXL8mWmRdEfGscSJ+7EgePNgt0s=
@@ -32,15 +34,16 @@ github.com/chmduquesne/rollinghash v4.0.0+incompatible/go.mod h1:Uc2I36RRfTAf7Dg
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/d4l3k/messagediff v1.2.1 h1:ZcAIMYsUg0EAp9X+tt8/enBE/Q8Yd5kzPynLyKptt9U=
github.com/d4l3k/messagediff v1.2.1/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BMXYYRWTLOJKlh+lOBt6nUQgXAfB7oVIQt5cNreqSLI=
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:rZfgFAXFS/z/lEd6LJmf9HVZ1LkgYiHx5pHhV5DR16M=
github.com/ebitengine/purego v0.8.0 h1:JbqvnEzRvPpxhCJzJJ2y0RbiZ8nyjccVUrSM3q+GvvE=
github.com/ebitengine/purego v0.8.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
@@ -54,16 +57,16 @@ github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl5
github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-ldap/ldap/v3 v3.4.8 h1:loKJyspcRezt2Q3ZRMq2p/0v8iOurlmeXDPw6fikSvQ=
github.com/go-ldap/ldap/v3 v3.4.8/go.mod h1:qS3Sjlu76eHfHGpUdWkAXQTw4beih+cHsco2jXlIXrk=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -82,11 +85,12 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20240528025155-186aa0362fba h1:ql1qNgCyOB7iAEk8JTNM+zJrgIbnyCKX/wdlyPufP5g=
github.com/google/pprof v0.0.0-20240528025155-186aa0362fba/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
github.com/google/pprof v0.0.0-20241001023024-f4c0cfd0cf1d h1:Jaz2JzpQaQXyET0AjLBXShrthbpqMkhGiEfkcQAiAUs=
github.com/google/pprof v0.0.0-20241001023024-f4c0cfd0cf1d/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
@@ -124,20 +128,26 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/compress v1.17.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0=
github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0=
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
github.com/maruel/panicparse/v2 v2.3.1 h1:NtJavmbMn0DyzmmSStE8yUsmPZrZmudPH7kplxBinOA=
github.com/maruel/panicparse/v2 v2.3.1/go.mod h1:s3UmQB9Fm/n7n/prcD2xBGDkwXD6y2LeZnhbEXvs9Dg=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
@@ -147,10 +157,10 @@ github.com/maxbrunsfeld/counterfeiter/v6 v6.8.1/go.mod h1:eyp4DdUJAKkr9tvxR3jWhw
github.com/maxmind/geoipupdate/v6 v6.1.0 h1:sdtTHzzQNJlXF5+fd/EoPTucRHyMonYt/Cok8xzzfqA=
github.com/maxmind/geoipupdate/v6 v6.1.0/go.mod h1:cZYCDzfMzTY4v6dKRdV7KTB6SStxtn3yFkiJ1btTGGc=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
github.com/miscreant/miscreant.go v0.0.0-20200214223636-26d376326b75 h1:cUVxyR+UfmdEAZGJ8IiKld1O0dbGotEnkMolG5hfMSY=
github.com/miscreant/miscreant.go v0.0.0-20200214223636-26d376326b75/go.mod h1:pBbZyGwC5i16IBkjVKoy/sznA8jPD/K9iedwe1ESE6w=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY=
@@ -161,18 +171,18 @@ github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vv
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA=
github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To=
github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4=
github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk=
github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/oschwald/geoip2-golang v1.11.0 h1:hNENhCn1Uyzhf9PTmquXENiWS6AlxAEnBII6r8krA3w=
github.com/oschwald/geoip2-golang v1.11.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo=
github.com/oschwald/maxminddb-golang v1.13.0 h1:R8xBorY71s84yO06NgTmQvqvTvlS/bnYZrrWX1MElnU=
github.com/oschwald/maxminddb-golang v1.13.0/go.mod h1:BU0z8BfFVhi1LQaonTwwGQlsHUEu9pWNdMfmq4ztm0o=
github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -186,16 +196,18 @@ github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI=
github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.54.0 h1:ZlZy0BgJhTwVZUn7dLOkwCZHUkrAqd3WYtcFCWnM1D8=
github.com/prometheus/common v0.54.0/go.mod h1:/TQgMJP5CuVYveyT7n/0Ix8yLNNXy9yRSkhnLTHPDIQ=
github.com/prometheus/common v0.60.0 h1:+V9PAREWNvJMAuJ1x1BaWl9dewMW4YrHZQbx0sJNllA=
github.com/prometheus/common v0.60.0/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/quic-go/quic-go v0.44.0 h1:So5wOr7jyO4vzL2sd8/pD9Kesciv91zSk8BoFngItQ0=
github.com/quic-go/quic-go v0.44.0/go.mod h1:z4cx/9Ny9UtGITIPzmPTXh1ULfOyWh4qGQlpnPcWmek=
github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4=
github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/quic-go/quic-go v0.47.0 h1:yXs3v7r2bm1wmPTYNLKAAJTHMYkPEsfYJmTazXrCZ7Y=
github.com/quic-go/quic-go v0.47.0/go.mod h1:3bCapYsJvXGZcipOHuu7plYtaV6tnF+z7wIFsU0WK9E=
github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=
github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM=
@@ -208,8 +220,8 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8=
github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM=
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
github.com/shirou/gopsutil/v4 v4.24.9 h1:KIV+/HaHD5ka5f570RZq+2SaeFsb/pq+fp2DGNWYoOI=
github.com/shirou/gopsutil/v4 v4.24.9/go.mod h1:3fkaHNeYsUFCGZ8+9vZVWtbyM1k2eRnlL+bWO8Bxa/Q=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -230,6 +242,10 @@ github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDd
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48=
github.com/thejerf/suture/v4 v4.0.5 h1:F1E/4FZwXWqvlWDKEUo6/ndLtxGAUzMmNqkrMknZbAA=
github.com/thejerf/suture/v4 v4.0.5/go.mod h1:gu9Y4dXNUWFrByqRt30Rm9/UZ0wzRSt9AJS6xu/ZGxU=
github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY=
github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.22.15 h1:nuqt+pdC/KqswQKhETJjo7pvn/k4xMUxgW6liI7XpnM=
github.com/urfave/cli v1.22.15/go.mod h1:wSan1hmo5zeyLGBjRJbzRTNk8gwoYa2B9n4q9dmRIc0=
@@ -242,8 +258,8 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8=
go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
@@ -255,16 +271,16 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc h1:O9NuF4s+E/PvMIy+9IUZB9znFwUIXEWSstNjek6VpVg=
golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -282,16 +298,16 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -321,8 +337,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -336,10 +352,10 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
@@ -347,8 +363,8 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw=
golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE=
golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -362,8 +378,8 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@@ -372,8 +388,12 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkep
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=

View File

@@ -304,11 +304,7 @@ a.toggler:hover {
text-decoration: none;
}
/**
* Panel padding decrease
*/
.panel-collapse .panel-body {
.panel-body.less-padding {
padding: 5px;
}

View File

@@ -30,6 +30,7 @@
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "Una ordre externa gestiona la versió. Ha d'eliminar el fitxer de la carpeta compartida. Si el camí a l'aplicació conté espais, s'ha de citar.",
"Anonymous Usage Reporting": "Informe anònim d'ús",
"Anonymous usage report format has changed. Would you like to move to the new format?": "El format de l'informe d'ús anònim ha canviat. Voleu canviar a aquest nou format?",
"Applied to LAN": "Aplicat a LAN",
"Apply": "Aplica",
"Are you sure you want to override all remote changes?": "Esteu segur que voleu anul·lar tots els canvis remots?",
"Are you sure you want to permanently delete all these files?": "Segur que voleu esborrar tots aquests fitxers permanentment?",
@@ -38,6 +39,7 @@
"Are you sure you want to restore {%count%} files?": "Segur que voleu restaurar {{count}} fitxers?",
"Are you sure you want to revert all local changes?": "Esteu segur que voleu revertir tots els canvis locals?",
"Are you sure you want to upgrade?": "Esteu segur que voleu actualitzar?",
"Authentication Required": "Autenticació necessària",
"Authors": "Autors",
"Auto Accept": "Auto Acceptar",
"Automatic Crash Reporting": "Informe automàtic d'incidències",
@@ -205,6 +207,7 @@
"Included Software": "Programari inclòs",
"Incoming Rate Limit (KiB/s)": "Límit de velocitat d'entrada (KiB/s)",
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Una configuració incorrecta pot malmetre els continguts de la teva carpeta i que Syncthing esdevingui inoperatiu.",
"Incorrect user name or password.": "Nom d'usuari o contrasenya incorrecta.",
"Internally used paths:": "Camins utilitzats internament:",
"Introduced By": "Introduït per",
"Introducer": "Introductor",
@@ -236,7 +239,10 @@
"Log File": "Fitxer de registre",
"Log In": "Inicia la sessió",
"Log Out": "Tanca la sessió",
"Log in to see paths information.": "Inicieu sessió per veure la informació dels camins.",
"Log in to see version information.": "Inicieu sessió per veure la informació de la versió.",
"Log tailing paused. Scroll to the bottom to continue.": "S'ha posat en pausa el seguiment del registre. Desplaceu-vos cap a la part inferior per continuar.",
"Login failed, see Syncthing logs for details.": "No s'ha pogut iniciar la sessió; consulteu els registres de Syncthing per obtenir més informació.",
"Logs": "Registres",
"Major Upgrade": "Actualització major",
"Mass actions": "Accions massives",
@@ -432,11 +438,13 @@
"The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.": "L'interval, en segons, per executar la neteja al directori de versions. Zero per desactivar la neteja periòdica.",
"The maximum age must be a number and cannot be blank.": "La màxima antiguitat ha de ser un número i no pot estar en blanc.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Temps màxim en mantenir una versió (en dies, si es deixa en 0 es mantenen les versions per sempre).",
"The number of connections must be a non-negative number.": "El nombre de connexions ha de ser un nombre no negatiu.",
"The number of days must be a number and cannot be blank.": "El nombre de dies ha de ser un número i no pot estar en blanc.",
"The number of days to keep files in the trash can. Zero means forever.": "El nombre de dies per guardar els fitxers a la paperera. Zero significa per sempre.",
"The number of old versions to keep, per file.": "El nombre de versions antigues que es mantenen per fitxer.",
"The number of versions must be a number and cannot be blank.": "El nombre de versions ha de ser un número i no es pot deixar en blanc.",
"The path cannot be blank.": "El camí no pot estar en blanc.",
"The rate limit is applied to the accumulated traffic of all connections to this device.": "El límit de velocitat s'aplica al trànsit acumulat de totes les connexions a aquest dispositiu.",
"The rate limit must be a non-negative number (0: no limit)": "El límit de velocitat ha de ser un nombre positiu (0: sense límit)",
"The remote device has not accepted sharing this folder.": "El dispositiu remot no ha acceptat compartir aquesta carpeta.",
"The remote device has paused this folder.": "El dispositiu remot ha posat en pausa aquesta carpeta.",
@@ -484,6 +492,8 @@
"User": "Usuari",
"User Home": "Carpeta d'inici de l'usuari",
"Username/Password has not been set for the GUI authentication. Please consider setting it up.": "El nom d'usuari/contrasenya no s'ha establert per a l'autenticació de la GUI. Penseu a configurar-lo.",
"Using a QUIC connection over LAN": "Utilitzant una connexió QUIC per LAN",
"Using a QUIC connection over WAN": "Utilitzant una connexió QUIC a través de WAN",
"Using a direct TCP connection over LAN": "Utilitzant una connexió TCP directa per LAN",
"Using a direct TCP connection over WAN": "Utilitzant una connexió TCP directa a través de WAN",
"Version": "Versió",
@@ -504,6 +514,7 @@
"Watching for changes discovers most changes without periodic scanning.": "Observant els canvis descobreix la majoria dels canvis sense escanejar periòdicament.",
"When adding a new device, keep in mind that this device must be added on the other side too.": "Quan s'afegeix un nou dispositiu, recorda que aquest dispositiu tambè s'ha d'afegir a l'altre banda.",
"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.": "Quan s'afegeix una nova carpeta recorda que el ID d'aquesta s'utilitza per lligar repositoris entre els dispositius. Es distingeix entre majúscules i minúscules i ha de ser exactament iguals entre tots els dispositius.",
"When set to more than one on both devices, Syncthing will attempt to establish multiple concurrent connections. If the values differ, the highest will be used. Set to zero to let Syncthing decide.": "Quan s'estableix en més d'un als dos dispositius, Syncthing intentarà establir diverses connexions simultànies. Si els valors són diferents, s'utilitzarà el més alt. Establiu a zero per deixar que Sincronització decideixi.",
"Yes": "Si",
"Yesterday": "Ahir",
"You can also copy and paste the text into a new message manually.": "També podeu copiar i enganxar el text en un missatge nou manualment.",

View File

@@ -1,5 +1,5 @@
{
"A device with that ID is already added.": "Zařízení s takovým identifikátorem již přidáno.",
"A device with that ID is already added.": "Zařízení s takovým identifikátorem je již přidáno.",
"A negative number of days doesn't make sense.": "Záporný počet dní nedává smysl.",
"A new major version may not be compatible with previous versions.": "Nová hlavní verze nemusí být kompatibilní s předchozími verzemi.",
"API Key": "Klíč k API",
@@ -477,8 +477,8 @@
"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}}).",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Varování: toto umístění je podsložkou existující „{{otherFolder}}“.",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Varování, toto umístění e podsložkou existující „{{otherFolderLabel}}“ ({{otherFolder}}).",
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "Pozor: Pokud používáte externí sledování změn jako {{syncthingInotify}}, měly byste se ujistit, že je toto sledování vypnuto.",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Varování, toto umístění je podsložkou existující „{{otherFolderLabel}}“ ({{otherFolder}}).",
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "Pozor: Pokud používáte externí sledování změn jako {{syncthingInotify}}, měli byste se ujistit, že je toto sledování vypnuto.",
"Watch for Changes": "Sledovat změny",
"Watching for Changes": "Sledování změn",
"Watching for changes discovers most changes without periodic scanning.": "Sledování změn odhalí většinu změn ještě před periodickým skenováním.",

View File

@@ -187,7 +187,7 @@
"GUI Theme": "GUI-tema",
"General": "Generelt",
"Generate": "Opret",
"Global Discovery": "Globalt opslag",
"Global Discovery": "Globalt opdagelse",
"Global Discovery Servers": "Globale opslagsservere",
"Global State": "Global tilstand",
"Help": "Hjælp",
@@ -386,6 +386,7 @@
"Staggered File Versioning": "Forskudt filversionering",
"Start Browser": "Start browser",
"Statistics": "Statistikker",
"Stay logged in": "Forbliv logget ind",
"Stopped": "Stoppet",
"Stores and syncs only encrypted data. Folders on all connected devices need to be set up with the same password or be of type \"{%receiveEncrypted%}\" too.": "Gemmer og synkroniserer kun krypterede data. Mapper på alle tilsluttede enheder skal være oprettet med samme adgangskode eller også være af typen \"{{receiveEncrypted}}\".",
"Subject:": "Emne:",

View File

@@ -302,7 +302,7 @@
"Prefix indicating that the pattern should be matched without case sensitivity": "Prefijo que indica que el patrón debe coincidir sin distinguir mayúsculas de minúsculas",
"Preparing to Sync": "Preparándose para Sincronizar",
"Preview": "Vista previa",
"Preview Usage Report": "Informe de uso de vista previa",
"Preview Usage Report": "Previsualizar el Informe de Uso",
"QR code": "Código QR",
"QUIC LAN": "QUIC LAN",
"QUIC WAN": "QUIC WAN",

View File

@@ -0,0 +1,555 @@
{
"A device with that ID is already added.": "Tá gléas leis an aitheantas sin curtha leis cheana féin.",
"A negative number of days doesn't make sense.": "Níl ciall le líon diúltach laethanta.",
"A new major version may not be compatible with previous versions.": "B'fhéidir nach bhfuil mórleagan nua comhoiriúnach le leaganacha roimhe seo.",
"API Key": "Eochair API",
"About": "Maidir",
"Action": "Gníomh",
"Actions": "Gníomhartha",
"Active filter rules": "Rialacha gníomhacha scagaire",
"Add": "Cuir Leis",
"Add Device": "Cuir Gléas Leis",
"Add Folder": "Cuir Fillteán Leis",
"Add Remote Device": "Cuir Cianghléas Leis",
"Add devices from the introducer to our device list, for mutually shared folders.": "Cuir gléasanna ón introducer lenár liosta gléasanna, le haghaidh fillteáin a roinntear go frithpháirteach.",
"Add filter entry": "Cuir iontráil scagaire leis",
"Add ignore patterns": "Cuir patrúin neamhairde leis",
"Add new folder?": "Cuir fillteán nua leis?",
"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.": "Ina theannta sin méadófar an t-eatramh iomlán athscanta (amanna 60, i.e. mainneachtain nua 1h). Is féidir leat é a chumrú de láimh do gach fillteán níos déanaí tar éis Uimh.",
"Address": "Seoladh",
"Addresses": "Seoltaí",
"Advanced": "Ardrang",
"Advanced Configuration": "Ardchumraíocht",
"All Data": "Na Sonraí Go Léir",
"All Time": "Gach Am",
"All folders shared with this device must be protected by a password, such that all sent data is unreadable without the given password.": "Ní mór gach fillteán a roinntear leis an ngléas seo a chosaint le pasfhocal, ionas nach féidir na sonraí go léir a sheoltar a léamh gan an pasfhocal tugtha.",
"Allow Anonymous Usage Reporting?": "Ceadaigh Tuairisciú Úsáide Gan Ainm?",
"Allowed Networks": "Líonraí Ceadaithe",
"Alphabetic": "Aibítreach",
"Altered by ignoring deletes.": "Athraithe trí neamhaird a dhéanamh ar scriosadh.",
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "Láimhseálann ordú seachtrach an leagan. Caithfidh sé an comhad a bhaint den fhillteán comhroinnte. Má tá spásanna sa chosán chuig an bhfeidhmchlár, ba chóir é a lua.",
"Anonymous Usage Reporting": "Tuairisciú Úsáide Gan Ainm",
"Anonymous usage report format has changed. Would you like to move to the new format?": "Tá athrú tagtha ar fhormáid na tuarascála úsáide gan ainm. Ar mhaith leat bogadh go dtí an fhormáid nua?",
"Applied to LAN": "Curtha i bhfeidhm ar LAN",
"Apply": "Iarratas a dhéanamh",
"Are you sure you want to override all remote changes?": "An bhfuil tú cinnte go bhfuil fonn ort gach athrú iargúlta a shárú?",
"Are you sure you want to permanently delete all these files?": "An bhfuil tú cinnte go bhfuil fonn ort na comhaid seo go léir a scriosadh go buan?",
"Are you sure you want to remove device {%name%}?": "An bhfuil tú cinnte go bhfuil fonn ort an gléas {{name}}a bhaint?",
"Are you sure you want to remove folder {%label%}?": "An bhfuil tú cinnte go bhfuil fonn ort fillteán {{label}}a bhaint?",
"Are you sure you want to restore {%count%} files?": "An bhfuil tú cinnte go bhfuil fonn ort comhaid {{count}} a aischur?",
"Are you sure you want to revert all local changes?": "An bhfuil tú cinnte go bhfuil fonn ort na hathruithe áitiúla go léir a chur ar ais?",
"Are you sure you want to upgrade?": "An bhfuil tú cinnte go bhfuil fonn ort uasghrádú a dhéanamh?",
"Authentication Required": "Fíordheimhniú de dhíth",
"Authors": "Údair",
"Auto Accept": "Glac go hUathoibríoch",
"Automatic Crash Reporting": "Tuairisciú Uathoibríoch Tuairteála",
"Automatic upgrade now offers the choice between stable releases and release candidates.": "Cuireann uasghrádú uathoibríoch anois an rogha idir eisiúintí cobhsaí agus iarrthóirí scaoilte.",
"Automatic upgrades": "Uasghrádú uathoibríoch",
"Automatic upgrades are always enabled for candidate releases.": "Cumasaítear uasghrádú uathoibríoch i gcónaí le haghaidh eisiúintí iarrthóra.",
"Automatically create or share folders that this device advertises at the default path.": "Cruthaigh nó comhroinn fillteáin go huathoibríoch a fhógraíonn an gléas seo ag an gcosán réamhshocraithe.",
"Available debug logging facilities:": "Áiseanna logála dífhabhtaithe atá ar fáil:",
"Be careful!": "Bí cúramach!",
"Body:": "Comhlacht:",
"Bugs": "Fabhtanna",
"Cancel": "Cuir ar ceal",
"Changelog": "ChangelogName",
"Clean out after": "Glan amach tar éis",
"Cleaning Versions": "Leaganacha Glantacháin",
"Cleanup Interval": "Eatramh Glanta",
"Click to see full identification string and QR code.": "Cliceáil chun teaghrán aitheantais iomlán agus cód QR a fheiceáil.",
"Close": "Dún",
"Command": "Ordú",
"Comment, when used at the start of a line": "Trácht a dhéanamh, nuair a úsáidtear é ag tús líne",
"Compression": "Comhbhrú",
"Configuration Directory": "Comhadlann Cumraíochta",
"Configuration File": "Comhad Cumraíochta",
"Configured": "Cumraithe",
"Connected (Unused)": "Ceangailte (Neamhúsáidte)",
"Connection Error": "Earráid naisc",
"Connection Management": "Bainistíocht Ceangail",
"Connection Type": "Cineál Ceangail",
"Connections": "Naisc",
"Connections via relays might be rate limited by the relay": "D'fhéadfadh naisc trí athsheachadáin a bheith ráta teoranta ag an sealaíochta",
"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.": "Tá faire leanúnach le haghaidh athruithe ar fáil anois laistigh de Syncthing. Braithfidh sé seo athruithe ar an diosca agus eiseoidh sé scanadh ar na cosáin mhodhnaithe amháin. Is iad na buntáistí ná go ndéantar athruithe a iomadú níos tapúla agus go bhfuil gá le scanadh níos lú iomlán.",
"Copied from elsewhere": "Cóipeáladh ó áiteanna eile",
"Copied from original": "Cóipeáladh ón mbunleagan",
"Copied!": "Cóipeáladh!",
"Copy": "Cóipeáil",
"Copy failed! Try to select and copy manually.": "Theip ar chóip! Déan iarracht a roghnú agus a chóipeáil de láimh.",
"Currently Shared With Devices": "Comhroinnte faoi láthair le gléasanna",
"Custom Range": "Raon Saincheaptha",
"Danger!": "Contúirt!",
"Database Location": "Suíomh an Bhunachair Sonraí",
"Debugging Facilities": "Áiseanna Dífhabhtaithe",
"Default": "Réamhshocrú",
"Default Configuration": "Cumraíocht Réamhshocraithe",
"Default Device": "Gléas Réamhshocraithe",
"Default Folder": "Fillteán Réamhshocraithe",
"Default Ignore Patterns": "Patrúin Neamhairde Réamhshocraithe",
"Defaults": "Réamhshocruithe",
"Delete": "Scrios",
"Delete Unexpected Items": "Scrios Míreanna Gan Choinne",
"Deleted {%file%}": "Scriosta {{file}}",
"Deselect All": "Díroghnaigh Gach Rud",
"Deselect devices to stop sharing this folder with.": "Díroghnaigh gléasanna chun stop a chur le comhroinnt an fhillteáin seo.",
"Deselect folders to stop sharing with this device.": "Díroghnaigh fillteáin chun stop a chur le comhroinnt leis an ngléas seo.",
"Device": "Gléas",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Device \"{{name}}\" ({{device}} at {{address}}) ag iarraidh ceangal. Cuir gléas nua leis?",
"Device Certificate": "Teastas Gléis",
"Device ID": "Aitheantas gléis",
"Device Identification": "Aitheantas gléis",
"Device Name": "Ainm an Ghléis",
"Device Status": "Stádas an Ghléis",
"Device is untrusted, enter encryption password": "Tá an gléas neamhiontaofa, iontráil focal faire criptithe",
"Device rate limits": "Teorainneacha ráta gléis",
"Device that last modified the item": "Gléas a d'athraigh an mhír go deireanach",
"Devices": "Gléasanna",
"Disable Crash Reporting": "Díchumasaigh Tuairisciú Tuairteála",
"Disabled": "Díchumasaithe",
"Disabled periodic scanning and disabled watching for changes": "Scanadh tréimhsiúil díchumasaithe agus daoine faoi mhíchumas ag faire ar athruithe",
"Disabled periodic scanning and enabled watching for changes": "Scanadh tréimhsiúil díchumasaithe agus cumasaíodh faire le haghaidh athruithe",
"Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:": "Scanadh tréimhsiúil díchumasaithe agus theip ar shocrú faire le haghaidh athruithe, ag triail gach 1m:",
"Disables comparing and syncing file permissions. Useful on systems with nonexistent or custom permissions (e.g. FAT, exFAT, Synology, Android).": "Díchumasaigh comparáid agus sioncronú ceadanna comhaid. Úsáideach ar chórais le ceadanna nonexistent nó saincheaptha (m.sh. FAT, exFAT, Synology, Android).",
"Discard": "Ná Sábháil",
"Disconnected": "Dícheangailte",
"Disconnected (Inactive)": "Dícheangailte (Neamhghníomhach)",
"Disconnected (Unused)": "Dícheangailte (Neamhúsáidte)",
"Discovered": "Aimsíodh",
"Discovery": "Fionnachtain",
"Discovery Failures": "Teipeanna Fionnachtana",
"Discovery Status": "Stádas Fionnachtana",
"Dismiss": "Ruaig",
"Do not add it to the ignore list, so this notification may recur.": "Ná cuir leis an liosta neamhairde é, mar sin d'fhéadfadh an fógra seo tarlú arís.",
"Do not restore": "Ná hathchóirigh",
"Do not restore all": "Ná cuir gach rud ar ais",
"Do you want to enable watching for changes for all your folders?": "An bhfuil fonn ort féachaint ar athruithe do d'fhillteáin go léir?",
"Documentation": "Doiciméadú",
"Download Rate": "Ráta Íosluchtaithe",
"Downloaded": "Íoslódáilte",
"Downloading": "Á Íosluchtú",
"Edit": "Cuir in eagar",
"Edit Device": "Cuir Gléas in Eagar",
"Edit Device Defaults": "Cuir Réamhshocruithe gléis in Eagar",
"Edit Folder": "Cuir Fillteán in Eagar",
"Edit Folder Defaults": "Cuir Réamhshocruithe an Fhillteáin in Eagar",
"Editing {%path%}.": "Eagarthóireacht {{path}}.",
"Enable Crash Reporting": "Cumasaigh Tuairisciú Tuairteanna",
"Enable NAT traversal": "Cumasaigh traversal NAT",
"Enable Relaying": "Cumasaigh Athsheachadadh",
"Enabled": "Cumasaithe",
"Enables sending extended attributes to other devices, and applying incoming extended attributes. May require running with elevated privileges.": "Cumasaigh tréithe leathnaithe a sheoladh chuig gléasanna eile, agus tréithe sínte isteach a chur i bhfeidhm. D'fhéadfadh sé go mbeadh gá le rith le pribhléidí ardaithe.",
"Enables sending extended attributes to other devices, but not applying incoming extended attributes. This can have a significant performance impact. Always enabled when \"Sync Extended Attributes\" is enabled.": "Cumasaigh tréithe leathnaithe a sheoladh chuig gléasanna eile, ach gan tréithe leathnaithe ag teacht isteach a chur i bhfeidhm. D'fhéadfadh tionchar suntasach feidhmíochta a bheith aige seo. Cumasaíodh i gcónaí nuair a chumasaítear \"Sync Extended Attributes\".",
"Enables sending ownership information to other devices, and applying incoming ownership information. Typically requires running with elevated privileges.": "Cumasaíonn sé faisnéis úinéireachta a sheoladh chuig gléasanna eile, agus faisnéis úinéireachta isteach a chur i bhfeidhm. De ghnáth éilíonn rith le pribhléidí ardaithe.",
"Enables sending ownership information to other devices, but not applying incoming ownership information. This can have a significant performance impact. Always enabled when \"Sync Ownership\" is enabled.": "Cumasaíonn sé faisnéis úinéireachta a sheoladh chuig gléasanna eile, ach gan faisnéis úinéireachta isteach a chur i bhfeidhm. D'fhéadfadh tionchar suntasach feidhmíochta a bheith aige seo. Cumasaíodh i gcónaí nuair a chumasaítear \"Úinéireacht Shioncronaithe\".",
"Enter a non-negative number (e.g., \"2.35\") and select a unit. Percentages are as part of the total disk size.": "Iontráil uimhir neamh-dhiúltach (m.sh., \"2.35\") agus roghnaigh aonad. Tá céatadáin mar chuid de mhéid iomlán an diosca.",
"Enter a non-privileged port number (1024 - 65535).": "Cuir isteach uimhir phoirt neamhphribhléid (1024 - 65535).",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Cuir isteach camóg scartha (\"tcp://ip:port\", \"tcp://host:port\") seoltaí nó \"dinimiciúil\" chun fionnachtain uathoibríoch an tseolta a dhéanamh.",
"Enter ignore patterns, one per line.": "Iontráil patrúin neamhaird, ceann in aghaidh an líne.",
"Enter up to three octal digits.": "Iontráil suas le trí dhigit ochtach.",
"Error": "Earráid",
"Extended Attributes": "Tréithe Breisithe",
"Extended Attributes Filter": "Scagaire Tréithe Breisithe",
"External": "Seachtrach",
"External File Versioning": "Leagan Comhad Seachtrach",
"Failed Items": "Míreanna Teipthe",
"Failed to load file versions.": "Theip ar luchtú leaganacha comhaid.",
"Failed to load ignore patterns.": "Theip ar phatrúin neamhairde a luchtú.",
"Failed to setup, retrying": "Theip ar thus, ag triail arís",
"Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": "Táthar ag súil le mainneachtain ceangal le freastalaithe IPv6 mura bhfuil nascacht IPv6 ann.",
"File Pull Order": "Ordú Tarraingthe Comhad",
"File Versioning": "Leagan Comhaid",
"Files are moved to .stversions directory when replaced or deleted by Syncthing.": "Bogtar comhaid go comhadlann .stversions nuair a chuirtear sioncronú ina n-ionad nó nuair a scriostar iad.",
"Files are moved to date stamped versions in a .stversions directory when replaced or deleted by Syncthing.": "Bogtar comhaid go dtí seo leaganacha stampáilte i gcomhadlann .stversions nuair a chuirtear sioncronú ina n-ionad nó nuair a scriostar iad.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Cosnaítear comhaid ó athruithe a dhéantar ar ghléasanna eile, ach seolfar athruithe a dhéantar ar an ngléas seo chuig an gcuid eile den bhraisle.",
"Files are synchronized from the cluster, but any changes made locally will not be sent to other devices.": "Déantar comhaid a shioncrónú ón mbraisle, ach ní sheolfar aon athruithe a dhéantar go háitiúil chuig gléasanna eile.",
"Filesystem Watcher Errors": "Earráidí Faireoir an Chórais Comhad",
"Filter by date": "Scag de réir dáta",
"Filter by name": "Scag de réir ainm",
"Folder": "Fillteán",
"Folder ID": "Aitheantas an fhillteáin",
"Folder Label": "Lipéad Fillteáin",
"Folder Path": "Conair an Fhillteáin",
"Folder Status": "Stádas an Fhillteáin",
"Folder Type": "Cineál Fillteáin",
"Folder type \"{%receiveEncrypted%}\" can only be set when adding a new folder.": "Ní féidir cineál fillteáin \"{{receiveEncrypted}}\" a shocrú ach amháin nuair a chuirtear fillteán nua leis.",
"Folder type \"{%receiveEncrypted%}\" cannot be changed after adding the folder. You need to remove the folder, delete or decrypt the data on disk, and add the folder again.": "Ní féidir cineál fillteáin \"{{receiveEncrypted}}\" a athrú tar éis an fillteán a chur leis. Ní mór duit an fillteán a bhaint, na sonraí ar an diosca a scriosadh nó a dhíchriptiú, agus an fillteán a chur leis arís.",
"Folders": "Fillteáin",
"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.": "Maidir leis na fillteáin seo a leanas, tharla earráid agus tú ag tosú ag faire ar athruithe. Beidh sé a retried gach nóiméad, mar sin d'fhéadfadh na hearráidí dul amach go luath. Má leanann siad ar aghaidh, déan iarracht an bhuncheist a shocrú agus cabhair a iarraidh mura féidir leat.",
"Forever": "Go brách",
"Full Rescan Interval (s)": "Eatramh Rescan Iomlán (í)",
"GUI": "Comhéadan Grafach",
"GUI / API HTTPS Certificate": "Teastas GUI / API HTTPS",
"GUI Authentication Password": "Pasfhocal Fíordheimhnithe GUI",
"GUI Authentication User": "Úsáideoir Fíordheimhnithe GUI",
"GUI Authentication: Set User and Password": "Fíordheimhniú Grafach: Socraigh Úsáideoir agus Pasfhocal",
"GUI Listen Address": "Seoladh Éisteachta GUI",
"GUI Override Directory": "Comhadlann Sáraithe GUI",
"GUI Theme": "Téama grafach",
"General": "Ginearálta",
"Generate": "Gin",
"Global Discovery": "Fionnachtain Dhomhanda",
"Global Discovery Servers": "Freastalaithe Fionnachtana Domhanda",
"Global State": "Stát Domhanda",
"Help": "Cabhair",
"Hint: only deny-rules detected while the default is deny. Consider adding \"permit any\" as last rule.": "Leid: níor aimsíodh ach rialacha diúltaithe agus an réamhshocrú á séanadh. Smaoinigh ar \"cead ar bith\" a chur leis mar riail dheireanach.",
"Home page": "Leathanach baile",
"However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.": "Mar sin féin, léiríonn do shocruithe reatha go mb'fhéidir nár mhaith leat é a chumasú. Tá tuairisciú uathoibríoch tuairteála díchumasaithe againn duit.",
"Identification": "Aitheantas",
"If untrusted, enter encryption password": "Mura bhfuil muinín agat as, iontráil focal faire criptithe",
"If you want to prevent other users on this computer from accessing Syncthing and through it your files, consider setting up authentication.": "Más mian leat cosc a chur ar úsáideoirí eile ar an ríomhaire seo rochtain a fháil ar Syncthing agus trí do chuid comhad, smaoinigh ar fhíordheimhniú a bhunú.",
"Ignore": "Déan neamhaird de",
"Ignore Patterns": "Déan neamhaird de Phatrúin",
"Ignore Permissions": "Déan neamhaird de cheadanna",
"Ignore patterns can only be added after the folder is created. If checked, an input field to enter ignore patterns will be presented after saving.": "Ní féidir patrúin neamhaird a chur leis ach amháin tar éis an fillteán a chruthú. Má sheiceáil, beidh réimse ionchur chun dul isteach patrúin neamhaird a chur i láthair tar éis a shábháil.",
"Ignored Devices": "Gléasanna Neamhaird",
"Ignored Folders": "Fillteáin Neamhaird",
"Ignored at": "Neamhaird déanta air ag",
"Included Software": "Bogearraí san áireamh",
"Incoming Rate Limit (KiB/s)": "Teorainn Ráta Isteach (KiB/s)",
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "D'fhéadfadh cumraíocht mhícheart dochar a dhéanamh d'inneachar d'fhillteáin agus sioncronú a dhéanamh do-oibrithe.",
"Incorrect user name or password.": "Ainm úsáideora nó pasfhocal mícheart.",
"Internally used paths:": "Cosáin a úsáidtear go hinmheánach:",
"Introduced By": "Tugtha isteach ag",
"Introducer": "Réamhrá",
"Introduction": "Réamhrá",
"Inversion of the given condition (i.e. do not exclude)": "Inbhéartú an choinníll áirithe (i.e. ná cuir as an áireamh)",
"Keep Versions": "Coinnigh Leaganacha",
"LDAP": "LDAPName",
"Largest First": "An Chéad Cheann is Mó",
"Last 30 Days": "Laethanta 30 seo caite",
"Last 7 Days": "Laethanta 7 seo caite",
"Last Month": "An Mhí Seo Caite",
"Last Scan": "An scanadh is déanaí",
"Last seen": "Feicthe go deireanach",
"Latest Change": "An tAthrú is Déanaí",
"Learn more": "Faigh tuilleadh eolais",
"Learn more at {%url%}": "Tuilleadh eolais ag {{url}}",
"Limit": "Teorainn",
"Listener Failures": "Teipeanna an Éisteora",
"Listener Status": "Stádas an éisteora",
"Listeners": "Éisteoirí",
"Loading data...": "Sonraí á luchtú...",
"Loading...": "Á Luchtú...",
"Local Additions": "Breiseanna Logánta",
"Local Discovery": "Fionnachtain Áitiúil",
"Local State": "Stát Áitiúil",
"Local State (Total)": "Stát Áitiúil (Iomlán)",
"Locally Changed Items": "Míreanna Athraithe go hÁitiúil",
"Log": "Logáil",
"Log File": "Logchomhad",
"Log In": "Logáil isteach",
"Log Out": "Logáil Amach",
"Log in to see paths information.": "Logáil isteach chun faisnéis faoi chosáin a fheiceáil.",
"Log in to see version information.": "Logáil isteach chun faisnéis faoin leagan a fheiceáil.",
"Log tailing paused. Scroll to the bottom to continue.": "Bhí eireaball loga ar sos. Scrollaigh go bun an leathanaigh chun leanúint ar aghaidh.",
"Login failed, see Syncthing logs for details.": "Theip ar logáil isteach, féach logaí sioncronaithe le haghaidh sonraí.",
"Logs": "Logchomhaid",
"Major Upgrade": "Uasghrádú Mór",
"Mass actions": "Gníomhartha Aifrinn",
"Maximum Age": "Aois Uasta",
"Maximum single entry size": "Uasmhéid iontrála aonair",
"Maximum total size": "Uasmhéid iomlán",
"Metadata Only": "Meiteashonraí Amháin",
"Minimum Free Disk Space": "Íosspás Diosca Saor in Aisce",
"Mod. Device": "Mòd. Gléas",
"Mod. Time": "Mòd. Am",
"More than a month ago": "Níos mó ná mí ó shin",
"More than a week ago": "Níos mó ná seachtain ó shin",
"More than a year ago": "Níos mó ná bliain ó shin",
"Move to top of queue": "Bog go barr na scuaine",
"Multi level wildcard (matches multiple directory levels)": "Saoróg il-leibhéil (meaitseálann sé leibhéil eolaire éagsúla)",
"Never": "Riamh",
"New Device": "Gléas Nua",
"New Folder": "Fillteán Nua",
"Newest First": "An Chéad Cheann is Nuaí",
"No": "Ní hea",
"No File Versioning": "Gan Leagan Comhaid",
"No files will be deleted as a result of this operation.": "Ní scriosfar aon chomhaid mar thoradh ar an oibríocht seo.",
"No rules set": "Níl aon rialacha leagtha síos",
"No upgrades": "Gan uasghrádú",
"Not shared": "Gan roinnt",
"Notice": "Fógra",
"Number of Connections": "Líon na nasc",
"OK": "Ceart go leor",
"Off": "As",
"Oldest First": "An Chéad duine is sine",
"Optional descriptive label for the folder. Can be different on each device.": "Lipéad tuairisciúil roghnach don fhillteán. Is féidir a bheith difriúil ar gach gléas.",
"Options": "Roghanna",
"Out of Sync": "As Sioncronú",
"Out of Sync Items": "As Míreanna Sioncronaithe",
"Outgoing Rate Limit (KiB/s)": "Teorainn Ráta Amach (KiB/s)",
"Override": "Sáraigh",
"Override Changes": "Sáraigh Athruithe",
"Ownership": "Úinéireacht",
"Password": "Pasfhocal",
"Path": "Conair",
"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": "Conair chuig an bhfillteán ar an ríomhaire logánta. Cruthófar é mura bhfuil sé ann. Is féidir an carachtar tilde (~) a úsáid mar aicearra le haghaidh",
"Path where versions should be stored (leave empty for the default .stversions directory in the shared folder).": "Conair inar chóir leaganacha a stóráil (fág folamh don chomhadlann réamhshocraithe .stversions san fhillteán comhroinnte).",
"Paths": "Cosáin",
"Pause": "ginideach: Dhún na nGall",
"Pause All": "Cuir Gach Rud ar Sos",
"Paused": "Curtha ar sos",
"Paused (Unused)": "Sosanna (ligthe i ndearmad)",
"Pending changes": "Athruithe ar feitheamh",
"Periodic scanning at given interval and disabled watching for changes": "Scanadh tréimhsiúil ag eatramh áirithe agus daoine faoi mhíchumas ag faire ar athruithe",
"Periodic scanning at given interval and enabled watching for changes": "Scanadh tréimhsiúil ag eatramh áirithe agus cumasaíodh faire le haghaidh athruithe",
"Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:": "Scanadh tréimhsiúil ag eatramh áirithe agus theip ar shocrú faire le haghaidh athruithe, ag triail gach 1m:",
"Permanently add it to the ignore list, suppressing further notifications.": "Cuir go buan é leis an liosta neamhairde, ag cur fógraí breise faoi chois.",
"Please consult the release notes before performing a major upgrade.": "Féach ar na nótaí scaoilte sula ndéanfaidh tú uasghrádú mór.",
"Please set a GUI Authentication User and Password in the Settings dialog.": "Socraigh Úsáideoir Fíordheimhnithe GUI agus Pasfhocal sa dialóg Socruithe.",
"Please wait": "Fan, le do thoil",
"Prefix indicating that the file can be deleted if preventing directory removal": "Réimír a léiríonn gur féidir an comhad a scriosadh má chuirtear cosc ar bhaint eolaire",
"Prefix indicating that the pattern should be matched without case sensitivity": "Réimír a léiríonn gur chóir an patrún a mheaitseáil gan íogaireacht cáis",
"Preparing to Sync": "Ag ullmhú le sioncronú",
"Preview": "Réamhamharc",
"Preview Usage Report": "Tuairisc ar Úsáid Réamhamhairc",
"QR code": "Cód QR",
"QUIC LAN": "LAN QUIC",
"QUIC WAN": "WAN QUIC",
"Quick guide to supported patterns": "Treoir thapa maidir le patrúin tacaithe",
"Random": "Randamach",
"Receive Encrypted": "Faigh Criptithe",
"Receive Only": "Faigh Amháin",
"Received data is already encrypted": "Tá sonraí a fuarthas criptithe cheana féin",
"Recent Changes": "Athruithe le Déanaí",
"Reduced by ignore patterns": "Laghdaithe ag patrúin neamhaird",
"Relay LAN": "Athsheachadán LAN",
"Relay WAN": "Athsheachadán WAN",
"Release Notes": "Nótaí Eisiúna",
"Release candidates contain the latest features and fixes. They are similar to the traditional bi-weekly Syncthing releases.": "Tá na gnéithe agus na socruithe is déanaí ag iarrthóirí scaoilte. Tá siad cosúil leis na heisiúintí traidisiúnta sioncronaithe dé-sheachtainiúla.",
"Remote Devices": "Gléasanna Cianda",
"Remote GUI": "Comhéadan Grafach cianda",
"Remove": "Bain",
"Remove Device": "Bain Gléas",
"Remove Folder": "Bain Fillteán",
"Required identifier for the folder. Must be the same on all cluster devices.": "Aitheantóir riachtanach don fhillteán. Ní mór a bheith mar an gcéanna ar gach gléas braisle.",
"Rescan": "Cealaigh",
"Rescan All": "Cealaigh Gach Rud",
"Rescans": "Athscannáin",
"Restart": "Atosaigh",
"Restart Needed": "Atosaigh de Dhíth",
"Restarting": "Ag atosú",
"Restore": "Athchóirigh",
"Restore Versions": "Athchóirigh Leaganacha",
"Resume": "Athdhúisigh",
"Resume All": "Atosaigh Gach Rud",
"Reused": "Athúsáid",
"Revert": "Fill",
"Revert Local Changes": "Fill athruithe logánta",
"Save": "Sábháil",
"Saving changes": "Athruithe á sábháil",
"Scan Time Remaining": "Scan an t-am atá fágtha",
"Scanning": "Scanadh",
"See external versioning help for supported templated command line parameters.": "Féach cabhair leagan seachtrach le haghaidh paraiméadair líne ordaithe teimpléadaithe tacaithe.",
"Select All": "Roghnaigh Gach Rud",
"Select a version": "Roghnaigh leagan",
"Select additional devices to share this folder with.": "Roghnaigh gléasanna breise chun an fillteán seo a chomhroinnt leis.",
"Select additional folders to share with this device.": "Roghnaigh fillteáin bhreise le comhroinnt leis an ngléas seo.",
"Select latest version": "Roghnaigh an leagan is déanaí",
"Select oldest version": "Roghnaigh an leagan is sine",
"Send & Receive": "Seol & Faigh",
"Send Extended Attributes": "Seol Tréithe Breisithe",
"Send Only": "Seol Amháin",
"Send Ownership": "Seol Úinéireacht",
"Set Ignores on Added Folder": "Socraigh neamhaird ar fhillteán breise",
"Settings": "Socruithe",
"Share": "Roinn",
"Share Folder": "Comhroinn Fillteán",
"Share by Email": "Comhroinn trí Ríomhphost",
"Share by SMS": "Comhroinn de réir SMS",
"Share this folder?": "Comhroinn an fillteán seo?",
"Shared Folders": "Fillteáin Chomhroinnte",
"Shared With": "Roinnte le",
"Sharing": "Comhroinnt",
"Show ID": "Taispeáin Aitheantas",
"Show QR": "Taispeáin QR",
"Show detailed discovery status": "Taispeáin stádas mionsonraithe aimsithe",
"Show detailed listener status": "Taispeáin stádas mionsonraithe an éisteora",
"Show diff with previous version": "Taispeáin diff leis an leagan roimhe seo",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Taispeántar é in ionad ID gléis i stádas na braisle. Fógrófar é do ghléasanna eile mar ainm réamhshocraithe roghnach.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Taispeántar é in ionad ID gléis i stádas na braisle. Déanfar é a nuashonrú go dtí an t-ainm a fhógraíonn an gléas má fhágtar folamh é.",
"Shutdown": "Múchadh",
"Shutdown Complete": "Múchadh Críochnaithe",
"Simple": "Simplí",
"Simple File Versioning": "Leagan Simplí Comhad",
"Single level wildcard (matches within a directory only)": "Saoróg leibhéal aonair (meaitseálann sé laistigh d'eolaire amháin)",
"Size": "Méid",
"Smallest First": "An Chéad Cheann is Lú",
"Some discovery methods could not be established for finding other devices or announcing this device:": "Níorbh fhéidir roinnt modhanna aimsithe a bhunú chun gléasanna eile a aimsiú nó chun an gléas seo a fhógairt:",
"Some items could not be restored:": "Níorbh fhéidir roinnt míreanna a aischur:",
"Some listening addresses could not be enabled to accept connections:": "Níorbh fhéidir roinnt seoltaí éisteachta a chumasú chun glacadh le naisc:",
"Source Code": "Cód Foinseach",
"Stable releases and release candidates": "Eisiúintí cobhsaí agus iarrthóirí scaoilte",
"Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "Tá moill thart ar choicís ar scaoileadh cobhsaí. Le linn an ama seo téann siad trí thástáil mar iarrthóirí scaoilte.",
"Stable releases only": "Eisiúintí cobhsaí amháin",
"Staggered": "Tuislithe",
"Staggered File Versioning": "Leagan Comhad Staggered",
"Start Browser": "Tosaigh Brabhsálaí",
"Statistics": "Staitisticí",
"Stay logged in": "Fan logáilte isteach",
"Stopped": "Stoptha",
"Stores and syncs only encrypted data. Folders on all connected devices need to be set up with the same password or be of type \"{%receiveEncrypted%}\" too.": "Ní dhéanann siopaí agus sioncronaithe ach sonraí criptithe. Ní mór fillteáin ar gach gléas nasctha a chur ar bun leis an bpasfhocal céanna nó a bheith de chineál \"{{receiveEncrypted}}\" freisin.",
"Subject:": "Ábhar:",
"Support": "Tacaíocht",
"Support Bundle": "Beart Tacaíochta",
"Sync Extended Attributes": "Sioncrónaigh Tréithe Breisithe",
"Sync Ownership": "Sioncrónaigh Úinéireacht",
"Sync Protocol Listen Addresses": "Sioncrónaigh Seoltaí Éisteachta an Phrótacail",
"Sync Status": "Stádas sioncronaithe",
"Syncing": "Sioncronú",
"Syncthing device ID for \"{%devicename%}\"": "Aitheantas gléis á shioncronú le haghaidh \"{{devicename}}\"",
"Syncthing has been shut down.": "Tá an sioncronú múchta.",
"Syncthing includes the following software or portions thereof:": "Áirítear leis an sioncronú na bogearraí seo a leanas nó codanna díobh:",
"Syncthing is Free and Open Source Software licensed as MPL v2.0.": "Tá Syncthing Free and Open Source Software ceadúnaithe mar MPL v2.0.",
"Syncthing is a continuous file synchronization program. It synchronizes files between two or more computers in real time, safely protected from prying eyes. Your data is your data alone and you deserve to choose where it is stored, whether it is shared with some third party, and how it's transmitted over the internet.": "Is clár leanúnach sioncronaithe comhad é sioncronú. Sioncrónaíonn sé comhaid idir dhá ríomhaire nó níos mó i bhfíor-am, cosanta go sábháilte ó shúile prying. Is é do chuid sonraí amháin do chuid sonraí agus is fiú duit a roghnú cá stóráiltear iad, cibé an roinntear iad le tríú páirtí éigin, agus conas a tharchuirtear iad ar an idirlíon.",
"Syncthing is listening on the following network addresses for connection attempts from other devices:": "Tá sioncronú ag éisteacht ar na seoltaí líonra seo a leanas le haghaidh iarrachtaí ceangail ó ghléasanna eile:",
"Syncthing is not listening for connection attempts from other devices on any address. Only outgoing connections from this device may work.": "Níl sioncronú ag éisteacht le hiarrachtaí ceangail ó ghléasanna eile ar aon seoladh. Ní féidir ach naisc amach ón ngléas seo a bheith ag obair.",
"Syncthing is restarting.": "Tá sioncronú ag atosú.",
"Syncthing is saving changes.": "Tá athruithe á sábháil ag sioncronú.",
"Syncthing is upgrading.": "Tá uasghrádú á dhéanamh ar shioncronú.",
"Syncthing now supports automatically reporting crashes to the developers. This feature is enabled by default.": "Tacaíonn sioncronú anois le tuairteanna a thuairisciú go huathoibríoch do na forbróirí. Cumasaítear an ghné seo de réir réamhshocraithe.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Is cosúil go bhfuil sioncronú síos, nó tá fadhb le do nasc Idirlín. Ag baint triail as…",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Is cosúil go bhfuil fadhb ag sioncronú d'iarratas a phróiseáil. Athnuaigh an leathanach nó atosaigh Sioncronú má leanann an fhadhb ar aghaidh.",
"TCP LAN": "LAN TCP",
"TCP WAN": "WAN TCP",
"Take me back": "Tóg ar ais mé",
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "Tá an seoladh GUI sáraithe ag roghanna tosaithe. Ní thiocfaidh athruithe anseo i bhfeidhm fad is atá an sárú i bhfeidhm.",
"The Syncthing Authors": "Na húdair sioncronaithe",
"The Syncthing admin interface is configured to allow remote access without a password.": "Tá an comhéadan riaracháin Syncthing cumraithe chun cianrochtain a cheadú gan phasfhocal.",
"The aggregated statistics are publicly available at the URL below.": "Tá na staitisticí comhiomlánaithe ar fáil go poiblí ag an URL thíos.",
"The cleanup interval cannot be blank.": "Ní féidir leis an eatramh glanta a bheith bán.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Sábháladh an chumraíocht ach níor cuireadh i ngníomh í. Ní mór sioncronú a atosú chun an chumraíocht nua a ghníomhachtú.",
"The device ID cannot be blank.": "Ní féidir aitheantas an ghléis a bheith bán.",
"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).": "Is féidir ID an ghléis atá le cur isteach anseo a fháil sa dialóg \"Gníomhartha > Taispeáin ID\" ar an ngléas eile. Tá spásanna agus daiseanna roghnach (neamhaird).",
"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.": "Seoltar an tuarascáil úsáide criptithe gach lá. Úsáidtear é chun ardáin choitianta, méideanna fillteáin agus leaganacha feidhmchláir a rianú. Má athraítear an tacar sonraí tuairiscithe, spreagfar thú leis an dialóg seo arís.",
"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.": "Níl cuma bhailí ar aitheantas an ghléis iontráilte. Ba chóir go mbeadh sé ina teaghrán carachtar 52 nó 56 comhdhéanta de litreacha agus uimhreacha, le spásanna agus dashes a bheith roghnach.",
"The folder ID cannot be blank.": "Ní féidir aitheantas an fhillteáin a bheith bán.",
"The folder ID must be unique.": "Caithfidh aitheantas an fhillteáin a bheith uathúil.",
"The folder content on other devices will be overwritten to become identical with this device. Files not present here will be deleted on other devices.": "Déanfar inneachar an fhillteáin ar ghléasanna eile a fhorscríobh le bheith comhionann leis an ngléas seo. Scriosfar comhaid nach bhfuil i láthair anseo ar ghléasanna eile.",
"The folder content on this device will be overwritten to become identical with other devices. Files newly added here will be deleted.": "Déanfar inneachar an fhillteáin ar an ngléas seo a fhorscríobh le bheith comhionann le gléasanna eile. Scriosfar comhaid nua a cuireadh leis anseo.",
"The folder path cannot be blank.": "Ní féidir le cosán an fhillteáin a bheith bán.",
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "Úsáidtear na heatraimh seo a leanas: don chéad uair an chloig coinnítear leagan gach 30 soicind, don chéad lá coinnítear leagan gach uair an chloig, don chéad 30 lá coinnítear leagan gach lá, go dtí an aois uasta a choinnítear leagan gach seachtain.",
"The following items could not be synchronized.": "Níorbh fhéidir na míreanna seo a leanas a shioncrónú.",
"The following items were changed locally.": "Athraíodh na míreanna seo a leanas go háitiúil.",
"The following methods are used to discover other devices on the network and announce this device to be found by others:": "Úsáidtear na modhanna seo a leanas chun gléasanna eile ar an líonra a aimsiú agus chun an gléas seo a fhógairt le haimsiú ag daoine eile:",
"The following text will automatically be inserted into a new message.": "Cuirfear an téacs seo a leanas isteach i dteachtaireacht nua go huathoibríoch.",
"The following unexpected items were found.": "Aimsíodh na míreanna gan choinne seo a leanas.",
"The interval must be a positive number of seconds.": "Caithfidh an t-eatramh a bheith ina líon dearfach soicind.",
"The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.": "An t-eatramh, i soicindí, le haghaidh glanadh a reáchtáil san eolaire leaganacha. Nialas chun glanadh tréimhsiúil a dhíchumasú.",
"The maximum age must be a number and cannot be blank.": "Caithfidh an aois uasta a bheith ina uimhir agus ní féidir léi a bheith bán.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "An t-uasmhéid ama chun leagan a choinneáil (i laethanta, socraithe go 0 chun leaganacha a choinneáil go deo).",
"The number of connections must be a non-negative number.": "Ní mór líon na nasc a bheith ina uimhir neamh-dhiúltach.",
"The number of days must be a number and cannot be blank.": "Caithfidh líon na laethanta a bheith ina uimhir agus ní féidir leo a bheith bán.",
"The number of days to keep files in the trash can. Zero means forever.": "Líon na laethanta chun comhaid a choinneáil sa bhruscar. Ciallaíonn nialas go deo.",
"The number of old versions to keep, per file.": "Líon na seanleaganacha le coinneáil, in aghaidh an chomhaid.",
"The number of versions must be a number and cannot be blank.": "Caithfidh líon na leaganacha a bheith ina uimhir agus ní féidir leo a bheith bán.",
"The path cannot be blank.": "Ní féidir leis an gcosán a bheith bán.",
"The rate limit is applied to the accumulated traffic of all connections to this device.": "Cuirtear an teorainn ráta i bhfeidhm ar thrácht carntha gach nasc leis an ngléas seo.",
"The rate limit must be a non-negative number (0: no limit)": "Ní mór an teorainn ráta a bheith ina uimhir neamh-dhiúltach (0: gan teorainn)",
"The remote device has not accepted sharing this folder.": "Níor ghlac an gléas cianda leis an bhfillteán seo a chomhroinnt.",
"The remote device has paused this folder.": "Chuir an gléas cianda an fillteán seo ar sos.",
"The rescan interval must be a non-negative number of seconds.": "Ní mór an t-eatramh rescan a bheith ina líon neamh-diúltach soicind.",
"There are no devices to share this folder with.": "Níl aon ghléas ann chun an fillteán seo a roinnt leis.",
"There are no file versions to restore.": "Níl aon leaganacha comhaid le cur ar ais.",
"There are no folders to share with this device.": "Níl aon fhillteáin le roinnt leis an ngléas seo.",
"They are retried automatically and will be synced when the error is resolved.": "Déantar iad a aisghabháil go huathoibríoch agus déanfar iad a shioncronú nuair a réitítear an earráid.",
"This Device": "An Gléas seo",
"This Month": "An mhí seo",
"This can easily give hackers access to read and change any files on your computer.": "Is féidir é seo a thabhairt go héasca hackers rochtain a léamh agus aon chomhaid ar do ríomhaire a athrú.",
"This device cannot automatically discover other devices or announce its own address to be found by others. Only devices with statically configured addresses can connect.": "Ní féidir leis an ngléas seo gléasanna eile a aimsiú go huathoibríoch ná a sheoladh féin a fhógairt le haimsiú ag daoine eile. Ní féidir ach le gléasanna le seoltaí atá cumraithe go statically ceangal.",
"This is a major version upgrade.": "Is uasghrádú mór leagan é seo.",
"This setting controls the free space required on the home (i.e., index database) disk.": "Rialaíonn an socrú seo an spás saor in aisce a theastaíonn ar an diosca baile (i.e., bunachar sonraí innéacs).",
"Time": "Am",
"Time the item was last modified": "Am a athraíodh an mhír go deireanach",
"To connect with the Syncthing device named \"{%devicename%}\", add a new remote device on your end with this ID:": "Chun ceangal a dhéanamh leis an ngléas Sioncronaithe darb ainm \"{{devicename}}\", cuir gléas cianda nua ar do dheireadh leis an ID seo:",
"To permit a rule, have the checkbox checked. To deny a rule, leave it unchecked.": "Chun riail a cheadú, seiceáil an ticbhosca. To deny a rule, é a fhágáil gan seiceáil.",
"Today": "Inniu",
"Trash Can": "Is féidir le bruscar",
"Trash Can File Versioning": "Is féidir le bruscar leagan comhaid",
"Type": "Cineál",
"UNIX Permissions": "Ceadanna UNIX",
"Unavailable": "Níl sé ar fáil",
"Unavailable/Disabled by administrator or maintainer": "Níl sé ar fáil/Díchumasaithe ag riarthóir nó cothaitheoir",
"Undecided (will prompt)": "Neamhdhearbhaithe (tabharfaidh sé leid)",
"Unexpected Items": "Míreanna Gan Choinne",
"Unexpected items have been found in this folder.": "Aimsíodh míreanna gan choinne san fhillteán seo.",
"Unignore": "Gan Neamhaird",
"Unknown": "Neamhaithnid",
"Unshared": "Neamhroinnte",
"Unshared Devices": "Gléasanna Neamhroinnte",
"Unshared Folders": "Fillteáin Neamhroinnte",
"Untrusted": "Neamhiontaofa",
"Up to Date": "Cothrom le dáta",
"Updated {%file%}": "Nuashonraithe {{file}}",
"Upgrade": "Uasghrádú",
"Upgrade To {%version%}": "Uasghrádú go {{version}}",
"Upgrading": "Uasghrádú",
"Upload Rate": "Ráta Uasluchtaithe",
"Uptime": "Aga fónaimh",
"Usage reporting is always enabled for candidate releases.": "Cumasaítear tuairisciú úsáide i gcónaí le haghaidh eisiúintí iarrthóra.",
"Use HTTPS for GUI": "Úsáid HTTPS le haghaidh GUI",
"Use notifications from the filesystem to detect changed items.": "Bain úsáid as fógraí ón gcóras comhad chun míreanna athraithe a bhrath.",
"User": "Úsáideoir",
"User Home": "Baile Úsáideora",
"Username/Password has not been set for the GUI authentication. Please consider setting it up.": "Níor socraíodh ainm úsáideora/pasfhocal le haghaidh fíordheimhniú an GUI. Smaoinigh, le do thoil, ar é a chur ar bun.",
"Using a QUIC connection over LAN": "Ag baint úsáide as nasc QUIC thar LAN",
"Using a QUIC connection over WAN": "Ag baint úsáide as nasc QUIC thar WAN",
"Using a direct TCP connection over LAN": "Ag baint úsáide as nasc TCP díreach thar LAN",
"Using a direct TCP connection over WAN": "Ag baint úsáide as nasc TCP díreach thar WAN",
"Version": "Leagan",
"Versions": "Leaganacha",
"Versions Path": "Conair na Leaganacha",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Scriostar leaganacha go huathoibríoch má tá siad níos sine ná an aois uasta nó má sháraíonn siad líon na gcomhad a cheadaítear in eatramh.",
"Waiting to Clean": "Ag Fanacht le Glanadh",
"Waiting to Scan": "Ag fanacht le Scanadh",
"Waiting to Sync": "Ag fanacht le sioncronú",
"Warning": "Rabhadh",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Rabhadh, is máthairchomhadlann é an cosán seo d'fhillteán atá ann cheana féin \"{{otherFolder}}\".",
"Warning, this path is a parent directory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Rabhadh, is máthairchomhadlann é an cosán seo d'fhillteán atá ann cheana féin \"{{otherFolderLabel}}\" ({{otherFolder}}).",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Rabhadh, is fochomhadlann é an cosán seo d'fhillteán atá ann cheana féin \"{{otherFolder}}\".",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Rabhadh, is fochomhadlann é an cosán seo d'fhillteán atá ann cheana féin \"{{otherFolderLabel}}\" ({{otherFolder}}).",
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "Rabhadh: Má tá uaireadóir seachtrach á úsáid agat mar {{syncthingInotify}}, ba chóir duit a chinntiú go bhfuil sé díghníomhachtaithe.",
"Watch for Changes": "Bí ag faire ar athruithe",
"Watching for Changes": "Ag Faire ar Athruithe",
"Watching for changes discovers most changes without periodic scanning.": "Má bhreathnaíonn tú ar athruithe, faightear amach an chuid is mó de na hathruithe gan scanadh tréimhsiúil.",
"When adding a new device, keep in mind that this device must be added on the other side too.": "Agus gléas nua á chur leis, coinnigh i gcuimhne go gcaithfear an gléas seo a chur leis ar an taobh eile freisin.",
"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.": "Agus fillteán nua á chur leis, coinnigh i gcuimhne go n-úsáidtear ID an Fhillteáin chun fillteáin a cheangal le chéile idir gléasanna. Tá siad cásíogair agus ní mór iad a mheaitseáil go díreach idir gach feiste.",
"When set to more than one on both devices, Syncthing will attempt to establish multiple concurrent connections. If the values differ, the highest will be used. Set to zero to let Syncthing decide.": "Nuair a shocraítear níos mó ná ceann amháin ar an dá ghléas, déanfaidh Syncthing iarracht naisc chomhthráthacha iolracha a bhunú. Má tá na luachanna difriúil, úsáidfear an ceann is airde. Socraigh go nialas chun ligean do Syncthing cinneadh a dhéanamh.",
"Yes": "Tá",
"Yesterday": "Inné",
"You can also copy and paste the text into a new message manually.": "Is féidir leat an téacs a chóipeáil agus a ghreamú isteach i dteachtaireacht nua de láimh.",
"You can also select one of these nearby devices:": "Is féidir leat ceann de na gléasanna in aice láimhe seo a roghnú freisin:",
"You can change your choice at any time in the Settings dialog.": "Is féidir leat do rogha a athrú ag am ar bith sa dialóg Socruithe.",
"You can read more about the two release channels at the link below.": "Is féidir leat tuilleadh a léamh faoin dá chainéal scaoilte ag an nasc thíos.",
"You have no ignored devices.": "Níl aon ghléasanna neamhairde agat.",
"You have no ignored folders.": "Níl aon fhillteáin neamhairde agat.",
"You have unsaved changes. Do you really want to discard them?": "Tá athruithe gan sábháil agat. An bhfuil tú cinnte gur mian leat iad a chaitheamh i leataobh?",
"You must keep at least one version.": "Ní mór duit leagan amháin ar a laghad a choinneáil.",
"You should never add or change anything locally in a \"{%receiveEncrypted%}\" folder.": "Níor chóir duit aon rud a chur leis nó a athrú go háitiúil i bhfillteán \"{{receiveEncrypted}}\".",
"Your SMS app should open to let you choose the recipient and send it from your own number.": "Ba chóir go mbeadh d'aip SMS oscailte chun ligean duit an faighteoir a roghnú agus é a sheoladh ó d'uimhir féin.",
"Your email app should open to let you choose the recipient and send it from your own address.": "Ba cheart d'aip ríomhphoist a oscailt chun ligean duit an faighteoir a roghnú agus é a sheoladh ó do sheoladh féin.",
"days": "laethanta",
"deleted": "scriosta",
"deny": "diúltú",
"directories": "Eolairí",
"file": "comhad",
"files": "comhaid",
"folder": "fillteán",
"full documentation": "doiciméadú iomlán",
"items": "míreanna",
"modified": "modhnaithe",
"permit": "cead",
"seconds": "soicind",
"theme": {
"name": {
"black": "Dubh",
"dark": "Dorcha",
"default": "Réamhshocrú",
"light": "Solas"
}
},
"unknown device": "gléas anaithnid",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} ag iarraidh fillteán a roinnt \"{{folder}}\".",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} ag iarraidh fillteán a roinnt \"{{folderlabel}}\" ({{folder}}).",
"{%reintroducer%} might reintroduce this device.": "D'fhéadfadh {{reintroducer}} an gléas seo a athbhunú."
}

View File

@@ -10,7 +10,7 @@
"Add Device": "Engadir dispositivo",
"Add Folder": "Engadir cartafol",
"Add Remote Device": "Engadir dispositivo remoto",
"Add devices from the introducer to our device list, for mutually shared folders.": "Engadir dispositivos desde o enviador ao noso dispositivo, para cartafoles mutuamente compartidos.",
"Add devices from the introducer to our device list, for mutually shared folders.": "Engadir dispositivos desde o enviador ao noso dispositivo, para cartafois mutuamente compartidos.",
"Add filter entry": "Engadir unha entrada ao filtro",
"Add ignore patterns": "Engadir patróns a ignorar",
"Add new folder?": "Engadir novo cartafol?",
@@ -21,7 +21,7 @@
"Advanced Configuration": "Configuración avanzada",
"All Data": "Todos os datos",
"All Time": "Todo o tempo",
"All folders shared with this device must be protected by a password, such that all sent data is unreadable without the given password.": "Todos os cartafoles compartidos con este dispositivo teñen que estar protexidos por un contrasinal, de modo que os datos enviados sexan ilexibles sen o constrasinal indicado.",
"All folders shared with this device must be protected by a password, such that all sent data is unreadable without the given password.": "Todos os cartafois compartidos con este dispositivo teñen que estar protexidos por un contrasinal, de modo que os datos enviados sexan ilexibles sen o constrasinal indicado.",
"Allow Anonymous Usage Reporting?": "Permitir o informe de uso anónimo?",
"Allowed Networks": "Redes permitidas",
"Alphabetic": "Alfabética",
@@ -155,5 +155,312 @@
"Help": "Axuda",
"Home page": "Páxina de inicio",
"Identification": "Identificación",
"LDAP": "LDAP"
"Incoming Rate Limit (KiB/s)": "Límite de Descaga (KiB/s)",
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Unha configuración incorrecta pode danar os contidos do teu cartafol e deixar Syncthing inutilizable.",
"Incorrect user name or password.": "Nome de usuario ou contrasinal incorrecto.",
"Inversion of the given condition (i.e. do not exclude)": "Inversión da condición dada (por exemplo, non excluír)",
"Keep Versions": "Manter Versións",
"LDAP": "LDAP",
"Largest First": "Máis Grande Primeiros",
"Last 30 Days": "Últimos 30 Días",
"Last 7 Days": "Últimos 7 Días",
"Last Month": "Último mes",
"Last Scan": "Último escaneamento",
"Last seen": "Visto por última vez",
"Latest Change": "Último cambio",
"Limit": "Límite",
"Loading data...": "Cargando datos...",
"Loading...": "Cargando...",
"Local Additions": "Adicións\tlocais",
"Local Discovery": "Descubrimento Local",
"Local State": "Estado Local",
"Local State (Total)": "Estado Local (Total)",
"Locally Changed Items": "Ítems Modificados Localmente",
"Log": "Rexistro",
"Log File": "Ficheiro de Rexistro",
"Log In": "Iniciar Sesión",
"Log Out": "Pechar Sesión",
"Log in to see paths information.": "Inicia sesión para ver información das rutas.",
"Log in to see version information.": "Inicia sesión para ver información da versión.",
"Login failed, see Syncthing logs for details.": "Fallou o inicio de sesión, vexa os rexistros de Syngthing para máis detalles.",
"Logs": "Rexistros",
"Major Upgrade": "Actualización Maior",
"Mass actions": "Accións en masa",
"Maximum Age": "Idade Máxima",
"Maximum total size": "Tamaño máximo total",
"Metadata Only": "Só Metadatos",
"Minimum Free Disk Space": "Espacio Mínimo Libre no Disco",
"More than a month ago": "Fai máis dun mes",
"More than a week ago": "Fai máis dunha semana",
"More than a year ago": "Fai máis dun ano",
"Move to top of queue": "Mover a enriba da cola",
"Never": "Nunca",
"New Device": "Dispositivo Novo",
"New Folder": "Cartafol Novo",
"Newest First": "Máis Novo Primeiro",
"No": "Non",
"No File Versioning": "Sen Versionado de Ficheiros",
"No files will be deleted as a result of this operation.": "Non se eliminará ningún ficheiro como resultado desta operación.",
"No rules set": "Sen regras",
"No upgrades": "Sen actualizacións",
"Not shared": "Non compartido",
"Number of Connections": "Número de Conexións",
"Oldest First": "Máis Vellos Primeiro",
"Optional descriptive label for the folder. Can be different on each device.": "Etiqueta descritiva opcional para o cartafol. Pode ser distinta en cada dispositivo",
"Options": "Opcións",
"Outgoing Rate Limit (KiB/s)": "Límite de Saída (KiB/s)",
"Override": "Sobrescribir",
"Override Changes": "Sobrescribir os Cambios",
"Ownership": "Propiedade",
"Password": "Contrasinal",
"Path": "Ruta",
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Ruta ao cartafol no computador local. Crearase de non existir. A tilde (~) pode usarse como atallo para<",
"Path where versions should be stored (leave empty for the default .stversions directory in the shared folder).": "Ruta onde deben gardarse as versión (deixar baleiro para o directorio .stversions por defecto no cartafol compartido).",
"Paths": "Rutas",
"Pause": "Parar",
"Pause All": "Parar Todas",
"Paused": "Parada",
"Paused (Unused)": "Parada (Sen uso)",
"Pending changes": "Cambios pendentes",
"Permanently add it to the ignore list, suppressing further notifications.": "Engadilo permanentemente á lista de ignorados, suprimindo notificacións futuras.",
"Please consult the release notes before performing a major upgrade.": "Por favor consulte as notas de lanzamento antes de realizar unha actualización maior.",
"Please set a GUI Authentication User and Password in the Settings dialog.": "Por favor configure un Usuario e Contrasinal de Autenticación para a GUI no diálogo de Configuración.",
"Please wait": "Por favor espere",
"Prefix indicating that the pattern should be matched without case sensitivity": "Prefixo que indica que o patrón debe coincidir sen distinguir maiúsculas e minúsculas",
"Preparing to Sync": "Preparandose para Sincronizar",
"Preview": "Vista previa",
"Preview Usage Report": "Vista Previa do Informe de Uso",
"QR code": "Código QR",
"QUIC LAN": "QUIC LAN",
"QUIC WAN": "QUIC WAN",
"Quick guide to supported patterns": "Guía rápida dos patróns soportados",
"Random": "Aleatorio",
"Receive Only": "Só Recibir",
"Received data is already encrypted": "Os datos recibidos xa están encriptados",
"Recent Changes": "Cambios Recentes",
"Reduced by ignore patterns": "Reducido por patróns de ignorar",
"Relay LAN": "Relevo LAN",
"Relay WAN": "Relevo WAN",
"Release Notes": "Notas de Lanzamento",
"Release candidates contain the latest features and fixes. They are similar to the traditional bi-weekly Syncthing releases.": "Os candidatos de lanzamento conteñen as últimas versións e arranxos. Son parecidas aos lanzamentos bisemanais tradicionais de Syncthing.",
"Remote Devices": "Dispositivos Remotos",
"Remote GUI": "GUI Remota",
"Remove": "Quitar",
"Remove Device": "Quitar o Dispositivo",
"Remove Folder": "Quitar o Cartafol",
"Required identifier for the folder. Must be the same on all cluster devices.": "Identificador requirido para o cartafol. Debe de ser o mesmo en todos os dispositivos do clúster.",
"Rescan": "Reescanear",
"Rescan All": "Reescanear Todo",
"Rescans": "Reescaneos",
"Restart": "Reiniciar",
"Restart Needed": "Reinicio Requirido",
"Restarting": "Reiniciando",
"Restore": "Restablecer",
"Restore Versions": "Restablecer Versións",
"Resume": "Continuar",
"Resume All": "Continuar Todo",
"Reused": "Reutilizado",
"Revert": "Desfacer",
"Revert Local Changes": "Desfacer os Cambios Locais",
"Save": "Gardar",
"Saving changes": "Gardando os cambios",
"Scan Time Remaining": "Tempo Restante de Reescaneo",
"Scanning": "Escaneando",
"Select All": "Seleccionar Todo",
"Select a version": "Seleccionar unha versión",
"Select additional devices to share this folder with.": "Seleccione dispositivos adicionais cos que compartir este cartafol.",
"Select additional folders to share with this device.": "Seleccione cartafois adicionais para compatir con este dispositivo.",
"Select latest version": "Seleccionar a última versión",
"Select oldest version": "Seleccionar a versión máis vella",
"Send & Receive": "Enviar e Recibir",
"Send Extended Attributes": "Enviar Atributos Extensos",
"Send Only": "Só Enviar",
"Send Ownership": "Enviar Propiedade",
"Settings": "Configuración",
"Share": "Compartir",
"Share Folder": "Compartir Cartafol",
"Share by Email": "Compartir por Correo Electrónico",
"Share by SMS": "Compartir por SMS",
"Share this folder?": "Compartir este cartafol?",
"Shared Folders": "Cartafois Compartidos",
"Shared With": "Compartido Con",
"Sharing": "Compartindo",
"Show ID": "Mostrar ID",
"Show QR": "Mostrar QR",
"Show detailed discovery status": "Mostrar estado detallado do descubrimento",
"Show detailed listener status": "Mostrar estado detallado da escoita",
"Show diff with previous version": "Mostrar a diferencia coa versión anterior",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Mostrado en lugar do ID de Dispositivo no estado do clúster. Anunciarase a outros dispositivos como nome por defecto opcional.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Mostrado en lugar do ID de Dispositivo no estado do clúster. Actualizarase ao nome que anuncia o dispositivo de se deixar baleiro.",
"Shutdown": "Apagar",
"Shutdown Complete": "Apagado Completado",
"Simple": "Simple",
"Simple File Versioning": "Versionado de Ficheiros Sinxelo",
"Single level wildcard (matches within a directory only)": "Comodín de primeiro nivel (só coincide ao nivel do directorio)",
"Size": "Tamaño",
"Smallest First": "Os máis pequenos primeiro",
"Some discovery methods could not be established for finding other devices or announcing this device:": "Algúns métodos de descubrimento non se puideron establecer para descubrir outros dispositivo ou anunciarse:",
"Some items could not be restored:": "Non se puideron recuperar algúns ítems:",
"Some listening addresses could not be enabled to accept connections:": "Algunhas direccións de escoita non se puideron habilitar para aceptar conexións:",
"Source Code": "Código Fonte",
"Stable releases and release candidates": "Versións estables e candidatos de lanzamento",
"Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "As versións estables retrásanse ao redor de dúas semanas. Durante este tempo próbanse como candidatas de lanzamento.",
"Stable releases only": "Só versións estables",
"Start Browser": "Iniciar o Buscador",
"Statistics": "Estadísticas",
"Stay logged in": "Manter a sesión",
"Stopped": "Parado",
"Stores and syncs only encrypted data. Folders on all connected devices need to be set up with the same password or be of type \"{%receiveEncrypted%}\" too.": "Só almacena e sincroniza datos encriptados. Os cartafois en todos os dispositivos conectados necesitan configuarse co mesmo constrasinal ou ser do tipo \"{{receiveEncrypted}}\" tamén.",
"Subject:": "Asunto:",
"Sync Extended Attributes": "Sincronizar os Atributos Extendidos",
"Sync Ownership": "Sincronizar Propiedade",
"Sync Protocol Listen Addresses": "Direccións de Escoita do Protocolo de Sincronización",
"Sync Status": "Estado da Sincronización",
"Syncing": "Sincronizando",
"Syncthing device ID for \"{%devicename%}\"": "Sincronizando o ID de dispositivo para \"{{devicename}}\"",
"Syncthing has been shut down.": "Apagouse Syncthing.",
"Syncthing includes the following software or portions thereof:": "Syncthing inclúe todo o seguinte software ou porcións dos mesmos:",
"Syncthing is Free and Open Source Software licensed as MPL v2.0.": "Syncthing e Software Libre licenciado baixo a MPL v2.0.",
"Syncthing is a continuous file synchronization program. It synchronizes files between two or more computers in real time, safely protected from prying eyes. Your data is your data alone and you deserve to choose where it is stored, whether it is shared with some third party, and how it's transmitted over the internet.": "Syncthing e un programa de sincronización de ficheiros continua. Sincroniza ficheiros entre dous ou máis computadores en tempo real, de maneira segura protexida de miradas indiscretas. Os teus datos son só teus e mereces elixir onde se gardan, se é cunha terceira parte, e como se transmiten pola rede.",
"Syncthing is listening on the following network addresses for connection attempts from other devices:": "Syncthing está escoitando nas seguintes direccións de rede intentos de conexión doutros dispositivos:",
"Syncthing is not listening for connection attempts from other devices on any address. Only outgoing connections from this device may work.": "Syncthing non está escoitando intentos de conexión doutros dispositivos en ningunha dirección. Só funcionarán as conexións saíntes deste dispositivo.",
"Syncthing is restarting.": "Syncthing estase a reiniciar.",
"Syncthing is saving changes.": "Syncthing esta a gardar os cambios.",
"Syncthing is upgrading.": "Syncthing estase a actualizar.",
"Syncthing now supports automatically reporting crashes to the developers. This feature is enabled by default.": "Syncthing agora pode reportar faios de xeito automático aos desenvolvedores. Esta característica está habilitada por defecto.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing parece estar apagado, ou hai un problema coa túa conexión de rede. Volvendo a intentalo…",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Parece que Syncthing está a sufrir un problema procesando a túa petición. Por favor refresque a páxina ou reinicie Synthing se o problema perdura.",
"TCP LAN": "LAN TCP",
"TCP WAN": "WAN TCP",
"Take me back": "Léveme de volta",
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "A dirección da GUI sobrescríbese polas opcións de arranque. Os cambios aquí non terán efecto mentres que a invalidación estea activa.",
"The Syncthing Authors": "Os Autores de Syncthing",
"The Syncthing admin interface is configured to allow remote access without a password.": "A inteface de aministración de Syncthing está configurada para permitir o acceso remoto sen ningún contrasinal.",
"The aggregated statistics are publicly available at the URL below.": "As estatísticas agregadas son públicas na URL de debaixo.",
"The cleanup interval cannot be blank.": "O intervalo de limpeza non pode estar en branco.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Gardouse a configuración pero non se activou. Ten que reiniciar Syncthing para activar a configuración nova.",
"The device ID cannot be blank.": "O ID de dispositivo non pode estar en branco.",
"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.": "O informe de uso encriptado envíase diariamente. Úsase para seguir plataformas comús, tamaños de cartafois e versións da aplicación. Se os datos que se envían cambian, preguntaráselle con este diálogo outra vez.",
"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.": "O ID de dispositivo introducido non parece válido. Ten que ser unha cadea de 52 ou 56 caracteres consistente de letras e números, sendo opcionais os espacios e guións.",
"The folder ID cannot be blank.": "O ID de cartafol non pode estar en branco.",
"The folder ID must be unique.": "O ID de cartafol ten que ser único.",
"The folder content on other devices will be overwritten to become identical with this device. Files not present here will be deleted on other devices.": "O contenido do cartafol en outros dispositivos será sobrescrito para volverse identico con este dispositivo. Os ficheiros que non estean aquí eliminaranse nos outros dispositivos.",
"The folder content on this device will be overwritten to become identical with other devices. Files newly added here will be deleted.": "O contenido do cartafol neste dispositivo será sobrescrito para volverse idéntico aos outros dispositivos. Os ficheiros novos serán eliminados.",
"The folder path cannot be blank.": "A ruta do cartafol non pode estar en branco.",
"The following items could not be synchronized.": "Non se puido sincronizar os seguintes ítems.",
"The following items were changed locally.": "Cambiáronse localmente os seguintes ítems.",
"The following methods are used to discover other devices on the network and announce this device to be found by others:": "Os seguintes métodos úsanse para descubrir outros dispositivos na rede e anunciar este dispositivo para que o encontren outros:",
"The following text will automatically be inserted into a new message.": "O seguinte texto insertarse automaticamente nunha nova mensaxe.",
"The following unexpected items were found.": "Atopáronse os seguintes ítems inesperados.",
"The interval must be a positive number of seconds.": "O intervalo ten que ser un número positivo de segundos.",
"The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.": "O intervalo, en segundos, para executar a limpeza no directorio de versions. Cero para deshabilitar a limpeza periódica.",
"The maximum age must be a number and cannot be blank.": "A idade máxima ten que ser un número e non pode estar en branco.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Tempo máximo para manter unha versión (en días, poñer a 0 para manter as versión para sempre).",
"The number of connections must be a non-negative number.": "O número de conexións ten que ser un número non negativo.",
"The number of days must be a number and cannot be blank.": "O número de días ten que ser un número e non pode estar en branco.",
"The number of days to keep files in the trash can. Zero means forever.": "O número de días que manter os ficheiros no lixo. Cero é para sempre.",
"The number of old versions to keep, per file.": "O número de versión vellas a manter, por ficheiro.",
"The number of versions must be a number and cannot be blank.": "O número de versións ten que ser un número e non pode estar en branco.",
"The path cannot be blank.": "A ruta non pode estar en branco.",
"The rate limit is applied to the accumulated traffic of all connections to this device.": "O límite aplícase ao tráfico acumulado de todas as conexións deste dispositivo.",
"The rate limit must be a non-negative number (0: no limit)": "O límite ten que ser un número non negativo (0: sen límite)",
"The remote device has not accepted sharing this folder.": "O dispositivo remoto non aceptou a compartir este cartafol.",
"The remote device has paused this folder.": "O dispositivo remoto parou este cartafol.",
"The rescan interval must be a non-negative number of seconds.": "O intervalo de reescaneo ten que ser un número non negativo de segundos.",
"There are no devices to share this folder with.": "Non hai dispositivos cos que compartir este cartafol.",
"There are no file versions to restore.": "Non hai versións de ficheriso para restaurar.",
"There are no folders to share with this device.": "Non hai cartafois que compartir con este dispositivo.",
"This Device": "Este Dispositivo",
"This Month": "Este Mes",
"This can easily give hackers access to read and change any files on your computer.": "Esto pode dar acceso fácil a hackers para ler e cambiar ficheiros no teu compturador.",
"This device cannot automatically discover other devices or announce its own address to be found by others. Only devices with statically configured addresses can connect.": "Este dispositivo non pode descubrir outros dispositivos automaticamente ou anunciar a súa dirección a outros. So se poden contectar dispositivos con direccións estáticas configuradas.",
"This is a major version upgrade.": "Esta é unha actualización maior.",
"This setting controls the free space required on the home (i.e., index database) disk.": "Este axuste controla o espacio en disco dispoñible necesario no disco principal (p.ej. índice da base de datos).",
"Time": "Hora",
"Time the item was last modified": "Hora na que se modificou o ficheiro por última vez",
"To connect with the Syncthing device named \"{%devicename%}\", add a new remote device on your end with this ID:": "Para conectar co dispositivo de Syncthing chamado \"{{devicename}}\", engada un dispositivo remoto novo con este ID:",
"To permit a rule, have the checkbox checked. To deny a rule, leave it unchecked.": "Para permitir una regra, marque esta caixa. Para negar una gregra, deixea sen marcar.",
"Today": "Hoxe",
"Trash Can": "Cubo do Lixo",
"Trash Can File Versioning": "Vesionado de Ficheiros co Lixo",
"Type": "Tipo",
"UNIX Permissions": "Permisos UNIX",
"Unavailable": "Non dispoñible",
"Unavailable/Disabled by administrator or maintainer": "Non dispoñible/Deshabilitado por un administrador ou mantedor",
"Undecided (will prompt)": "Sen decidir (preguntará)",
"Unexpected Items": "Ítems non espeardos",
"Unexpected items have been found in this folder.": "Atopáronse ítems non esperados neste cartafol.",
"Unignore": "Des-ignorar",
"Unknown": "Descoñecido",
"Unshared": "Des-compartido",
"Unshared Devices": "Dispositivos des-compartidos",
"Unshared Folders": "Cartafois des-compartidos",
"Untrusted": "Sen confiar",
"Up to Date": "Ao día",
"Updated {%file%}": "Actualizouse {{file}}",
"Upgrade": "Actualizar",
"Upgrade To {%version%}": "Actualizar A {{version}}",
"Upgrading": "Actualizando",
"Upload Rate": "Velocidade de Subida",
"Uptime": "Tempo de funcionamento",
"Usage reporting is always enabled for candidate releases.": "O informe de uso sempre está activado para as versións candidatas.",
"Use HTTPS for GUI": "Utilizar HTTPS para a GUI",
"Use notifications from the filesystem to detect changed items.": "Utilizar notificacións do sistema de ficheiros para detectar os ficheiros cambiados.",
"User": "Usuario",
"User Home": "Inicio do Usuario",
"Username/Password has not been set for the GUI authentication. Please consider setting it up.": "Non se configuraron o usuario e contrasinal para a autenticación da GUI. Por favor considere configuralo.",
"Using a QUIC connection over LAN": "Utilizando unha conexión QUIC en LAN",
"Using a QUIC connection over WAN": "Utilizando unha conexión QUIC en WAN",
"Using a direct TCP connection over LAN": "Utilizando unha conexión TCP directa en LAN",
"Using a direct TCP connection over WAN": "Utilizando unha conexión TCP directa en WAN",
"Version": "Versión",
"Versions": "Versións",
"Versions Path": "Ruta das Versións",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "As versións elimínanse automaticamente se son máis vellas que a idade máxima ou sobrepasan o número de ficheiros permitidos nun intervalo.",
"Waiting to Clean": "Esperando para Limpar",
"Waiting to Scan": "Esperando para Escanear",
"Waiting to Sync": "Esperando para Sincronizar",
"Warning": "Aviso",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Aviso, esta ruta é un directorio pai para outro cartafol existente \"{{otherFolder}}\".",
"Warning, this path is a parent directory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Aviso, esta ruta e un directorio pai dun cartafol existente \"{{otherFolderLabel}}\" ({{otherFolder}}).",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Aviso, esta ruta é un subdirectorio dun cartafol existente \"{{otherFolder}}\".",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Aviso, esta ruta é un subdirectorio de un cartafol existente \"{{otherFolderLabel}}\" ({{otherFolder}}).",
"When adding a new device, keep in mind that this device must be added on the other side too.": "Ao engadir un dispositivo novo, teña en mente que tamén debe engadir o dispositivo do outro lado.",
"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.": "Ao engadir un cartafol novo, teña en mete que o ID de Cartafol úsase para enlazar os cartafois entre dispositivos. Son sensíbles a maiúsculas e teñen que coincidir exactamente entre dispositivos.",
"Yes": "Sí",
"Yesterday": "Onte",
"You can also copy and paste the text into a new message manually.": "Tamén pode copiar e pegar o texto nunha mensaxe de xeito manual.",
"You can also select one of these nearby devices:": "Tamén pode seleccionar un destes dispositivos cercanos:",
"You can change your choice at any time in the Settings dialog.": "Tamén pode cambiar a súa decisión en calqueira momento no diálogo de Opcións.",
"You can read more about the two release channels at the link below.": "Pode ler máis sobre as dúas canles de lanzamentos na ligazón de debaixo.",
"You have no ignored devices.": "Non ten dispositivos ignorados.",
"You have no ignored folders.": "Non ten cartafois ignorados.",
"You have unsaved changes. Do you really want to discard them?": "Ten cambios sen gardar. Realmente quere descartalos?",
"You must keep at least one version.": "Ten que manter ao menos unha versión.",
"You should never add or change anything locally in a \"{%receiveEncrypted%}\" folder.": "Nunca debe engadir nen cambiar nada localmente nun cartafol \"{{receiveEncrypted}}\".",
"Your SMS app should open to let you choose the recipient and send it from your own number.": "A súa aplicación SMS debe abrirse para deixarlle escoller un destinatario e envialo desde o seu propio número.",
"Your email app should open to let you choose the recipient and send it from your own address.": "A súa amplicación de correo debe abrirse para deixarlle escoller un destinatario e envialo desde a súa propia dirección.",
"days": "días",
"deleted": "eliminado",
"deny": "denegar",
"directories": "directorios",
"file": "ficheiro",
"files": "ficheiros",
"folder": "cartafol",
"full documentation": "documentación completa",
"items": "ítems",
"modified": "modificado",
"permit": "permitir",
"seconds": "segundos",
"theme": {
"name": {
"black": "Negro",
"dark": "Escuro",
"default": "Predeterminado",
"light": "Claro"
}
},
"unknown device": "dispositivo descoñecido",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} quere compartir o cartafol \"{{cartafol}}\".",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} quere compartir o cartafol \"{{folderlabel}}\" ({{folder}})."
}

View File

@@ -30,6 +30,7 @@
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "Perintah eksternal menangani pemversian. Ia harus menghapus berkas dari folder yang dibagi. Jika lokasi aplikasi terdapat spasi, itu harus dikutip.",
"Anonymous Usage Reporting": "Pelaporan Penggunaan Anonim",
"Anonymous usage report format has changed. Would you like to move to the new format?": "Format pelaporan penggunaan anonim telah berubah. Maukah anda pindah menggunakan format yang baru?",
"Applied to LAN": "Digunakan di LAN",
"Apply": "Terapkan",
"Are you sure you want to override all remote changes?": "Apakah anda yakin ingin menimpa semua perubahan jarak jauh?",
"Are you sure you want to permanently delete all these files?": "Apakah anda yakin ingin menghapus semua berkas berikut secara permanen?",
@@ -38,6 +39,7 @@
"Are you sure you want to restore {%count%} files?": "Apakah anda yakin ingin memulihkan {{count}} berkas?",
"Are you sure you want to revert all local changes?": "Apakah anda yakin ingin mengembalikan semua perubahan lokal?",
"Are you sure you want to upgrade?": "Apakah anda yakin ingin meningkatkan?",
"Authentication Required": "Otentikasi diperlukan",
"Authors": "Penulis",
"Auto Accept": "Terima Otomatis",
"Automatic Crash Reporting": "Pelaporan Crash Otomatis",
@@ -48,6 +50,7 @@
"Available debug logging facilities:": "Fasilitas log debug yang ada:",
"Be careful!": "Harap hati-hati!",
"Body:": "Badan:",
"Bugs": "Bugs",
"Cancel": "Batal",
"Changelog": "Log Perubahan",
"Clean out after": "Bersihkan setelah",
@@ -63,6 +66,7 @@
"Configured": "Terkonfigurasi",
"Connected (Unused)": "Terkoneksi (Tidak Digunakan)",
"Connection Error": "Koneksi Galat",
"Connection Management": "Pengaturan Koneksi",
"Connection Type": "Tipe Koneksi",
"Connections": "Koneksi",
"Connections via relays might be rate limited by the relay": "Koneksi melalui relai mungkin dibatasi oleh relai",
@@ -77,6 +81,7 @@
"Danger!": "Bahaya!",
"Database Location": "Lokasi Database",
"Debugging Facilities": "Fasilitas Debug",
"Default": "Default",
"Default Configuration": "Konfigurasi Bawaan",
"Default Device": "Perangkat Bawaan",
"Default Folder": "Folder Bawaan",

View File

@@ -1,6 +1,6 @@
{
"A device with that ID is already added.": "Устройство с таким ID уже добавлено.",
"A negative number of days doesn't make sense.": "Отрицательное число дней не имеет значения.",
"A negative number of days doesn't make sense.": "Число дней не может быть отрицательным.",
"A new major version may not be compatible with previous versions.": "Новое обновление основной версии может быть несовместимо с предыдущими версиями.",
"API Key": "Ключ API",
"About": "О программе",

View File

@@ -21,7 +21,7 @@
"Advanced": "Avancerat",
"Advanced Configuration": "Avancerad konfiguration",
"All Data": "Alla data",
"All Time": "All tid",
"All Time": "Någonsin",
"All folders shared with this device must be protected by a password, such that all sent data is unreadable without the given password.": "Alla mappar som delas med denna enhet måste skyddas av ett lösenord, så att alla skickade data är oläsliga utan det angivna lösenordet.",
"Allow Anonymous Usage Reporting?": "Tillåt anonym användarstatistiksrapportering?",
"Allowed Networks": "Tillåtna nätverk",

View File

@@ -1,62 +1,62 @@
{
"A device with that ID is already added.": "已添加过相同 ID 的设备。",
"A device with that ID is already added.": "已添加过相同 ID 的设备。",
"A negative number of days doesn't make sense.": "天数不能为负。",
"A new major version may not be compatible with previous versions.": "新的版本可能与前的版本之间无法兼容。",
"A new major version may not be compatible with previous versions.": "新的主要版本可能与前的版本兼容。",
"API Key": "API 密钥",
"About": "关于",
"Action": "操作",
"Actions": "操作",
"Active filter rules": "活跃的过滤器规则",
"Active filter rules": "活动的筛选规则",
"Add": "添加",
"Add Device": "添加设备",
"Add Folder": "添加文件夹",
"Add Remote Device": "添加远程设备",
"Add devices from the introducer to our device list, for mutually shared folders.": "将这个设备上那些,跟本机有着共同文件夹的“远程设备”,都添加到本机的“远程设备列表。",
"Add filter entry": "添加过滤器条目",
"Add ignore patterns": "加忽略模式",
"Add devices from the introducer to our device list, for mutually shared folders.": "将中介中的设备添加到我们的设备列表中,用于相互共享的文件夹。",
"Add filter entry": "添加筛选器条目",
"Add ignore patterns": "加忽略模式",
"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 小时)。也可以在选择“否”后手动配置每个文件夹的时间。",
"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": "地址",
"Addresses": "地址列表",
"Addresses": "地址",
"Advanced": "高级",
"Advanced Configuration": "高级配置",
"All Data": "所有数据",
"All Time": "所有时间",
"All folders shared with this device must be protected by a password, such that all sent data is unreadable without the given password.": "与此设备共享的所有文件夹都必须有密码保护,这样所有发送的数据在没有密码的情况下是不可读的。",
"Allow Anonymous Usage Reporting?": "允许匿名使用报告?",
"Allow Anonymous Usage Reporting?": "允许发送匿名使用报告吗?",
"Allowed Networks": "允许的网络",
"Alphabetic": "字母顺序",
"Altered by ignoring deletes.": "被“忽略删除”修改。",
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "外部命令接管了版本控制。该外部命令必须自行从共享文件夹中删除该文件。如果应用程序的路径包含空格,应用半角引号括起来。",
"Altered by ignoring deletes.": "通过忽略删除进行更改。",
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "外部命令处理版本控制。必须从共享文件夹中移除文件。如果应用程序的路径包含空格,应用半角引号括起来。",
"Anonymous Usage Reporting": "匿名使用报告",
"Anonymous usage report format has changed. Would you like to move to the new format?": "匿名使用情况的报告格式已经变更。是否要迁移到新格式?",
"Anonymous usage report format has changed. Would you like to move to the new format?": "匿名使用报告格式已更。是否要切换到新格式?",
"Applied to LAN": "已应用到局域网",
"Apply": "应用",
"Are you sure you want to override all remote changes?": "确定要覆盖所有远程更改吗?",
"Are you sure you want to permanently delete all these files?": "确认要永久删除这些文件",
"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}} 个文件",
"Are you sure you want to revert all local changes?": "确定要撤销所有本地更改吗?",
"Are you sure you want to upgrade?": "确定要升级",
"Are you sure you want to override all remote changes?": "是否确定要覆盖所有远程更改",
"Are you sure you want to permanently delete all these files?": "是否确定要永久删除所有这些文件?",
"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}} 个文件?",
"Are you sure you want to revert all local changes?": "是否确定要还原所有本地更改",
"Are you sure you want to upgrade?": "是否确定要升级?",
"Authentication Required": "需要身份验证",
"Authors": "作者",
"Auto Accept": "自动接受",
"Automatic Crash Reporting": "自动发送崩溃报告",
"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.": "在本机默认文件夹中,自动创建或共享这个设备共享出来的所有文件夹。",
"Automatic upgrades are always enabled for candidate releases.": "候选版本始终启用自动升级。",
"Automatically create or share folders that this device advertises at the default path.": "自动创建或共享此设备在默认路径上显示的文件夹。",
"Available debug logging facilities:": "可用的调试日志功能:",
"Be careful!": "小心!",
"Body:": "正文:",
"Bugs": "问题回报",
"Bugs": "问题反馈",
"Cancel": "取消",
"Changelog": "更新日志",
"Clean out after": "在该时间后清除",
"Cleaning Versions": "清版本",
"Cleaning Versions": "清版本",
"Cleanup Interval": "清除间隔",
"Click to see full identification string and QR code.": "击查看完整的识字符串和二维码。",
"Click to see full identification string and QR code.": "击查看完整的识字符串和二维码。",
"Close": "关闭",
"Command": "命令",
"Comment, when used at the start of a line": "注释,在行首使用",
@@ -65,17 +65,17 @@
"Configuration File": "配置文件",
"Configured": "已配置",
"Connected (Unused)": "已连接(未使用)",
"Connection Error": "连接错",
"Connection Error": "连接错",
"Connection Management": "连接管理",
"Connection Type": "连接类型",
"Connections": "连接",
"Connections via relays might be rate limited by the relay": "经由中继的连接可能会被中继限制速率",
"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.": "Syncthing 现在可以持续监视更改。这将检测磁盘上的更改,然后对有修改的路径发起扫描。这样的好处是更改可以更快地传播,且需要的完扫描更少。",
"Copied from elsewhere": "从其设备复制",
"Connections via relays might be rate limited by the relay": "通过中继的连接可能受中继的速率限制",
"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.": "现在,Syncthing 中提供了持续监视更改的功能。这将检测磁盘上的更改,并仅对修改的路径进行扫描。好处是更改传播得更快,所需的完扫描更少。",
"Copied from elsewhere": "从其设备复制",
"Copied from original": "从源复制",
"Copied!": "已复制!",
"Copy": "复制",
"Copy failed! Try to select and copy manually.": "复制失败!尝试手动选择复制。",
"Copy failed! Try to select and copy manually.": "复制失败!尝试手动选择复制。",
"Currently Shared With Devices": "当前设备已共享",
"Custom Range": "自定义范围",
"Danger!": "危险!",
@@ -86,48 +86,48 @@
"Default Device": "默认设备",
"Default Folder": "默认文件夹",
"Default Ignore Patterns": "默认忽略模式",
"Defaults": "默认",
"Defaults": "默认",
"Delete": "删除",
"Delete Unexpected Items": "删除特殊项目",
"Deleted {%file%}": "{{file}} 已删除",
"Delete Unexpected Items": "删除意外项目",
"Deleted {%file%}": "删除了 {{file}}",
"Deselect All": "取消全选",
"Deselect devices to stop sharing this folder with.": "取消选择设备以停止共享此文件夹。",
"Deselect devices to stop sharing this folder with.": "取消选择要停止与之共享此文件夹的设备。",
"Deselect folders to stop sharing with this device.": "取消选择文件夹以停止与此设备共享。",
"Device": "设备",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "设备 \"{{name}}\"(位于 {{address}} 的 {{device}}请求连接。是否添加新设备?",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "设备{{name}}”({{address}} 的 {{device}}想要连接。添加新设备?",
"Device Certificate": "设备证书",
"Device ID": "设备 ID",
"Device Identification": "设备标识",
"Device Name": "设备名",
"Device Status": "设备状态",
"Device is untrusted, enter encryption password": "设备不可信,请输入加密密码",
"Device is untrusted, enter encryption password": "设备不受信任,请输入加密密码",
"Device rate limits": "设备速率限制",
"Device that last modified the item": "最近修改项的设备",
"Device that last modified the item": "最近修改项的设备",
"Devices": "设备",
"Disable Crash Reporting": "禁用自动发送崩溃报告",
"Disable Crash Reporting": "禁用崩溃报告",
"Disabled": "已禁用",
"Disabled periodic scanning and disabled watching for changes": "已禁用定期扫描和更改监视",
"Disabled periodic scanning and enabled watching for changes": "已禁用定期扫描并启用更改监视",
"Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:": "已禁用定期扫描但设置更改监视失败,正在以每 1m 一次重试",
"Disables comparing and syncing file permissions. Useful on systems with nonexistent or custom permissions (e.g. FAT, exFAT, Synology, Android).": "禁用比较和同步文件权限。 适用于不存在或自定义权限的系统例如FATexFATSynologyAndroid。",
"Discard": "丢弃",
"Disconnected": "连接已断开",
"Disconnected (Inactive)": "断开连接(不活跃)",
"Disconnected (Unused)": "断开连接(未使用)",
"Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:": "已禁用定期扫描但设置更改监视失败,每分钟重试一次",
"Disables comparing and syncing file permissions. Useful on systems with nonexistent or custom permissions (e.g. FAT, exFAT, Synology, Android).": "禁用比较和同步文件权限。 适用于不存在或自定义权限的系统(例如 FATexFATSynologyAndroid。",
"Discard": "丢弃",
"Disconnected": "已断开连接",
"Disconnected (Inactive)": "断开连接(不活跃)",
"Disconnected (Unused)": "断开连接(未使用)",
"Discovered": "已发现",
"Discovery": "设备发现",
"Discovery Failures": "设备发现错误",
"Discovery Failures": "设备发现失败",
"Discovery Status": "设备发现状态",
"Dismiss": "忽略一次",
"Do not add it to the ignore list, so this notification may recur.": "不要将其加入忽视列表,因此这条通知可能会再出现。",
"Dismiss": "忽略",
"Do not add it to the ignore list, so this notification may recur.": "不要将其添加到忽略列表,因此通知可能会再出现。",
"Do not restore": "不要恢复",
"Do not restore all": "不要全部恢复",
"Do you want to enable watching for changes for all your folders?": "您想要启用监视所有文件夹的更改",
"Do you want to enable watching for changes for all your folders?": "是否要启用监视所有文件夹的更改?",
"Documentation": "文档",
"Download Rate": "下载速",
"Download Rate": "下载速",
"Downloaded": "已下载",
"Downloading": "下载中",
"Edit": "选项",
"Edit": "编辑",
"Edit Device": "编辑设备",
"Edit Device Defaults": "编辑设备默认值",
"Edit Folder": "编辑文件夹",
@@ -137,31 +137,31 @@
"Enable NAT traversal": "启用 NAT 穿透",
"Enable Relaying": "启用中继",
"Enabled": "已启用",
"Enables sending extended attributes to other devices, and applying incoming extended attributes. May require running with elevated privileges.": "启用发送扩展属性至其他设备,并应用传入的扩展属性。可能需要管理员权限。",
"Enables sending extended attributes to other devices, but not applying incoming extended attributes. This can have a significant performance impact. Always enabled when \"Sync Extended Attributes\" is enabled.": "启用发送扩展属性至其他设备,但不应用传入的扩展属性。这可能造成性能上的影响。“同步扩展属性”启用时此选项总是启用。",
"Enables sending ownership information to other devices, and applying incoming ownership information. Typically requires running with elevated privileges.": "启用发送所有权信息至其他设备,并应用传入的所有权信息。通常情况下需要管理员权限。",
"Enables sending ownership information to other devices, but not applying incoming ownership information. This can have a significant performance impact. Always enabled when \"Sync Ownership\" is enabled.": "启用发送所有权信息至其他设备,但不应用传入的所有权信息。此选项可能造成显著的性能影响。“同步所有权”启用时此选项总是启用。",
"Enter a non-negative number (e.g., \"2.35\") and select a unit. Percentages are as part of the total disk size.": "输入一个非负数例如“2.35”)并选择单位。%表示占磁盘总容量的百分比。",
"Enter a non-privileged port number (1024 - 65535).": "输入一个非特权端口号 (1024 - 65535)。",
"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”以自动发现设备地址。",
"Enables sending extended attributes to other devices, and applying incoming extended attributes. May require running with elevated privileges.": "启用发送扩展属性至其他设备,并应用传入的扩展属性。可能需要以更高的权限运行。",
"Enables sending extended attributes to other devices, but not applying incoming extended attributes. This can have a significant performance impact. Always enabled when \"Sync Extended Attributes\" is enabled.": "启用发送扩展属性至其他设备,但不应用传入的扩展属性。这可能会对性能产生重大影响。启用“同步扩展属性”时始终启用。",
"Enables sending ownership information to other devices, and applying incoming ownership information. Typically requires running with elevated privileges.": "启用发送所有权信息至其他设备,并应用传入的所有权信息。通常需要以更高的权限运行。",
"Enables sending ownership information to other devices, but not applying incoming ownership information. This can have a significant performance impact. Always enabled when \"Sync Ownership\" is enabled.": "启用发送所有权信息至其他设备,但不应用传入的所有权信息。这可能会对性能产生重大影响。启用“同步所有权”时始终启用。",
"Enter a non-negative number (e.g., \"2.35\") and select a unit. Percentages are as part of the total disk size.": "输入一个非负数例如“2.35”)并选择单位。% 表示占磁盘总容量的百分比。",
"Enter a non-privileged port number (1024 - 65535).": "输入非特权端口号1024 - 65535。",
"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进制数字。",
"Enter up to three octal digits.": "输入最多三个进制数字。",
"Error": "错误",
"Extended Attributes": "扩展属性",
"Extended Attributes Filter": "扩展属性过滤器",
"Extended Attributes Filter": "扩展属性筛选器",
"External": "外部",
"External File Versioning": "外部版本控制",
"External File Versioning": "外部文件版本控制",
"Failed Items": "失败的项目",
"Failed to load file versions.": "加载文件版本失败。",
"Failed to load ignore patterns.": "加载忽略模式失败。",
"Failed to setup, retrying": "设置失败,正在重试",
"Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": "如果本机没有配置IPv6则无法连接IPv6服务器是正常的。",
"Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": "如果本机没有配置 IPv6则无法连接 IPv6 服务器是正常的。",
"File Pull Order": "文件拉取顺序",
"File Versioning": "版本控制",
"Files are moved to .stversions directory 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 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.": "文件从集群同步,但本地所的任何更改都不会发送到其他设备。",
"File Versioning": "文件版本控制",
"Files are moved to .stversions directory 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.": "当 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 synchronized from the cluster, but any changes made locally will not be sent to other devices.": "文件从集群同步,但本地所的任何更改都不会发送到其他设备。",
"Filesystem Watcher Errors": "文件系统监视器错误",
"Filter by date": "按日期筛选",
"Filter by name": "按名称筛选",
@@ -172,18 +172,18 @@
"Folder Status": "文件夹状态",
"Folder Type": "文件夹类型",
"Folder type \"{%receiveEncrypted%}\" can only be set when adding a new folder.": "只能在添加新文件夹时设置文件夹类型“{{receiveEncrypted}}”。",
"Folder type \"{%receiveEncrypted%}\" cannot be changed after adding the folder. You need to remove the folder, delete or decrypt the data on disk, and add the folder again.": "添加文件夹后,无法更改文件夹类型“ {{receiveEncrypted}}”。您需要删除该文件夹,删除或解密磁盘上的数据,然后再次添加文件夹。",
"Folder type \"{%receiveEncrypted%}\" cannot be changed after adding the folder. You need to remove the folder, delete or decrypt the data on disk, and add the folder again.": "添加文件夹后,无法更改文件夹类型“{{receiveEncrypted}}”。您需要移除文件夹,删除或解密磁盘上的数据,然后重新添加文件夹。",
"Folders": "文件夹",
"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.": "开始监视下列文件夹时发生错误。由于每分钟都会重试,所以错误可能很快就消失。如果它们存在,请试着修复潜在问题,如不会则请求帮助。",
"Forever": "永",
"Full Rescan Interval (s)": "完扫描间隔(秒)",
"GUI": "图形用户界面",
"GUI / API HTTPS Certificate": "GUI / API HTTPS证书",
"GUI Authentication Password": "图形管理界面密码",
"GUI Authentication User": "图形管理界面用户",
"GUI Authentication: Set User and Password": "GUI身份验证设置用户和密码",
"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.": "对于以下文件夹,在开始监视更改时出错。它将每分钟重试一次,因此错误可能很快就消失。如果它们持续存在,请尝试解决根本问题,如果不能,请寻求帮助。",
"Forever": "永",
"Full Rescan Interval (s)": "完全重新扫描间隔(秒)",
"GUI": "GUI",
"GUI / API HTTPS Certificate": "GUI/API HTTPS 证书",
"GUI Authentication Password": "GUI 身份验证密码",
"GUI Authentication User": "GUI 身份验证用户",
"GUI Authentication: Set User and Password": "GUI 身份验证:设置用户和密码",
"GUI Listen Address": "GUI 监听地址",
"GUI Override Directory": "GUI覆盖目录",
"GUI Override Directory": "GUI 覆盖目录",
"GUI Theme": "GUI 主题",
"General": "常规",
"Generate": "生成",
@@ -193,31 +193,31 @@
"Help": "帮助",
"Hint: only deny-rules detected while the default is deny. Consider adding \"permit any\" as last rule.": "提示:默认拒绝时,仅检测拒绝规则。考虑添加“允许任何”为最后规则。",
"Home page": "主页",
"However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.": "我们已经为您关闭了自动崩溃报告发送功能,因为您当前的设置显示您可能并不想启用该功能。",
"Identification": "识",
"If untrusted, enter encryption password": "如想更安全,请输入加密密码",
"If you want to prevent other users on this computer from accessing Syncthing and through it your files, consider setting up authentication.": "如果要阻止此计算机上的其他用户访问Syncthing并通过它访问文件请考虑设置身份验证。",
"However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.": "我们已经为您禁用了自动发送崩溃报告,因为您当前的设置表明您可能并不想启用该选项。",
"Identification": "识",
"If untrusted, enter encryption password": "如果不受信任,请输入加密密码",
"If you want to prevent other users on this computer from accessing Syncthing and through it your files, consider setting up authentication.": "如果要阻止此计算机上的其他用户访问 Syncthing 并通过它访问文件,请考虑设置身份验证。",
"Ignore": "忽略",
"Ignore Patterns": "忽略模式",
"Ignore Permissions": "忽略文件权限",
"Ignore patterns can only be added after the folder is created. If checked, an input field to enter ignore patterns will be presented after saving.": "只有在文件夹创建后,才能对其增加忽略模式。勾选后,在保存后提供一个设置忽略模式的输入框。",
"Ignored Devices": "忽略的设备",
"Ignored Folders": "忽略的文件夹",
"Ignore Permissions": "忽略权限",
"Ignore patterns can only be added after the folder is created. If checked, an input field to enter ignore patterns will be presented after saving.": "只有在创建文件夹后才能添加忽略模式。勾选后,在保存后将显示用于设置忽略模式的输入框。",
"Ignored Devices": "忽略的设备",
"Ignored Folders": "忽略的文件夹",
"Ignored at": "已忽略于",
"Included Software": "包含软件",
"Incoming Rate Limit (KiB/s)": "下载速率限制 (KiB/s)",
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "错误的配置可能损坏您文件夹内的内容,使得 Syncthing 无法工作。",
"Included Software": "包含软件",
"Incoming Rate Limit (KiB/s)": "传入速率限制KiB/s",
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "不正确的配置可能损坏您文件夹内容,并导致 Syncthing 无法运行。",
"Incorrect user name or password.": "用户名或密码不正确。",
"Internally used paths:": "内部使用的路径:",
"Introduced By": "介绍自",
"Introducer": "作为中介",
"Introduction": "介绍",
"Inversion of the given condition (i.e. do not exclude)": "反转本条件(即不排除)",
"Inversion of the given condition (i.e. do not exclude)": "给定条件的反转(即不排除)",
"Keep Versions": "保留版本数量",
"LDAP": "LDAP",
"Largest First": "大文件优先",
"Last 30 Days": "最近30天",
"Last 7 Days": "最近7天",
"Largest First": "大优先",
"Last 30 Days": "最近 30 天",
"Last 7 Days": "最近 7 天",
"Last Month": "上个月",
"Last Scan": "最后扫描",
"Last seen": "最后可见",
@@ -225,12 +225,12 @@
"Learn more": "了解更多",
"Learn more at {%url%}": "了解更多请访问 {{url}}",
"Limit": "限制",
"Listener Failures": "侦听器失败",
"Listener Status": "侦听器状态",
"Listeners": "听程序",
"Listener Failures": "监听程序失败",
"Listener Status": "监听程序状态",
"Listeners": "听程序",
"Loading data...": "正在载入数据…",
"Loading...": "正在载入…",
"Local Additions": "本地添加",
"Local Additions": "本地添加",
"Local Discovery": "本地发现",
"Local State": "本地状态",
"Local State (Total)": "本地状态汇总",
@@ -238,11 +238,11 @@
"Log": "日志",
"Log File": "日志文件",
"Log In": "登录",
"Log Out": "注销",
"Log Out": "登出",
"Log in to see paths information.": "登录查看路径信息。",
"Log in to see version information.": "登查看版本信息。",
"Log tailing paused. Scroll to the bottom to continue.": "已暂停日志跟踪。滚动到底部继续。",
"Login failed, see Syncthing logs for details.": "登录失败,详情见 Syncthing 日志。",
"Log in to see version information.": "登查看版本信息。",
"Log tailing paused. Scroll to the bottom to continue.": "日志跟踪已暂停。滚动到底部继续。",
"Login failed, see Syncthing logs for details.": "登录失败,有关详细信息,请参阅 Syncthing 日志。",
"Logs": "日志",
"Major Upgrade": "重大更新",
"Mass actions": "批量操作",
@@ -256,14 +256,14 @@
"More than a month ago": "一个月前",
"More than a week ago": "一周前",
"More than a year ago": "一年前",
"Move to top of queue": "移动到队列顶",
"Move to top of queue": "移队列顶",
"Multi level wildcard (matches multiple directory levels)": "多级通配符(用以匹配多层文件夹)",
"Never": "从未",
"New Device": "新设备",
"New Folder": "新文件夹",
"Newest First": "新文件优先",
"Newest First": "新优先",
"No": "否",
"No File Versioning": "不启用版本控制",
"No File Versioning": "不启用文件版本控制",
"No files will be deleted as a result of this operation.": "此操作结果不会删除任何文件。",
"No rules set": "未设置规则",
"No upgrades": "无更新",
@@ -272,14 +272,14 @@
"Number of Connections": "连接数",
"OK": "确定",
"Off": "关闭",
"Oldest First": "旧文件优先",
"Optional descriptive label for the folder. Can be different on each device.": "可选的文件夹说明性标签。在不同设备上可以不一致。",
"Oldest First": "旧优先",
"Optional descriptive label for the folder. Can be different on each device.": "文件夹的可选描述性标签。每个设备上可能不同。",
"Options": "选项",
"Out of Sync": "失去同步",
"Out of Sync": "同步",
"Out of Sync Items": "未同步的项目",
"Outgoing Rate Limit (KiB/s)": "上传速度限制 (KiB/s)",
"Outgoing Rate Limit (KiB/s)": "传出速率限制(KiB/s",
"Override": "覆盖",
"Override Changes": "撤销改变",
"Override Changes": "覆盖更改",
"Ownership": "所有权",
"Password": "密码",
"Path": "路径",
@@ -287,247 +287,247 @@
"Path where versions should be stored (leave empty for the default .stversions directory in the shared folder).": "历史版本储存路径(留空则会默认存储在共享文件夹中的 .stversions 目录)。",
"Paths": "路径",
"Pause": "暂停",
"Pause All": "全部暂停",
"Pause All": "暂停全部",
"Paused": "已暂停",
"Paused (Unused)": "已暂停(未使用)",
"Pending changes": "待的更改",
"Periodic scanning at given interval and disabled watching for changes": "正以给定的间隔定期扫描并禁用更改监视",
"Periodic scanning at given interval and enabled watching for changes": "正以给定的间隔定期扫描并启用更改监视",
"Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:": "正以给定间隔定期扫描但设置更改监视失败,正在每分钟一次重试:",
"Permanently add it to the ignore list, suppressing further notifications.": "将其永久添加到忽略列表中,禁止进一步通知。",
"Pending changes": "待处理的更改",
"Periodic scanning at given interval and disabled watching for changes": "正以给定的间隔进行定期扫描并禁用更改监视",
"Periodic scanning at given interval and enabled watching for changes": "正以给定的间隔进行定期扫描并启用更改监视",
"Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:": "正以给定间隔进行定期扫描但设置更改监视失败,正在每分钟重试一次",
"Permanently add it to the ignore list, suppressing further notifications.": "将其永久添加到忽略列表中,以抑制进一步通知。",
"Please consult the release notes before performing a major upgrade.": "请在进行重大更新前查看发布说明。",
"Please set a GUI Authentication User and Password in the Settings dialog.": "请在设置对话框中设置 GUI 验证用户及其密码。",
"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": "此前缀表示,后面的模式在匹配时不区分大小写",
"Preparing to Sync": "准备同步",
"Preparing to Sync": "正在准备同步",
"Preview": "预览",
"Preview Usage Report": "预览使用报告",
"QR code": "二维码",
"QUIC LAN": "QUIC 局域网",
"QUIC WAN": "QUIC 广域网",
"Quick guide to supported patterns": "支持的通配符的简单教程",
"Random": "随机顺序",
"Quick guide to supported patterns": "支持模式的快速指南",
"Random": "随机",
"Receive Encrypted": "加密接收",
"Receive Only": "仅接收",
"Received data is already encrypted": "已加密接收到的数据",
"Received data is already encrypted": "接收到的数据已加密",
"Recent Changes": "最近更改",
"Reduced by ignore patterns": "已由忽略模式减",
"Reduced by ignore patterns": "通过忽略模式减",
"Relay LAN": "中继局域网",
"Relay WAN": "中继广域网",
"Release Notes": "发布说明",
"Release candidates contain the latest features and fixes. They are similar to the traditional bi-weekly Syncthing releases.": "发布候选版包含最新的特性和修复。它们跟传统的 Syncthing 双周发布版类似。",
"Release candidates contain the latest features and fixes. They are similar to the traditional bi-weekly Syncthing releases.": "候选版包含最新功能和修复程序。它们类似于传统的每两周一次的 Syncthing 发布。",
"Remote Devices": "远程设备",
"Remote GUI": "远程GUI",
"Remote GUI": "远程 GUI",
"Remove": "移除",
"Remove Device": "移除设备",
"Remove Folder": "移除文件夹",
"Required identifier for the folder. Must be the same on all cluster devices.": "必需的文件夹唯一标识。同一个文件夹在集群中的所有设备上ID必须相同。",
"Required identifier for the folder. Must be the same on all cluster devices.": "文件夹所需的标识符。所有集群设备上必须相同。",
"Rescan": "重新扫描",
"Rescan All": "全部重新扫描",
"Rescans": "重新扫描",
"Restart": "重启 Syncthing",
"Restart Needed": "需要重启 Syncthing",
"Restarting": "重启",
"Restart": "重启",
"Restart Needed": "需要重启",
"Restarting": "正在重启",
"Restore": "恢复",
"Restore Versions": "恢复历史版本",
"Resume": "恢复",
"Resume All": "全部恢复",
"Reused": "复用",
"Revert": "还原",
"Revert Local Changes": "恢复本地更改",
"Revert Local Changes": "还原本地更改",
"Save": "保存",
"Saving changes": "保存更改中",
"Scan Time Remaining": "扫描剩余时间",
"Scan Time Remaining": "剩余扫描时间",
"Scanning": "扫描中",
"See external versioning help for supported templated command line parameters.": "有关受支持的模板命令行参数,请参阅外部版本控制帮助。",
"Select All": "全选",
"Select a version": "选择版本",
"Select additional devices to share this folder with.": "选择其他共享此文件夹的设备。",
"Select additional folders to share with this device.": "选择要与此设备共享的其文件夹。",
"Select additional devices to share this folder with.": "选择要与之共享此文件夹的其他设备。",
"Select additional folders to share with this device.": "选择要与此设备共享的其文件夹。",
"Select latest version": "选择最新的版本",
"Select oldest version": "选择最旧的版本",
"Send & Receive": "发送接收",
"Send & Receive": "发送接收",
"Send Extended Attributes": "发送扩展属性",
"Send Only": "仅发送",
"Send Ownership": "发送所有权",
"Set Ignores on Added Folder": "在加的文件夹中设置忽略",
"Set Ignores on Added Folder": "在加的文件夹中设置忽略",
"Settings": "设置",
"Share": "共享",
"Share Folder": "共享文件夹",
"Share by Email": "通过电子邮件分享",
"Share by SMS": "通过短信分享",
"Share this folder?": "是否共享文件夹?",
"Share this folder?": "共享文件夹?",
"Shared Folders": "共享文件夹",
"Shared With": "共享给",
"Sharing": "共享",
"Show ID": "显示 ID",
"Show QR": "显示 QR 码",
"Show QR": "显示二维码",
"Show detailed discovery status": "显示详细的发现状态",
"Show detailed listener status": "显示详细的侦听器状态",
"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。如果设置为空,则会使用目标设备自报的默认名称。",
"Shutdown": "关闭 Syncthing",
"Show detailed listener status": "显示详细的监听程序状态",
"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。如果留空,将更新为设备通告的名称。",
"Shutdown": "关闭",
"Shutdown Complete": "关闭完成",
"Simple": "简单",
"Simple File Versioning": "简版本控制",
"Simple File Versioning": "简单文件版本控制",
"Single level wildcard (matches within a directory only)": "单级通配符(仅匹配单层文件夹)",
"Size": "大小",
"Smallest First": "小文件优先",
"Some discovery methods could not be established for finding other devices or announcing this device:": "有些探索方法无法用于发现其它设备或广播该设备:",
"Some items could not be restored:": "些项目无法恢复:",
"Some listening addresses could not be enabled to accept connections:": "某些监听地址无法接受连接:",
"Smallest First": "小优先",
"Some discovery methods could not be established for finding other devices or announcing this device:": "无法建立某些发现方法来查找其他设备或宣布此设备",
"Some items could not be restored:": "些项目无法恢复:",
"Some listening addresses could not be enabled to accept connections:": "无法启用某些监听地址接受连接",
"Source Code": "源代码",
"Stable releases and release candidates": "稳定版本和发布候选版",
"Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "稳定版本约延迟两个星期。这段时间它们作为发布候选版测试。",
"Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "稳定版本推迟了大约两周。在此期间,它们作为发布候选版进行测试。",
"Stable releases only": "仅稳定版本",
"Staggered": "交错",
"Staggered File Versioning": "阶段版本控制",
"Staggered": "阶段",
"Staggered File Versioning": "阶段文件版本控制",
"Start Browser": "启动浏览器",
"Statistics": "统计",
"Stay logged in": "保持登录",
"Stopped": "已停止",
"Stores and syncs only encrypted data. Folders on all connected devices need to be set up with the same password or be of type \"{%receiveEncrypted%}\" too.": "仅存储和同步加密数据。所有连接设备上的文件夹需要使用相同的密码设置,或者也必须设置为“ {{receiveEncrypted}}”类型。",
"Stores and syncs only encrypted data. Folders on all connected devices need to be set up with the same password or be of type \"{%receiveEncrypted%}\" too.": "仅存储和同步加密数据。所有连接设备上的文件夹需要使用相同的密码设置,或者也需要设置为“{{receiveEncrypted}}”类型。",
"Subject:": "主题:",
"Support": "支持",
"Support Bundle": "支持捆绑包",
"Sync Extended Attributes": "同步扩展属性",
"Sync Ownership": "同步所有权",
"Sync Protocol Listen Addresses": "协议监听地址",
"Sync Protocol Listen Addresses": "同步协议监听地址",
"Sync Status": "同步状态",
"Syncing": "同步中",
"Syncthing device ID for \"{%devicename%}\"": "\"{{devicename}}\" 的 Syncthing 设备 ID",
"Syncthing device ID for \"{%devicename%}\"": "{{devicename}}的 Syncthing 设备 ID",
"Syncthing has been shut down.": "Syncthing 已关闭。",
"Syncthing includes the following software or portions thereof:": "Syncthing 使用了下列软件或其中的一部分:",
"Syncthing is Free and Open Source Software licensed as MPL v2.0.": "Syncthing 是个以 MPL v2.0 授权的免费开源软件。",
"Syncthing is a continuous file synchronization program. It synchronizes files between two or more computers in real time, safely protected from prying eyes. Your data is your data alone and you deserve to choose where it is stored, whether it is shared with some third party, and how it's transmitted over the internet.": "Syncthing 是一个持续的文件同步程序。它在两台或更多的计算机之间实时同步文件,安全地保护它们不被窥视。的数据是私有的,有权选择它被储存在哪里,是否与一些第三方共享以及如何互联网传输。",
"Syncthing is listening on the following network addresses for connection attempts from other devices:": "Syncthing正在监听以下网络地址,以获取来自其他设备的连接尝试:",
"Syncthing is not listening for connection attempts from other devices on any address. Only outgoing connections from this device may work.": "Syncthing 不会在任何地址上侦听来自其他设备的连接尝试。只有来自设备的传出连接可能有效。",
"Syncthing is Free and Open Source Software licensed as MPL v2.0.": "Syncthing 是自由开源软件,许可证为 MPL v2.0。",
"Syncthing is a continuous file synchronization program. It synchronizes files between two or more computers in real time, safely protected from prying eyes. Your data is your data alone and you deserve to choose where it is stored, whether it is shared with some third party, and how it's transmitted over the internet.": "Syncthing 是续的文件同步程序。它在两台或多台计算机之间实时同步文件,安全地保护它们防止窥视。的数据是私有的,有权选择其存储位置、是否与第三方共享以及如何通过互联网传输。",
"Syncthing is listening on the following network addresses for connection attempts from other devices:": "Syncthing 正在以下网络地址监听来自其他设备的连接尝试",
"Syncthing is not listening for connection attempts from other devices on any address. Only outgoing connections from this device may work.": "Syncthing 不监听任何地址上其他设备的连接尝试。只有来自设备的传出连接才能工作。",
"Syncthing is restarting.": "Syncthing 正在重启。",
"Syncthing is saving changes.": "Syncthing 正保存更改。",
"Syncthing is upgrading.": "Syncthing 正在升级。",
"Syncthing now supports automatically reporting crashes to the developers. This feature is enabled by default.": "Syncthing 现在已经支持将崩溃报告自动发送给开发者。该功能默认开启。",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing 似乎关闭,或者您的网络连接存在故障。重试…",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing 在处理您的请求时似乎遇到问题。如果问题持续,请刷新页面或重启 Syncthing。",
"TCP LAN": "局域网TCP",
"TCP WAN": "广域网TCP",
"Take me back": "带我回去",
"Syncthing now supports automatically reporting crashes to the developers. This feature is enabled by default.": "Syncthing 现在支持自动向开发人员发送崩溃报告。默认情况下启用此功能。",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing 似乎关闭,或者您的互联网连接有问题。正在重试…",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing 在处理您的请求时似乎遇到问题。如果问题仍然存在,请刷新页面或重启 Syncthing。",
"TCP LAN": "TCP 局域网",
"TCP WAN": "TCP 广域网",
"Take me back": "返回",
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "GUI 地址已被启动选项覆盖。当覆盖存在时,此处的更改就不会生效。",
"The Syncthing Authors": "Syncthing作者",
"The Syncthing admin interface is configured to allow remote access without a password.": "当前配置允许在不使用密码的情况下远程访问 Syncthing 管理界面。",
"The aggregated statistics are publicly available at the URL below.": "全局统计数据公布于以下 URL。",
"The cleanup interval cannot be blank.": "清间隔不能为空。",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "置已保存,但是还未生效。Syncthing 需要重启以启用新的设置。",
"The Syncthing Authors": "Syncthing 作者",
"The Syncthing admin interface is configured to allow remote access without a password.": "Syncthing 管理界面配置为允许无密码远程访问。",
"The aggregated statistics are publicly available at the URL below.": "汇总统计可在以下 URL 公开获取。",
"The cleanup interval cannot be blank.": "清间隔不能为空。",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "置已保存但未激活。Syncthing 必须重启才能激活新配置。",
"The device ID cannot be blank.": "设备 ID 不能为空。",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "在这里所需要输入的设备 ID可以在目标设备的“操作->显示 ID”中看到。空格和横线可选(将会被忽略)。",
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes, and app versions. If the reported data set is changed you will be prompted with this dialog again.": "经过加密的使用报告每天发送。它用跟踪统计使用本软件的平台文件夹大小,以及本软件的版本。如果报告的内容有任何变化,本对话框会再次弹出提示您。",
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "输入的设备 ID 似乎无效。设备 ID 包含字母和数字,长度为 52 或 56空格和横线不计在内。",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "在此处输入的设备 ID 可以在另一台设备的“操作 > 显示 ID”对话框中找到。空格和破折号是可选的(忽略)。",
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes, and app versions. If the reported data set is changed you will be prompted with this dialog again.": "加密的使用情况报告每天发送一次。它用跟踪常见平台文件夹大小和应用版本。如果报告的数据集发生更改,系统将再次弹出此对话框提示您。",
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "输入的设备 ID 似乎无效。设备 ID 包含字母和数字,长度为 52 或 56空格和破折号是可选的。",
"The folder ID cannot be blank.": "文件夹 ID 不能为空。",
"The folder ID must be unique.": "文件夹 ID 不得重复。",
"The folder content on other devices will be overwritten to become identical with this device. Files not present here will be deleted on other devices.": "其设备上的文件夹内容将覆盖,与此设备相同。此处不存在的文件将在其他设备上删除。",
"The folder content on this device will be overwritten to become identical with other devices. Files newly added here will be deleted.": "设备上的文件夹内容将覆盖,使其与其他设备相同。此处新添加的文件将删除。",
"The folder content on other devices will be overwritten to become identical with this device. Files not present here will be deleted on other devices.": "其设备上的文件夹内容将覆盖,与此设备完全相同。此处不存在的文件将在其他设备上删除。",
"The folder content on this device will be overwritten to become identical with other devices. Files newly added here will be deleted.": "设备上的文件夹内容将覆盖,与其他设备相同。此处新添加的文件将删除。",
"The folder path cannot be blank.": "文件夹路径不能为空。",
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "保留的历史版本会遵循以下条件:最近一小时内的历史版本,更新间隔小于三十秒的仅保留一份。最近一天内的历史版本,更新间隔小于一小时的仅保留一份。最近一个月内的历史版本,更新间隔小于一天的仅保留一份。距离现在超过一个月且小于最长保留时间的,更新间隔小于一周的仅保留一份。",
"The following items could not be synchronized.": "下项目无法同步。",
"The following items were changed locally.": "下项目在本地更改。",
"The following methods are used to discover other devices on the network and announce this device to be found by others:": "以下方法用于发现网络上的其他设备并通知其他人发现设备:",
"The following text will automatically be inserted into a new message.": "以下文将自动插入新消息中。",
"The following unexpected items were found.": "找到了以下特殊项。",
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "使用以下间隔:最近一小时内的历史版本,更新间隔小于三十秒的仅保留一份。最近一天内的历史版本,更新间隔小于一小时的仅保留一份。最近一个月内的历史版本,更新间隔小于一天的仅保留一份。距离现在超过一个月且小于最长保留时间的,更新间隔小于一周的仅保留一份。",
"The following items could not be synchronized.": "下项目无法同步。",
"The following items were changed locally.": "下项目在本地更改。",
"The following methods are used to discover other devices on the network and announce this device to be found by others:": "以下方法用于发现网络上的其他设备并通知其他人发现设备:",
"The following text will automatically be inserted into a new message.": "以下文将自动插入新消息中。",
"The following unexpected items were found.": "发现以下意外项目。",
"The interval must be a positive number of seconds.": "间隔必须为正数秒。",
"The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.": "在版本目录中运行清理的间隔。0表示禁用定期清。",
"The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.": "在版本目录中运行清理的间隔。0 表示禁用定期清。",
"The maximum age must be a number and cannot be blank.": "最长保留时间必须为数字,且不能为空。",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "历史版本保留的最长天数0 为永久保存。",
"The number of connections must be a non-negative number.": "连接数必须是非负数。",
"The number of days must be a number and cannot be blank.": "天数必须为数字,且不能为空。",
"The number of days to keep files in the trash can. Zero means forever.": "文件保存在回收站的天数。表示永久。",
"The number of old versions to keep, per file.": "每个文件保留的版本数量上限。",
"The number of days to keep files in the trash can. Zero means forever.": "文件保存在回收站的天数。0 表示永久。",
"The number of old versions to keep, per file.": "每个文件保留的版本数。",
"The number of versions must be a number and cannot be blank.": "保留版本数量必须为数字,且不能为空。",
"The path cannot be blank.": "路径不能为空。",
"The rate limit is applied to the accumulated traffic of all connections to this device.": "到这台设备所有连接的累流量被实施了速率限制。",
"The rate limit must be a non-negative number (0: no limit)": "传输速度限制为非负0 表示不限制)",
"The remote device has not accepted sharing this folder.": "远程设备尚未允许分享此文件夹。",
"The remote device has paused this folder.": "远程设备已停此文件夹。",
"The rescan interval must be a non-negative number of seconds.": "扫描间隔单位为秒,且不能为负数。",
"There are no devices to share this folder with.": "没有共享此文件夹的设备。",
"The rate limit is applied to the accumulated traffic of all connections to this device.": "速率限制适用于到此设备所有连接的累流量。",
"The rate limit must be a non-negative number (0: no limit)": "速率限制必须是非负数0:无限制)",
"The remote device has not accepted sharing this folder.": "远程设备尚未接受共享此文件夹。",
"The remote device has paused this folder.": "远程设备已停此文件夹。",
"The rescan interval must be a non-negative number of seconds.": "重新扫描间隔必须为非负数。",
"There are no devices to share this folder with.": "没有可与之共享此文件夹的设备。",
"There are no file versions to restore.": "没有可供恢复的文件版本。",
"There are no folders to share with this device.": "没有文件夹与此设备共享。",
"They are retried automatically and will be synced when the error is resolved.": "系统将会自动重试,错误解决时,它们将会被同步。",
"This Device": "当前设备",
"There are no folders to share with this device.": "没有与此设备共享的文件夹。",
"They are retried automatically and will be synced when the error is resolved.": "系统将会自动重试,并在错误解决同步。",
"This Device": "设备",
"This Month": "本月",
"This can easily give hackers access to read and change any files on your computer.": "这会让骇客能够轻而易举地访问及修改您的文件。",
"This device cannot automatically discover other devices or announce its own address to be found by others. Only devices with statically configured addresses can connect.": "此设备无法自动发现其设备或广播自己的地址被其他人发现。只有具有静态配置地址的设备才能连接。",
"This is a major version upgrade.": "这是一重大版本更新。",
"This setting controls the free space required on the home (i.e., index database) disk.": "此设置控制主(例如索引数据库)磁盘上需要的可用空间。",
"This device cannot automatically discover other devices or announce its own address to be found by others. Only devices with statically configured addresses can connect.": "此设备无法自动发现其设备,也无法向其他人宣布自己的地址。只有具有静态配置地址的设备才能连接。",
"This is a major version upgrade.": "这是一重大版本升级。",
"This setting controls the free space required on the home (i.e., index database) disk.": "此设置控制主磁盘(即索引数据库)上所需的可用空间。",
"Time": "时间",
"Time the item was last modified": "该项最近修改的时间",
"To connect with the Syncthing device named \"{%devicename%}\", add a new remote device on your end with this ID:": "要与名为\"{{devicename}}\"的Syncthing设备连接,请在的终端上用这个ID添加一个新的远程设备:",
"To permit a rule, have the checkbox checked. To deny a rule, leave it unchecked.": "要允许条规则,请选复选框。要拒绝条规则,不勾选即可。",
"Time the item was last modified": "最近修改项目的时间",
"To connect with the Syncthing device named \"{%devicename%}\", add a new remote device on your end with this ID:": "要连接名为“{{devicename}}”的 Syncthing 设备,请在的终端添加具有此 ID 的新远程设备:",
"To permit a rule, have the checkbox checked. To deny a rule, leave it unchecked.": "要允许条规则,请选复选框。要拒绝条规则,请将其保留为未选中状态。",
"Today": "今天",
"Trash Can": "回收站",
"Trash Can File Versioning": "回收站版本控制",
"Trash Can File Versioning": "回收站文件版本控制",
"Type": "类型",
"UNIX Permissions": "UNIX权限",
"UNIX Permissions": "UNIX 权限",
"Unavailable": "无效",
"Unavailable/Disabled by administrator or maintainer": "无效/禁用(由管理员或维护者)",
"Undecided (will prompt)": "定(将提示)",
"Unexpected Items": "特殊项目",
"Unexpected items have been found in this folder.": "在此文件夹中发现了意外项目。",
"Unignore": "解除忽略",
"Undecided (will prompt)": "未决定(将提示)",
"Unexpected Items": "意外项目",
"Unexpected items have been found in this folder.": "在此文件夹中发现了意外项目。",
"Unignore": "取消忽略",
"Unknown": "未知",
"Unshared": "非共享",
"Unshared Devices": "非共享设备",
"Unshared Folders": "非共享文件夹",
"Untrusted": "不可信的",
"Up to Date": "同步完成",
"Updated {%file%}": "{{file}} 已更新",
"Untrusted": "不受信任",
"Up to Date": "最新",
"Updated {%file%}": "已更新 {{file}}",
"Upgrade": "更新",
"Upgrade To {%version%}": "升级至版本 {{version}}",
"Upgrading": "升级中",
"Upload Rate": "上传速",
"Uptime": "启动",
"Usage reporting is always enabled for candidate releases.": "发布候选版总是会启用使用报告。",
"Use HTTPS for GUI": "使用加密连接到图形管理页面",
"Use notifications from the filesystem to detect changed items.": "使用文件系统的通知来检测更改的项目。",
"User": "用户",
"Upload Rate": "上传速",
"Uptime": "启动时间",
"Usage reporting is always enabled for candidate releases.": "发布候选版始终启用使用报告。",
"Use HTTPS for GUI": "使用 HTTPS 连接到 GUI",
"Use notifications from the filesystem to detect changed items.": "使用来自文件系统的通知来检测更改的项目。",
"User": "用户",
"User Home": "用户主目录",
"Username/Password has not been set for the GUI authentication. Please consider setting it up.": "尚未为GUI身份验证设置用户名/密码。 请考虑进行设置。",
"Using a QUIC connection over LAN": "正使用局域网 QUIC 连接",
"Using a QUIC connection over WAN": "正使用广域网 QUIC 连接",
"Using a direct TCP connection over LAN": "通过局域网使用直接TCP连接",
"Using a direct TCP connection over WAN": "通过广域网使用直接TCP连接",
"Username/Password has not been set for the GUI authentication. Please consider setting it up.": "尚未为 GUI 身份验证设置用户名/密码。请考虑设置。",
"Using a QUIC connection over LAN": "正在通过局域网使用 QUIC 连接",
"Using a QUIC connection over WAN": "正在通过广域网使用 QUIC 连接",
"Using a direct TCP connection over LAN": "正在通过局域网使用直接 TCP 连接",
"Using a direct TCP connection over WAN": "正在通过广域网使用直接 TCP 连接",
"Version": "版本",
"Versions": "历史版本",
"Versions Path": "历史版本路径",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "超过最长保留时间,或者不满足下列条件的历史版本,将会被删除。",
"Waiting to Clean": "等待清",
"Waiting to Scan": "等待扫描",
"Waiting to Sync": "等待同步",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "超过最长保留时间,或者不满足下列条件的历史版本,则会自动删除。",
"Waiting to Clean": "正在等待清",
"Waiting to Scan": "正在等待扫描",
"Waiting to Sync": "正在等待同步",
"Warning": "警告",
"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}}\"的下级目录。",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "警告,路径是有文件夹\"{{otherFolderLabel}}\" ({{otherFolder}})的下级目录。",
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "警告:如果你在使用外部的监视器如 {{syncthingInotify}},你应该确保它已经取消激活。",
"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}}的下级目录。",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "警告,路径是有文件夹{{otherFolderLabel}}”({{otherFolder}}的下级目录。",
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "警告:如果您使用的是 {{syncthingInotify}} 等外部监视程序,则应确保其已停用。",
"Watch for Changes": "监视更改",
"Watching for Changes": "正在监视更改",
"Watching for changes discovers most changes without periodic scanning.": "对更改的监视无需定期扫描就可以发现大多数更改。",
"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 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 区分大小写。",
"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 用于在不同设备间建立联系。它们区分大小写,必须在所有设备之间完全匹配。",
"When set to more than one on both devices, Syncthing will attempt to establish multiple concurrent connections. If the values differ, the highest will be used. Set to zero to let Syncthing decide.": "当两台设备上的连接数均被设为大于 1 时Syncthing 会尝试建立多个并行连接。如果两台设备上的设置的连接数不同,则会使用最大的连接数。设为 0 表示让 Syncthing 自行决定。",
"Yes": "是",
"Yesterday": "昨天",
"You can also copy and paste the text into a new message manually.": "你也可以手动将文本复制并粘贴到新消息中。",
"You can also select one of these nearby devices:": "您可以从这些附近的设备中选择",
"You can change your choice at any time in the Settings dialog.": "您可以在任何时候在设置对话框中更改选择。",
"You can read more about the two release channels at the link below.": "您可以从以下链接读取更多关于两个发渠道的信息。",
"You have no ignored devices.": "没有忽略的设备。",
"You have no ignored folders.": "没有忽略的文件夹。",
"You have unsaved changes. Do you really want to discard them?": "有未保存的更改。你真的要丢弃它们",
"You can also copy and paste the text into a new message manually.": "您还可以手动将文本复制并粘贴到新消息中。",
"You can also select one of these nearby devices:": "您可以选择以下附近的设备之一",
"You can change your choice at any time in the Settings dialog.": "您可以在设置对话框中随时更改您的选择。",
"You can read more about the two release channels at the link below.": "您可以在下面的链接中阅读有关这两个发渠道的更多信息。",
"You have no ignored devices.": "没有忽略的设备。",
"You have no ignored folders.": "没有忽略的文件夹。",
"You have unsaved changes. Do you really want to discard them?": "有未保存的更改。是否确定要丢弃它们?",
"You must keep at least one version.": "您必须保留至少一个版本。",
"You should never add or change anything locally in a \"{%receiveEncrypted%}\" folder.": "您绝对不应在“ {{receiveEncrypted}}”文件夹中添加或更改任何本地内容。",
"Your SMS app should open to let you choose the recipient and send it from your own number.": "的短信应用程序应该打开,让选择收件人并从自己的号码发送。",
"Your email app should open to let you choose the recipient and send it from your own address.": "的电子邮件应用程序应该打开,让选择收件人并从自己的地址发送。",
"You should never add or change anything locally in a \"{%receiveEncrypted%}\" folder.": "您永远不应该在本地添加或更改“{{receiveEncrypted}}”文件夹中的任何内容。",
"Your SMS app should open to let you choose the recipient and send it from your own number.": "的短信应用应该打开,让选择接收者并从自己的号码发送。",
"Your email app should open to let you choose the recipient and send it from your own address.": "的电子邮件应用应该打开,让选择收件人并从自己的地址发送。",
"days": "天",
"deleted": "已删除",
"deny": "拒绝",
@@ -549,7 +549,7 @@
}
},
"unknown device": "未知设备",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} 想“{{folder}}” 文件夹共享给您。",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} 想要共享 \"{{folderlabel}}\" ({{folder}}) 文件夹给您。",
"{%reintroducer%} might reintroduce this device.": "{{reintroducer}}可能会重新引入此设备。"
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} 想要共享文件夹“{{folder}}”。",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} 想要共享文件夹“{{folderlabel}}”({{folder}}。",
"{%reintroducer%} might reintroduce this device.": "{{reintroducer}} 可能会重新引入此设备。"
}

View File

@@ -279,7 +279,7 @@
"Out of Sync Items": "未同步項目",
"Outgoing Rate Limit (KiB/s)": "連出速率限制 (KiB/s)",
"Override": "覆蓋",
"Override Changes": "覆蓋變",
"Override Changes": "覆蓋變",
"Ownership": "所有權",
"Password": "密碼",
"Path": "路徑",

View File

@@ -1 +1 @@
var langPrettyprint = {"ar":"Arabic","bg":"Bulgarian","ca":"Catalan","ca@valencia":"Valencian","cs":"Czech","da":"Danish","de":"German","el":"Greek","en":"English","en-GB":"English (United Kingdom)","es":"Spanish","eu":"Basque","fil":"Filipino","fr":"French","fy":"Frisian","hi":"Hindi","hu":"Hungarian","id":"Indonesian","it":"Italian","ja":"Japanese","ko-KR":"Korean","lt":"Lithuanian","nl":"Dutch","pl":"Polish","pt-BR":"Portuguese (Brazil)","pt-PT":"Portuguese (Portugal)","ro-RO":"Romanian","ru":"Russian","sk":"Slovak","sl":"Slovenian","sv":"Swedish","tr":"Turkish","uk":"Ukrainian","zh-CN":"Chinese (Simplified)","zh-HK":"Chinese (Traditional, Hong Kong)","zh-TW":"Chinese (Traditional)"}
var langPrettyprint = {"ar":"Arabic","bg":"Bulgarian","ca":"Catalan","ca@valencia":"Valencian","cs":"Czech","da":"Danish","de":"German","el":"Greek","en":"English","en-GB":"English (United Kingdom)","es":"Spanish","eu":"Basque","fil":"Filipino","fr":"French","fy":"Frisian","ga":"Irish","hi":"Hindi","hu":"Hungarian","id":"Indonesian","it":"Italian","ja":"Japanese","ko-KR":"Korean","lt":"Lithuanian","nl":"Dutch","pl":"Polish","pt-BR":"Portuguese (Brazil)","pt-PT":"Portuguese (Portugal)","ro-RO":"Romanian","ru":"Russian","sk":"Slovak","sl":"Slovenian","sv":"Swedish","tr":"Turkish","uk":"Ukrainian","zh-CN":"Chinese (Simplified Han script)","zh-HK":"Chinese (Traditional Han script, Hong Kong)","zh-TW":"Chinese (Traditional Han script)"}

View File

@@ -1 +1 @@
var validLangs = ["ar","bg","ca","ca@valencia","cs","da","de","el","en","en-GB","es","eu","fil","fr","fy","hi","hu","id","it","ja","ko-KR","lt","nl","pl","pt-BR","pt-PT","ro-RO","ru","sk","sl","sv","tr","uk","zh-CN","zh-HK","zh-TW"]
var validLangs = ["ar","bg","ca","ca@valencia","cs","da","de","el","en","en-GB","es","eu","fil","fr","fy","ga","hi","hu","id","it","ja","ko-KR","lt","nl","pl","pt-BR","pt-PT","ro-RO","ru","sk","sl","sv","tr","uk","zh-CN","zh-HK","zh-TW"]

View File

@@ -413,7 +413,7 @@
</h4>
</button>
<div id="folder-{{$index}}" class="panel-collapse collapse">
<div class="panel-body">
<div class="panel-body less-padding">
<table class="table table-condensed table-striped table-auto">
<tbody>
<tr class="visible-xs">
@@ -681,7 +681,7 @@
</h4>
</button>
<div id="device-this" class="panel-collapse collapse in">
<div class="panel-body">
<div class="panel-body less-padding">
<table class="table table-condensed table-striped table-auto">
<tbody>
<tr>
@@ -796,7 +796,7 @@
</h4>
</button>
<div id="device-{{$index}}" class="panel-collapse collapse">
<div class="panel-body">
<div class="panel-body less-padding">
<table class="table table-condensed table-striped table-auto">
<tbody>
<tr class="visible-xs">

View File

@@ -30,7 +30,7 @@
<h4 class="text-center" translate>The Syncthing Authors</h4>
<div class="row">
<div class="col-md-12" id="contributor-list">
Jakob Borg, Audrius Butkevicius, Jesse Lucas, Simon Frei, Tomasz Wilczyński, Alexander Graf, Alexandre Viau, Anderson Mesquita, André Colomb, Antony Male, Ben Schulz, Caleb Callaway, Daniel Harte, Eric P, Evgeny Kuznetsov, Lars K.W. Gohlke, Lode Hoste, Michael Ploujnikov, Nate Morrison, Philippe Schommers, Ryan Sullivan, Sergey Mishin, Stefan Tatschner, Wulf Weich, bt90, greatroar, Aaron Bieber, Adam Piggott, Adel Qalieh, Alan Pope, Alberto Donato, Aleksey Vasenev, Alessandro G., Alex Lindeman, Alex Xu, Alexander Seiler, Alexandre Alves, Aman Gupta, Anatoli Babenia, Andreas Sommer, Andrew Dunham, Andrew Meyer, Andrew Rabert, Andrey D, Anjan Momi, Anthony Goeckner, Antoine Lamielle, Anur, Aranjedeath, Arkadiusz Tymiński, Aroun, Arthur Axel fREW Schmidt, Artur Zubilewicz, Aurélien Rainone, BAHADIR YILMAZ, Bart De Vries, Beat Reichenbach, Ben Curthoys, Ben Shepherd, Ben Sidhom, Benedikt Heine, Benedikt Morbach, Benjamin Nater, Benno Fünfstück, Benny Ng, Boqin Qin, Boris Rybalkin, Brandon Philips, Brendan Long, Brian R. Becker, Carsten Hagemann, Catfriend1, Cathryne Linenweaver, Cedric Staniewski, Chih-Hsuan Yen, Choongkyu, Chris Howie, Chris Joel, Chris Tonkinson, Christian Kujau, Christian Prescott, Colin Kennedy, Cromefire_, Cyprien Devillez, Dale Visser, Dan, Daniel Barczyk, Daniel Bergmann, Daniel Martí, Daniel Padrta, Darshil Chanpura, David Rimmer, DeflateAwning, Denis A., Dennis Wilson, DerRockWolf, Devon G. Redekopp, Dimitri Papadopoulos Orfanos, Dmitry Saveliev, Domenic Horner, Dominik Heidler, Elias Jarlebring, Elliot Huffman, Emil Hessman, Emil Lundberg, Eng Zer Jun, Eric Lesiuta, Erik Meitner, Evan Spensley, Federico Castagnini, Felix, Felix Ableitner, Felix Lampe, Felix Unterpaintner, Francois-Xavier Gsell, Frank Isemann, Gahl Saraf, Gilli Sigurdsson, Gleb Sinyavskiy, Graham Miln, Greg, Han Boetes, HansK-p, Harrison Jones, Heiko Zuerker, Hugo Locurcio, Iain Barnett, Ian Johnson, Ikko Ashimine, Ilya Brin, Iskander Sharipov, Jaakko Hannikainen, Jacek Szafarkiewicz, Jack Croft, Jacob, Jake Peterson, James O'Beirne, James Patterson, Jaroslav Lichtblau, Jaroslav Malec, Jaspitta, Jauder Ho, Jaya Chithra, Jaya Kumar, Jeffery To, Jens Diemer, Jerry Jacobs, Jochen Voss, Johan Andersson, Johan Vromans, John Rinehart, Jonas Thelemann, Jonathan, Jonathan Cross, Jonta, Jose Manuel Delicado, Julian Lehrhuber, Jörg Thalheim, Jędrzej Kula, K.B.Dharun Krishna, Kalle Laine, Karol Różycki, Kebin Liu, Keith Harrison, Keith Turner, Kelong Cong, Ken'ichi Kamada, Kevin Allen, Kevin Bushiri, Kevin White, Jr., Kurt Fitzner, LSmithx2, Lars Lehtonen, Laurent Arnoud, Laurent Etiemble, Leo Arias, Liu Siyuan, Lord Landon Agahnim, Lukas Lihotzki, Luke Hamburg, Majed Abdulaziz, Marc Laporte, Marc Pujol, Marcin Dziadus, Marcus Legendre, Mario Majila, Mark Pulford, Martchus, Martin Polehla, Mateusz Naściszewski, Mateusz Ż, Matic Potočnik, Matt Burke, Matt Robenolt, Matteo Ruina, Maurizio Tomasi, Max, Max Schulze, MaximAL, Maxime Thirouin, Maximilian, MichaIng, Michael Jephcote, Michael Rienstra, Michael Tilli, Migelo, Mike Boone, MikeLund, MikolajTwarog, Mingxuan Lin, Naveen, Nicholas Rishel, Nick Busey, Nico Stapelbroek, Nicolas Braud-Santoni, Nicolas Perraut, Niels Peter Roest, Nils Jakobi, NinoM4ster, 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, Phani Rithvij, Phil Davis, Phill Luby, Pier Paolo Ramon, Piotr Bejda, Pramodh KP, Quentin Hibon, Rahmi Pruitt, Richard Hartmann, Robert Carosi, Roberto Santalla, Robin Schoonover, Roman Zaynetdinov, Ross Smith II, Ruslan Yevdokymov, Ryan Qian, Sacheendra Talluri, Scott Klupfel, Sertonix, Severin von Wnuck-Lipinski, Shaarad Dalvi, Simon Mwepu, Sly_tom_cat, Stefan Kuntz, Steven Eckhoff, Suhas Gundimeda, Sven Bachmann, Taylor Khan, Thomas, Thomas Hipp, Tim Abell, Tim Howes, Tim Nordenfur, Tobias Klauser, Tobias Nygren, Tobias Tom, Tom Jakubowski, Tommy Thorn, Tully Robinson, Tyler Brazier, Tyler Kropp, Unrud, Veeti Paananen, Victor Buinsky, Vik, Vil Brekin, Vladimir Rusinov, WangXi, Will Rouesnel, William A. Kennington III, Xavier O., Yannic A., andresvia, andyleap, boomsquared, chenrui, chucic, cjc7373, cui fliter, d-volution, derekriemer, desbma, diemade, digital, entity0xfe, georgespatton, ghjklw, guangwu, gudvinr, ignacy123, janost, jaseg, jelle van der Waa, jtagcat, klemens, kylosus, luchenhan, luzpaz, marco-m, mclang, mv1005, nf, orangekame3, otbutz, overkill, perewa, red_led, rubenbe, sec65, vapatel2, villekalliomaki, wangguoliang, wouter bolsterlee, xarx00, xjtdy888, 佛跳墙, 落心
Jakob Borg, Audrius Butkevicius, Jesse Lucas, Simon Frei, Tomasz Wilczyński, Alexander Graf, Alexandre Viau, Anderson Mesquita, André Colomb, Antony Male, Ben Schulz, Caleb Callaway, Daniel Harte, Emil Lundberg, Eric P, Evgeny Kuznetsov, Lars K.W. Gohlke, Lode Hoste, Michael Ploujnikov, Nate Morrison, Philippe Schommers, Ryan Sullivan, Sergey Mishin, Stefan Tatschner, Wulf Weich, bt90, greatroar, Aaron Bieber, Adam Piggott, Adel Qalieh, Alan Pope, Alberto Donato, Aleksey Vasenev, Alessandro G., Alex Lindeman, Alex Xu, Alexander Seiler, Alexandre Alves, Aman Gupta, Anatoli Babenia, Andreas Sommer, Andrew Dunham, Andrew Meyer, Andrew Rabert, Andrey D, Anjan Momi, Anthony Goeckner, Antoine Lamielle, Anur, Aranjedeath, Arkadiusz Tymiński, Aroun, Arthur Axel fREW Schmidt, Artur Zubilewicz, Aurélien Rainone, BAHADIR YILMAZ, Bart De Vries, Beat Reichenbach, Ben Curthoys, Ben Shepherd, Ben Sidhom, Benedikt Heine, Benedikt Morbach, Benjamin Nater, Benno Fünfstück, Benny Ng, Boqin Qin, Boris Rybalkin, Brandon Philips, Brendan Long, Brian R. Becker, Carsten Hagemann, Catfriend1, Cathryne Linenweaver, Cedric Staniewski, Chih-Hsuan Yen, Choongkyu, Chris Howie, Chris Joel, Chris Tonkinson, Christian Kujau, Christian Prescott, Colin Kennedy, Cromefire_, Cyprien Devillez, Dale Visser, Dan, Daniel Barczyk, Daniel Bergmann, Daniel Martí, Daniel Padrta, Darshil Chanpura, David Rimmer, DeflateAwning, Denis A., Dennis Wilson, DerRockWolf, Devon G. Redekopp, Dimitri Papadopoulos Orfanos, Dmitry Saveliev, Domenic Horner, Dominik Heidler, Elias Jarlebring, Elliot Huffman, Emil Hessman, Eng Zer Jun, Eric Lesiuta, Erik Meitner, Evan Spensley, Federico Castagnini, Felix, Felix Ableitner, Felix Lampe, Felix Unterpaintner, Francois-Xavier Gsell, Frank Isemann, Gahl Saraf, Gilli Sigurdsson, Gleb Sinyavskiy, Graham Miln, Greg, Gusted, Han Boetes, HansK-p, Harrison Jones, Heiko Zuerker, Hugo Locurcio, Iain Barnett, Ian Johnson, Ikko Ashimine, Ilya Brin, Iskander Sharipov, Jaakko Hannikainen, Jacek Szafarkiewicz, Jack Croft, Jacob, Jake Peterson, James O'Beirne, James Patterson, Jaroslav Lichtblau, Jaroslav Malec, Jaspitta, Jauder Ho, Jaya Chithra, Jaya Kumar, Jeffery To, Jens Diemer, Jerry Jacobs, Jochen Voss, Johan Andersson, Johan Vromans, John Rinehart, Jonas Thelemann, Jonathan, Jonathan Cross, Jonta, Jose Manuel Delicado, Julian Lehrhuber, Jörg Thalheim, Jędrzej Kula, K.B.Dharun Krishna, Kalle Laine, Karol Różycki, Kebin Liu, Keith Harrison, Keith Turner, Kelong Cong, Ken'ichi Kamada, Kevin Allen, Kevin Bushiri, Kevin White, Jr., Kurt Fitzner, LSmithx2, Lars Lehtonen, Laurent Arnoud, Laurent Etiemble, Leo Arias, Liu Siyuan, Lord Landon Agahnim, Lukas Lihotzki, Luke Hamburg, Majed Abdulaziz, Marc Laporte, Marc Pujol, Marcin Dziadus, Marcus Legendre, Mario Majila, Mark Pulford, Martchus, Martin Polehla, Mateusz Naściszewski, Mateusz Ż, Matic Potočnik, Matt Burke, Matt Robenolt, Matteo Ruina, Maurizio Tomasi, Max, Max Schulze, MaximAL, Maxime Thirouin, Maximilian, MichaIng, Michael Jephcote, Michael Rienstra, Michael Tilli, Migelo, Mike Boone, MikeLund, MikolajTwarog, Mingxuan Lin, Naveen, Nicholas Rishel, Nick Busey, Nico Stapelbroek, Nicolas Braud-Santoni, Nicolas Perraut, Niels Peter Roest, Nils Jakobi, NinoM4ster, 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, Phani Rithvij, Phil Davis, Phill Luby, Pier Paolo Ramon, Piotr Bejda, Pramodh KP, Quentin Hibon, Rahmi Pruitt, Richard Hartmann, Robert Carosi, Roberto Santalla, Robin Schoonover, Roman Zaynetdinov, Ross Smith II, Ruslan Yevdokymov, Ryan Qian, Sacheendra Talluri, Scott Klupfel, Sertonix, Severin von Wnuck-Lipinski, Shaarad Dalvi, Simon Mwepu, Simon Pickup, Sly_tom_cat, Sonu Kumar Saw, Stefan Kuntz, Steven Eckhoff, Suhas Gundimeda, Sven Bachmann, Taylor Khan, Thomas, Thomas Hipp, Tim Abell, Tim Howes, Tim Nordenfur, Tobias Klauser, Tobias Nygren, Tobias Tom, Tom Jakubowski, Tommy Thorn, Tommy van der Vorst, Tully Robinson, Tyler Brazier, Tyler Kropp, Unrud, Veeti Paananen, Victor Buinsky, Vik, Vil Brekin, Vladimir Rusinov, WangXi, Will Rouesnel, William A. Kennington III, Xavier O., Yannic A., andresvia, andyleap, boomsquared, chenrui, chucic, cjc7373, cui fliter, d-volution, derekriemer, desbma, diemade, digital, entity0xfe, georgespatton, ghjklw, guangwu, gudvinr, ignacy123, janost, jaseg, jelle van der Waa, jtagcat, klemens, kylosus, luchenhan, luzpaz, marco-m, maxice8, mclang, mv1005, nf, orangekame3, otbutz, overkill, perewa, red_led, rubenbe, sec65, vapatel2, villekalliomaki, wangguoliang, wouter bolsterlee, xarx00, xjtdy888, 佛跳墙, 落心
</div>
</div>
</div>
@@ -54,7 +54,6 @@ Jakob Borg, Audrius Butkevicius, Jesse Lucas, Simon Frei, Tomasz Wilczyński, Al
<li><a href="https://github.com/calmh/xdr">calmh/xdr</a>, Copyright &copy; 2014 Jakob Borg.</li>
<li><a href="https://github.com/chmduquesne/rollinghash">chmduquesne/rollinghash</a>, Copyright &copy; 2015 Christophe-Marie Duquesne.</li>
<li><a href="https://github.com/d4l3k/messagediff">d4l3k/messagediff</a>, Copyright &copy; 2015 Tristan Rice.</li>
<li><a href="https://github.com/flynn-archive/go-shlex">flynn-archive/go-shlex</a>, Copyright &copy; 2012 Google Inc.</li>
<li><a href="https://github.com/gobwas/glob">gobwas/glob</a>, Copyright &copy; 2016 Sergey Kamardin.</li>
<li><a href="https://github.com/gogo/protobuf">gogo/protobuf</a>, Copyright &copy; 2013 The GoGo Authors.</li>
<li><a href="https://github.com/golang/groupcache">golang/groupcache</a>, Copyright &copy; 2013 Google Inc.</li>
@@ -62,10 +61,8 @@ Jakob Borg, Audrius Butkevicius, Jesse Lucas, Simon Frei, Tomasz Wilczyński, Al
<li><a href="https://github.com/golang/snappy">golang/snappy</a>, Copyright &copy; 2011 The Snappy-Go Authors.</li>
<li><a href="https://github.com/jackpal/gateway">jackpal/gateway</a>, Copyright &copy; 2010 Jack Palevich.</li>
<li><a href="https://github.com/kballard/go-shellquote">kballard/go-shellquote</a>, Copyright &copy; 2014 Kevin Ballard.</li>
<li><a href="https://github.com/lib/pq">lib/pq</a>, Copyright &copy; 2011-2013, 'pq' Contributors, portions Copyright &copy; 2011 Blake Mizerany.</li>
<li><a href="https://github.com/mattn/go-isatty">mattn/go-isatty</a>, Copyright &copy; Yasuhiro MATSUMOTO.</li>
<li><a href="https://github.com/matttproud/golang_protobuf_extensions">matttproud/golang_protobuf_extensions</a>, Copyright &copy; 2012 Matt T. Proud.</li>
<li><a href="https://github.com/minio/sha256-simd">minio/sha256-simd</a>, Copyright &copy; 2016-2017 Minio, Inc.</li>
<li><a href="https://github.com/oschwald/geoip2-golang">oschwald/geoip2-golang</a>, Copyright &copy; 2015, Gregory J. Oschwald.</li>
<li><a href="https://github.com/oschwald/maxminddb-golang">oschwald/maxminddb-golang</a>, Copyright &copy; 2015, Gregory J. Oschwald.</li>
<li><a href="https://github.com/petermattis/goid">petermattis/goid</a>, Copyright &copy; 2015-2016 Peter Mattis.</li>

View File

@@ -59,30 +59,35 @@ angular.module('syncthing.core')
// Find the first language in the list provided by the user's browser
// that is a prefix of a language we have available. That is, "en"
// sent by the browser will match "en" or "en-US", while "zh-TW" will
// match only "zh-TW" and not "zh-CN".
// match only "zh-TW" and not "zh" or "zh-CN".
var i,
lang,
browserLang,
matching,
locale = _defaultLocale;
locale = _defaultLocale; // Fallback if nothing matched
for (i = 0; i < langs.length; i++) {
lang = langs[i];
browserLang = langs[i];
if (lang.length < 2) {
if (browserLang.length < 2) {
continue;
}
matching = _availableLocales.filter(function (possibleLang) {
// The langs returned by the /rest/langs call will be in lower
// case. We compare to the lowercase version of the language
// code we have as well.
// The langs returned by the /rest/svc/langs call will be in
// lower case. We compare to the lowercase version of the
// language code we have as well.
possibleLang = possibleLang.toLowerCase();
if (possibleLang.length > lang.length) {
return possibleLang.indexOf(lang) === 0;
} else {
return lang.indexOf(possibleLang) === 0;
if (possibleLang.indexOf(browserLang) !== 0) {
// Prefix does not match
return false;
}
if (possibleLang.length > browserLang.length) {
// Must match up to the next hyphen separator
return possibleLang[browserLang.length] === '-';
}
// Same length, exact match
return true;
});
if (matching.length >= 1) {
@@ -90,7 +95,6 @@ angular.module('syncthing.core')
break;
}
}
// Fallback if nothing matched
useLocale(locale);
});
}

11
gui/default/syncthing/core/syncthingController.js Executable file → Normal file
View File

@@ -16,6 +16,15 @@ angular.module('syncthing.core')
LocaleService.autoConfigLocale();
if (!$scope.authenticated) {
function setVersionFromHeader(_data, _status, headers) {
var version = headers('X-Syncthing-Version');
if (version) {
$scope.version = { version: version };
}
}
// Get index.html again (likely cached) to retrieve the version header
$http.get('').success(setVersionFromHeader).error(setVersionFromHeader);
// Can't proceed yet - wait for the page reload after successful login.
return;
}
@@ -1447,7 +1456,7 @@ angular.module('syncthing.core')
// Assume hasRemoteGUIAddress is true or we would not be here
var conn = $scope.connections[deviceCfg.deviceID];
// Use regex to filter out scope ID from IPv6 addresses.
return 'http://' + replaceAddressPort(conn.address, deviceCfg.remoteGUIPort).replace('%.*?\]:', ']:');
return 'http://' + replaceAddressPort(conn.address, deviceCfg.remoteGUIPort).replace(/%.*?\]:/, ']:');
};
function replaceAddressPort(address, newPort) {

View File

@@ -13,7 +13,7 @@
<h4 class="panel-title" translate tabindex="0">GUI</h4>
</div>
<div id="guiConfig" class="panel-collapse collapse" role="tabpanel" aria-labelledby="guiHeading">
<div class="panel-body">
<div class="panel-body less-padding">
<form class="form-horizontal" role="form">
<div ng-repeat="(key, value) in advancedConfig.gui" ng-init="type = inputTypeFor(key, value)" ng-if="type != 'skip'" class="form-group">
<label for="guiInput{{$index}}" class="col-sm-4 control-label">{{key | uncamel}}&nbsp;<a href="{{docsURL('users/config#config-option-gui.')}}{{key | lowercase}}" target="_blank"><span class="fas fa-question-circle"></span></a></label>
@@ -32,7 +32,7 @@
<h4 class="panel-title" tabindex="0" translate>Options</h4>
</div>
<div id="optionsConfig" class="panel-collapse collapse" role="tabpanel" aria-labelledby="optionsHeading">
<div class="panel-body">
<div class="panel-body less-padding">
<form class="form-horizontal" role="form">
<div ng-repeat="(key, value) in advancedConfig.options" ng-if="inputTypeFor(key, value) != 'skip'" class="form-group">
<label for="optionsInput{{$index}}" class="col-sm-4 control-label">{{key | uncamel}}&nbsp;<a href="{{docsURL('users/config#config-option-options.')}}{{key | lowercase}}" target="_blank"><span class="fas fa-question-circle"></span></a></label>
@@ -51,7 +51,7 @@
<h4 class="panel-title" tabindex="0" translate>LDAP</h4>
</div>
<div id="ldapConfig" class="panel-collapse collapse" role="tabpanel" aria-labelledby="ldapHeading">
<div class="panel-body">
<div class="panel-body less-padding">
<form class="form-horizontal" role="form">
<div ng-repeat="(key, value) in advancedConfig.ldap" ng-if="inputTypeFor(key, value) != 'skip'" class="form-group">
<label for="ldapInput{{$index}}" class="col-sm-4 control-label">{{key | uncamel}}&nbsp;<a href="{{docsURL('users/config#config-option-ldap.')}}{{key | lowercase}}" target="_blank"><span class="fas fa-question-circle"></span></a></label>
@@ -70,7 +70,7 @@
<h4 class="panel-title" translate>Folders</h4>
</div>
<div id="advancedFolders" class="panel-collapse collapse" role="tabpanel" aria-labelledby="advancedFoldersHeading">
<div class="panel-body">
<div class="panel-body less-padding">
<div class="panel panel-default" ng-repeat="folder in advancedConfig.folders" ng-init="folderIndex = $index">
<div class="panel-heading" role="tab" id="folder{{folderIndex}}Heading" data-toggle="collapse" data-parent="#advancedFolders" href="#folder{{folderIndex}}Config" aria-expanded="false" aria-controls="folder{{folderIndex}}Config" style="cursor: pointer;">
<h4 ng-if="folder.label.length == 0" class="panel-title" tabindex="0">
@@ -81,7 +81,7 @@
</h4>
</div>
<div id="folder{{folderIndex}}Config" class="panel-collapse collapse" role="tabpanel" aria-labelledby="folder{{folderIndex}}Heading">
<div class="panel-body">
<div class="panel-body less-padding">
<form class="form-horizontal" role="form">
<div ng-repeat="(key, value) in folder" ng-if="inputTypeFor(key, value) != 'skip'" class="form-group">
<label for="folder{{folderIndex}}Input{{$index}}" class="col-sm-4 control-label">{{key | uncamel}}&nbsp;<a href="{{docsURL('users/config#config-option-folder.')}}{{key | lowercase}}" target="_blank"><span class="fas fa-question-circle"></span></a></label>
@@ -103,7 +103,7 @@
<h4 class="panel-title" tabindex="0" translate>Devices</h4>
</div>
<div id="advancedDevices" class="panel-collapse collapse" role="tabpanel" aria-labelledby="advancedDevicesHeading">
<div class="panel-body">
<div class="panel-body less-padding">
<div class="panel panel-default" ng-repeat="device in advancedConfig.devices" ng-init="deviceIndex = $index">
<div class="panel-heading" role="tab" id="device{{deviceIndex}}Heading" data-toggle="collapse" data-parent="#advancedDevices" href="#device{{deviceIndex}}Config" aria-expanded="false" aria-controls="device{{deviceIndex}}Config" style="cursor: pointer;">
<h4 class="panel-title" tabindex="0">
@@ -111,7 +111,7 @@
</h4>
</div>
<div id="device{{deviceIndex}}Config" class="panel-collapse collapse" role="tabpanel" aria-labelledby="device{{deviceIndex}}Heading">
<div class="panel-body">
<div class="panel-body less-padding">
<form class="form-horizontal" role="form">
<div ng-repeat="(key, value) in device" ng-if="inputTypeFor(key, value) != 'skip'" class="form-group">
<label for="device{{deviceIndex}}Input{{$index}}" class="col-sm-4 control-label">{{key | uncamel}}&nbsp;<a href="{{docsURL('users/config#config-option-device.')}}{{key | lowercase}}" target="_blank"><span class="fas fa-question-circle"></span></a></label>
@@ -133,7 +133,7 @@
<h4 class="panel-title" tabindex="0" translate>Defaults</h4>
</div>
<div id="advancedDefaults" class="panel-collapse collapse" role="tabpanel" aria-labelledby="advancedDefaultsHeading">
<div class="panel-body">
<div class="panel-body less-padding">
<div class="panel panel-default">
<div class="panel-heading" role="tab" id="advancedDefaultFolderHeading" data-toggle="collapse" data-parent="#advancedDefaults" href="#advancedDefaultFolder" aria-expanded="false" aria-controls="advancedDefaultFolder" style="cursor: pointer;">

View File

@@ -11,7 +11,7 @@
</h4>
</button>
<div id="remoteNeed-{{$index}}" class="panel-collapse" ng-class="{collapse: sizeOf(remoteNeedFolders) > 1}">
<div class="panel-body">
<div class="panel-body less-padding">
<table class="table table-striped">
<thead>
<tr>

View File

@@ -41,6 +41,7 @@
* @prop {string} [delimiter]
* @prop {DigitReplacements} [_digitReplacements]
* @prop {boolean} [_numberFirst]
* @prop {boolean} [_hideCountIf2]
*/
/**
@@ -116,6 +117,7 @@
// minute -> د stands for "دقيقة"
// second -> ث stands for "ثانية"
ar: assign(language("س", "ش", "أ", "ي", "س", "د", "ث", "م ث", ","), {
_hideCountIf2: true,
_digitReplacements: ["۰", "١", "٢", "٣", "٤", "٥", "٦", "٧", "٨", "٩"]
}),
// български (Bulgarian)
@@ -177,7 +179,7 @@
// ಕನ್ನಡ (Kannada)
kn: language("ವ", "ತ", "ವ", "ದ", "ಗಂ", "ನಿ", "ಸೆ", "ಮಿಸೆ"),
// 한국어 (Korean)
ko: language("년", "", "주", "일", "시", "분", "초", "밀리초"),
ko: language("년", "", "주", "일", "시", "분", "초", "밀리초"),
// Kurdî (Kurdish)
ku: language("sal", "m", "h", "r", "s", "d", "ç", "ms", ","),
// ລາວ (Lao)
@@ -464,19 +466,25 @@
: Math.floor(unitCount * Math.pow(10, maxDecimalPoints)) /
Math.pow(10, maxDecimalPoints);
var countStr = normalizedUnitCount.toString();
if (digitReplacements) {
if (language._hideCountIf2 && unitCount === 2) {
formattedCount = "";
for (var i = 0; i < countStr.length; i++) {
var char = countStr[i];
if (char === ".") {
formattedCount += decimal;
} else {
// @ts-ignore because `char` should always be 0-9 at this point.
formattedCount += digitReplacements[char];
}
}
spacer = "";
} else {
formattedCount = countStr.replace(".", decimal);
if (digitReplacements) {
formattedCount = "";
for (var i = 0; i < countStr.length; i++) {
var char = countStr[i];
if (char === ".") {
formattedCount += decimal;
} else {
// @ts-ignore because `char` should always be 0-9 at this point.
formattedCount += digitReplacements[char];
}
}
} else {
formattedCount = countStr.replace(".", decimal);
}
}
var languageWord = language[unitName];

0
gui/default/vendor/bootstrap/config.json vendored Executable file → Normal file
View File

0
gui/default/vendor/bootstrap/css/bootstrap-theme.css vendored Executable file → Normal file
View File

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