Compare commits

..

209 Commits

Author SHA1 Message Date
Jakob Borg
0945304a79 build: fix detection of next rc version 2025-06-20 11:17:23 +02:00
Jakob Borg
9703dd9f57 build: import release workflow changes from main 2025-06-20 11:12:05 +02:00
yparitcher
259e9ef08e fix(protocol): slightly loosen/correct ownership comparison criteria (fixes #9879) (#10176)
Only Require either matching UID & GID OR matching Names.

If the 2 devices have a different Name => UID mapping, they can never be
totaly equal. Therefore when syncing we try matching the Name and fall
back to the UID. However when scanning for changes we currently require
both the Name & UID to match. This leads to forever having out of sync
files back and forth, or local additions when receive only.

This patch does not change the sending behavoir. It only change what we
decide is equal for exisiting files with mismapped Name => UID,

The added testcases show the change: Test 1,5,6 are the same as current.
Test 2,3 Are what change with this patch (from false to true). Test 4 is
a subset of test 2 they is currently special cased as true, which does
not chnage.

Co-authored-by: Jakob Borg <jakob@kastelo.net>
2025-06-20 09:55:42 +02:00
Simon Frei
6a0c6128d8 fix(watchaggregator): properly handle sub-second watch durations (fixes #9927) (#10179)
I'll let Audrius words from the ticket explain this :)

> I'm a bit lost, time.Duration is an int64, yet watcher delay is float,
> anything sub 1s gets rounded down to 0, so you just end up going into
an
> infinite loop.


https://github.com/syncthing/syncthing/issues/9927#issuecomment-2967736106
2025-06-15 10:29:33 +02:00
Jakob Borg
b05ece0681 build: more resilient pushes to releases 2025-06-07 13:18:58 +02:00
Jakob Borg
e9133ef82b docs: link to Docker image, APT, in release notes 2025-06-05 19:19:05 +02:00
Jakob Borg
67ba20d777 build: also create relaysrv and discosrv releases 2025-06-05 19:19:05 +02:00
Jakob Borg
21da0d7890 fix(stupgrades): return latest stable & pre for each major 2025-06-05 19:19:05 +02:00
ardevd
ebbe57d0ab fix(syncthing): avoid writing panic log to nil fd (#10154)
### Purpose

This change fixes a logical bug in the panic log writing where we could
end up writing to a uninitialized file descriptor.

On the very first iteration, `panicFd` is nil. We enter the if `panicFd
== nil { … }` block, check for “panic:” or “fatal error:”, and if
neither matches, we skip instantiating `panicFd` altogether. However,
immediately after, still within `if panicFd == nil { … }`, we call
`panicFd.WriteString("Panic at ...")`. But `panicFd` would in this case
be `nil`, which will cause a run‐time panic.

It's not clear to me why panicFd is only initialized if the lines start
with "panic:" or "fatal error:" so I've left that logic untouched. With
this change we at least avoid the risk of writing to a nil
filedescriptor.
## Authorship

Your name and email will be added automatically to the AUTHORS file
based on the commit metadata.

---------

Co-authored-by: Jakob Borg <jakob@kastelo.net>
2025-06-03 07:39:21 +02:00
Jakob Borg
f4abc71dcc chore: copyright in next-version script 2025-06-02 20:41:42 +02:00
Jakob Borg
8aa02da93a build: include "v" prefix in version tags... 2025-06-02 19:59:45 +02:00
Jakob Borg
0e560486db build: use own script instead of svu
We use a slightly different handling of features between prereleases.
2025-06-02 19:49:23 +02:00
Syncthing Release Automation
57d413099d chore(gui, man, authors): update docs, translations, and contributors 2025-06-02 05:24:58 +00:00
Jakob Borg
1fdf07933c feat(config): expose folder and device info as metrics (fixes #9519) (#10148)
Tihs makes it easier to use metrics based on device and folder labels,
names, and other attributes. Other metrics which are based on folder or
device ID can be joined with these info metrics to enrich their label
sets.

```
# HELP syncthing_config_device_info Provides additional information labels on devices
# TYPE syncthing_config_device_info gauge
syncthing_config_device_info{device="I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU",introducer="false",name="s1",paused="false",untrusted="false"} 1

# HELP syncthing_config_folder_info Provides additional information labels on folders
# TYPE syncthing_config_folder_info gauge
syncthing_config_folder_info{folder="default",label="The default folder",path="s2",paused="false",type="sendreceive"} 1
```

With this you can e.g. query for

```
syncthing_connections_active * on(device) group_left syncthing_config_device_info
```

Fixes #9519 
Closes #10074 
Closes #10147
2025-05-31 17:09:23 +02:00
Jakob Borg
c50678618f chore: add issue types to GitHub issue templates 2025-05-30 11:57:27 +02:00
Jakob Borg
8094b459e4 build: remove schedule from PR metadata job
It shouldn't have touched non-PR issues, but it did
2025-05-30 11:57:27 +02:00
Simon Frei
6765867a2e chore(protocol): only allow enc. password changes on cluster config (#10145)
In practice we already always call SetPassword and ClusterConfig
together. However it's not just "sensible" to do that, it's required: If
the passwords change, the remote device needs to know about that to
check that the enc. setup is valid/consistent (e.g. tokens match,
folder-type is appropriate, ...).
And with the passwords set later, there's no point in adding them as
part of creating a new connection.

This is a "followup" (if one can call it that 4 years later :) ) to
resp. fix for the following commit:
924b96856f

Co-authored-by: Jakob Borg <jakob@kastelo.net>
2025-05-30 09:52:47 +02:00
Simon Frei
4fb8ee6a6f chore(protocol): don't start connection routines a second time (#10146) 2025-05-30 06:28:42 +00:00
Jakob Borg
674834ccf4 build: properly propagate build tags to Debian build (#10144)
Previously all were ignored except noupgrade which was hard coded...
2025-05-29 15:06:57 +00:00
Jakob Borg
3bd2bff23b fix(protocol): avoid deadlock with concurrent connection start and close (#10140) 2025-05-29 14:56:58 +00:00
Jakob Borg
40660c5fb7 build: add labeler workflow for PRs (#10143)
Use labels to categorise release notes
2025-05-29 10:04:08 +02:00
Jakob Borg
d940d094a1 build(deps): update our notify package from upstream (#10142) 2025-05-28 15:04:24 +00:00
Jakob Borg
9d67727989 build(deps): update dependencies (#10141) 2025-05-28 13:52:08 +00:00
Jakob Borg
6f51700a7f docs: general notes about v2 coming (#10135)
This adds a file that will be prepended to release notes (tag messages,
GitHub releases, forum posts) for v1 releases. I'd like there to be
something there to flag that things are going to change.
2025-05-27 10:01:04 +02:00
Marcel Meyer
598915193a refactor: use slices package for sorting (#10136)
Few more complicated usages of the sort packages are left.

### Purpose

Make progress towards replacing the sort package with slices package.
2025-05-26 20:37:49 +02:00
Jakob Borg
905e5ec07f build: handle multiple general release notes 2025-05-26 16:27:23 +02:00
Jakob Borg
4075b886d0 build: no need to build on the branches that just trigger tags 2025-05-26 15:21:21 +02:00
Jakob Borg
cade790198 build: use specific token for pushing release tags 2025-05-26 14:13:02 +02:00
Luke Hamburg
98555a9a80 fix(gui): update uncamel() to handle strings like 'IDs' (fixes #10128) (#10131)
> ⚠️ resubmission targeting `main` instead of `v2`

### Purpose

Updates `uncamel()` function in
[uncamelFilter.js](https://github.com/syncthing/syncthing/blob/v2/gui/default/syncthing/core/uncamelFilter.js)
to fix camelCase conversion edge cases, see #10128

This adds an array called `reservedStrings` which will be printed as-is,
e.g. `IDs`, `LAN` etc. I pre-populated this with what I believe makes
sense, but of course this is easily updated.

### Testing

I compiled all the config variables I could find in
`syncthing/lib/config/*configuration.go` and tested this new function
against them. Everything seemed to pass.

### Screenshot


![Image](https://github.com/user-attachments/assets/af8c9821-58b3-4a6a-8462-bead8a6d845a)
2025-05-26 11:43:38 +00:00
Marcel Meyer
48b757cac1 refactor: use slices package for sort (#10132)
The sort package is still used in places that were not trivial to
change. Since Go 1.21 slices package can be uswed for sort. See
https://go.dev/doc/go1.21#slices

### Purpose

Make some progress with the migration to a more up-to-date syntax.
2025-05-26 13:37:26 +02:00
Jakob Borg
58c85fc9db build: process for automatic release tags (#10133)
Make the release tagging consistent. Push to release branch to create a
stable release; push to release-rc to release a new candidate.
2025-05-26 13:33:53 +02:00
Syncthing Release Automation
ddd98a818a chore(gui, man, authors): update docs, translations, and contributors 2025-05-26 03:55:20 +00:00
Jakob Borg
64b5a1b738 fix(syncthing): ensure both config and data dirs exist at startup (fixes #10126) (#10127)
Previously we'd only ensure the config dir, which is often but not
always the same as the data dir.

Fixes #10126
2025-05-25 08:10:17 +02:00
Ashish Bhate
1a131a56f2 fix(versioner): fix perms of created folders (fixes #9626) (#10105)
As suggested in the linked issue, I've updated the versioner code to use
the permissions of the corresponding directory in the synced folder,
when creating the folder in the versions directory

### Testing
- Some tests are included with the PR. Happy to add more if you think
there are some edge-cases that we're missing.
- I've tested manually on linux to confirm the permissions of the
created directories.
- I haven't tested on Windows or OSX (I don't have access to these OS)
2025-05-24 07:35:32 +02:00
pullmerge
beda37f28b refactor: use slices.Contains to simplify code (#10121)
There is a [new function](https://pkg.go.dev/slices@go1.21.0#Contains)
added in the go1.21 standard library, which can make the code more
concise and easy to read.
2025-05-23 10:36:06 +00:00
Jakob Borg
2532ac35cf build(deps): update dependency due to build breakage (#10120) 2025-05-21 06:52:29 +00:00
Jakob Borg
bcd30ceaec chore: move golangci-lint & meta to separate PR-only workflow (#10119)
For now. Existing code is not golangci-lint clean, but new PRs should
be, ideally.
2025-05-21 08:32:49 +02:00
Jakob Borg
9a3493c2f4 build: reactivate golangci-lint (#10118)
With DeepSource becoming (imho) less and less useful, let's get this one
back on track. It will likely require adjusting over time.
2025-05-20 14:03:43 +02:00
André Colomb
fa404d5a0d chore(gui): add Serbian (sr) translation template (#10116)
Based on user request from Weblate, user `@vlazic`.
2025-05-19 21:06:38 +00:00
Syncthing Release Automation
73ad18fbfb chore(gui, man, authors): update docs, translations, and contributors 2025-05-19 03:56:31 +00:00
Syncthing Release Automation
1dd264894a chore(gui, man, authors): update docs, translations, and contributors 2025-05-12 03:54:02 +00:00
Marcus B Spencer
8c3d2f3bc5 fix(config): mark audit log options as needing restart (fixes #10099) (#10100)
### Testing

Change the `auditEnabled` option and you should get a prompt in the Web
GUI.
Restart and change the `auditFile` option, and you should get that same
prompt.

The prompt you should get is shown in the screenshots below.

### Screenshots


![Screenshot_20250507_122546](https://github.com/user-attachments/assets/23ce7c42-5e60-4f88-ac58-f312a9a1f5cc)

Co-authored-by: Jakob Borg <jakob@kastelo.net>
2025-05-09 10:49:11 +00:00
Hazem Krimi
702ed8ecc1 fix(config): deep copy configuration defaults (fixes #9916) (#10101)
### Purpose

Setting default configuration was not working properly since the
defaults struct is not deeply copied.

### Testing

Try running commands to change default configuration and either inspect
`config.xml` or `/rest/config` result to see the applied changed.
Example:
```
./syncthing cli config defaults folder versioning params set keep 5
```
2025-05-09 07:40:32 +02:00
Syncthing Release Automation
b038650810 chore(gui, man, authors): update docs, translations, and contributors 2025-05-05 03:52:42 +00:00
Hazem Krimi
a16bf555c0 feat(gui): close a modal when pressing ESC after switching modal tabs (fixes #9489) (#10092)
### Purpose

As stated in #9489 after clicking on a tab link to switch tabs in a
modal you can no longer close the modal through clicking the ESC key
unless you click anywhere on the modal to focus on it again.

### Testing

- Click on a modal that has tabs like "Settings" or "Add Folder" and
switch tabs then click on ESC.
- Check if clicking outside of a modal in the backdrop should still
close the modal.

### Demo


https://github.com/user-attachments/assets/a010db9a-72f7-4160-a7db-ddfebffb4834
2025-05-02 14:53:54 +00:00
Jakob Borg
cd6ea60fa1 build(deps): update dependencies (#10091)
Without bumping Go version
2025-05-01 18:15:37 +00:00
domain
0bf21d9db2 fix(strelaysrv): make the session limiter session-dependent (fixes #10072) (#10073)
### Purpose

Make the session limiter only apply to current session.

### Testing

Relay 2 or more sessions and check if the sum of the connection speed
can exceed the specified per-session rate.

2 sessions (-global-rate=50000000 and -per-session-rate=6250000):


![图片](https://github.com/user-attachments/assets/133e531a-ed49-4890-aef7-821c628bcfc8)

1 session (-global-rate=50000000 and -per-session-rate=6250000):


![图片](https://github.com/user-attachments/assets/ac89ea53-2d8e-4347-9bbc-4780d85e38d7)
2025-04-30 14:25:01 +00:00
Jakob Borg
f61843ef2e build: artifact uploads destination OCI 2025-04-29 14:01:25 -05:00
Syncthing Release Automation
23e8366f8d chore(gui, man, authors): update docs, translations, and contributors 2025-04-28 03:52:12 +00:00
Ross Smith II
93e72cc83f chore(gui): use go list --deps for dependency list (#10071) 2025-04-26 02:24:31 +00:00
Marcus B Spencer
190dff142c feat(config): add option for audit file (fixes #9481) (#10066) 2025-04-23 22:32:23 +07:00
bt90
c667ada63a chore(api): log X-Forwarded-For (#10035)
### Purpose

Fix https://github.com/syncthing/syncthing/issues/9336

The `emitLoginAttempt` function now checks for the presence of an
`X-Forwarded-For` header. The IP from this header is only used if the
connecting host is either on loopback or on the same LAN.

In the case of a host pretending to be a proxy, we'd still have both IPs
in the logs, which should make this much less critical from a security
standpoint.

### Testing

1. directly via localhost
2. via proxy an localhost

#### Logs

```
[3JPXJ] 2025/04/11 15:00:40 INFO: Wrong credentials supplied during API authorization from 127.0.0.1
[3JPXJ] 2025/04/11 15:03:04 INFO: Wrong credentials supplied during API authorization from 192.168.178.5 proxied by 127.0.0.1
```

#### Event API

```
  {
    "id": 23,
    "globalID": 23,
    "time": "2025-04-11T15:00:40.578577402+02:00",
    "type": "LoginAttempt",
    "data": {
      "remoteAddress": "127.0.0.1",
      "success": false,
      "username": "sdfsd"
    }
  },
  {
    "id": 24,
    "globalID": 24,
    "time": "2025-04-11T15:03:04.423403976+02:00",
    "type": "LoginAttempt",
    "data": {
      "proxy": "127.0.0.1",
      "remoteAddress": "192.168.178.5",
      "success": false,
      "username": "sdfsd"
    }
  }
```

### Documentation

https://github.com/syncthing/docs/pull/907

---------

Co-authored-by: Jakob Borg <jakob@kastelo.net>
2025-04-23 06:01:13 +00:00
Ross Smith II
93ae30d889 chore(gui): update dependency copyrights, add script for periodic maintenance (#10067)
### Purpose

This PR parses the output of `go mod graph` and updates the copyright
list in our [about
modal](486eebc4ac/gui/default/syncthing/core/aboutModalView.html (L38)).

If there are no changes, the program is silent. Otherwise, it reports
what additions, and deletions it made. It does not rewrite existing
copyright notices, but it does remove notices that we no longer use, as
well as add new ones.

It uses a GitHub API to try to determine the copyright string in the
license file. If one is not found, it defaults to `Copyright &copy;
<this_year> the <owner/repo> authors`. If a proper copyright is found,
simply update the notice in `aboutModalView.html`, and it will be used.
2025-04-23 12:41:05 +07:00
Syncthing Release Automation
486eebc4ac chore(gui, man, authors): update docs, translations, and contributors 2025-04-21 03:52:26 +00:00
Jakob Borg
ff33d976d1 chore(syncthing): remove support for TLS 1.2 sync connections (#10064)
This cleans up the option to allow old TLS 1.2 sync connections. The
flag existed for compatibility with old Syncthing versions that don't
support TLS 1.3, which is approximately Syncthing 1.2.2 (September 2019)
and older. ("Approximately" because it depends on the Go version it's
built with and that's when we switched to building with Go 1.13.)

Ref #10062 because it reminded me this exists.
2025-04-21 10:30:43 +07:00
TheCreeper
69890b4282 fix(osutil): give threads same I/O priority on Linux (#10063) 2025-04-21 02:30:52 +00:00
Jakob Borg
533c9a6ab0 chore(stun): switch lookup warning to debug level 2025-04-17 07:29:10 +07:00
Syncthing Release Automation
9521bb3931 chore(gui, man, authors): update docs, translations, and contributors 2025-04-14 03:51:01 +00:00
Jakob Borg
e46a0f99c3 chore: add missing copyright in new files from infra branch (#10055)
Let's see if it passes
2025-04-13 09:25:16 +00:00
Jakob Borg
ed97e365b2 Merge branch 'infrastructure'
* infrastructure:
  feat(stdiscosrv): configurable desired not-found rate
  chore(blobs): generalised blob storage
  chore(stdiscosrv): path style s3
  feat(ursv): add os/arch/distribution metric
  chore(strelaypoolsrv): limit number of returned relays
  build(infra): run in Docker environment for pushes
  chore(stupgrades): expose latest release as a metric
2025-04-13 09:41:45 +02:00
Jakob Borg
b4776ea4e0 feat(stdiscosrv): configurable desired not-found rate 2025-04-13 09:41:16 +02:00
Jakob Borg
b5ffd0a796 chore(blobs): generalised blob storage 2025-04-13 09:41:16 +02:00
Jakob Borg
c74299b59a chore(stdiscosrv): path style s3 2025-04-13 09:40:14 +02:00
Jakob Borg
8b6d837483 feat(ursv): add os/arch/distribution metric 2025-04-13 09:40:14 +02:00
Jakob Borg
3e74b3dee2 chore(strelaypoolsrv): limit number of returned relays
Avoid unnecessarily enormous responses by returning a random subset of
relays.
2025-04-13 09:40:14 +02:00
Jakob Borg
2902da996c build(infra): run in Docker environment for pushes 2025-04-13 09:40:14 +02:00
Jakob Borg
f6f144bf17 chore(stupgrades): expose latest release as a metric 2025-04-13 09:40:11 +02:00
Sébastien WENSKE
ab5c42f4a0 feat(api, gui): allow authentication bypass for metrics (#10045)
### Purpose

Give the ability to skip authentication for prometheus metrics
("/metrics").

### Testing

When authentication is enabled and "Metrics Without Auth" is checked
(not the default), the "/metrics" path remains accessible even when
disconnected.

### Screenshots


![image](https://github.com/user-attachments/assets/144b696b-dd72-46f4-94d5-cd21848e4a4c)

### Documentation

https://github.com/syncthing/docs/pull/906
2025-04-13 07:35:57 +00:00
Jakob Borg
7db3f7eaac Merge branch 'release-1.29.5'
* release-1.29.5:
  build: push artifacts to Azure (#10044)
  fix(syncthing): use separate lock file instead of locking the certificate (fixes #10053) (#10054)
2025-04-12 14:57:04 +02:00
Jakob Borg
f0b666269b build: push artifacts to Azure (#10044)
Provider migration
2025-04-12 14:55:24 +02:00
Jakob Borg
190a59842c fix(syncthing): use separate lock file instead of locking the certificate (fixes #10053) (#10054)
Apparently that nukes the cert under some circumstances on some Windows
🤷
2025-04-12 14:49:23 +02:00
Jakob Borg
40888c1a66 fix(syncthing): use separate lock file instead of locking the certificate (fixes #10053) (#10054)
Apparently that nukes the cert under some circumstances on some Windows
🤷
2025-04-12 14:46:57 +02:00
Jakob Borg
fa0d933e49 fix(gui): fix previous commit 2025-04-09 15:39:09 +02:00
tomasz1986
8372c0288f fix(gui): mark unseen disconnected devices as inactive (#10048)
Currently, the "Disconnected (Inactive)" status is only given to devices
that have not been seen for 7 days or longer. However, this is not the
case when adding a new device, or after resetting the database. Those
devices are only marked as "Disconnected", and they will stay like that
even if a long time passes without any connectivity. Moreover, the lack
of an "Inactive" status may confuse the user to believe that their
disconnect is only temporary.

For this reason, always mark devices that have not been seen yet as
"Disconnected (Inactive)".

Signed-off-by: Tomasz Wilczyński <twilczynski@naver.com>
2025-04-08 22:08:00 +02:00
Paul Donald
5f5d672a7d fix(strings): differentiate setup(n) and set(v) up (#10024)
Correct GUI strings, translations and comments to use proper grammar.
2025-04-08 12:45:05 +00:00
Tommy van der Vorst
d23cd197e1 chore(fs): changes to allow Filesystem to be implemented externally (#10040)
### Purpose

The `fs.Filesystem` interface contains two parts that cannot be
implemented externally because they are private:

* `filesystemWrapperType`: this PR changes `unwrapFilesystem` to
downcast to a specific concrete type
* `underlying`: this PR simply moves it to an unexported interface

### Testing

Regular tests pass.
2025-04-08 12:39:39 +00:00
bt90
d7ca483df1 chore(config): resolve primary STUN servers via SRV record (fixes #10029) (#10031)
### Purpose

Fixes #10029

### Testing

```
[3JPXJ] 2025/04/03 14:36:44.601454 stun.go:146: DEBUG: Running stun for Stun@udp://[::]:22000 via fyc5mja4mz5s0vmz1txx.syncthing.net:9999
[3JPXJ] 2025/04/03 14:36:54.185157 stun.go:170: DEBUG: Stun@udp://[::]:22000 stun discovery on fyc5mja4mz5s0vmz1txx.syncthing.net:9999 resulted in no address
[3JPXJ] 2025/04/03 14:36:54.185204 stun.go:146: DEBUG: Running stun for Stun@udp://[::]:22000 via stun.internetcalls.com:3478
```

### Documentation

https://github.com/syncthing/docs/pull/904
2025-04-08 12:23:57 +00:00
Jakob Borg
e48be98cd5 build: push artifacts to Azure (#10044)
Provider migration
2025-04-08 09:43:19 +02:00
Syncthing Release Automation
e9a2ff3aa6 chore(gui, man, authors): update docs, translations, and contributors 2025-04-07 03:50:00 +00:00
Jakob Borg
2301f72c5b fix(config): zero filesystemtype is "basic" (#10038)
For legacy purposes
2025-04-04 19:28:39 +00:00
Tommy van der Vorst
f7c8efd93c fix(config): properly apply defaults when reading folder configuration (#10034) 2025-04-04 16:46:12 +00:00
Sébastien WENSKE
3e7ccf7c48 chore(model): add metric for total number of conflicts (#10037) 2025-04-04 09:24:04 -07:00
bt90
6bc2784e9a build: replace underscore in Debian version (#10032)
The workflow building Debian packages chokes on branches containing
underscores:

```
{:timestamp=>"2025-04-03T10:31:46.749835+0000", :message=>"Invalid package configuration: The version looks invalid for Debian packages. Debian version field must contain only alphanumerics and . (period), + (plus), - (hyphen) or ~ (tilde). I have '1.29.5~dev.13.ga38df11f~srv_stun' which which isn't valid.", :level=>:error}
```

This replaces the offending `_` with a `~` which should yield a valid
version.
2025-04-03 14:28:33 +02:00
Tommy van der Vorst
f15d50c2e8 feat(fs, config): add support for custom filesystem type construction (#9887)
For Synctrain I would like to create a virtual filesystem that exposes
iOS' photo library. This can only be accessed through APIs.
2025-04-03 10:12:23 +02:00
Jakob Borg
f9007ed106 build(deps): update dependencies (#10020)
deps deps deps
2025-04-02 08:51:37 +02:00
bt90
05cc6b0f43 chore(fs): speed up case normalization (#10013)
### Purpose

Resurrecting https://github.com/syncthing/syncthing/pull/9365

### Testing

Current benchmark results:

```
goos: linux
goarch: amd64
pkg: github.com/syncthing/syncthing/lib/fs
cpu: AMD EPYC 7763 64-Core Processor                
                                           │  ../old.txt  │             ../new.txt              │
                                           │    sec/op    │   sec/op     vs base                │
UnicodeLowercase/ASCII_lowercase-4            43.68n ± 4%   12.19n ± 1%  -72.09% (p=0.000 n=10)
UnicodeLowercase/ASCII_mixedcase_start-4     200.65n ± 2%   59.49n ± 3%  -70.35% (p=0.000 n=10)
UnicodeLowercase/ASCII_mixedcase_end-4        95.50n ± 2%   59.10n ± 2%  -38.12% (p=0.000 n=10)
UnicodeLowercase/Latin1_lowercase-4           122.5n ± 1%   131.4n ± 1%   +7.27% (p=0.000 n=10)
UnicodeLowercase/Latin1_mixedcase_start-4     339.9n ± 2%   309.2n ± 1%   -9.05% (p=0.000 n=10)
UnicodeLowercase/Latin1_mixedcase_end-4       183.6n ± 2%   174.3n ± 1%   -5.04% (p=0.000 n=10)
UnicodeLowercase/Unicode_lowercase-4          456.6n ± 1%   440.5n ± 1%   -3.53% (p=0.000 n=10)
UnicodeLowercase/Unicode_mixedcase_start-4    625.9n ± 1%   595.6n ± 1%   -4.83% (p=0.000 n=10)
UnicodeLowercase/Unicode_mixedcase_end-4      516.2n ± 1%   495.5n ± 1%   -4.02% (p=0.000 n=10)
geomean                                       214.1n        150.4n       -29.72%

                                           │  ../old.txt  │             ../new.txt              │
                                           │     B/op     │    B/op     vs base                 │
UnicodeLowercase/ASCII_lowercase-4           0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
UnicodeLowercase/ASCII_mixedcase_start-4     24.00 ± 0%     24.00 ± 0%       ~ (p=1.000 n=10) ¹
UnicodeLowercase/ASCII_mixedcase_end-4       24.00 ± 0%     24.00 ± 0%       ~ (p=1.000 n=10) ¹
UnicodeLowercase/Latin1_lowercase-4          0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
UnicodeLowercase/Latin1_mixedcase_start-4    32.00 ± 0%     32.00 ± 0%       ~ (p=1.000 n=10) ¹
UnicodeLowercase/Latin1_mixedcase_end-4      32.00 ± 0%     32.00 ± 0%       ~ (p=1.000 n=10) ¹
UnicodeLowercase/Unicode_lowercase-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
UnicodeLowercase/Unicode_mixedcase_start-4   48.00 ± 0%     48.00 ± 0%       ~ (p=1.000 n=10) ¹
UnicodeLowercase/Unicode_mixedcase_end-4     48.00 ± 0%     48.00 ± 0%       ~ (p=1.000 n=10) ¹
geomean                                                 ²               +0.00%                ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                                           │  ../old.txt  │             ../new.txt              │
                                           │  allocs/op   │ allocs/op   vs base                 │
UnicodeLowercase/ASCII_lowercase-4           0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
UnicodeLowercase/ASCII_mixedcase_start-4     1.000 ± 0%     1.000 ± 0%       ~ (p=1.000 n=10) ¹
UnicodeLowercase/ASCII_mixedcase_end-4       1.000 ± 0%     1.000 ± 0%       ~ (p=1.000 n=10) ¹
UnicodeLowercase/Latin1_lowercase-4          0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
UnicodeLowercase/Latin1_mixedcase_start-4    1.000 ± 0%     1.000 ± 0%       ~ (p=1.000 n=10) ¹
UnicodeLowercase/Latin1_mixedcase_end-4      1.000 ± 0%     1.000 ± 0%       ~ (p=1.000 n=10) ¹
UnicodeLowercase/Unicode_lowercase-4         0.000 ± 0%     0.000 ± 0%       ~ (p=1.000 n=10) ¹
UnicodeLowercase/Unicode_mixedcase_start-4   1.000 ± 0%     1.000 ± 0%       ~ (p=1.000 n=10) ¹
UnicodeLowercase/Unicode_mixedcase_end-4     1.000 ± 0%     1.000 ± 0%       ~ (p=1.000 n=10) ¹
geomean                                                 ²               +0.00%                ²
¹ all samples are equal
² summaries must be >0 to compute geomean
```

I think the `+7%` for the lowercase Latin1 testcase is easily outweighed
by the ASCII and unicode improvements 🙂
2025-04-01 13:41:57 +02:00
Marcus B Spencer
1efcfeb3ad chore(config): remove discontinued secondary STUN servers (fixes #10011) (#10012)
Similarly to #10009, we will remove some discontinued STUN servers,
except instead of being the official primary server, it's some
unofficial secondary STUN servers.

### Testing

Use a STUN client (like [`pystun3`](https://pypi.org/project/pystun3))
to probe that the removed STUN servers are inactive.

### Documentation

syncthing/docs#902
2025-03-31 06:41:33 +00:00
Syncthing Release Automation
93195911bd chore(gui, man, authors): update docs, translations, and contributors 2025-03-31 03:50:00 +00:00
Jakob Borg
6085e3a5eb fix(stun): better error handling (ref #10008) (#10010) 2025-03-30 11:56:29 -07:00
Marcus B Spencer
e5b72da607 fix(config): remove discontinued primary STUN server (fixes #10008) (#10009)
The mechanism for primary STUN servers, is still intact, in case this
gets retried with a different domain.

### Purpose

As seen in [stun.syncthing.net doesn’t resolve
anymore](https://forum.syncthing.net/t/stun-syncthing-net-doesnt-resolve-anymore/24075/2?u=marbens)
on the forums, stun.syncthing.net has been shut down, so I think it's
probably a good idea to remove it.

### Testing

1. Have two or more devices
2. Disable Relaying
3. Have no Internet ports open on either end for incoming connections
trigger STUN)
4. Enable the `stun` debugging facility in the Actions -> Logs ->
Debugging Facilities
5. Verify that it doesn't output something like this within a few
seconds:
```
2025-03-30 05:51:32 Enabled debug data for "stun"
2025-03-30 05:51:47 Starting stun for Stun@udp://[::]:22000
2025-03-30 05:51:47 Running stun for Stun@udp://[::]:22000 via stun.syncthing.net:3478
2025-03-30 05:51:47 Stun@udp://[::]:22000 stun addr resolution on stun.syncthing.net:3478: lookup stun.syncthing.net: no such host
```

---------

Co-authored-by: Jakob Borg <jakob@kastelo.net>
2025-03-30 15:17:55 +02:00
mathias4833
0d6117d585 fix(gui): validate device ID in canonical form (fixes #7291) (#10006)
### Purpose

In the GUI, the device ID validation was case-sensitive and didn’t
account for dash variations, which allowed users to enter an existing
device ID without receiving proper feedback.

This fix ensures the ID is validated in its canonical form, thus
preventing the user from submitting the request if the device ID already
exists.

### Testing

To test this change, try adding a new device with an ID that matches an
existing device, but with a different case or dashes.
2025-03-29 17:52:02 +01:00
tomasz1986
629971687d feat(gui): explanation to options enabled or disabled per folder type (#9367)
Currently, some options are automatically enabled or disabled depending
on the folder type. However, there is no explanation in the GUI on why
the options are like that. Thus, add short explanatory notes to each
case, where the option is either disabled or enabled according to the
current folder type.
2025-03-28 15:17:08 +00:00
Tommy van der Vorst
3c955a9706 chore(lib): expose model methods to obtain progress (#9886)
### Purpose

This exposes four methods from `Model` through `Internals`. It allows
apps like Synctrain to obtain information about local/remote need and
sync progress.

### Testing

No testing seems necessary, functions are exported verbatim.

### Screenshots

N/a

### Documentation

Not public API, I am aware this interface may change at any time.

## Authorship

OK.

Co-authored-by: Ross Smith II <ross@smithii.com>
Co-authored-by: Jakob Borg <jakob@kastelo.net>
2025-03-28 13:44:01 +00:00
Ross Smith II
7f3c8dbff1 build: move nightly build schedule to separate workflow (#10000)
This allows users to easily disable nightly builds in their forks,
simply by disabling the
build-nightly action.

### Testing

I tested it in my fork, and it works.
2025-03-27 09:31:51 +00:00
Jakob Borg
7762e39fb3 chore(syncthing): use file lock on certificate to prevent multiple instances (#10003)
This adds the locking from the SQLite branch, in preparation, so that we
do not inadvertently permit running an instance of each.
2025-03-27 09:26:21 +00:00
Jakob Borg
4235b2c406 chore(ur): add RSS to reported stats (#10002)
For easier comparison in the future.
2025-03-27 10:17:10 +01:00
Syncthing Release Automation
3fd090bfa7 chore(gui, man, authors): update docs, translations, and contributors 2025-03-24 03:49:44 +00:00
Syncthing Release Automation
6dfa54efa6 chore(gui, man, authors): update docs, translations, and contributors 2025-03-17 03:49:35 +00:00
mathias4833
67575e1736 fix(api): prevent tilde expansion in path suggestions (fixes #9990) (#9992)
### Purpose

Path autocompletion wasn't working when using `~` as a shortcut for the
home directory. The issue occurred because the tilde was expanded to
/home/user, which caused the suggestion to no longer match the input
(thus preventing the autocompletion from appearing in the suggestion
list).

To fix this, I replaced the custom `parentAndBase` function, which
handled path splitting in a more complex way, with `filepath.Split` from
the standard `path/filepath` package. This prevents tilde expansion
while keeping the expected behavior for path splitting.

### Testing

The issue has been tested manually on Linux.

### Screenshots


![screenshot](https://github.com/user-attachments/assets/49dd96e2-6d75-4476-946d-0dfb2ac474ef)
2025-03-15 21:06:38 +01:00
Jakob Borg
65923fc255 fix(syncthing): don't auto upgrade to higher major on startup (#9989)
We avoided upgrading to newer major versions during normal auto upgrade
procedures, but currently not in the initial upgrade check on startup.
2025-03-13 07:59:19 +00:00
Jakob Borg
aea763868f build(deps): update dependencies (#9988) 2025-03-13 07:41:05 +00:00
Syncthing Release Automation
26b134ae7b chore(gui, man, authors): update docs, translations, and contributors 2025-03-10 03:45:18 +00:00
Emil Lundberg
893071d2ba refactor(api): extract method configMuxBuilder.postAdjustGui and add test coverage (#9979)
This is extracted from PR #9175. This deduplicates `SetPassword` calls
and makes `postAdjustGui` a single place where PR #9175 can add
another adjustment step for sanitizing changes to WebAuthn
credentials.

This also adds tests to validate that the refactored logic was not
broken.
2025-03-09 21:31:06 +01:00
Emil Lundberg
435f2d2178 refactor(api): make shutdown timeout configurable for tests (#9980)
This is extracted from PR #9175, which adds some tests that seem to
need a longer shutdown timeout when running on GitHub Actions.
2025-03-07 12:50:33 +01:00
Emil Lundberg
8461ca539b refactor(api): deduplicate HTTP test helpers and allow session cookie access (#9977)
These refactorizations were made in [PR #9175][1] to accommodate a few
new variants of authentication method and body content. On request from
reviewers, this PR extracts it as a smaller refactorization to review in
isolation.

### Purpose

This extracts a shared `httpRequest` base function from `httpGet` and
`httpPost`, which will be used in PR #9175 for new helper functions
`httpGetCsrf` (hiding all optional parameters except the CSRF token),
`httpPostCsrf` (same) and `httpPostCsrfAuth` (hiding basic auth
parameters). A `getSessionCookie` function is also extracted from
`hasSessionCookie` and will be used to test that concurrent WebAuthn
authentications result in separate sessions (indicated by different
session cookies).
2025-03-07 11:07:01 +01:00
Jakob Borg
fb977dc61d build: correct API call for Weblate statistics
Something changed...
2025-03-03 08:11:29 +01:00
Jakob Borg
ee7ab4ce25 build(deps): update dependencies (#9978) 2025-03-01 21:33:13 +00:00
polyfloyd
c3ce9713d9 chore(etc): remove /usr/bin prefix from Linux .desktop files (#9966)
Exec accepts just the program name and will look for it in $PATH. This
makes these files work on NixOS which does not create a /usr/bin
directory.
2025-02-28 20:59:07 +01:00
Jakob Borg
6a147091c5 build: use Go 1.24, minimum is Go 1.23 (#9960) 2025-02-12 10:16:46 +01:00
Jakob Borg
6208c36417 fix(policy): do not require multiple maintainers for build changes 2025-02-12 09:47:09 +01:00
Syncthing Release Automation
453fd20eeb chore(gui, man, authors): update docs, translations, and contributors 2025-02-10 03:46:14 +00:00
Tommy van der Vorst
28f0cffdb6 chore(fs): build kqueue instead of fsevents watcher on iOS (#9950)
### Purpose

On iOS, the FSEvents API for watching files (also used on macOS) is not
available, but `kqueue` is. This PR ensures `kqueue` support is built on
iOS instead of the FSEvents based watcher implementation.

Before this PR, you could already use the `kqueue` build option to force
its usage. Unfortunately `gomobile`, the tool that I use to build
Syncthing for iOS and macOS for Synctrain, does not support setting
different build flags for iOS and macOS (unless I build separately for
each, which is a bit of a hassle because XCode nonsense). I am assuming
there are good reasons to support FSEvents even though `kqueue` is also
available on macOS (but I'm not sure why?). I do know FSEvents has been
working fine for me on macOS so it seems best to use FSEvents on macOS
and kqueue on iOS.

Note that this also requires https://github.com/syncthing/notify/pull/4
to be merged in `synchting/notify` (until that is done, this PR will
fail to build on iOS due to `notify` still trying to link to `fsevents`
stuff when the `kqueue` build flag is not set).

### Testing

I compiled both `syncthing/notify` and syncthing with this PR applied,
and used that to successfully build the Synctrain iOS app, which after
this PR works fine and should follow up file changes a bit quicker.

### Screenshots

n/a

### Documentation

n/a

## Authorship

Your name and email will be added automatically to the AUTHORS file
based on the commit metadata.

---------

Co-authored-by: Jakob Borg <jakob@kastelo.net>
2025-02-07 15:40:53 +00:00
Jakob Borg
87c16c6cf5 build(deps): update dependencies (#9951) 2025-02-07 08:44:26 +00:00
dashangcun
5495c98e63 refactor: using slices.Contains to simplify the code (#9918)
### Purpose

This is a [new function](https://pkg.go.dev/slices@go1.21.0#Contains)
added in the go1.21 standard library, which can make the code more
concise and easy to read.

### Testing

Describe what testing has been done, and how the reviewer can test the
change
if new tests are not included.

### Screenshots

If this is a GUI change, include screenshots of the change. If not,
please
feel free to just delete this section.

### Documentation

If this is a user visible change (including API and protocol changes),
add a link here
to the corresponding pull request on https://github.com/syncthing/docs
or describe
the documentation changes necessary.

## Authorship

Your name and email will be added automatically to the AUTHORS file
based on the commit metadata.

Signed-off-by: dashangcun <907225865@qq.com>
Co-authored-by: Jakob Borg <jakob@kastelo.net>
2025-02-07 08:21:24 +00:00
Jakob Borg
da7d5ce608 build: switch to cloud code signing for Windows (#9948)
The requirements for Windows code signing changed in 2023, so that newly
generated certificates can only be stored in hardware modules. Luckily,
I managed to snag a three year certificate before that so it hasn't
affected us so much. Now though, it does, because our cert is expiring
in March.

This changes the code signing process for Windows to use a cloud
service, Azure Trusted Signing. This appears to work equally well and
outsources the problem entirely, while also being cheaper than the
actual certificate was to begin with. 🤷

The signing entity will be Kastelo AB and not the Syncthing Foundation,
because the latter is almost impossible to get a certificate for as it's
not a normal corporate entity whose existence can be verified, etc. This
is also how it was prior to the latest certificate; it's not ideal, but
I think it's acceptable under the circumstances.
2025-02-06 10:43:23 +01:00
Syncthing Release Automation
b300c297c6 chore(gui, man, authors): update docs, translations, and contributors 2025-02-03 03:45:29 +00:00
Syncthing Release Automation
124673f7a8 chore(gui, man, authors): update docs, translations, and contributors 2025-01-27 03:45:14 +00:00
Jakob Borg
0395cf2bc0 fix(model): clarify errors on Windows user/group lookup (fixes #9929) (#9930)
Currently, this just results in a very ambiguous `setting metadata: lookup
failed` while it could report what it's looking up and why it failed
(not found, etc).
2025-01-20 09:59:05 +01:00
Syncthing Release Automation
36cd70040a chore(gui, man, authors): update docs, translations, and contributors 2025-01-20 03:45:10 +00:00
Jakob Borg
1fbd396ffa chore(scanner): don't warn about cancelled scan (#9920)
We expect a context cancellation when a folder is restarted during a
scan.
2025-01-13 17:35:55 +00:00
Syncthing Release Automation
2834bad85e chore(gui, man, authors): update docs, translations, and contributors 2025-01-13 03:47:27 +00:00
Jakob Borg
516f3e29e8 chore(proto): change symlinktarget to be byte sequence (fixes #9913) (#9914) 2025-01-11 17:38:29 +01:00
Jakob Borg
ab20c16982 fix(api): don't crash requests after failing to unmarshal tokens (fixes #9909) (#9912)
Unclear why this would happen, but apparently it does?
2025-01-09 21:47:10 +01:00
Jakob Borg
0c1df81ee9 fix(scanner): don't warn when legitimately skipping a directory (#9911)
Such as ignored directories etc.
2025-01-09 18:42:18 +00:00
Jakob Borg
d324b2ac86 fix(db): correct unsafe RLock order (fixes #9906) (#9910)
Marshal() was called with the read lock held, and in turn called
Created() which also takes the read lock. This is fine by itself, but
there is a risk of deadlock if another call to lock the mutex happens
concurrently, as the lock call will block the inner rlock and the outer
rlock can never become unlocked.

It's an easy fix as marshalling is guaranteed to be called with a read
lock and does not need to call any methods that read lock themselves.
2025-01-09 19:33:04 +01:00
Jakob Borg
0231089b99 fix(db): add JSON tags to types used in API returns (fixes #9907) (#9908)
These types are also used in the API, and hence need JSON tags.
2025-01-09 09:26:34 +00:00
Jakob Borg
74ffb85467 fix(model): correct type of fileInfoType in API browse response (fixes #9904) (#9905)
This was broken in the Protobuf refactor. The reason is that with the
previous library, generated types would have a JSON marshalling method
that would automatically return strings for enums. In the current
library you need to use the jsonpb marshaller for that, but these are
hand crafted structs so we can't do that. The easy solution is to just
use strings directly, since this is an API-only type anyway.
2025-01-08 11:08:33 +01:00
Syncthing Release Automation
dcd280e6e2 chore(gui, man, authors): update docs, translations, and contributors 2025-01-06 03:47:27 +00:00
Jakob Borg
4e56dbd883 build(deps): update dependencies (#9900) 2025-01-01 19:49:52 +00:00
Syncthing Release Automation
13d7881b80 chore(gui, man, authors): update docs, translations, and contributors 2024-12-30 03:46:20 +00:00
Simon Frei
1d2c53bf3c chore(scanner): avoid scan failures and report if they happen (#9888) 2024-12-28 17:09:38 +01:00
Syncthing Release Automation
9c449c966b chore(gui, man, authors): update docs, translations, and contributors 2024-12-23 03:46:12 +00:00
Jakob Borg
ec2d4638e3 build: fix publish-nightly tools checkout 2024-12-22 09:02:11 +01:00
André Colomb
0ce92befc8 chore(gui): fix merge conflict on Weblate (#9882)
Commit dee920a840 was merged into
Weblate's repository, but later replaced on main by
2167ce9656. This led to a merge conflict,
which this commit fixes cleanly. After merging, we can unlock Weblate
again.
2024-12-21 21:54:17 +00:00
Jakob Borg
2167ce9656 build: reinstate docker push for main 2024-12-21 16:17:21 +01:00
Jakob Borg
0dc85d74aa build: also push to ghcr.io 2024-12-21 15:43:49 +01:00
Jakob Borg
b6e3f8037b build: workaround for tag builds
The GitHub checkout action does weird stuff with tags which breaks `git
describe`. This works around that so we get proper release builds on tag
pushes.
2024-12-21 14:35:04 +01:00
Jakob Borg
00e7161a8f build: compat.json should be in the signed packages bundle 2024-12-20 10:34:29 +01:00
Jakob Borg
b5a7879eca fix(stdiscosrv): handle announcements properly :p (#9881)
Further protobuf refactor damage, also adding some better debugging
2024-12-19 20:43:46 +00:00
Jakob Borg
4355dc69ea fix(discovery): properly unmarshal local discovery (#9880)
Damage from recent protobuf refactoring
2024-12-19 20:16:44 +00:00
Jakob Borg
371ba69447 build: slightly streamline build dependencies 2024-12-19 15:08:17 +01:00
Jakob Borg
79ef57d0ea fix(scanner): continue walk after special files (fixes #9872) (#9877)
We must skip unix sockets, fifos, etc when scanning as these are not
filetypes we can handle. Currently we return a "bug" error, which
results in the walk being aborted and the rest of the tree being
essentially invisible to Syncthing. Instead, just ignore these files and
continue onwards.

This might well be #9859 as well but I can't confirm.
2024-12-19 08:27:53 +01:00
Jakob Borg
8bd6bdd397 chore(model): clarify log message (fixes #9875) (#9876) 2024-12-18 07:56:06 +00:00
Simon Frei
ce3248cea7 chore(fs): add debug logging for case cache registry (#9869)
Clearly something is up with it, but I still have no clue what. This
might give some clue when affected user enable debug logging.
2024-12-16 12:04:24 +01:00
Jakob Borg
99a6f3a5b6 docs: update section on code signing 2024-12-16 11:42:34 +01:00
Jakob Borg
c2e10dc156 build: skip Docker push for main, reserve for release 2024-12-16 11:39:13 +01:00
Jakob Borg
fc914f3237 build: sign asc files using ezapt
And same keys as APT archive
2024-12-16 11:35:41 +01:00
Jakob Borg
811d3752d0 build: loki vars are in secrets 2024-12-16 09:26:21 +01:00
Jakob Borg
00827dd5c1 build: consolidate release environment for actions
signing & docker -> release
2024-12-16 08:57:35 +01:00
Syncthing Release Automation
a981c21d27 chore(gui, man, authors): update docs, translations, and contributors 2024-12-16 03:50:41 +00:00
Jakob Borg
163dc122f3 build: update compat.yaml for Go 1.24 (rc1) (fixes #9870) 2024-12-15 11:09:14 +01:00
Jakob Borg
83727e0824 build(deps): update dependencies (#9866) 2024-12-10 14:33:47 +01:00
Jakob Borg
529d3ef764 ci: reduce frequency of dependabot nags 2024-12-10 09:53:46 +01:00
Jakob Borg
fefbf4dcc8 build: run release flows on tag pushes 2024-12-09 08:32:15 +01:00
André Colomb
b9c6d3ae09 fix(config): skip GUI port probing for UNIX sockets (fixes #9855) (#9858)
When creating an initial default config, we usually probe for a free
TCP port.  But when a UNIX socket is specified via the `STGUIADDRESS=`
override or the `--gui-address=unix:///...` command line syntax, parsing
that option will fail during port probing.

The solution is to just skip the port probing when the address is
determined to specify something other than a TCP socket.

### Testing

Start with a fresh home directory each time.
1. Specify a UNIX socket for the GUI (works with this PR):

TMPHOME=$(mktemp -d); ./syncthing --home=$TMPHOME
--gui-address=unix://$TMPHOME/socket

2. Specify no GUI address (probes for a free port if default is taken,
   as before):

       TMPHOME=$(mktemp -d); ./syncthing --home=$TMPHOME

3. Specify a TCP GUI address (probes whether the given port is taken,
   as before):

TMPHOME=$(mktemp -d); ./syncthing --home=$TMPHOME
--gui-address=127.0.0.1:8385
2024-12-09 07:24:42 +00:00
Syncthing Release Automation
7bea8c758a chore(gui, man, authors): update docs, translations, and contributors 2024-12-09 03:50:45 +00:00
Alex Ionescu
479c0d3f16 fix(gui): reflect folder password visibility by changing button icon (#9857)
Make the "toggle password visibility" button on encrypted
folders change its icon from `fa-eye` to `fa-eye-slash` while the
password is visible.
2024-12-08 21:20:31 +01:00
Jakob Borg
d9ce7c3166 build: build all the things (#9845)
Build packages for stdiscosrv and strelaysrv as well.
2024-12-05 10:09:33 -05:00
Jakob Borg
69979996d9 build(infra): also push Docker images to ghcr.io 2024-12-03 07:54:06 -05:00
Jakob Borg
da58e5c50c build(deps): update dependencies (#9852) 2024-12-03 12:48:10 +00:00
Syncthing Release Automation
44e259142f chore(gui, man, authors): update docs, translations, and contributors 2024-12-02 03:50:30 +00:00
Jakob Borg
77970d5113 refactor: use modern Protobuf encoder (#9817)
At a high level, this is what I've done and why:

- I'm moving the protobuf generation for the `protocol`, `discovery` and
`db` packages to the modern alternatives, and using `buf` to generate
because it's nice and simple.
- After trying various approaches on how to integrate the new types with
the existing code, I opted for splitting off our own data model types
from the on-the-wire generated types. This means we can have a
`FileInfo` type with nicer ergonomics and lots of methods, while the
protobuf generated type stays clean and close to the wire protocol. It
does mean copying between the two when required, which certainly adds a
small amount of inefficiency. If we want to walk this back in the future
and use the raw generated type throughout, that's possible, this however
makes the refactor smaller (!) as it doesn't change everything about the
type for everyone at the same time.
- I have simply removed in cold blood a significant number of old
database migrations. These depended on previous generations of generated
messages of various kinds and were annoying to support in the new
fashion. The oldest supported database version now is the one from
Syncthing 1.9.0 from Sep 7, 2020.
- I changed config structs to be regular manually defined structs.

For the sake of discussion, some things I tried that turned out not to
work...

### Embedding / wrapping

Embedding the protobuf generated structs in our existing types as a data
container and keeping our methods and stuff:

```
package protocol

type FileInfo struct {
  *generated.FileInfo
}
```

This generates a lot of problems because the internal shape of the
generated struct is quite different (different names, different types,
more pointers), because initializing it doesn't work like you'd expect
(i.e., you end up with an embedded nil pointer and a panic), and because
the types of child types don't get wrapped. That is, even if we also
have a similar wrapper around a `Vector`, that's not the type you get
when accessing `someFileInfo.Version`, you get the `*generated.Vector`
that doesn't have methods, etc.

### Aliasing

```
package protocol

type FileInfo = generated.FileInfo
```

Doesn't help because you can't attach methods to it, plus all the above.

### Generating the types into the target package like we do now and
attaching methods

This fails because of the different shape of the generated type (as in
the embedding case above) plus the generated struct already has a bunch
of methods that we can't necessarily override properly (like `String()`
and a bunch of getters).

### Methods to functions

I considered just moving all the methods we attach to functions in a
specific package, so that for example

```
package protocol

func (f FileInfo) Equal(other FileInfo) bool
```

would become

```
package fileinfos

func Equal(a, b *generated.FileInfo) bool
```

and this would mostly work, but becomes quite verbose and cumbersome,
and somewhat limits discoverability (you can't see what methods are
available on the type in auto completions, etc). In the end I did this
in some cases, like in the database layer where a lot of things like
`func (fv *FileVersion) IsEmpty() bool` becomes `func fvIsEmpty(fv
*generated.FileVersion)` because they were anyway just internal methods.

Fixes #8247
2024-12-01 16:50:17 +01:00
André Colomb
2b8ee4c7a5 chore(gui): cache input type in each advanced settings category (#9802)
Each section in the advanced settings dialog has similar code to insert
repeated input fields for each option. But only the first section (GUI
options) was adjusted in #9743 to avoid calling the `inputTypeFor()`
function repeatedly.

Apply the same caching to a locally scoped variable for each ng-repeat
entry by defining it in an ng-init directive.
2024-12-01 12:30:05 +00:00
bt90
be952e5f2d chore(config): add Chinese STUN servers (#9843) 2024-11-30 08:33:55 +01:00
Jakob Borg
43ebac4242 fix(model): create fileset under lock (#9840)
I came accross this in another context and didn't investigate fully, but
literally ten lines above this code, in another method, we say that
filesets _must_ be created under the lock. It's either one or the other
and I'm taking the safer route here.

---------

Co-authored-by: Simon Frei <freisim93@gmail.com>
2024-11-28 13:41:44 +00:00
Jakob Borg
f08a0ed01c build(deps): update dependencies (#9833) 2024-11-25 07:25:24 +00:00
Jakob Borg
612fdff377 build: automatically update APT repository on release
This uses https://github.com/kastelo/ezapt to generate and sign the
archive, and uploads it to blob storage.
2024-11-24 22:55:12 +01:00
Jakob Borg
8ccb7f1924 fix(protocol): allow encrypted-to-encrypted connections again
Encrypted-to-encrypted connections (i.e., ones where both sides set a
password) used to work but were broken in the 1.28.0 release. The
culprit is the 5342bec1b refactor which slightly changed how the request
was constructed, resulting in a bad block hash field.

Co-authored-by: Simon Frei <freisim93@gmail.com>
2024-11-24 22:55:12 +01:00
André Colomb
65d0ca8aa9 fix(config): respect GUI address override in fresh default config (fixes #9783) (#9675)
### Purpose

When generating a new `config.xml` file with default options, the GUI
address is populated with a hard-coded default value of
`127.0.0.1:8384`, except for a random free port if that default one is
occupied. This is independent from the GUI configuration default address
defined in the protobuf description. More importantly, it ignores any
`STGUIADDRESS` override given via environment variable or command-line
option, thus probing for the default port instead of the one specified
via override.

The `ProbeFreePorts()` function now respects the override, by reading
the `GUIConfiguration.Address()` method instead of using hard-coded
defaults.

When not calling `ProbeFreePorts()`, the override should still be
persisted rather than the default address. This happens only when
generating a fresh default `config.xml`, never on an existing one.
2024-11-19 11:01:43 +00:00
Jakob Borg
e82ed6e3d3 style: gofumpt all the things (#9829)
Literally `gofumpt -w .` from the top level dir. Guaranteed to be minor
style changes only and nothing else.

@imsodin per request?
2024-11-19 11:32:56 +01:00
Syncthing Release Automation
4b815fc086 chore(gui, man, authors): update docs, translations, and contributors 2024-11-18 03:49:35 +00:00
Jakob Borg
7eaf843de2 chore(api): add block and goroutine profiles to support bundle (#9824) 2024-11-16 09:43:17 +01:00
Jakob Borg
110e1ae6f9 fix(model): don't panic in index consistency print (fixes #9821) (#9823)
We try to compare to the last fileinfo, but apparently we can end up
here with an empty file list and crash on out of index.
2024-11-14 19:59:34 +00:00
Jakob Borg
896f9725ec chore: update policy to allow approvals by contributors (#9818)
This adds `allow_contributor: true` which allows approvals by
contributors to the PR (but still not the author themself, which is a
different thing). This allows things like pushing minor fixups while
also approving.

The `ignore_update_merges: true` option makes it so that someone is not
considered a "contributor" just because they push the merge button to
update the branch. In principle this is not needed given the above, but
I like it for clarity.
2024-11-12 10:50:19 +01:00
Tobias Frölich
1a529e9d5d fix(gui): expand tildes for subdir check (fixes #9400) (#9788)
### Purpose

This closes #9400 by always expanding tildes when parent/subdir checks
are done.

### Testing

I tested this by creating folders with paths to parent or subdirectories
of the default folder that include a tilde in their path as shown in the
attached screenshots.
With this change, overlap will be detected regardless of wether or not
tildes are used in other folder paths.

### Screenshots

Default Folder:

![2024-10-26-At-08h40m33s](https://github.com/user-attachments/assets/07df090c-4481-41ec-b741-d2785fc848d5)
Newly created folder (parent directory in this case)

![2024-10-26-At-08h40m13s](https://github.com/user-attachments/assets/636fa1fd-41dc-44d9-ac90-0a4937c9921c)

---------

Signed-off-by: tobifroe <froeltob@pm.me>
2024-11-12 09:24:00 +01:00
Hireworks
36ef17df8f fix(model): check if remote folder state before pulling files (fixes #9686) (#9732)
### Purpose

As discussed in #9686 
Syncthing currently does not check folderstate on remote device before
pulling. If no devices have a valid folderstate (i.e all devices have
the folder paused) it will still attempt to pull. On large folders this
will cause a hanging "Syncing" status.

This checks whether at least one connected device has the file available
and has a valid folderstate.

### Testing
Tested locally on multiple devices.
We're new to Go (all our stuff is Python) so please bear with!
Interested if there may be a better place to slot this in.

Thanks,
Jon

---------

Co-authored-by: Simon Frei <freisim93@gmail.com>
2024-11-12 08:51:52 +01:00
Syncthing Release Automation
955ac7775e chore(gui, man, authors): update docs, translations, and contributors 2024-11-11 03:46:06 +00:00
tomasz1986
8f69e874c4 chore(gui): group logout/restart/shutdown buttons together in Actions menu (#9801)
Currently, the "Restart" and "Shutdown" buttons are displayed in the
middle of the Actions menu. On the other hand, the "Log Out" button is
displayed at the very bottom. However, in other cases, e.g. the menus in
operating systems like Windows or macOS, these kind of buttons are
usually grouped together.

Therefore, move the "Restart" and "Shutdown" buttons down, so that they
are listed together with the "Log Out" button. Also, change the order,
so that it goes from the least impactful ("Log Out") to the most
impactful ("Shutdown").

Signed-off-by: Tomasz Wilczyński <twilczynski@naver.com>

### Screenshots

#### Before


![image](https://github.com/user-attachments/assets/a51438ef-bb6f-4535-a972-8c1bc1dffa02)

#### After


![image](https://github.com/user-attachments/assets/535762d6-6f26-44ab-a402-db87bdcbfb36)

Signed-off-by: Tomasz Wilczyński <twilczynski@naver.com>
2024-11-07 16:47:00 +01:00
Syncthing Release Automation
ac06fd97e9 chore(gui, man, authors): update docs, translations, and contributors 2024-11-04 03:47:49 +00:00
Syncthing Release Automation
3726b7d112 chore(gui, man, authors): update docs, translations, and contributors 2024-10-28 03:48:15 +00:00
Ross Smith II
377200591e fix(fs): fix directory junction handling (fixes #9775) (#9786)
### Purpose

This fixes #9775. I also improved the comments as they were lacking.

My apologies for introducing this bug. In summary, the bug was
```
mode = mode ^ (ModeSymlink | ModeIrregular)
```
didn't correctly reset those bits. This correctly resets them:
```
mode = mode &^ ModeSymlink &^ ModeIrregular
```
Tested and working in Windows 11 version 10.0.22631.4317. I didn't test
in other versions, but I'm sure this is the only issue.
2024-10-27 16:08:38 +01:00
Kapil Sareen
4afc898c2f fix(model): don't sync symbolic links on Android (fixes #9725) (#9782) 2024-10-26 09:29:38 +00:00
Simon Frei
ff7e4fef55 chore(nat, upnp): Make failure logging less reptitive (ref #9324) (#9785)
Currently we log on every single one of 10 retries deep in the upnp
stack. However we also return the failure as an error, which is bubbled
up a while until it's logged at debug level. Switch that around, such
that the repeat logging happens at debug level but the top-level happens
at info. There's some chance that this will newly log errors from
nat-pmp that were previously hidden in debug level - I hope those are
useful and not too numerous.

Also potentially this can even close #9324, my (very limited)
understanding of the reports/discussion there is that there's likely no
problem with syncthing beyond the excessive logging, it's some weird
router behaviour.
2024-10-25 21:04:22 +00:00
Simon Frei
9ffddb1923 fix(gui): apply small screen CSS changes earlier (fixes #9590) (#9756)
These CSS overrides address issues that are already present on wider
screens, so apply it there. Some experiments show we might even want to
up the limit more, but I am chicken and lazy, so I propose to use the
existing 470px media block.

Supersedes another PR after not getting any reaction to feedback there:
https://github.com/syncthing/syncthing/pull/9591#issuecomment-2212586134

Co-authored-by: Jakob Borg <jakob@kastelo.net>
2024-10-25 22:49:22 +02:00
André Colomb
896b857fc4 fix(gui): hide lists with [object Object] in advanced settings (#9743)
As discussed in
https://github.com/syncthing/syncthing/pull/9175#discussion_r1730431703,
entries in advanced settings are unusable if they are comprised of a
list of objects. It just displays `[object Object], [object Object],
[object Object]`, e.g. for the devices a folder is shared with.

Filter out these config elements by detecting an array whose members are
not all strings or numbers, and setting them to `skip` type.

Fix some unnecessary repetition in calling `inputTypeFor()`, since it is
already cached in the `ng-init` directive.
2024-10-25 22:22:12 +02:00
Terrance
acc5d2675b fix(gui): add dark scheme styles for disabled checkboxes (fixes #9776) (#9777)
### Purpose

Fixes #9776 by tweaking the text/background colours of disabled checkbox
panels when dark mode is enabled.

It was [noted on that
issue](https://github.com/syncthing/syncthing/issues/9776#issuecomment-2424828520)
that there's a bigger issue around the correctness of using the
`disabled` attribute on a `<div>` in the first place, but this PR does
not attempt to change that.

### Testing

I've hooked up the GUI files against a release build as suggested below.

### Screenshots

Using the dark theme, or the default theme with a system dark scheme:


![image](https://github.com/user-attachments/assets/3c6bfa77-cc7a-4f3e-a5c2-83daf54dcc34)

Using the black theme:


![image](https://github.com/user-attachments/assets/768db657-aa52-4db0-8455-5194a00fc143)

These borrow the colours from dark theme text inputs and black theme
tabs for a consistent look (initially I tried the text colour of
disabled text inputs, but that produced some poor contrast).
2024-10-22 14:03:32 +02:00
Syncthing Release Automation
6ece4c1fd2 chore(gui, man, authors): update docs, translations, and contributors 2024-10-21 03:47:52 +00:00
Jakob Borg
cc09f0170d build(deps): update dependencies (#9773) 2024-10-17 13:05:53 +00:00
Syncthing Release Automation
bb234d6c0e chore(gui, man, authors): update docs, translations, and contributors 2024-10-14 03:47:49 +00:00
Syncthing Release Automation
e6acc64758 chore(gui, man, authors): update docs, translations, and contributors 2024-10-07 03:47:13 +00:00
André Colomb
f18cf545b9 fix(gui): improve device ID readability in black and dark themes (fixes #9757) (#9758) 2024-10-06 00:48:19 +02:00
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
461 changed files with 13630 additions and 33756 deletions

2
.github/CODEOWNERS vendored
View File

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

View File

@@ -1,6 +1,7 @@
name: Feature request
description: File a new feature request
labels: ["enhancement", "needs-triage"]
type: Feature
body:
- type: textarea

View File

@@ -1,6 +1,7 @@
name: Bug report
description: If you're actually looking for support instead, see "I need help / I have a question".
labels: ["bug", "needs-triage"]
type: Bug
body:
- type: markdown
attributes:

View File

@@ -3,11 +3,11 @@ updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: weekly
interval: monthly
open-pull-requests-limit: 10
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: weekly
interval: monthly
open-pull-requests-limit: 10

23
.github/labeler.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
version: 1
labels:
- label: enhancement
title: ^feat\b
- label: bug
title: ^fix\b
- label: documentation
title: ^docs\b
- label: chore
title: ^chore\b
- label: chore
title: ^refactor\b
- label: build
title: ^build\b
- label: dependencies
title: ^build\(deps\)\b

17
.github/release.yml vendored Normal file
View File

@@ -0,0 +1,17 @@
changelog:
exclude:
labels:
- dependencies
categories:
- title: Fixes
labels:
- bug
- title: Features
labels:
- enhancement
- title: Other
labels:
- '*'

View File

@@ -7,11 +7,15 @@ on:
- infra-*
env:
GO_VERSION: "~1.23.0"
GO_VERSION: "~1.24.0"
CGO_ENABLED: "0"
BUILD_USER: docker
BUILD_HOST: github.syncthing.net
permissions:
contents: read
packages: write
jobs:
docker-syncthing:
name: Build and push Docker images
@@ -41,6 +45,13 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build binaries
run: |
for arch in arm64 amd64; do
@@ -53,13 +64,13 @@ jobs:
- name: Set Docker tags (all branches)
run: |
tags=syncthing/${{ matrix.pkg }}:${{ github.sha }}
tags=docker.io/syncthing/${{ matrix.pkg }}:${{ github.sha }},ghcr.io/syncthing/infra/${{ matrix.pkg }}:${{ github.sha }}
echo "TAGS=$tags" >> $GITHUB_ENV
- name: Set Docker tags (latest)
if: github.ref == 'refs/heads/infrastructure'
run: |
tags=syncthing/${{ matrix.pkg }}:latest,${{ env.TAGS }}
tags=docker.io/syncthing/${{ matrix.pkg }}:latest,ghcr.io/syncthing/infra/${{ matrix.pkg }}:latest,${{ env.TAGS }}
echo "TAGS=$tags" >> $GITHUB_ENV
- name: Build and push

18
.github/workflows/build-nightly.yaml vendored Normal file
View File

@@ -0,0 +1,18 @@
name: Build Syncthing (Nightly)
on:
schedule:
# Run nightly build at 05:00 UTC
- cron: '00 05 * * *'
workflow_dispatch:
permissions:
contents: write
packages: write
jobs:
build-syncthing:
uses: ./.github/workflows/build-syncthing.yaml
# if we only want nightlies to run for specific users:
# if: contains(fromJSON('["syncthing", "calmh"]'), github.repository_owner)
secrets: inherit

View File

@@ -3,16 +3,17 @@ name: Build Syncthing
on:
pull_request:
push:
schedule:
# Run nightly build at 05:00 UTC
- cron: '00 05 * * *'
branches-ignore:
- release
- release-rc*
workflow_call:
workflow_dispatch:
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.23.0"
GO_VERSION: "~1.24.0"
# Optimize compatibility on the slow archictures.
GO386: softfloat
@@ -48,7 +49,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.22.6", "~1.23.0"]
go: ["~1.23.0", "~1.24.0"]
runs-on: ${{ matrix.runner }}
steps:
- name: Set git to use LF
@@ -83,31 +84,11 @@ jobs:
go run build.go test | go-test-json-to-loki
env:
GOFLAGS: "-json"
LOKI_URL: ${{ vars.LOKI_URL }}
LOKI_USER: ${{ vars.LOKI_USER }}
LOKI_URL: ${{ secrets.LOKI_URL }}
LOKI_USER: ${{ secrets.LOKI_USER }}
LOKI_PASSWORD: ${{ secrets.LOKI_PASSWORD }}
LOKI_LABELS: "go=${{ matrix.go }},runner=${{ matrix.runner }},repo=${{ github.repository }},ref=${{ github.ref }}"
#
# Meta checks for formatting, copyright, etc
#
correctness:
name: Check correctness
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
cache: false
check-latest: true
- name: Check correctness
run: |
go test -v ./meta
#
# The basic checks job is a virtual one that depends on the matrix tests,
# the correctness checks, and various builds that we always do. This makes
@@ -122,11 +103,11 @@ jobs:
runs-on: ubuntu-latest
needs:
- build-test
- correctness
- package-linux
- package-cross
- package-source
- package-debian
- package-windows
- govulncheck
steps:
- uses: actions/checkout@v4
@@ -137,8 +118,6 @@ jobs:
package-windows:
name: Package for Windows
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/release' || startsWith(github.ref, 'refs/heads/release-'))
environment: signing
runs-on: windows-latest
steps:
- name: Set git to use LF
@@ -153,6 +132,7 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.ref }} # https://github.com/actions/checkout/issues/882
- uses: actions/setup-go@v5
with:
@@ -178,22 +158,74 @@ jobs:
- name: Create packages
run: |
go run build.go -goarch amd64 zip
go run build.go -goarch arm zip
go run build.go -goarch arm64 zip
go run build.go -goarch 386 zip
$targets = 'syncthing', 'stdiscosrv', 'strelaysrv'
$archs = 'amd64', 'arm', 'arm64', '386'
foreach ($arch in $archs) {
foreach ($tgt in $targets) {
go run build.go -goarch $arch zip $tgt
}
}
env:
CGO_ENABLED: "0"
CODESIGN_SIGNTOOL: ${{ secrets.CODESIGN_SIGNTOOL }}
CODESIGN_CERTIFICATE_BASE64: ${{ secrets.CODESIGN_CERTIFICATE_BASE64 }}
CODESIGN_CERTIFICATE_PASSWORD: ${{ secrets.CODESIGN_CERTIFICATE_PASSWORD }}
CODESIGN_TIMESTAMP_SERVER: ${{ secrets.CODESIGN_TIMESTAMP_SERVER }}
- name: Archive artifacts
uses: actions/upload-artifact@v4
with:
name: unsigned-packages-windows
path: "*.zip"
codesign-windows:
name: Codesign for Windows
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/release-nightly' || startsWith(github.ref, 'refs/tags/v'))
environment: release
runs-on: windows-latest
needs:
- package-windows
steps:
- name: Download artifacts
uses: actions/download-artifact@v4
with:
name: unsigned-packages-windows
path: packages
- name: Extract packages
working-directory: packages
run: |
$files = Get-ChildItem "." -Filter *.zip
foreach ($file in $files) {
7z x $file.Name
}
- name: Sign files with Trusted Signing
uses: azure/trusted-signing-action@v0.5.1
with:
azure-tenant-id: ${{ secrets.AZURE_TRUSTED_SIGNING_TENANT_ID }}
azure-client-id: ${{ secrets.AZURE_TRUSTED_SIGNING_CLIENT_ID }}
azure-client-secret: ${{ secrets.AZURE_TRUSTED_SIGNING_CLIENT_SECRET }}
endpoint: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }}
trusted-signing-account-name: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT }}
certificate-profile-name: ${{ secrets.AZURE_TRUSTED_SIGNING_PROFILE }}
files-folder: ${{ github.workspace }}\packages
files-folder-filter: exe
files-folder-recurse: true
file-digest: SHA256
timestamp-rfc3161: http://timestamp.acs.microsoft.com
timestamp-digest: SHA256
- name: Repackage packages
working-directory: packages
run: |
$files = Get-ChildItem "." -Filter *.zip
foreach ($file in $files) {
Remove-Item $file.Name
7z a -tzip $file.Name $file.BaseName
}
- name: Archive artifacts
uses: actions/upload-artifact@v4
with:
name: packages-windows
path: syncthing-windows-*.zip
path: "packages/*.zip"
#
# Linux
@@ -206,6 +238,7 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.ref }} # https://github.com/actions/checkout/issues/882
- uses: actions/setup-go@v5
with:
@@ -229,7 +262,9 @@ jobs:
run: |
archs=$(go tool dist list | grep linux | sed 's#linux/##')
for goarch in $archs ; do
go run build.go -goarch "$goarch" tar
for tgt in syncthing stdiscosrv strelaysrv ; do
go run build.go -goarch "$goarch" tar "$tgt"
done
done
env:
CGO_ENABLED: "0"
@@ -239,7 +274,7 @@ jobs:
with:
name: packages-linux
path: |
syncthing-linux-*.tar.gz
*.tar.gz
compat.json
#
@@ -248,13 +283,14 @@ jobs:
package-macos:
name: Package for macOS
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/release' || startsWith(github.ref, 'refs/heads/release-'))
environment: signing
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/release-nightly' || startsWith(github.ref, 'refs/tags/v'))
environment: release
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.ref }} # https://github.com/actions/checkout/issues/882
- uses: actions/setup-go@v5
with:
@@ -301,7 +337,9 @@ jobs:
- name: Create package (amd64)
run: |
go run build.go -goarch amd64 zip
for tgt in syncthing stdiscosrv strelaysrv ; do
go run build.go -goarch amd64 zip "$tgt"
done
env:
CGO_ENABLED: "1"
@@ -315,7 +353,9 @@ jobs:
go "\$@"
EOT
chmod 755 xgo.sh
go run build.go -gocmd ./xgo.sh -goarch arm64 zip
for tgt in syncthing stdiscosrv strelaysrv ; do
go run build.go -gocmd ./xgo.sh -goarch arm64 zip "$tgt"
done
env:
CGO_ENABLED: "1"
@@ -339,15 +379,14 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: packages-macos
path: syncthing-*.zip
path: "*.zip"
notarize-macos:
name: Notarize for macOS
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/release' || startsWith(github.ref, 'refs/heads/release-'))
environment: signing
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/release-nightly' || startsWith(github.ref, 'refs/tags/v'))
environment: release
needs:
- package-macos
- basics
runs-on: macos-latest
steps:
- name: Download artifacts
@@ -359,7 +398,7 @@ jobs:
run: |
APPSTORECONNECT_API_KEY_PATH="$RUNNER_TEMP/apikey.p8"
echo "$APPSTORECONNECT_API_KEY" | base64 -d -o "$APPSTORECONNECT_API_KEY_PATH"
for file in syncthing-macos-*.zip ; do
for file in *-macos-*.zip ; do
xcrun notarytool submit \
-k "$APPSTORECONNECT_API_KEY_PATH" \
-d "$APPSTORECONNECT_API_KEY_ID" \
@@ -382,6 +421,7 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.ref }} # https://github.com/actions/checkout/issues/882
- uses: actions/setup-go@v5
with:
@@ -424,9 +464,11 @@ jobs:
goos="${plat%/*}"
goarch="${plat#*/}"
echo "::group ::$plat"
if ! go run build.go -goos "$goos" -goarch "$goarch" tar 2>/dev/null; then
echo "::warning ::Failed to build for $plat"
fi
for tgt in syncthing stdiscosrv strelaysrv ; do
if ! go run build.go -goos "$goos" -goarch "$goarch" tar "$tgt" 2>/dev/null; then
echo "::warning ::Failed to build $tgt for $plat"
fi
done
echo "::endgroup::"
done
env:
@@ -436,7 +478,7 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: packages-other
path: syncthing-*.tar.gz
path: "*.tar.gz"
#
# Source
@@ -449,6 +491,7 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.ref }} # https://github.com/actions/checkout/issues/882
- uses: actions/setup-go@v5
with:
@@ -484,11 +527,10 @@ jobs:
sign-for-upgrade:
name: Sign for upgrade
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/release' || startsWith(github.ref, 'refs/heads/release-'))
environment: signing
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/release-nightly' || startsWith(github.ref, 'refs/tags/v'))
environment: release
needs:
- basics
- package-windows
- codesign-windows
- package-linux
- package-macos
- package-cross
@@ -498,6 +540,7 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.ref }} # https://github.com/actions/checkout/issues/882
- uses: actions/checkout@v4
with:
@@ -531,30 +574,44 @@ jobs:
env:
STSIGTOOL_PRIVATE_KEY: ${{ secrets.STSIGTOOL_PRIVATE_KEY }}
- name: Create and sign .asc files
- name: Create shasum files
run: |
sudo apt update
sudo apt -y install gnupg
export SIGNING_KEY="$RUNNER_TEMP/gpg-secret.asc"
echo "$GNUPG_SIGNING_KEY_BASE64" | base64 -d > "$SIGNING_KEY"
gpg --import < "$SIGNING_KEY"
pushd packages
files=(*.tar.gz *.zip)
sha1sum "${files[@]}" | gpg --clearsign > sha1sum.txt.asc
sha256sum "${files[@]}" | gpg --clearsign > sha256sum.txt.asc
gpg --sign --armour --detach syncthing-source-*.tar.gz
sha1sum "${files[@]}" > sha1sum.txt
sha256sum "${files[@]}" > sha256sum.txt
popd
rm -f "$SIGNING_KEY" .gnupg
version=$(go run build.go version)
echo "VERSION=$version" >> $GITHUB_ENV
- name: Sign shasum files
uses: docker://ghcr.io/kastelo/ezapt:latest
with:
args:
sign
packages/sha1sum.txt packages/sha256sum.txt
env:
GNUPG_SIGNING_KEY_BASE64: ${{ secrets.GNUPG_SIGNING_KEY_BASE64 }}
EZAPT_KEYRING_BASE64: ${{ secrets.APT_GPG_KEYRING_BASE64 }}
- name: Sign source
uses: docker://ghcr.io/kastelo/ezapt:latest
with:
args:
sign --detach --ascii
packages/syncthing-source-${{ env.VERSION }}.tar.gz
env:
EZAPT_KEYRING_BASE64: ${{ secrets.APT_GPG_KEYRING_BASE64 }}
- name: Archive artifacts
uses: actions/upload-artifact@v4
with:
name: packages-signed
path: packages/*
path: |
packages/*.tar.gz
packages/*.zip
packages/*.asc
packages/*.json
#
# Debian
@@ -567,6 +624,7 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.ref }} # https://github.com/actions/checkout/issues/882
- uses: actions/setup-go@v5
with:
@@ -597,7 +655,9 @@ jobs:
- name: Package for Debian
run: |
for arch in amd64 i386 armhf armel arm64 ; do
go run build.go -no-upgrade -installsuffix=no-upgrade -goarch "$arch" deb
for tgt in syncthing stdiscosrv strelaysrv ; do
go run build.go -no-upgrade -installsuffix=no-upgrade -goarch "$arch" deb "$tgt"
done
done
env:
BUILD_USER: debian
@@ -615,10 +675,9 @@ jobs:
publish-nightly:
name: Publish nightly build
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && startsWith(github.ref, 'refs/heads/release-nightly')
environment: signing
environment: release
needs:
- sign-for-upgrade
- notarize-macos
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -657,7 +716,7 @@ jobs:
RCLONE_CONFIG_OBJSTORE_REGION: ${{ secrets.S3_REGION }}
RCLONE_CONFIG_OBJSTORE_ACL: public-read
with:
args: sync packages objstore:${{ secrets.S3_BUCKET }}/nightly
args: sync -v --no-update-modtime packages objstore:nightly
#
# Push release artifacts to Spaces
@@ -665,8 +724,10 @@ jobs:
publish-release-files:
name: Publish release files
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/release'
environment: signing
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/release' || startsWith(github.ref, 'refs/tags/v'))
environment: release
permissions:
contents: write
needs:
- sign-for-upgrade
- package-debian
@@ -675,6 +736,7 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.ref }} # https://github.com/actions/checkout/issues/882
- name: Download signed packages
uses: actions/download-artifact@v4
@@ -710,7 +772,7 @@ jobs:
RCLONE_CONFIG_OBJSTORE_REGION: ${{ secrets.S3_REGION }}
RCLONE_CONFIG_OBJSTORE_ACL: public-read
with:
args: sync packages objstore:${{ secrets.S3_BUCKET }}/release/${{ env.VERSION }}
args: sync -v --no-update-modtime packages objstore:release/${{ env.VERSION }}
- name: Push to object store (latest)
uses: docker://docker.io/rclone/rclone:latest
@@ -723,7 +785,121 @@ jobs:
RCLONE_CONFIG_OBJSTORE_REGION: ${{ secrets.S3_REGION }}
RCLONE_CONFIG_OBJSTORE_ACL: public-read
with:
args: sync objstore:${{ secrets.S3_BUCKET }}/release/${{ env.VERSION }} objstore:${{ secrets.S3_BUCKET }}/release/latest
args: sync -v --no-update-modtime objstore:release/${{ env.VERSION }} objstore:release/latest
- name: Create GitHub releases and push binaries
run: |
maybePrerelease=""
if [[ $VERSION == *-* ]]; then
maybePrerelease="--prerelease"
fi
export GH_PROMPT_DISABLED=1
if ! gh release view --json name "$VERSION" >/dev/null 2>&1 ; then
gh release create "$VERSION" \
$maybePrerelease \
--title "$VERSION" \
--notes-from-tag
fi
gh release upload --clobber "$VERSION" \
packages/*.asc packages/*.json \
packages/syncthing-*.tar.gz \
packages/syncthing-*.zip \
packages/syncthing_*.deb
PKGS=$(pwd)/packages
cd /tmp # gh will not release for repo x while inside repo y
for repo in relaysrv discosrv ; do
export GH_REPO="syncthing/$repo"
if ! gh release view --json name "$VERSION" >/dev/null 2>&1 ; then
gh release create "$VERSION" \
$maybePrerelease \
--title "$VERSION" \
--notes "https://github.com/syncthing/syncthing/releases/tag/$VERSION"
fi
gh release upload --clobber "$VERSION" \
$PKGS/*.asc \
$PKGS/*${repo}*
done
env:
GH_TOKEN: ${{ secrets.ACTIONS_GITHUB_TOKEN }}
#
# Push Debian/APT archive
#
publish-apt:
name: Publish APT
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/release-nightly' || startsWith(github.ref, 'refs/tags/v'))
environment: release
needs:
- package-debian
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.ref }} # https://github.com/actions/checkout/issues/882
- name: Download packages
uses: actions/download-artifact@v4
with:
name: debian-packages
path: packages
- name: Set version
run: |
version=$(go run build.go version)
echo "Version: $version"
echo "VERSION=$version" >> $GITHUB_ENV
# Decide whether packages should go to stable, candidate or nightly
- name: Prepare packages
run: |
kind=stable
if [[ $VERSION == *-rc.[0-9] ]] ; then
kind=candidate
elif [[ $VERSION == *-* ]] ; then
kind=nightly
fi
echo "Kind: $kind"
mkdir -p packages/syncthing/$kind
mv packages/*.deb packages/syncthing/$kind
- name: Pull archive
uses: docker://docker.io/rclone/rclone:latest
env:
RCLONE_CONFIG_OBJSTORE_TYPE: s3
RCLONE_CONFIG_OBJSTORE_PROVIDER: ${{ secrets.S3_PROVIDER }}
RCLONE_CONFIG_OBJSTORE_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY_ID }}
RCLONE_CONFIG_OBJSTORE_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_ACCESS_KEY }}
RCLONE_CONFIG_OBJSTORE_ENDPOINT: ${{ secrets.S3_ENDPOINT }}
RCLONE_CONFIG_OBJSTORE_REGION: ${{ secrets.S3_REGION }}
RCLONE_CONFIG_OBJSTORE_ACL: public-read
with:
args: sync objstore:apt/dists dists
- name: Update archive
uses: docker://ghcr.io/kastelo/ezapt:latest
with:
args:
publish
--add packages
--dists dists
env:
EZAPT_KEYRING_BASE64: ${{ secrets.APT_GPG_KEYRING_BASE64 }}
- name: Push archive
uses: docker://docker.io/rclone/rclone:latest
env:
RCLONE_CONFIG_OBJSTORE_TYPE: s3
RCLONE_CONFIG_OBJSTORE_PROVIDER: ${{ secrets.S3_PROVIDER }}
RCLONE_CONFIG_OBJSTORE_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY_ID }}
RCLONE_CONFIG_OBJSTORE_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_ACCESS_KEY }}
RCLONE_CONFIG_OBJSTORE_ENDPOINT: ${{ secrets.S3_ENDPOINT }}
RCLONE_CONFIG_OBJSTORE_REGION: ${{ secrets.S3_REGION }}
RCLONE_CONFIG_OBJSTORE_ACL: public-read
with:
args: sync -v --no-update-modtime dists objstore:apt/dists
#
# Build and push to Docker Hub
@@ -732,8 +908,11 @@ jobs:
docker-syncthing:
name: Build and push Docker images
runs-on: ubuntu-latest
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/release' || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/infrastructure' || startsWith(github.ref, 'refs/heads/release-'))
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/release-nightly' || github.ref == 'refs/heads/infrastructure' || startsWith(github.ref, 'refs/tags/v'))
environment: docker
permissions:
contents: read
packages: write
strategy:
matrix:
pkg:
@@ -754,6 +933,7 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.ref }} # https://github.com/actions/checkout/issues/882
- uses: actions/setup-go@v5
with:
@@ -793,9 +973,18 @@ jobs:
uses: docker/login-action@v3
if: env.DOCKER_PUSH == 'true'
with:
registry: docker.io
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@v3
if: env.DOCKER_PUSH == 'true'
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
@@ -807,13 +996,13 @@ jobs:
echo Release version, pushing to :latest and version tags
major=${version%.*.*}
minor=${version%.*}
tags=${{ matrix.image }}:$version,${{ matrix.image }}:$major,${{ matrix.image }}:$minor,${{ matrix.image }}:latest
tags=docker.io/${{ matrix.image }}:$version,ghcr.io/${{ matrix.image }}:$version,docker.io/${{ matrix.image }}:$major,ghcr.io/${{ matrix.image }}:$major,docker.io/${{ matrix.image }}:$minor,ghcr.io/${{ matrix.image }}:$minor,docker.io/${{ matrix.image }}:latest,ghcr.io/${{ matrix.image }}:latest
elif [[ $version == *-rc.@([0-9]|[0-9][0-9]) ]] ; then
echo Release candidate, pushing to :rc
tags=${{ matrix.image }}:rc
echo Release candidate, pushing to :rc and version tags
tags=docker.io/${{ matrix.image }}:$version,ghcr.io/${{ matrix.image }}:$version,docker.io/${{ matrix.image }}:rc,ghcr.io/${{ matrix.image }}:rc
else
echo Development version, pushing to :edge
tags=${{ matrix.image }}:edge
tags=docker.io/${{ matrix.image }}:edge,ghcr.io/${{ matrix.image }}:edge
fi
echo "DOCKER_TAGS=$tags" >> $GITHUB_ENV
echo "VERSION=$version" >> $GITHUB_ENV

49
.github/workflows/pr-linters.yaml vendored Normal file
View File

@@ -0,0 +1,49 @@
name: Run PR linters
on:
pull_request:
workflow_dispatch:
permissions:
contents: read
pull-requests: read
jobs:
#
# golangci-lint runs a suite of static analysis checks on the code
#
golangci:
runs-on: ubuntu-latest
name: Golangci-lint
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: 'stable'
- name: ensure asset generation
run: go run build.go assets
- name: golangci-lint
uses: golangci/golangci-lint-action@v8
with:
only-new-issues: true
#
# Meta checks for formatting, copyright, etc
#
meta:
name: Meta checks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: 'stable'
- run: |
go run build.go assets
go test -v ./meta

27
.github/workflows/pr-metadata.yaml vendored Normal file
View File

@@ -0,0 +1,27 @@
name: PR metadata
on:
pull_request_target:
types:
- opened
- reopened
- edited
- synchronize
permissions:
contents: read
pull-requests: write
jobs:
#
# Set labels on PRs, which are then used to categorise release notes
#
labels:
name: Set labels
runs-on: ubuntu-latest
steps:
- uses: srvaroa/labeler@v1
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"

View File

@@ -0,0 +1,60 @@
name: Release Syncthing
on:
push:
branches:
- release
- release-rc*
permissions:
contents: write
jobs:
create-release-tag:
name: Create release tag
runs-on: ubuntu-latest
environment: release
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.ref }} # https://github.com/actions/checkout/issues/882
token: ${{ secrets.ACTIONS_GITHUB_TOKEN }}
- uses: actions/setup-go@v5
with:
go-version: stable
- name: Determine version to release
run: |
if [[ "$GITHUB_REF_NAME" == "release" ]] ; then
next=$(go run ./script/next-version.go)
else
next=$(go run ./script/next-version.go --pre)
fi
echo "NEXT=$next" >> $GITHUB_ENV
echo "Next version is $next"
prev=$(git describe --exclude "*-*" --abbrev=0)
echo "PREV=$prev" >> $GITHUB_ENV
echo "Previous version is $prev"
- name: Determine release notes
run: |
go run ./script/relnotes.go --new-ver "$NEXT" --branch "$GITHUB_REF_NAME" --prev-ver "$PREV" > notes.md
env:
GITHUB_TOKEN: ${{ secrets.ACTIONS_GITHUB_TOKEN }}
- name: Create and push tag
run: |
git config --global user.name 'Syncthing Release Automation'
git config --global user.email 'release@syncthing.net'
git tag -a -F notes.md --cleanup=whitespace "$NEXT"
git push origin "$NEXT"
- name: Trigger the build
uses: benc-uk/workflow-dispatch@v1
with:
workflow: build-syncthing.yaml
ref: refs/tags/${{ env.NEXT }}
token: ${{ secrets.ACTIONS_GITHUB_TOKEN }}

View File

@@ -1,26 +1,67 @@
linters-settings:
maligned:
suggest-new: true
version: "2"
linters:
enable-all: true
default: all
disable:
- goimports
- cyclop
- depguard
- lll
- gochecknoinits
- gochecknoglobals
- gofmt
- scopelint
- gocyclo
- exhaustive
- exhaustruct
- forbidigo
- funlen
- wsl
- gochecknoglobals
- gochecknoinits
- gocognit
- goconst
- gocyclo
- godox
service:
golangci-lint-version: 1.21.x
prepare:
- rm -f go.sum # 1.12 -> 1.13 issues with QUIC-go
- GO111MODULE=on go mod vendor
- go run build.go assets
- gomoddirectives
- inamedparam
- interfacebloat
- ireturn
- lll
- maintidx
- mnd
- musttag
- nestif
- nlreturn
- nonamedreturns
- paralleltest
- prealloc
- predeclared
- protogetter
- recvcheck
- revive
- tagalign
- tagliatelle
- testpackage
- usetesting # go 1.24
- varnamelen
- whitespace
- wrapcheck
- wsl
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- internal/gen
- cmd/dev
- repos
- third_party$
- builtin$
- examples$
formatters:
enable:
- gofumpt
exclusions:
generated: lax
paths:
- internal/gen
- cmd/dev
- repos
- third_party$
- builtin$
- examples$

98
.policy.yml Normal file
View File

@@ -0,0 +1,98 @@
# 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
- ^LICENSE
requires:
count: 1
teams:
- syncthing/maintainers
options:
ignore_update_merges: true
allow_contributor: true
# Regular pull requests require approval by an active contributor
- name: is approved by a syncthing contributor
requires:
count: 1
teams:
- syncthing/contributors
options:
ignore_update_merges: true
allow_contributor: true
# 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

18
AUTHORS
View File

@@ -20,6 +20,7 @@ Alan Pope <alan@popey.com>
Alberto Donato <albertodonato@users.noreply.github.com>
Aleksey Vasenev <margtu-fivt@ya.ru>
Alessandro G. (alessandro.g89) <alessandro.g89@gmail.com>
Alex Ionescu <github@ionescu.sh>
Alex Lindeman <139387+aelindeman@users.noreply.github.com>
Alex Xu <alex.hello71@gmail.com>
Alexander Graf (alex2108) <register-github@alex-graf.de>
@@ -47,6 +48,7 @@ Arkadiusz Tymiński <gevleeog@gmail.com>
Aroun <login@b-vo.fr>
Arthur Axel fREW Schmidt (frioux) <frew@afoolishmanifesto.com> <frioux@gmail.com>
Artur Zubilewicz <AkaZecik@users.noreply.github.com>
Ashish Bhate <bhate.ashish@gmail.com>
Audrius Butkevicius (AudriusButkevicius) <audrius.butkevicius@gmail.com> <github@audrius.rocks>
Aurélien Rainone <476650+arl@users.noreply.github.com>
BAHADIR YILMAZ <bahadiryilmaz32@gmail.com>
@@ -96,6 +98,7 @@ Daniel Harte (norgeous) <daniel@harte.me> <daniel@danielharte.co.uk> <norgeous@u
Daniel Martí (mvdan) <mvdan@mvdan.cc>
Daniel Padrta <64928366+danpadcz@users.noreply.github.com>
Darshil Chanpura (dtchanpura) <dtchanpura@gmail.com> <dcprime314@gmail.com>
dashangcun <907225865@qq.com>
David Rimmer (dinosore) <dinosore@dbrsoftware.co.uk>
deepsource-autofix[bot] <62050782+deepsource-autofix[bot]@users.noreply.github.com>
DeflateAwning <11021263+DeflateAwning@users.noreply.github.com>
@@ -111,6 +114,7 @@ diemade <spamkill@posteo.ch>
digital <didev@dinid.net>
Dimitri Papadopoulos Orfanos <3234522+DimitriPapadopoulos@users.noreply.github.com>
Dmitry Saveliev (dsaveliev) <d.e.saveliev@gmail.com>
domain <32405309+szu17dmy@users.noreply.github.com>
Domenic Horner <domenic@tgxn.net>
Dominik Heidler (asdil12) <dominik@heidler.eu>
Elias Jarlebring (jarlebring) <jarlebring@gmail.com>
@@ -145,7 +149,9 @@ 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>
Hazem Krimi <me@hazemkrimi.tech>
Heiko Zuerker (Smiley73) <heiko@zuerker.org>
Hireworks <129852174+hireworksltd@users.noreply.github.com>
Hugo Locurcio <hugo.locurcio@hugo.pro>
Iain Barnett <iainspeed@gmail.com>
Ian Johnson (anonymouse64) <ian.johnson@canonical.com> <person.uwsome@gmail.com>
@@ -189,6 +195,7 @@ Jörg Thalheim <Mic92@users.noreply.github.com>
Jędrzej Kula <kula.jedrek@gmail.com>
K.B.Dharun Krishna <kbdharunkrishna@gmail.com>
Kalle Laine <pahakalle@protonmail.com>
Kapil Sareen <kapilsareen584@gmail.com>
Karol Różycki (krozycki) <rozycki.karol@gmail.com>
Kebin Liu <lkebin@gmail.com>
Keith Harrison <keithh@protonmail.com>
@@ -217,8 +224,10 @@ luzpaz <luzpaz@users.noreply.github.com>
Majed Abdulaziz (majedev) <majed.alhajry@gmail.com>
Marc Laporte (marclaporte) <marc@marclaporte.com> <marc@laporte.name>
Marc Pujol (kilburn) <kilburn@la3.org>
Marcel Meyer <mm.marcelmeyer@gmail.com>
Marcin Dziadus (marcindziadus) <dziadus.marcin@gmail.com>
marco-m <marco.molteni@laposte.net>
Marcus B Spencer <marcus@marcusspencer.xyz> <marcus@marcusspencer.us>
Marcus Legendre <marcus.legendre@gmail.com>
Mario Majila <mariustshipichik@gmail.com>
Mark Pulford (mpx) <mark@kyne.com.au>
@@ -226,6 +235,7 @@ Martchus <martchus@gmx.net>
Martin Polehla <p0l0us@users.noreply.github.com>
Mateusz Naściszewski (mateon1) <matin1111@wp.pl>
Mateusz Ż <thedead4fun@live.com>
mathias4833 <67101597+mathias4833@users.noreply.github.com>
Matic Potočnik <hairyfotr@gmail.com>
Matt Burke (burkemw3) <mburke@amplify.com> <burkemw3@gmail.com>
Matt Robenolt <matt@ydekproductions.com>
@@ -233,6 +243,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>
@@ -270,6 +281,7 @@ Oyebanji Jacob Mayowa <oyebanji05@gmail.com>
Pablo <pbaeyens31+github@gmail.com>
Pascal Jungblut (pascalj) <github@pascalj.com> <mail@pascal-jungblut.com>
Paul Brit <paulbrit44@gmail.com>
Paul Donald <newtwen+github@gmail.com>
Pawel Palenica (qepasa) <pawelpalenica11@gmail.com>
Paweł Rozlach <vespian@users.noreply.github.com>
perewa <cavalcante.ten@gmail.com>
@@ -283,7 +295,9 @@ Philippe Schommers (filoozoom) <philippe@schommers.be>
Phill Luby (pluby) <phill.luby@newredo.com>
Pier Paolo Ramon <ramonpierre@gmail.com>
Piotr Bejda (piobpl) <piotrb10@gmail.com>
polyfloyd <polyfloyd@users.noreply.github.com>
Pramodh KP (pramodhkp) <pramodh.p@directi.com> <1507241+pramodhkp@users.noreply.github.com>
pullmerge <166967364+pullmerge@users.noreply.github.com>
Quentin Hibon <qh.public@yahoo.com>
Rahmi Pruitt <rjpruitt16@gmail.com>
red_led <red-led@users.noreply.github.com>
@@ -316,12 +330,16 @@ Suhas Gundimeda (snugghash) <suhas.gundimeda@gmail.com> <snugghash@gmail.com>
Sven Bachmann <dev@mcbachmann.de>
Syncthing Automation <automation@syncthing.net>
Syncthing Release Automation <release@syncthing.net>
Sébastien WENSKE <sebastien@wenske.fr>
Taylor Khan (nelsonkhan) <nelsonkhan@gmail.com>
Terrance <git@terrance.allofti.me>
TheCreeper <TheCreeper@users.noreply.github.com>
Thomas <9749173+uhthomas@users.noreply.github.com>
Thomas Hipp <thomashipp@gmail.com>
Tim Abell (timabell) <tim@timwise.co.uk>
Tim Howes (timhowes) <timhowes@berkeley.edu>
Tim Nordenfur <tim@gurka.se>
Tobias Frölich <40638719+tobifroe@users.noreply.github.com>
Tobias Klauser <tobias.klauser@gmail.com>
Tobias Nygren (tnn2) <tnn@nygren.pp.se>
Tobias Tom (tobiastom) <t.tom@succont.de>

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

@@ -82,13 +82,11 @@ build process.
## Signed Releases
As of v0.10.15 and onwards, release binaries are GPG signed with the key
D26E6ED000654A3E, available from https://syncthing.net/security/ and
most key servers.
There is also a built-in automatic upgrade mechanism (disabled in some
distribution channels) which uses a compiled in ECDSA signature. macOS
binaries are also properly code signed.
Release binaries are GPG signed with the key available from
https://syncthing.net/security/. There is also a built-in automatic
upgrade mechanism (disabled in some distribution channels) which uses a
compiled in ECDSA signature. macOS and Windows binaries are also
code-signed.
## Documentation

12
buf.gen.yaml Normal file
View File

@@ -0,0 +1,12 @@
version: v2
managed:
enabled: true
override:
- file_option: go_package_prefix
value: github.com/syncthing/syncthing/internal/gen
plugins:
- remote: buf.build/protocolbuffers/go:v1.35.1
out: .
opt: module=github.com/syncthing/syncthing
inputs:
- directory: proto

10
buf.yaml Normal file
View File

@@ -0,0 +1,10 @@
version: v2
modules:
- path: proto
name: github.com/syncthing/syncthing
lint:
use:
- STANDARD
breaking:
use:
- WIRE_JSON

251
build.go
View File

@@ -15,7 +15,6 @@ import (
"bytes"
"compress/flate"
"compress/gzip"
"encoding/base64"
"encoding/json"
"errors"
"flag"
@@ -86,7 +85,6 @@ var targets = map[string]target{
"all": {
// Only valid for the "build" and "install" commands as it lacks all
// the archive creation stuff. buildPkgs gets filled out in init()
tags: []string{"purego"},
},
"syncthing": {
// The default target for "build", "install", "tar", "zip", "deb", etc.
@@ -97,40 +95,40 @@ var targets = map[string]target{
buildPkgs: []string{"github.com/syncthing/syncthing/cmd/syncthing"},
binaryName: "syncthing", // .exe will be added automatically for Windows builds
archiveFiles: []archiveFile{
{src: "{{binary}}", dst: "{{binary}}", perm: 0755},
{src: "README.md", dst: "README.txt", perm: 0644},
{src: "LICENSE", dst: "LICENSE.txt", perm: 0644},
{src: "AUTHORS", dst: "AUTHORS.txt", perm: 0644},
{src: "{{binary}}", dst: "{{binary}}", perm: 0o755},
{src: "README.md", dst: "README.txt", perm: 0o644},
{src: "LICENSE", dst: "LICENSE.txt", perm: 0o644},
{src: "AUTHORS", dst: "AUTHORS.txt", perm: 0o644},
// All files from etc/ and extra/ added automatically in init().
},
systemdService: "syncthing@*.service",
installationFiles: []archiveFile{
{src: "{{binary}}", dst: "deb/usr/bin/{{binary}}", perm: 0755},
{src: "README.md", dst: "deb/usr/share/doc/syncthing/README.txt", perm: 0644},
{src: "LICENSE", dst: "deb/usr/share/doc/syncthing/LICENSE.txt", perm: 0644},
{src: "AUTHORS", dst: "deb/usr/share/doc/syncthing/AUTHORS.txt", perm: 0644},
{src: "man/syncthing.1", dst: "deb/usr/share/man/man1/syncthing.1", perm: 0644},
{src: "man/syncthing-config.5", dst: "deb/usr/share/man/man5/syncthing-config.5", perm: 0644},
{src: "man/syncthing-stignore.5", dst: "deb/usr/share/man/man5/syncthing-stignore.5", perm: 0644},
{src: "man/syncthing-device-ids.7", dst: "deb/usr/share/man/man7/syncthing-device-ids.7", perm: 0644},
{src: "man/syncthing-event-api.7", dst: "deb/usr/share/man/man7/syncthing-event-api.7", perm: 0644},
{src: "man/syncthing-faq.7", dst: "deb/usr/share/man/man7/syncthing-faq.7", perm: 0644},
{src: "man/syncthing-networking.7", dst: "deb/usr/share/man/man7/syncthing-networking.7", perm: 0644},
{src: "man/syncthing-rest-api.7", dst: "deb/usr/share/man/man7/syncthing-rest-api.7", perm: 0644},
{src: "man/syncthing-security.7", dst: "deb/usr/share/man/man7/syncthing-security.7", perm: 0644},
{src: "man/syncthing-versioning.7", dst: "deb/usr/share/man/man7/syncthing-versioning.7", perm: 0644},
{src: "etc/linux-systemd/system/syncthing@.service", dst: "deb/lib/systemd/system/syncthing@.service", perm: 0644},
{src: "etc/linux-systemd/user/syncthing.service", dst: "deb/usr/lib/systemd/user/syncthing.service", perm: 0644},
{src: "etc/linux-sysctl/30-syncthing.conf", dst: "deb/usr/lib/sysctl.d/30-syncthing.conf", perm: 0644},
{src: "etc/firewall-ufw/syncthing", dst: "deb/etc/ufw/applications.d/syncthing", perm: 0644},
{src: "etc/linux-desktop/syncthing-start.desktop", dst: "deb/usr/share/applications/syncthing-start.desktop", perm: 0644},
{src: "etc/linux-desktop/syncthing-ui.desktop", dst: "deb/usr/share/applications/syncthing-ui.desktop", perm: 0644},
{src: "assets/logo-32.png", dst: "deb/usr/share/icons/hicolor/32x32/apps/syncthing.png", perm: 0644},
{src: "assets/logo-64.png", dst: "deb/usr/share/icons/hicolor/64x64/apps/syncthing.png", perm: 0644},
{src: "assets/logo-128.png", dst: "deb/usr/share/icons/hicolor/128x128/apps/syncthing.png", perm: 0644},
{src: "assets/logo-256.png", dst: "deb/usr/share/icons/hicolor/256x256/apps/syncthing.png", perm: 0644},
{src: "assets/logo-512.png", dst: "deb/usr/share/icons/hicolor/512x512/apps/syncthing.png", perm: 0644},
{src: "assets/logo-only.svg", dst: "deb/usr/share/icons/hicolor/scalable/apps/syncthing.svg", perm: 0644},
{src: "{{binary}}", dst: "deb/usr/bin/{{binary}}", perm: 0o755},
{src: "README.md", dst: "deb/usr/share/doc/syncthing/README.txt", perm: 0o644},
{src: "LICENSE", dst: "deb/usr/share/doc/syncthing/LICENSE.txt", perm: 0o644},
{src: "AUTHORS", dst: "deb/usr/share/doc/syncthing/AUTHORS.txt", perm: 0o644},
{src: "man/syncthing.1", dst: "deb/usr/share/man/man1/syncthing.1", perm: 0o644},
{src: "man/syncthing-config.5", dst: "deb/usr/share/man/man5/syncthing-config.5", perm: 0o644},
{src: "man/syncthing-stignore.5", dst: "deb/usr/share/man/man5/syncthing-stignore.5", perm: 0o644},
{src: "man/syncthing-device-ids.7", dst: "deb/usr/share/man/man7/syncthing-device-ids.7", perm: 0o644},
{src: "man/syncthing-event-api.7", dst: "deb/usr/share/man/man7/syncthing-event-api.7", perm: 0o644},
{src: "man/syncthing-faq.7", dst: "deb/usr/share/man/man7/syncthing-faq.7", perm: 0o644},
{src: "man/syncthing-networking.7", dst: "deb/usr/share/man/man7/syncthing-networking.7", perm: 0o644},
{src: "man/syncthing-rest-api.7", dst: "deb/usr/share/man/man7/syncthing-rest-api.7", perm: 0o644},
{src: "man/syncthing-security.7", dst: "deb/usr/share/man/man7/syncthing-security.7", perm: 0o644},
{src: "man/syncthing-versioning.7", dst: "deb/usr/share/man/man7/syncthing-versioning.7", perm: 0o644},
{src: "etc/linux-systemd/system/syncthing@.service", dst: "deb/lib/systemd/system/syncthing@.service", perm: 0o644},
{src: "etc/linux-systemd/user/syncthing.service", dst: "deb/usr/lib/systemd/user/syncthing.service", perm: 0o644},
{src: "etc/linux-sysctl/30-syncthing.conf", dst: "deb/usr/lib/sysctl.d/30-syncthing.conf", perm: 0o644},
{src: "etc/firewall-ufw/syncthing", dst: "deb/etc/ufw/applications.d/syncthing", perm: 0o644},
{src: "etc/linux-desktop/syncthing-start.desktop", dst: "deb/usr/share/applications/syncthing-start.desktop", perm: 0o644},
{src: "etc/linux-desktop/syncthing-ui.desktop", dst: "deb/usr/share/applications/syncthing-ui.desktop", perm: 0o644},
{src: "assets/logo-32.png", dst: "deb/usr/share/icons/hicolor/32x32/apps/syncthing.png", perm: 0o644},
{src: "assets/logo-64.png", dst: "deb/usr/share/icons/hicolor/64x64/apps/syncthing.png", perm: 0o644},
{src: "assets/logo-128.png", dst: "deb/usr/share/icons/hicolor/128x128/apps/syncthing.png", perm: 0o644},
{src: "assets/logo-256.png", dst: "deb/usr/share/icons/hicolor/256x256/apps/syncthing.png", perm: 0o644},
{src: "assets/logo-512.png", dst: "deb/usr/share/icons/hicolor/512x512/apps/syncthing.png", perm: 0o644},
{src: "assets/logo-only.svg", dst: "deb/usr/share/icons/hicolor/scalable/apps/syncthing.svg", perm: 0o644},
},
},
"stdiscosrv": {
@@ -142,23 +140,22 @@ var targets = map[string]target{
buildPkgs: []string{"github.com/syncthing/syncthing/cmd/stdiscosrv"},
binaryName: "stdiscosrv", // .exe will be added automatically for Windows builds
archiveFiles: []archiveFile{
{src: "{{binary}}", dst: "{{binary}}", perm: 0755},
{src: "cmd/stdiscosrv/README.md", dst: "README.txt", perm: 0644},
{src: "LICENSE", dst: "LICENSE.txt", perm: 0644},
{src: "AUTHORS", dst: "AUTHORS.txt", perm: 0644},
{src: "{{binary}}", dst: "{{binary}}", perm: 0o755},
{src: "cmd/stdiscosrv/README.md", dst: "README.txt", perm: 0o644},
{src: "LICENSE", dst: "LICENSE.txt", perm: 0o644},
{src: "AUTHORS", dst: "AUTHORS.txt", perm: 0o644},
},
systemdService: "stdiscosrv.service",
installationFiles: []archiveFile{
{src: "{{binary}}", dst: "deb/usr/bin/{{binary}}", perm: 0755},
{src: "cmd/stdiscosrv/README.md", dst: "deb/usr/share/doc/syncthing-discosrv/README.txt", perm: 0644},
{src: "LICENSE", dst: "deb/usr/share/doc/syncthing-discosrv/LICENSE.txt", perm: 0644},
{src: "AUTHORS", dst: "deb/usr/share/doc/syncthing-discosrv/AUTHORS.txt", perm: 0644},
{src: "man/stdiscosrv.1", dst: "deb/usr/share/man/man1/stdiscosrv.1", perm: 0644},
{src: "cmd/stdiscosrv/etc/linux-systemd/stdiscosrv.service", dst: "deb/lib/systemd/system/stdiscosrv.service", perm: 0644},
{src: "cmd/stdiscosrv/etc/linux-systemd/default", dst: "deb/etc/default/syncthing-discosrv", perm: 0644},
{src: "cmd/stdiscosrv/etc/firewall-ufw/stdiscosrv", dst: "deb/etc/ufw/applications.d/stdiscosrv", perm: 0644},
{src: "{{binary}}", dst: "deb/usr/bin/{{binary}}", perm: 0o755},
{src: "cmd/stdiscosrv/README.md", dst: "deb/usr/share/doc/syncthing-discosrv/README.txt", perm: 0o644},
{src: "LICENSE", dst: "deb/usr/share/doc/syncthing-discosrv/LICENSE.txt", perm: 0o644},
{src: "AUTHORS", dst: "deb/usr/share/doc/syncthing-discosrv/AUTHORS.txt", perm: 0o644},
{src: "man/stdiscosrv.1", dst: "deb/usr/share/man/man1/stdiscosrv.1", perm: 0o644},
{src: "cmd/stdiscosrv/etc/linux-systemd/stdiscosrv.service", dst: "deb/lib/systemd/system/stdiscosrv.service", perm: 0o644},
{src: "cmd/stdiscosrv/etc/linux-systemd/default", dst: "deb/etc/default/syncthing-discosrv", perm: 0o644},
{src: "cmd/stdiscosrv/etc/firewall-ufw/stdiscosrv", dst: "deb/etc/ufw/applications.d/stdiscosrv", perm: 0o644},
},
tags: []string{"purego"},
},
"strelaysrv": {
name: "strelaysrv",
@@ -169,44 +166,30 @@ var targets = map[string]target{
buildPkgs: []string{"github.com/syncthing/syncthing/cmd/strelaysrv"},
binaryName: "strelaysrv", // .exe will be added automatically for Windows builds
archiveFiles: []archiveFile{
{src: "{{binary}}", dst: "{{binary}}", perm: 0755},
{src: "cmd/strelaysrv/README.md", dst: "README.txt", perm: 0644},
{src: "cmd/strelaysrv/LICENSE", dst: "LICENSE.txt", perm: 0644},
{src: "LICENSE", dst: "LICENSE.txt", perm: 0644},
{src: "AUTHORS", dst: "AUTHORS.txt", perm: 0644},
{src: "{{binary}}", dst: "{{binary}}", perm: 0o755},
{src: "cmd/strelaysrv/README.md", dst: "README.txt", perm: 0o644},
{src: "cmd/strelaysrv/LICENSE", dst: "LICENSE.txt", perm: 0o644},
{src: "LICENSE", dst: "LICENSE.txt", perm: 0o644},
{src: "AUTHORS", dst: "AUTHORS.txt", perm: 0o644},
},
systemdService: "strelaysrv.service",
installationFiles: []archiveFile{
{src: "{{binary}}", dst: "deb/usr/bin/{{binary}}", perm: 0755},
{src: "cmd/strelaysrv/README.md", dst: "deb/usr/share/doc/syncthing-relaysrv/README.txt", perm: 0644},
{src: "cmd/strelaysrv/LICENSE", dst: "deb/usr/share/doc/syncthing-relaysrv/LICENSE.txt", perm: 0644},
{src: "LICENSE", dst: "deb/usr/share/doc/syncthing-relaysrv/LICENSE.txt", perm: 0644},
{src: "AUTHORS", dst: "deb/usr/share/doc/syncthing-relaysrv/AUTHORS.txt", perm: 0644},
{src: "man/strelaysrv.1", dst: "deb/usr/share/man/man1/strelaysrv.1", perm: 0644},
{src: "cmd/strelaysrv/etc/linux-systemd/strelaysrv.service", dst: "deb/lib/systemd/system/strelaysrv.service", perm: 0644},
{src: "cmd/strelaysrv/etc/linux-systemd/default", dst: "deb/etc/default/syncthing-relaysrv", perm: 0644},
{src: "cmd/strelaysrv/etc/firewall-ufw/strelaysrv", dst: "deb/etc/ufw/applications.d/strelaysrv", perm: 0644},
{src: "{{binary}}", dst: "deb/usr/bin/{{binary}}", perm: 0o755},
{src: "cmd/strelaysrv/README.md", dst: "deb/usr/share/doc/syncthing-relaysrv/README.txt", perm: 0o644},
{src: "cmd/strelaysrv/LICENSE", dst: "deb/usr/share/doc/syncthing-relaysrv/LICENSE.txt", perm: 0o644},
{src: "LICENSE", dst: "deb/usr/share/doc/syncthing-relaysrv/LICENSE.txt", perm: 0o644},
{src: "AUTHORS", dst: "deb/usr/share/doc/syncthing-relaysrv/AUTHORS.txt", perm: 0o644},
{src: "man/strelaysrv.1", dst: "deb/usr/share/man/man1/strelaysrv.1", perm: 0o644},
{src: "cmd/strelaysrv/etc/linux-systemd/strelaysrv.service", dst: "deb/lib/systemd/system/strelaysrv.service", perm: 0o644},
{src: "cmd/strelaysrv/etc/linux-systemd/default", dst: "deb/etc/default/syncthing-relaysrv", perm: 0o644},
{src: "cmd/strelaysrv/etc/firewall-ufw/strelaysrv", dst: "deb/etc/ufw/applications.d/strelaysrv", perm: 0o644},
},
},
"strelaypoolsrv": {
name: "strelaypoolsrv",
debname: "syncthing-relaypoolsrv",
debdeps: []string{"libc6"},
description: "Syncthing Relay Pool Server",
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/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/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},
},
binaryName: "strelaypoolsrv",
},
"stupgrades": {
name: "stupgrades",
@@ -244,13 +227,13 @@ func initTargets() {
// and "extra" dirs.
syncthingPkg := targets["syncthing"]
for _, file := range listFiles("etc") {
syncthingPkg.archiveFiles = append(syncthingPkg.archiveFiles, archiveFile{src: file, dst: file, perm: 0644})
syncthingPkg.archiveFiles = append(syncthingPkg.archiveFiles, archiveFile{src: file, dst: file, perm: 0o644})
}
for _, file := range listFiles("extra") {
syncthingPkg.archiveFiles = append(syncthingPkg.archiveFiles, archiveFile{src: file, dst: file, perm: 0644})
syncthingPkg.archiveFiles = append(syncthingPkg.archiveFiles, archiveFile{src: file, dst: file, perm: 0o644})
}
for _, file := range listFiles("extra") {
syncthingPkg.installationFiles = append(syncthingPkg.installationFiles, archiveFile{src: file, dst: "deb/usr/share/doc/syncthing/" + filepath.Base(file), perm: 0644})
syncthingPkg.installationFiles = append(syncthingPkg.installationFiles, archiveFile{src: file, dst: "deb/usr/share/doc/syncthing/" + filepath.Base(file), perm: 0o644})
}
targets["syncthing"] = syncthingPkg
}
@@ -347,7 +330,7 @@ func runCommand(cmd string, target target) {
writeCompatJSON()
case "deb":
buildDeb(target)
buildDeb(target, tags)
case "vet":
metalintShort()
@@ -405,7 +388,6 @@ func parseFlags() {
func test(tags []string, pkgs ...string) {
lazyRebuildAssets()
tags = append(tags, "purego")
args := []string{"test", "-tags", strings.Join(tags, " ")}
if long {
timeout = longTimeout
@@ -439,7 +421,7 @@ func bench(tags []string, pkgs ...string) {
func integration(bench bool) {
lazyRebuildAssets()
args := []string{"test", "-v", "-timeout", "60m", "-tags"}
tags := "purego,integration"
tags := "integration"
if bench {
tags += ",benchmark"
}
@@ -627,7 +609,7 @@ func buildZip(target target, tags []string) {
fmt.Println(filename)
}
func buildDeb(target target) {
func buildDeb(target target, tags []string) {
os.RemoveAll("deb")
// "goarch" here is set to whatever the Debian packages expect. We correct
@@ -641,7 +623,7 @@ func buildDeb(target target) {
goarch = "arm"
}
build(target, []string{"noupgrade"})
build(target, append(tags, "noupgrade"))
for i := range target.installationFiles {
target.installationFiles[i].src = strings.Replace(target.installationFiles[i].src, "{{binary}}", target.BinaryName(), 1)
@@ -663,6 +645,9 @@ func buildDeb(target target) {
// than just 0.14.26. This rectifies that.
debver = strings.Replace(debver, "-", "~", -1)
}
if strings.Contains(debver, "_") {
debver = strings.Replace(debver, "_", "~", -1)
}
args := []string{
"-t", "deb",
"-s", "dir",
@@ -750,7 +735,7 @@ func shouldBuildSyso(dir string) (string, error) {
}
jsonPath := filepath.Join(dir, "versioninfo.json")
err = os.WriteFile(jsonPath, bs, 0644)
err = os.WriteFile(jsonPath, bs, 0o644)
if err != nil {
return "", errors.New("failed to create " + jsonPath + ": " + err.Error())
}
@@ -809,7 +794,7 @@ func copyFile(src, dst string, perm os.FileMode) error {
}
copy:
os.MkdirAll(filepath.Dir(dst), 0777)
os.MkdirAll(filepath.Dir(dst), 0o777)
if err := os.WriteFile(dst, in, perm); err != nil {
return err
}
@@ -869,7 +854,7 @@ func buildNextGenGUI() bool {
for _, src := range listFiles("next-gen-gui/dist") {
rel, _ := filepath.Rel("next-gen-gui/dist", src)
dst := filepath.Join("gui", rel)
if err := copyFile(src, dst, 0644); err != nil {
if err := copyFile(src, dst, 0o644); err != nil {
fmt.Println("copy:", err)
os.Exit(1)
}
@@ -925,22 +910,9 @@ func updateDependencies() {
}
func proto() {
pv := protobufVersion()
repo := "https://github.com/gogo/protobuf.git"
path := filepath.Join("repos", "protobuf")
runPrint(goCmd, "install", fmt.Sprintf("github.com/gogo/protobuf/protoc-gen-gogofast@%v", pv))
os.MkdirAll("repos", 0755)
if _, err := os.Stat(path); err != nil {
runPrint("git", "clone", repo, path)
} else {
runPrintInDir(path, "git", "fetch")
}
runPrintInDir(path, "git", "checkout", pv)
runPrint(goCmd, "generate", "github.com/syncthing/syncthing/cmd/stdiscosrv")
runPrint(goCmd, "generate", "proto/generate.go")
// buf needs to be installed
// https://buf.build/docs/installation/
runPrint("buf", "generate")
}
func testmocks() {
@@ -1375,10 +1347,7 @@ func zipFile(out string, files []archiveFile) {
}
func codesign(target target) {
switch goos {
case "windows":
windowsCodesign(target.BinaryName())
case "darwin":
if goos == "darwin" {
macosCodesign(target.BinaryName())
}
}
@@ -1402,70 +1371,6 @@ func macosCodesign(file string) {
}
}
func windowsCodesign(file string) {
st := "signtool.exe"
if path := os.Getenv("CODESIGN_SIGNTOOL"); path != "" {
st = path
}
for i, algo := range []string{"sha1", "sha256"} {
args := []string{"sign", "/fd", algo}
if f := os.Getenv("CODESIGN_CERTIFICATE_FILE"); f != "" {
args = append(args, "/f", f)
} else if b := os.Getenv("CODESIGN_CERTIFICATE_BASE64"); b != "" {
// Decode the PFX certificate from base64.
bs, err := base64.RawStdEncoding.DecodeString(b)
if err != nil {
log.Println("Codesign: signing failed: decoding base64:", err)
return
}
// Write it to a temporary file
f, err := os.CreateTemp("", "codesign-*.pfx")
if err != nil {
log.Println("Codesign: signing failed: creating temp file:", err)
return
}
_ = f.Chmod(0600) // best effort remove other users' access
defer os.Remove(f.Name())
if _, err := f.Write(bs); err != nil {
log.Println("Codesign: signing failed: writing temp file:", err)
return
}
if err := f.Close(); err != nil {
log.Println("Codesign: signing failed: closing temp file:", err)
return
}
// Use that when signing
args = append(args, "/f", f.Name())
}
if p := os.Getenv("CODESIGN_CERTIFICATE_PASSWORD"); p != "" {
args = append(args, "/p", p)
}
if tr := os.Getenv("CODESIGN_TIMESTAMP_SERVER"); tr != "" {
switch algo {
case "sha256":
args = append(args, "/tr", tr, "/td", algo)
default:
args = append(args, "/t", tr)
}
}
if i > 0 {
args = append(args, "/as")
}
args = append(args, file)
bs, err := runError(st, args...)
if err != nil {
log.Printf("Codesign: signing failed: %v: %s", err, string(bs))
return
}
log.Println("Codesign: successfully signed", file, "using", algo)
}
}
func metalint() {
lazyRebuildAssets()
runPrint(goCmd, "test", "-run", "Metalint", "./meta")
@@ -1483,14 +1388,6 @@ func (t target) BinaryName() string {
return t.binaryName
}
func protobufVersion() string {
bs, err := runError(goCmd, "list", "-f", "{{.Version}}", "-m", "github.com/gogo/protobuf")
if err != nil {
log.Fatal("Getting protobuf version:", err)
}
return string(bs)
}
func currentAndLatestVersions(n int) ([]string, error) {
bs, err := runError("git", "tag", "--sort", "taggerdate")
if err != nil {

View File

@@ -23,10 +23,11 @@ case "${1:-default}" in
prerelease)
script authors
script copyrights
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

@@ -15,6 +15,9 @@ import (
"strings"
"time"
"google.golang.org/protobuf/proto"
"github.com/syncthing/syncthing/internal/gen/discoproto"
_ "github.com/syncthing/syncthing/lib/automaxprocs"
"github.com/syncthing/syncthing/lib/beacon"
"github.com/syncthing/syncthing/lib/discover"
@@ -75,20 +78,21 @@ func recv(bc beacon.Interface) {
continue
}
var ann discover.Announce
ann.Unmarshal(data[4:])
var ann discoproto.Announce
proto.Unmarshal(data[4:], &ann)
if ann.ID == myID {
id, _ := protocol.DeviceIDFromBytes(ann.Id)
if id == myID {
// This is one of our own fake packets, don't print it.
continue
}
// Print announcement details for the first packet from a given
// device ID and source address, or if -all was given.
key := ann.ID.String() + src.String()
key := id.String() + src.String()
if all || !seen[key] {
log.Printf("Announcement from %v\n", src)
log.Printf(" %v at %s\n", ann.ID, strings.Join(ann.Addresses, ", "))
log.Printf(" %v at %s\n", id, strings.Join(ann.Addresses, ", "))
seen[key] = true
}
}
@@ -96,11 +100,11 @@ func recv(bc beacon.Interface) {
// sends fake discovery announcements once every second
func send(bc beacon.Interface) {
ann := discover.Announce{
ID: myID,
ann := &discoproto.Announce{
Id: myID[:],
Addresses: []string{"tcp://fake.example.com:12345"},
}
bs, _ := ann.Marshal()
bs, _ := proto.Marshal(ann)
for {
bc.Send(bs)

View File

@@ -8,6 +8,7 @@ package main
import (
"bytes"
"cmp"
"compress/gzip"
"context"
"io"
@@ -15,7 +16,7 @@ import (
"math"
"os"
"path/filepath"
"sort"
"slices"
"time"
)
@@ -177,8 +178,8 @@ func (d *diskStore) inventory() error {
})
return nil
})
sort.Slice(d.currentFiles, func(i, j int) bool {
return d.currentFiles[i].mtime < d.currentFiles[j].mtime
slices.SortFunc(d.currentFiles, func(a, b currentFile) int {
return cmp.Compare(a.mtime, b.mtime)
})
var oldest time.Duration
if len(d.currentFiles) > 0 {

View File

@@ -71,7 +71,6 @@ func (l *githubSourceCodeLoader) Load(filename string, line, context int) ([][]b
url := urlPrefix + l.version + filename[idx:]
resp, err := l.client.Get(url)
if err != nil {
fmt.Println("Loading source:", err)
return nil, 0

View File

@@ -52,5 +52,5 @@ func compressAndWrite(bs []byte, fullPath string) error {
gw.Close()
// Create an output file with the compressed report
return os.WriteFile(fullPath, buf.Bytes(), 0644)
return os.WriteFile(fullPath, buf.Bytes(), 0o644)
}

View File

@@ -10,7 +10,7 @@ to NAT or firewall issues.
There is very little reason why you'd want to run this yourself, as
`relaypoolsrv` is just used for announcement and lookup of public relay
servers. If you are looking to setup a private or a public relay, please
servers. If you are looking to set up a private or a public relay, please
check the documentation for
[relaysrv](https://github.com/syncthing/relaysrv), which also explains how
to join the default public pool.
@@ -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

@@ -23,11 +23,13 @@ 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/infra/strelaypoolsrv/auto"
"github.com/syncthing/syncthing/lib/assets"
_ "github.com/syncthing/syncthing/lib/automaxprocs"
"github.com/syncthing/syncthing/lib/geoip"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/rand"
"github.com/syncthing/syncthing/lib/relay/client"
"github.com/syncthing/syncthing/lib/sync"
"github.com/syncthing/syncthing/lib/tlsutil"
@@ -109,6 +111,7 @@ var (
requestProcessors = 8
geoipLicenseKey = os.Getenv("GEOIP_LICENSE_KEY")
geoipAccountID, _ = strconv.Atoi(os.Getenv("GEOIP_ACCOUNT_ID"))
maxRelaysReturned = 100
requests chan request
@@ -140,6 +143,7 @@ func main() {
flag.IntVar(&requestQueueLen, "request-queue", requestQueueLen, "Queue length for incoming test requests")
flag.IntVar(&requestProcessors, "request-processors", requestProcessors, "Number of request processor routines")
flag.StringVar(&geoipLicenseKey, "geoip-license-key", geoipLicenseKey, "License key for GeoIP database")
flag.IntVar(&maxRelaysReturned, "max-relays-returned", maxRelaysReturned, "Maximum number of relays returned for a normal endpoint query")
flag.Parse()
@@ -330,6 +334,10 @@ func handleEndpointShort(rw http.ResponseWriter, r *http.Request) {
relays = append(relays, relayShort{URL: slimURL(r.URL)})
}
mut.RUnlock()
if len(relays) > maxRelaysReturned {
rand.Shuffle(relays)
relays = relays[:maxRelaysReturned]
}
_ = json.NewEncoder(rw).Encode(map[string][]relayShort{
"relays": relays,

View File

@@ -7,15 +7,19 @@
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"log/slog"
"net"
"net/http"
"os"
"regexp"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/alecthomas/kong"
@@ -27,7 +31,7 @@ import (
type cli struct {
Listen string `default:":8080" help:"Listen address"`
MetricsListen string `default:":8081" help:"Listen address for metrics"`
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"`
@@ -36,6 +40,10 @@ type cli struct {
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)
@@ -46,24 +54,47 @@ 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() {
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)
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.Handle("/meta.json", httpcache.SinglePath(&githubReleases{url: params.URL}, params.CacheTime))
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)
}
log.Println("Forwarding", path, "to", url)
mux.Handle(path, httpcache.SinglePath(&proxy{url: url}, params.CacheTime))
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{
@@ -73,60 +104,76 @@ func server(params *cli) error {
WriteTimeout: 10 * time.Second,
}
srv.SetKeepAlivesEnabled(false)
return srv.ListenAndServe()
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 {
url string
cache *cachedReleases
}
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)
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
}
sort.Sort(upgrade.SortByRelease(rels))
rels = filterForLatest(rels)
w.Header().Set("Syncthing-Num-Releases", strconv.Itoa(len(rels)))
w.WriteHeader(http.StatusOK)
}
// 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 = ""
}
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()
}
buf := new(bytes.Buffer)
_ = json.NewEncoder(buf).Encode(rels)
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.Write(buf.Bytes())
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 {
url string
name string
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 {
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)
@@ -154,18 +201,174 @@ func (p *proxy) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// looking for a prerelease at all.
func filterForLatest(rels []upgrade.Release) []upgrade.Release {
var filtered []upgrade.Release
var havePre bool
havePre := make(map[string]bool)
haveStable := make(map[string]bool)
for _, rel := range rels {
if !rel.Prerelease {
// We found a stable version, we're good now.
major, _, _ := strings.Cut(rel.Tag, ".")
if !rel.Prerelease && !haveStable[major] {
// Remember the first non-pre for each major
filtered = append(filtered, rel)
break
haveStable[major] = true
continue
}
if rel.Prerelease && !havePre {
// We remember the first prerelease we find.
if rel.Prerelease && !havePre[major] && !haveStable[major] {
// We remember the first prerelease we find, unless we've
// already found a non-pre of the same major.
filtered = append(filtered, rel)
havePre = true
havePre[major] = 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
latestRel, latestPre string
}
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
}
latestRel, latestPre := "", ""
for _, rel := range rels {
if !rel.Prerelease && latestRel == "" {
latestRel = rel.Tag
}
if rel.Prerelease && latestPre == "" {
latestPre = rel.Tag
}
if latestRel != "" && latestPre != "" {
break
}
}
c.mut.Lock()
c.current = rels
if latestRel != c.latestRel || latestPre != c.latestPre {
metricLatestReleaseInfo.DeleteLabelValues(c.latestRel, c.latestPre)
metricLatestReleaseInfo.WithLabelValues(latestRel, latestPre).Set(1)
c.latestRel = latestRel
c.latestPre = latestPre
}
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,36 @@
// 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"})
metricLatestReleaseInfo = promauto.NewGaugeVec(prometheus.GaugeOpts{
Namespace: "syncthing",
Subsystem: "upgrade",
Name: "latest_release_info",
Help: "Release information",
}, []string{"latest_release", "latest_pre"})
)

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

@@ -8,22 +8,22 @@ package main
import (
"log"
"log/slog"
"os"
"github.com/alecthomas/kong"
"github.com/syncthing/syncthing/cmd/infra/ursrv/aggregate"
"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

@@ -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

@@ -11,16 +11,36 @@ import (
"github.com/prometheus/client_golang/prometheus/promauto"
)
var metricReportsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: "syncthing",
Subsystem: "ursrv",
Name: "reports_total",
}, []string{"version"})
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("duplicate")
metricReportsTotal.WithLabelValues("v1")
metricReportsTotal.WithLabelValues("v2")
metricReportsTotal.WithLabelValues("v3")
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

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 less-padding">
<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/">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>

View File

@@ -13,8 +13,12 @@ import (
"log"
amqp "github.com/rabbitmq/amqp091-go"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/thejerf/suture/v4"
"google.golang.org/protobuf/proto"
"github.com/syncthing/syncthing/internal/gen/discosrv"
"github.com/syncthing/syncthing/internal/protoutil"
"github.com/syncthing/syncthing/lib/protocol"
)
type amqpReplicator struct {
@@ -22,7 +26,7 @@ type amqpReplicator struct {
broker string
sender *amqpSender
receiver *amqpReceiver
outbox chan ReplicationRecord
outbox chan *discosrv.ReplicationRecord
}
func newAMQPReplicator(broker, clientID string, db database) *amqpReplicator {
@@ -31,7 +35,7 @@ func newAMQPReplicator(broker, clientID string, db database) *amqpReplicator {
sender := &amqpSender{
broker: broker,
clientID: clientID,
outbox: make(chan ReplicationRecord, replicationOutboxSize),
outbox: make(chan *discosrv.ReplicationRecord, replicationOutboxSize),
}
svc.Add(sender)
@@ -47,18 +51,18 @@ func newAMQPReplicator(broker, clientID string, db database) *amqpReplicator {
broker: broker,
sender: sender,
receiver: receiver,
outbox: make(chan ReplicationRecord, replicationOutboxSize),
outbox: make(chan *discosrv.ReplicationRecord, replicationOutboxSize),
}
}
func (s *amqpReplicator) send(key *protocol.DeviceID, ps []DatabaseAddress, seen int64) {
func (s *amqpReplicator) send(key *protocol.DeviceID, ps []*discosrv.DatabaseAddress, seen int64) {
s.sender.send(key, ps, seen)
}
type amqpSender struct {
broker string
clientID string
outbox chan ReplicationRecord
outbox chan *discosrv.ReplicationRecord
}
func (s *amqpSender) Serve(ctx context.Context) error {
@@ -73,12 +77,12 @@ func (s *amqpSender) Serve(ctx context.Context) error {
for {
select {
case rec := <-s.outbox:
size := rec.Size()
size := proto.Size(rec)
if len(buf) < size {
buf = make([]byte, size)
}
n, err := rec.MarshalTo(buf)
n, err := protoutil.MarshalTo(buf, rec)
if err != nil {
replicationSendsTotal.WithLabelValues("error").Inc()
return fmt.Errorf("replication marshal: %w", err)
@@ -111,8 +115,8 @@ func (s *amqpSender) String() string {
return fmt.Sprintf("amqpSender(%q)", s.broker)
}
func (s *amqpSender) send(key *protocol.DeviceID, ps []DatabaseAddress, seen int64) {
item := ReplicationRecord{
func (s *amqpSender) send(key *protocol.DeviceID, ps []*discosrv.DatabaseAddress, seen int64) {
item := &discosrv.ReplicationRecord{
Key: key[:],
Addresses: ps,
Seen: seen,
@@ -158,8 +162,8 @@ func (s *amqpReceiver) Serve(ctx context.Context) error {
continue
}
var rec ReplicationRecord
if err := rec.Unmarshal(msg.Body); err != nil {
var rec discosrv.ReplicationRecord
if err := proto.Unmarshal(msg.Body, &rec); err != nil {
replicationRecvsTotal.WithLabelValues("error").Inc()
return fmt.Errorf("replication unmarshal: %w", err)
}

View File

@@ -28,6 +28,7 @@ import (
"sync"
"time"
"github.com/syncthing/syncthing/internal/gen/discosrv"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/stringutil"
)
@@ -52,7 +53,7 @@ type apiSrv struct {
}
type replicator interface {
send(key *protocol.DeviceID, addrs []DatabaseAddress, seen int64)
send(key *protocol.DeviceID, addrs []*discosrv.DatabaseAddress, seen int64)
}
type requestID int64
@@ -65,7 +66,7 @@ type contextKey int
const idKey contextKey = iota
func newAPISrv(addr string, cert tls.Certificate, db database, repl replicator, useHTTP, compression bool) *apiSrv {
func newAPISrv(addr string, cert tls.Certificate, db database, repl replicator, useHTTP, compression bool, desiredNotFoundRate float64) *apiSrv {
return &apiSrv{
addr: addr,
cert: cert,
@@ -76,13 +77,13 @@ func newAPISrv(addr string, cert tls.Certificate, db database, repl replicator,
seenTracker: &retryAfterTracker{
name: "seenTracker",
bucketStarts: time.Now(),
desiredRate: 250,
desiredRate: desiredNotFoundRate / 2,
currentDelay: notFoundRetryUnknownMinSeconds,
},
notSeenTracker: &retryAfterTracker{
name: "notSeenTracker",
bucketStarts: time.Now(),
desiredRate: 250,
desiredRate: desiredNotFoundRate / 2,
currentDelay: notFoundRetryUnknownMaxSeconds / 2,
},
}
@@ -119,7 +120,9 @@ func (s *apiSrv) Serve(ctx context.Context) error {
ReadTimeout: httpReadTimeout,
WriteTimeout: httpWriteTimeout,
MaxHeaderBytes: httpMaxHeaderBytes,
ErrorLog: log.New(io.Discard, "", 0),
}
if !debug {
srv.ErrorLog = log.New(io.Discard, "", 0)
}
go func() {
@@ -195,7 +198,7 @@ func (s *apiSrv) handleGET(w http.ResponseWriter, req *http.Request) {
deviceID, err := protocol.DeviceIDFromString(req.URL.Query().Get("device"))
if err != nil {
if debug {
log.Println(reqID, "bad device param")
log.Println(reqID, "bad device param:", err)
}
lookupRequestsTotal.WithLabelValues("bad_request").Inc()
w.Header().Set("Retry-After", errorRetryAfterString())
@@ -280,6 +283,9 @@ func (s *apiSrv) handlePOST(remoteAddr *net.TCPAddr, w http.ResponseWriter, req
addresses := fixupAddresses(remoteAddr, ann.Addresses)
if len(addresses) == 0 {
if debug {
log.Println(reqID, "no addresses")
}
announceRequestsTotal.WithLabelValues("bad_request").Inc()
w.Header().Set("Retry-After", errorRetryAfterString())
http.Error(w, "Bad Request", http.StatusBadRequest)
@@ -287,6 +293,9 @@ func (s *apiSrv) handlePOST(remoteAddr *net.TCPAddr, w http.ResponseWriter, req
}
if err := s.handleAnnounce(deviceID, addresses); err != nil {
if debug {
log.Println(reqID, "handle:", err)
}
announceRequestsTotal.WithLabelValues("internal_error").Inc()
w.Header().Set("Retry-After", errorRetryAfterString())
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
@@ -297,6 +306,9 @@ func (s *apiSrv) handlePOST(remoteAddr *net.TCPAddr, w http.ResponseWriter, req
w.Header().Set("Reannounce-After", reannounceAfterString())
w.WriteHeader(http.StatusNoContent)
if debug {
log.Println(reqID, "announced", deviceID, addresses)
}
}
func (s *apiSrv) Stop() {
@@ -312,10 +324,12 @@ func (s *apiSrv) handleAnnounce(deviceID protocol.DeviceID, addresses []string)
slices.Sort(addresses)
addresses = slices.Compact(addresses)
dbAddrs := make([]DatabaseAddress, len(addresses))
dbAddrs := make([]*discosrv.DatabaseAddress, len(addresses))
for i := range addresses {
dbAddrs[i].Address = addresses[i]
dbAddrs[i].Expires = expire
dbAddrs[i] = &discosrv.DatabaseAddress{
Address: addresses[i],
Expires: expire,
}
}
seen := now.UnixNano()
@@ -326,7 +340,7 @@ func (s *apiSrv) handleAnnounce(deviceID protocol.DeviceID, addresses []string)
}
func handlePing(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(204)
w.WriteHeader(http.StatusNoContent)
}
func certificateBytes(req *http.Request) ([]byte, error) {
@@ -511,7 +525,7 @@ func (lrw *loggingResponseWriter) WriteHeader(code int) {
lrw.ResponseWriter.WriteHeader(code)
}
func addressStrs(dbAddrs []DatabaseAddress) []string {
func addressStrs(dbAddrs []*discosrv.DatabaseAddress) []string {
res := make([]string, len(dbAddrs))
for i, a := range dbAddrs {
res[i] = a.Address

View File

@@ -111,7 +111,7 @@ func BenchmarkAPIRequests(b *testing.B) {
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)
api := newAPISrv("127.0.0.1:0", tls.Certificate{}, db, nil, true, true, 1000)
srv := httptest.NewServer(http.HandlerFunc(api.handler))
kf := b.TempDir() + "/cert"

View File

@@ -4,9 +4,6 @@
// 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 ../../proto/scripts/protofmt.go database.proto
//go:generate protoc -I ../../ -I . --gogofast_out=. database.proto
package main
import (
@@ -25,7 +22,13 @@ import (
"time"
"github.com/puzpuzpuz/xsync/v3"
"google.golang.org/protobuf/proto"
"github.com/syncthing/syncthing/internal/blob"
"github.com/syncthing/syncthing/internal/gen/discosrv"
"github.com/syncthing/syncthing/internal/protoutil"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/rand"
)
type clock interface {
@@ -39,37 +42,48 @@ func (defaultClock) Now() time.Time {
}
type database interface {
put(key *protocol.DeviceID, rec DatabaseRecord) error
merge(key *protocol.DeviceID, addrs []DatabaseAddress, seen int64) error
get(key *protocol.DeviceID) (DatabaseRecord, error)
put(key *protocol.DeviceID, rec *discosrv.DatabaseRecord) error
merge(key *protocol.DeviceID, addrs []*discosrv.DatabaseAddress, seen int64) error
get(key *protocol.DeviceID) (*discosrv.DatabaseRecord, error)
}
type inMemoryStore struct {
m *xsync.MapOf[protocol.DeviceID, DatabaseRecord]
m *xsync.MapOf[protocol.DeviceID, *discosrv.DatabaseRecord]
dir string
flushInterval time.Duration
s3 *s3Copier
blobs blob.Store
objKey string
clock clock
}
func newInMemoryStore(dir string, flushInterval time.Duration, s3 *s3Copier) *inMemoryStore {
func newInMemoryStore(dir string, flushInterval time.Duration, blobs blob.Store) *inMemoryStore {
hn, err := os.Hostname()
if err != nil {
hn = rand.String(8)
}
s := &inMemoryStore{
m: xsync.NewMapOf[protocol.DeviceID, DatabaseRecord](),
m: xsync.NewMapOf[protocol.DeviceID, *discosrv.DatabaseRecord](),
dir: dir,
flushInterval: flushInterval,
s3: s3,
blobs: blobs,
objKey: hn + ".db",
clock: defaultClock{},
}
nr, err := s.read()
if os.IsNotExist(err) && s3 != nil {
// Try to read from AWS
fd, cerr := os.Create(path.Join(s.dir, "records.db"))
if os.IsNotExist(err) && blobs != nil {
// Try to read from blob storage
latestKey, cerr := blobs.LatestKey(context.Background())
if cerr != nil {
log.Println("Error creating database file:", err)
log.Println("Error finding database from blob storage:", cerr)
return s
}
if err := s3.downloadLatest(fd); err != nil {
log.Printf("Error reading database from S3: %v", err)
fd, cerr := os.Create(path.Join(s.dir, "records.db"))
if cerr != nil {
log.Println("Error creating database file:", cerr)
return s
}
if cerr := blobs.Download(context.Background(), latestKey, fd); cerr != nil {
log.Printf("Error downloading database from blob storage: %v", cerr)
}
_ = fd.Close()
nr, err = s.read()
@@ -82,7 +96,7 @@ func newInMemoryStore(dir string, flushInterval time.Duration, s3 *s3Copier) *in
return s
}
func (s *inMemoryStore) put(key *protocol.DeviceID, rec DatabaseRecord) error {
func (s *inMemoryStore) put(key *protocol.DeviceID, rec *discosrv.DatabaseRecord) error {
t0 := time.Now()
s.m.Store(*key, rec)
databaseOperations.WithLabelValues(dbOpPut, dbResSuccess).Inc()
@@ -90,16 +104,17 @@ func (s *inMemoryStore) put(key *protocol.DeviceID, rec DatabaseRecord) error {
return nil
}
func (s *inMemoryStore) merge(key *protocol.DeviceID, addrs []DatabaseAddress, seen int64) error {
func (s *inMemoryStore) merge(key *protocol.DeviceID, addrs []*discosrv.DatabaseAddress, seen int64) error {
t0 := time.Now()
newRec := DatabaseRecord{
newRec := &discosrv.DatabaseRecord{
Addresses: addrs,
Seen: seen,
}
oldRec, _ := s.m.Load(*key)
newRec = merge(oldRec, newRec)
if oldRec, ok := s.m.Load(*key); ok {
newRec = merge(oldRec, newRec)
}
s.m.Store(*key, newRec)
databaseOperations.WithLabelValues(dbOpMerge, dbResSuccess).Inc()
@@ -108,7 +123,7 @@ func (s *inMemoryStore) merge(key *protocol.DeviceID, addrs []DatabaseAddress, s
return nil
}
func (s *inMemoryStore) get(key *protocol.DeviceID) (DatabaseRecord, error) {
func (s *inMemoryStore) get(key *protocol.DeviceID) (*discosrv.DatabaseRecord, error) {
t0 := time.Now()
defer func() {
databaseOperationSeconds.WithLabelValues(dbOpGet).Observe(time.Since(t0).Seconds())
@@ -117,7 +132,7 @@ func (s *inMemoryStore) get(key *protocol.DeviceID) (DatabaseRecord, error) {
rec, ok := s.m.Load(*key)
if !ok {
databaseOperations.WithLabelValues(dbOpGet, dbResNotFound).Inc()
return DatabaseRecord{}, nil
return &discosrv.DatabaseRecord{}, nil
}
rec.Addresses = expire(rec.Addresses, s.clock.Now())
@@ -163,7 +178,7 @@ func (s *inMemoryStore) expireAndCalculateStatistics() {
current, currentIPv4, currentIPv6, currentIPv6GUA, last24h, last1w := 0, 0, 0, 0, 0, 0
n := 0
s.m.Range(func(key protocol.DeviceID, rec DatabaseRecord) bool {
s.m.Range(func(key protocol.DeviceID, rec *discosrv.DatabaseRecord) bool {
if n%1000 == 0 {
runtime.Gosched()
}
@@ -248,7 +263,7 @@ func (s *inMemoryStore) write() (err 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 {
s.m.Range(func(key protocol.DeviceID, value *discosrv.DatabaseRecord) bool {
if n%1000 == 0 {
runtime.Gosched()
}
@@ -258,16 +273,16 @@ func (s *inMemoryStore) write() (err error) {
// drop the record if it's older than a week
return true
}
rec := ReplicationRecord{
rec := &discosrv.ReplicationRecord{
Key: key[:],
Addresses: value.Addresses,
Seen: value.Seen,
}
s := rec.Size()
s := proto.Size(rec)
if s+4 > len(buf) {
buf = make([]byte, s+4)
}
n, err := rec.MarshalTo(buf[4:])
n, err := protoutil.MarshalTo(buf[4:], rec)
if err != nil {
rangeErr = err
return false
@@ -295,16 +310,16 @@ func (s *inMemoryStore) write() (err error) {
return err
}
// Upload to S3
if s.s3 != nil {
// Upload to blob storage
if s.blobs != nil {
fd, err = os.Open(dbf)
if err != nil {
log.Printf("Error uploading database to S3: %v", err)
log.Printf("Error uploading database to blob storage: %v", err)
return nil
}
defer fd.Close()
if err := s.s3.upload(fd); err != nil {
log.Printf("Error uploading database to S3: %v", err)
if err := s.blobs.Upload(context.Background(), s.objKey, fd); err != nil {
log.Printf("Error uploading database to blob storage: %v", err)
}
log.Println("Finished uploading database")
}
@@ -336,8 +351,8 @@ func (s *inMemoryStore) read() (int, error) {
if _, err := io.ReadFull(br, buf[:n]); err != nil {
return nr, err
}
rec := ReplicationRecord{}
if err := rec.Unmarshal(buf[:n]); err != nil {
rec := &discosrv.ReplicationRecord{}
if err := proto.Unmarshal(buf[:n], rec); err != nil {
return nr, err
}
key, err := protocol.DeviceIDFromBytes(rec.Key)
@@ -349,9 +364,9 @@ func (s *inMemoryStore) read() (int, error) {
continue
}
slices.SortFunc(rec.Addresses, DatabaseAddress.Cmp)
rec.Addresses = slices.CompactFunc(rec.Addresses, DatabaseAddress.Equal)
s.m.Store(key, DatabaseRecord{
slices.SortFunc(rec.Addresses, Cmp)
rec.Addresses = slices.CompactFunc(rec.Addresses, Equal)
s.m.Store(key, &discosrv.DatabaseRecord{
Addresses: expire(rec.Addresses, s.clock.Now()),
Seen: rec.Seen,
})
@@ -364,7 +379,7 @@ func (s *inMemoryStore) read() (int, error) {
// result is the union of the two address sets, with the newer expiry time
// chosen for any duplicates. The address list in a is overwritten and
// reused for the result.
func merge(a, b DatabaseRecord) DatabaseRecord {
func merge(a, b *discosrv.DatabaseRecord) *discosrv.DatabaseRecord {
// Both lists must be sorted for this to work.
a.Seen = max(a.Seen, b.Seen)
@@ -383,7 +398,7 @@ func merge(a, b DatabaseRecord) DatabaseRecord {
aIdx++
case 1:
// a > b, insert b before a
a.Addresses = append(a.Addresses[:aIdx], append([]DatabaseAddress{b.Addresses[bIdx]}, a.Addresses[aIdx:]...)...)
a.Addresses = append(a.Addresses[:aIdx], append([]*discosrv.DatabaseAddress{b.Addresses[bIdx]}, a.Addresses[aIdx:]...)...)
bIdx++
}
}
@@ -397,7 +412,7 @@ func merge(a, b DatabaseRecord) DatabaseRecord {
// 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 preserved.
func expire(addrs []DatabaseAddress, now time.Time) []DatabaseAddress {
func expire(addrs []*discosrv.DatabaseAddress, now time.Time) []*discosrv.DatabaseAddress {
cutoff := now.UnixNano()
naddrs := addrs[:0]
for i := range addrs {
@@ -415,13 +430,13 @@ func expire(addrs []DatabaseAddress, now time.Time) []DatabaseAddress {
return naddrs
}
func (d DatabaseAddress) Cmp(other DatabaseAddress) (n int) {
func Cmp(d, other *discosrv.DatabaseAddress) (n int) {
if c := cmp.Compare(d.Address, other.Address); c != 0 {
return c
}
return cmp.Compare(d.Expires, other.Expires)
}
func (d DatabaseAddress) Equal(other DatabaseAddress) bool {
func Equal(d, other *discosrv.DatabaseAddress) bool {
return d.Address == other.Address
}

View File

@@ -1,792 +0,0 @@
// Code generated by protoc-gen-gogo. DO NOT EDIT.
// source: database.proto
package main
import (
fmt "fmt"
_ "github.com/gogo/protobuf/gogoproto"
proto "github.com/gogo/protobuf/proto"
io "io"
math "math"
math_bits "math/bits"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package
type DatabaseRecord struct {
Addresses []DatabaseAddress `protobuf:"bytes,1,rep,name=addresses,proto3" json:"addresses"`
Seen int64 `protobuf:"varint,3,opt,name=seen,proto3" json:"seen,omitempty"`
}
func (m *DatabaseRecord) Reset() { *m = DatabaseRecord{} }
func (m *DatabaseRecord) String() string { return proto.CompactTextString(m) }
func (*DatabaseRecord) ProtoMessage() {}
func (*DatabaseRecord) Descriptor() ([]byte, []int) {
return fileDescriptor_b90fe3356ea5df07, []int{0}
}
func (m *DatabaseRecord) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
}
func (m *DatabaseRecord) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
if deterministic {
return xxx_messageInfo_DatabaseRecord.Marshal(b, m, deterministic)
} else {
b = b[:cap(b)]
n, err := m.MarshalToSizedBuffer(b)
if err != nil {
return nil, err
}
return b[:n], nil
}
}
func (m *DatabaseRecord) XXX_Merge(src proto.Message) {
xxx_messageInfo_DatabaseRecord.Merge(m, src)
}
func (m *DatabaseRecord) XXX_Size() int {
return m.Size()
}
func (m *DatabaseRecord) XXX_DiscardUnknown() {
xxx_messageInfo_DatabaseRecord.DiscardUnknown(m)
}
var xxx_messageInfo_DatabaseRecord proto.InternalMessageInfo
type ReplicationRecord struct {
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"`
}
func (m *ReplicationRecord) Reset() { *m = ReplicationRecord{} }
func (m *ReplicationRecord) String() string { return proto.CompactTextString(m) }
func (*ReplicationRecord) ProtoMessage() {}
func (*ReplicationRecord) Descriptor() ([]byte, []int) {
return fileDescriptor_b90fe3356ea5df07, []int{1}
}
func (m *ReplicationRecord) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
}
func (m *ReplicationRecord) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
if deterministic {
return xxx_messageInfo_ReplicationRecord.Marshal(b, m, deterministic)
} else {
b = b[:cap(b)]
n, err := m.MarshalToSizedBuffer(b)
if err != nil {
return nil, err
}
return b[:n], nil
}
}
func (m *ReplicationRecord) XXX_Merge(src proto.Message) {
xxx_messageInfo_ReplicationRecord.Merge(m, src)
}
func (m *ReplicationRecord) XXX_Size() int {
return m.Size()
}
func (m *ReplicationRecord) XXX_DiscardUnknown() {
xxx_messageInfo_ReplicationRecord.DiscardUnknown(m)
}
var xxx_messageInfo_ReplicationRecord proto.InternalMessageInfo
type DatabaseAddress struct {
Address string `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"`
Expires int64 `protobuf:"varint,2,opt,name=expires,proto3" json:"expires,omitempty"`
}
func (m *DatabaseAddress) Reset() { *m = DatabaseAddress{} }
func (m *DatabaseAddress) String() string { return proto.CompactTextString(m) }
func (*DatabaseAddress) ProtoMessage() {}
func (*DatabaseAddress) Descriptor() ([]byte, []int) {
return fileDescriptor_b90fe3356ea5df07, []int{2}
}
func (m *DatabaseAddress) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
}
func (m *DatabaseAddress) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
if deterministic {
return xxx_messageInfo_DatabaseAddress.Marshal(b, m, deterministic)
} else {
b = b[:cap(b)]
n, err := m.MarshalToSizedBuffer(b)
if err != nil {
return nil, err
}
return b[:n], nil
}
}
func (m *DatabaseAddress) XXX_Merge(src proto.Message) {
xxx_messageInfo_DatabaseAddress.Merge(m, src)
}
func (m *DatabaseAddress) XXX_Size() int {
return m.Size()
}
func (m *DatabaseAddress) XXX_DiscardUnknown() {
xxx_messageInfo_DatabaseAddress.DiscardUnknown(m)
}
var xxx_messageInfo_DatabaseAddress proto.InternalMessageInfo
func init() {
proto.RegisterType((*DatabaseRecord)(nil), "main.DatabaseRecord")
proto.RegisterType((*ReplicationRecord)(nil), "main.ReplicationRecord")
proto.RegisterType((*DatabaseAddress)(nil), "main.DatabaseAddress")
}
func init() { proto.RegisterFile("database.proto", fileDescriptor_b90fe3356ea5df07) }
var fileDescriptor_b90fe3356ea5df07 = []byte{
// 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) {
size := m.Size()
dAtA = make([]byte, size)
n, err := m.MarshalToSizedBuffer(dAtA[:size])
if err != nil {
return nil, err
}
return dAtA[:n], nil
}
func (m *DatabaseRecord) MarshalTo(dAtA []byte) (int, error) {
size := m.Size()
return m.MarshalToSizedBuffer(dAtA[:size])
}
func (m *DatabaseRecord) MarshalToSizedBuffer(dAtA []byte) (int, error) {
i := len(dAtA)
_ = i
var l int
_ = l
if m.Seen != 0 {
i = encodeVarintDatabase(dAtA, i, uint64(m.Seen))
i--
dAtA[i] = 0x18
}
if len(m.Addresses) > 0 {
for iNdEx := len(m.Addresses) - 1; iNdEx >= 0; iNdEx-- {
{
size, err := m.Addresses[iNdEx].MarshalToSizedBuffer(dAtA[:i])
if err != nil {
return 0, err
}
i -= size
i = encodeVarintDatabase(dAtA, i, uint64(size))
}
i--
dAtA[i] = 0xa
}
}
return len(dAtA) - i, nil
}
func (m *ReplicationRecord) Marshal() (dAtA []byte, err error) {
size := m.Size()
dAtA = make([]byte, size)
n, err := m.MarshalToSizedBuffer(dAtA[:size])
if err != nil {
return nil, err
}
return dAtA[:n], nil
}
func (m *ReplicationRecord) MarshalTo(dAtA []byte) (int, error) {
size := m.Size()
return m.MarshalToSizedBuffer(dAtA[:size])
}
func (m *ReplicationRecord) MarshalToSizedBuffer(dAtA []byte) (int, error) {
i := len(dAtA)
_ = i
var l int
_ = l
if m.Seen != 0 {
i = encodeVarintDatabase(dAtA, i, uint64(m.Seen))
i--
dAtA[i] = 0x18
}
if len(m.Addresses) > 0 {
for iNdEx := len(m.Addresses) - 1; iNdEx >= 0; iNdEx-- {
{
size, err := m.Addresses[iNdEx].MarshalToSizedBuffer(dAtA[:i])
if err != nil {
return 0, err
}
i -= size
i = encodeVarintDatabase(dAtA, i, uint64(size))
}
i--
dAtA[i] = 0x12
}
}
if len(m.Key) > 0 {
i -= len(m.Key)
copy(dAtA[i:], m.Key)
i = encodeVarintDatabase(dAtA, i, uint64(len(m.Key)))
i--
dAtA[i] = 0xa
}
return len(dAtA) - i, nil
}
func (m *DatabaseAddress) Marshal() (dAtA []byte, err error) {
size := m.Size()
dAtA = make([]byte, size)
n, err := m.MarshalToSizedBuffer(dAtA[:size])
if err != nil {
return nil, err
}
return dAtA[:n], nil
}
func (m *DatabaseAddress) MarshalTo(dAtA []byte) (int, error) {
size := m.Size()
return m.MarshalToSizedBuffer(dAtA[:size])
}
func (m *DatabaseAddress) MarshalToSizedBuffer(dAtA []byte) (int, error) {
i := len(dAtA)
_ = i
var l int
_ = l
if m.Expires != 0 {
i = encodeVarintDatabase(dAtA, i, uint64(m.Expires))
i--
dAtA[i] = 0x10
}
if len(m.Address) > 0 {
i -= len(m.Address)
copy(dAtA[i:], m.Address)
i = encodeVarintDatabase(dAtA, i, uint64(len(m.Address)))
i--
dAtA[i] = 0xa
}
return len(dAtA) - i, nil
}
func encodeVarintDatabase(dAtA []byte, offset int, v uint64) int {
offset -= sovDatabase(v)
base := offset
for v >= 1<<7 {
dAtA[offset] = uint8(v&0x7f | 0x80)
v >>= 7
offset++
}
dAtA[offset] = uint8(v)
return base
}
func (m *DatabaseRecord) Size() (n int) {
if m == nil {
return 0
}
var l int
_ = l
if len(m.Addresses) > 0 {
for _, e := range m.Addresses {
l = e.Size()
n += 1 + l + sovDatabase(uint64(l))
}
}
if m.Seen != 0 {
n += 1 + sovDatabase(uint64(m.Seen))
}
return n
}
func (m *ReplicationRecord) Size() (n int) {
if m == nil {
return 0
}
var l int
_ = l
l = len(m.Key)
if l > 0 {
n += 1 + l + sovDatabase(uint64(l))
}
if len(m.Addresses) > 0 {
for _, e := range m.Addresses {
l = e.Size()
n += 1 + l + sovDatabase(uint64(l))
}
}
if m.Seen != 0 {
n += 1 + sovDatabase(uint64(m.Seen))
}
return n
}
func (m *DatabaseAddress) Size() (n int) {
if m == nil {
return 0
}
var l int
_ = l
l = len(m.Address)
if l > 0 {
n += 1 + l + sovDatabase(uint64(l))
}
if m.Expires != 0 {
n += 1 + sovDatabase(uint64(m.Expires))
}
return n
}
func sovDatabase(x uint64) (n int) {
return (math_bits.Len64(x|1) + 6) / 7
}
func sozDatabase(x uint64) (n int) {
return sovDatabase(uint64((x << 1) ^ uint64((int64(x) >> 63))))
}
func (m *DatabaseRecord) Unmarshal(dAtA []byte) error {
l := len(dAtA)
iNdEx := 0
for iNdEx < l {
preIndex := iNdEx
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowDatabase
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
fieldNum := int32(wire >> 3)
wireType := int(wire & 0x7)
if wireType == 4 {
return fmt.Errorf("proto: DatabaseRecord: wiretype end group for non-group")
}
if fieldNum <= 0 {
return fmt.Errorf("proto: DatabaseRecord: illegal tag %d (wire type %d)", fieldNum, wire)
}
switch fieldNum {
case 1:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Addresses", wireType)
}
var msglen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowDatabase
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
msglen |= int(b&0x7F) << shift
if b < 0x80 {
break
}
}
if msglen < 0 {
return ErrInvalidLengthDatabase
}
postIndex := iNdEx + msglen
if postIndex < 0 {
return ErrInvalidLengthDatabase
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.Addresses = append(m.Addresses, DatabaseAddress{})
if err := m.Addresses[len(m.Addresses)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
return err
}
iNdEx = postIndex
case 3:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field Seen", wireType)
}
m.Seen = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowDatabase
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
m.Seen |= int64(b&0x7F) << shift
if b < 0x80 {
break
}
}
default:
iNdEx = preIndex
skippy, err := skipDatabase(dAtA[iNdEx:])
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
return ErrInvalidLengthDatabase
}
if (iNdEx + skippy) > l {
return io.ErrUnexpectedEOF
}
iNdEx += skippy
}
}
if iNdEx > l {
return io.ErrUnexpectedEOF
}
return nil
}
func (m *ReplicationRecord) Unmarshal(dAtA []byte) error {
l := len(dAtA)
iNdEx := 0
for iNdEx < l {
preIndex := iNdEx
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowDatabase
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
fieldNum := int32(wire >> 3)
wireType := int(wire & 0x7)
if wireType == 4 {
return fmt.Errorf("proto: ReplicationRecord: wiretype end group for non-group")
}
if fieldNum <= 0 {
return fmt.Errorf("proto: ReplicationRecord: illegal tag %d (wire type %d)", fieldNum, wire)
}
switch fieldNum {
case 1:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Key", wireType)
}
var byteLen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowDatabase
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
byteLen |= int(b&0x7F) << shift
if b < 0x80 {
break
}
}
if byteLen < 0 {
return ErrInvalidLengthDatabase
}
postIndex := iNdEx + byteLen
if postIndex < 0 {
return ErrInvalidLengthDatabase
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.Key = append(m.Key[:0], dAtA[iNdEx:postIndex]...)
if m.Key == nil {
m.Key = []byte{}
}
iNdEx = postIndex
case 2:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Addresses", wireType)
}
var msglen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowDatabase
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
msglen |= int(b&0x7F) << shift
if b < 0x80 {
break
}
}
if msglen < 0 {
return ErrInvalidLengthDatabase
}
postIndex := iNdEx + msglen
if postIndex < 0 {
return ErrInvalidLengthDatabase
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.Addresses = append(m.Addresses, DatabaseAddress{})
if err := m.Addresses[len(m.Addresses)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
return err
}
iNdEx = postIndex
case 3:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field Seen", wireType)
}
m.Seen = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowDatabase
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
m.Seen |= int64(b&0x7F) << shift
if b < 0x80 {
break
}
}
default:
iNdEx = preIndex
skippy, err := skipDatabase(dAtA[iNdEx:])
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
return ErrInvalidLengthDatabase
}
if (iNdEx + skippy) > l {
return io.ErrUnexpectedEOF
}
iNdEx += skippy
}
}
if iNdEx > l {
return io.ErrUnexpectedEOF
}
return nil
}
func (m *DatabaseAddress) Unmarshal(dAtA []byte) error {
l := len(dAtA)
iNdEx := 0
for iNdEx < l {
preIndex := iNdEx
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowDatabase
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
fieldNum := int32(wire >> 3)
wireType := int(wire & 0x7)
if wireType == 4 {
return fmt.Errorf("proto: DatabaseAddress: wiretype end group for non-group")
}
if fieldNum <= 0 {
return fmt.Errorf("proto: DatabaseAddress: illegal tag %d (wire type %d)", fieldNum, wire)
}
switch fieldNum {
case 1:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Address", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowDatabase
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
return ErrInvalidLengthDatabase
}
postIndex := iNdEx + intStringLen
if postIndex < 0 {
return ErrInvalidLengthDatabase
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.Address = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
case 2:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field Expires", wireType)
}
m.Expires = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowDatabase
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
m.Expires |= int64(b&0x7F) << shift
if b < 0x80 {
break
}
}
default:
iNdEx = preIndex
skippy, err := skipDatabase(dAtA[iNdEx:])
if err != nil {
return err
}
if (skippy < 0) || (iNdEx+skippy) < 0 {
return ErrInvalidLengthDatabase
}
if (iNdEx + skippy) > l {
return io.ErrUnexpectedEOF
}
iNdEx += skippy
}
}
if iNdEx > l {
return io.ErrUnexpectedEOF
}
return nil
}
func skipDatabase(dAtA []byte) (n int, err error) {
l := len(dAtA)
iNdEx := 0
depth := 0
for iNdEx < l {
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowDatabase
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
wireType := int(wire & 0x7)
switch wireType {
case 0:
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowDatabase
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
iNdEx++
if dAtA[iNdEx-1] < 0x80 {
break
}
}
case 1:
iNdEx += 8
case 2:
var length int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowDatabase
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
length |= (int(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
if length < 0 {
return 0, ErrInvalidLengthDatabase
}
iNdEx += length
case 3:
depth++
case 4:
if depth == 0 {
return 0, ErrUnexpectedEndOfGroupDatabase
}
depth--
case 5:
iNdEx += 4
default:
return 0, fmt.Errorf("proto: illegal wireType %d", wireType)
}
if iNdEx < 0 {
return 0, ErrInvalidLengthDatabase
}
if depth == 0 {
return iNdEx, nil
}
}
return 0, io.ErrUnexpectedEOF
}
var (
ErrInvalidLengthDatabase = fmt.Errorf("proto: negative length found during unmarshaling")
ErrIntOverflowDatabase = fmt.Errorf("proto: integer overflow")
ErrUnexpectedEndOfGroupDatabase = fmt.Errorf("proto: unexpected end of group")
)

View File

@@ -1,32 +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/.
syntax = "proto3";
package main;
import "repos/protobuf/gogoproto/gogo.proto";
option (gogoproto.goproto_getters_all) = false;
option (gogoproto.goproto_unkeyed_all) = false;
option (gogoproto.goproto_unrecognized_all) = false;
option (gogoproto.goproto_sizecache_all) = false;
message DatabaseRecord {
repeated DatabaseAddress addresses = 1 [(gogoproto.nullable) = false];
int64 seen = 3; // Unix nanos, last device announce
}
message ReplicationRecord {
bytes key = 1; // raw 32 byte device ID
repeated DatabaseAddress addresses = 2 [(gogoproto.nullable) = false];
int64 seen = 3; // Unix nanos, last device announce
}
message DatabaseAddress {
string address = 1;
int64 expires = 2; // Unix nanos
}

View File

@@ -12,6 +12,7 @@ import (
"testing"
"time"
"github.com/syncthing/syncthing/internal/gen/discosrv"
"github.com/syncthing/syncthing/lib/protocol"
)
@@ -39,7 +40,7 @@ func TestDatabaseGetSet(t *testing.T) {
// Put a record
rec.Addresses = []DatabaseAddress{
rec.Addresses = []*discosrv.DatabaseAddress{
{Address: "tcp://1.2.3.4:5", Expires: tc.Now().Add(time.Minute).UnixNano()},
}
if err := db.put(&protocol.EmptyDeviceID, rec); err != nil {
@@ -65,7 +66,7 @@ func TestDatabaseGetSet(t *testing.T) {
tc.wind(30 * time.Second)
addrs := []DatabaseAddress{
addrs := []*discosrv.DatabaseAddress{
{Address: "tcp://6.7.8.9:0", Expires: tc.Now().Add(time.Minute).UnixNano()},
}
if err := db.merge(&protocol.EmptyDeviceID, addrs, tc.Now().UnixNano()); err != nil {
@@ -112,7 +113,7 @@ func TestDatabaseGetSet(t *testing.T) {
// Set an address
addrs = []DatabaseAddress{
addrs = []*discosrv.DatabaseAddress{
{Address: "tcp://6.7.8.9:0", Expires: tc.Now().Add(time.Minute).UnixNano()},
}
if err := db.merge(&protocol.GlobalDeviceID, addrs, tc.Now().UnixNano()); err != nil {
@@ -134,28 +135,28 @@ func TestDatabaseGetSet(t *testing.T) {
func TestFilter(t *testing.T) {
// all cases are expired with t=10
cases := []struct {
a []DatabaseAddress
b []DatabaseAddress
a []*discosrv.DatabaseAddress
b []*discosrv.DatabaseAddress
}{
{
a: nil,
b: nil,
},
{
a: []DatabaseAddress{{Address: "a", Expires: 9}, {Address: "b", Expires: 9}, {Address: "c", Expires: 9}},
b: []DatabaseAddress{},
a: []*discosrv.DatabaseAddress{{Address: "a", Expires: 9}, {Address: "b", Expires: 9}, {Address: "c", Expires: 9}},
b: []*discosrv.DatabaseAddress{},
},
{
a: []DatabaseAddress{{Address: "a", Expires: 10}},
b: []DatabaseAddress{{Address: "a", Expires: 10}},
a: []*discosrv.DatabaseAddress{{Address: "a", Expires: 10}},
b: []*discosrv.DatabaseAddress{{Address: "a", Expires: 10}},
},
{
a: []DatabaseAddress{{Address: "a", Expires: 10}, {Address: "b", Expires: 10}, {Address: "c", Expires: 10}},
b: []DatabaseAddress{{Address: "a", Expires: 10}, {Address: "b", Expires: 10}, {Address: "c", Expires: 10}},
a: []*discosrv.DatabaseAddress{{Address: "a", Expires: 10}, {Address: "b", Expires: 10}, {Address: "c", Expires: 10}},
b: []*discosrv.DatabaseAddress{{Address: "a", Expires: 10}, {Address: "b", Expires: 10}, {Address: "c", Expires: 10}},
},
{
a: []DatabaseAddress{{Address: "a", Expires: 5}, {Address: "b", Expires: 15}, {Address: "c", Expires: 5}, {Address: "d", Expires: 15}, {Address: "e", Expires: 5}},
b: []DatabaseAddress{{Address: "b", Expires: 15}, {Address: "d", Expires: 15}},
a: []*discosrv.DatabaseAddress{{Address: "a", Expires: 5}, {Address: "b", Expires: 15}, {Address: "c", Expires: 5}, {Address: "d", Expires: 15}, {Address: "e", Expires: 5}},
b: []*discosrv.DatabaseAddress{{Address: "b", Expires: 15}, {Address: "d", Expires: 15}},
},
}
@@ -169,62 +170,62 @@ func TestFilter(t *testing.T) {
func TestMerge(t *testing.T) {
cases := []struct {
a, b, res []DatabaseAddress
a, b, res []*discosrv.DatabaseAddress
}{
{nil, nil, nil},
{
nil,
[]DatabaseAddress{{Address: "a", Expires: 10}},
[]DatabaseAddress{{Address: "a", Expires: 10}},
[]*discosrv.DatabaseAddress{{Address: "a", Expires: 10}},
[]*discosrv.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}},
[]*discosrv.DatabaseAddress{{Address: "a", Expires: 10}, {Address: "b", Expires: 10}, {Address: "c", Expires: 10}},
[]*discosrv.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}},
[]*discosrv.DatabaseAddress{{Address: "a", Expires: 10}},
[]*discosrv.DatabaseAddress{{Address: "a", Expires: 15}},
[]*discosrv.DatabaseAddress{{Address: "a", Expires: 15}},
},
{
[]DatabaseAddress{{Address: "a", Expires: 10}},
[]DatabaseAddress{{Address: "b", Expires: 15}},
[]DatabaseAddress{{Address: "a", Expires: 10}, {Address: "b", Expires: 15}},
[]*discosrv.DatabaseAddress{{Address: "a", Expires: 10}},
[]*discosrv.DatabaseAddress{{Address: "b", Expires: 15}},
[]*discosrv.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}},
[]*discosrv.DatabaseAddress{{Address: "a", Expires: 10}, {Address: "b", Expires: 15}},
[]*discosrv.DatabaseAddress{{Address: "a", Expires: 15}, {Address: "b", Expires: 15}},
[]*discosrv.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}},
[]*discosrv.DatabaseAddress{{Address: "a", Expires: 10}, {Address: "b", Expires: 15}},
[]*discosrv.DatabaseAddress{{Address: "b", Expires: 15}, {Address: "c", Expires: 20}},
[]*discosrv.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}},
[]*discosrv.DatabaseAddress{{Address: "a", Expires: 10}, {Address: "b", Expires: 15}},
[]*discosrv.DatabaseAddress{{Address: "b", Expires: 5}, {Address: "c", Expires: 20}},
[]*discosrv.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}},
[]*discosrv.DatabaseAddress{{Address: "y", Expires: 10}, {Address: "z", Expires: 10}},
[]*discosrv.DatabaseAddress{{Address: "a", Expires: 5}, {Address: "b", Expires: 15}},
[]*discosrv.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}},
[]*discosrv.DatabaseAddress{{Address: "a", Expires: 10}, {Address: "b", Expires: 15}, {Address: "d", Expires: 10}},
[]*discosrv.DatabaseAddress{{Address: "b", Expires: 5}, {Address: "c", Expires: 20}},
[]*discosrv.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})
rec := merge(&discosrv.DatabaseRecord{Addresses: tc.a}, &discosrv.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})
rec = merge(&discosrv.DatabaseRecord{Addresses: tc.b}, &discosrv.DatabaseRecord{Addresses: tc.a})
if fmt.Sprint(rec.Addresses) != fmt.Sprint(tc.res) {
t.Errorf("Incorrect result %v, expected %v", rec.Addresses, tc.res)
}
@@ -233,9 +234,9 @@ func TestMerge(t *testing.T) {
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})
ar := []*discosrv.DatabaseAddress{{Address: "a", Expires: 10}, {Address: "b", Expires: 15}}
br := []*discosrv.DatabaseAddress{{Address: "a", Expires: 15}, {Address: "b", Expires: 10}}
res := merge(&discosrv.DatabaseRecord{Addresses: ar}, &discosrv.DatabaseRecord{Addresses: br})
if len(res.Addresses) != 2 {
b.Fatal("wrong length")
}

View File

@@ -11,21 +11,24 @@ import (
"crypto/tls"
"log"
"net/http"
_ "net/http/pprof"
"os"
"os/signal"
"runtime"
"time"
_ "net/http/pprof"
"github.com/alecthomas/kong"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/thejerf/suture/v4"
"github.com/syncthing/syncthing/internal/blob"
"github.com/syncthing/syncthing/internal/blob/azureblob"
"github.com/syncthing/syncthing/internal/blob/s3"
_ "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/tlsutil"
"github.com/thejerf/suture/v4"
)
const (
@@ -57,12 +60,13 @@ const (
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"`
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"`
DesiredNotFoundRate float64 `group:"Listen" help:"Desired maximum rate of not-found replies (/s)" default:"1000"`
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"`
@@ -73,6 +77,10 @@ type CLI struct {
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"`
DBAzureBlobAccount string `name:"db-azure-blob-account" env:"DISCOVERY_DB_AZUREBLOB_ACCOUNT"`
DBAzureBlobKey string `name:"db-azure-blob-key" env:"DISCOVERY_DB_AZUREBLOB_KEY"`
DBAzureBlobContainer string `name:"db-azure-blob-container" env:"DISCOVERY_DB_AZUREBLOB_CONTAINER"`
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"`
@@ -116,19 +124,20 @@ func main() {
Timeout: 2 * time.Minute,
})
// If configured, use S3 for database backups.
var s3c *s3Copier
// If configured, use blob storage for database backups.
var blobs blob.Store
var err error
if cli.DBS3Endpoint != "" {
hostname, err := os.Hostname()
if err != nil {
log.Fatalf("Failed to get hostname: %v", err)
}
key := hostname + ".db"
s3c = newS3Copier(cli.DBS3Endpoint, cli.DBS3Region, cli.DBS3Bucket, key, cli.DBS3AccessKeyID, cli.DBS3SecretKey)
blobs, err = s3.NewSession(cli.DBS3Endpoint, cli.DBS3Region, cli.DBS3Bucket, cli.DBS3AccessKeyID, cli.DBS3SecretKey)
} else if cli.DBAzureBlobAccount != "" {
blobs, err = azureblob.NewBlobStore(cli.DBAzureBlobAccount, cli.DBAzureBlobKey, cli.DBAzureBlobContainer)
}
if err != nil {
log.Fatalf("Failed to create blob store: %v", err)
}
// Start the database.
db := newInMemoryStore(cli.DBDir, cli.DBFlushInterval, s3c)
db := newInMemoryStore(cli.DBDir, cli.DBFlushInterval, blobs)
main.Add(db)
// If we have an AMQP broker for replication, start that
@@ -141,7 +150,7 @@ func main() {
}
// Start the main API server.
qs := newAPISrv(cli.Listen, cert, db, repl, cli.HTTP, cli.Compression)
qs := newAPISrv(cli.Listen, cert, db, repl, cli.HTTP, cli.Compression, cli.DesiredNotFoundRate)
main.Add(qs)
// If we have a metrics port configured, start a metrics handler.

View File

@@ -1,97 +0,0 @@
// 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 (
"io"
"log"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/aws/aws-sdk-go/service/s3/s3manager"
)
type s3Copier struct {
endpoint string
region string
bucket string
key string
accessKeyID string
secretKey string
}
func newS3Copier(endpoint, region, bucket, key, accessKeyID, secretKey string) *s3Copier {
return &s3Copier{
endpoint: endpoint,
region: region,
bucket: bucket,
key: key,
accessKeyID: accessKeyID,
secretKey: secretKey,
}
}
func (s *s3Copier) upload(r io.Reader) error {
sess, err := session.NewSession(&aws.Config{
Region: aws.String(s.region),
Endpoint: aws.String(s.endpoint),
Credentials: credentials.NewStaticCredentials(s.accessKeyID, s.secretKey, ""),
})
if err != nil {
return err
}
uploader := s3manager.NewUploader(sess)
_, err = uploader.Upload(&s3manager.UploadInput{
Bucket: aws.String(s.bucket),
Key: aws.String(s.key),
Body: r,
})
return err
}
func (s *s3Copier) downloadLatest(w io.WriterAt) error {
sess, err := session.NewSession(&aws.Config{
Region: aws.String(s.region),
Endpoint: aws.String(s.endpoint),
Credentials: credentials.NewStaticCredentials(s.accessKeyID, s.secretKey, ""),
})
if err != nil {
return err
}
svc := s3.New(sess)
resp, err := svc.ListObjectsV2(&s3.ListObjectsV2Input{Bucket: aws.String(s.bucket)})
if err != nil {
return err
}
var lastKey string
var lastModified time.Time
var lastSize int64
for _, item := range resp.Contents {
if item.LastModified.After(lastModified) && *item.Size > lastSize {
lastKey = *item.Key
lastModified = *item.LastModified
lastSize = *item.Size
} else if lastModified.Sub(*item.LastModified) < 5*time.Minute && *item.Size > lastSize {
lastKey = *item.Key
lastSize = *item.Size
}
}
log.Println("Downloading database from", lastKey)
downloader := s3manager.NewDownloader(sess)
_, err = downloader.Download(w, &s3.GetObjectInput{
Bucket: aws.String(s.bucket),
Key: aws.String(lastKey),
})
return err
}

View File

@@ -12,9 +12,8 @@ import (
"time"
syncthingprotocol "github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/tlsutil"
"github.com/syncthing/syncthing/lib/relay/protocol"
"github.com/syncthing/syncthing/lib/tlsutil"
)
var (
@@ -185,7 +184,7 @@ func protocolConnectionHandler(tcpConn net.Conn, config *tls.Config, token strin
continue
}
// requestedPeer is the server, id is the client
ses := newSession(requestedPeer, id, sessionLimiter, globalLimiter)
ses := newSession(requestedPeer, id, sessionLimitBps, globalLimiter)
go ses.Serve()

View File

@@ -19,6 +19,8 @@ import (
"syscall"
"time"
"golang.org/x/time/rate"
_ "github.com/syncthing/syncthing/lib/automaxprocs"
"github.com/syncthing/syncthing/lib/build"
"github.com/syncthing/syncthing/lib/config"
@@ -30,7 +32,6 @@ import (
"github.com/syncthing/syncthing/lib/relay/protocol"
"github.com/syncthing/syncthing/lib/tlsutil"
_ "github.com/syncthing/syncthing/lib/upnp"
"golang.org/x/time/rate"
)
var (
@@ -50,7 +51,6 @@ var (
globalLimitBps int
overLimit atomic.Bool
descriptorLimit int64
sessionLimiter *rate.Limiter
globalLimiter *rate.Limiter
networkBufferSize int
@@ -227,9 +227,6 @@ func main() {
}
}
if sessionLimitBps > 0 {
sessionLimiter = rate.NewLimiter(rate.Limit(sessionLimitBps), 2*sessionLimitBps)
}
if globalLimitBps > 0 {
globalLimiter = rate.NewLimiter(rate.Limit(globalLimitBps), 2*globalLimitBps)
}

View File

@@ -27,7 +27,7 @@ var (
bytesProxied atomic.Int64
)
func newSession(serverid, clientid syncthingprotocol.DeviceID, sessionRateLimit, globalRateLimit *rate.Limiter) *session {
func newSession(serverid, clientid syncthingprotocol.DeviceID, sessionLimitBps int, globalRateLimit *rate.Limiter) *session {
serverkey := make([]byte, 32)
_, err := rand.Read(serverkey)
if err != nil {
@@ -40,12 +40,17 @@ func newSession(serverid, clientid syncthingprotocol.DeviceID, sessionRateLimit,
return nil
}
var sessionRateLimit *rate.Limiter
if sessionLimitBps > 0 {
sessionRateLimit = rate.NewLimiter(rate.Limit(sessionLimitBps), 2*sessionLimitBps)
}
ses := &session{
serverkey: serverkey,
serverid: serverid,
clientkey: clientkey,
clientid: clientid,
rateLimit: makeRateLimitFunc(sessionRateLimit, globalRateLimit),
limiter: sessionRateLimit,
connsChan: make(chan net.Conn),
conns: make([]net.Conn, 0, 2),
}
@@ -68,7 +73,6 @@ func findSession(key string) *session {
ses, ok := pendingSessions[key]
if !ok {
return nil
}
delete(pendingSessions, key)
return ses
@@ -110,6 +114,7 @@ type session struct {
clientid syncthingprotocol.DeviceID
rateLimit func(bytes int)
limiter *rate.Limiter
connsChan chan net.Conn
conns []net.Conn

View File

@@ -11,6 +11,10 @@ import (
"fmt"
"time"
"google.golang.org/protobuf/proto"
"github.com/syncthing/syncthing/internal/gen/bep"
"github.com/syncthing/syncthing/internal/gen/dbproto"
"github.com/syncthing/syncthing/lib/db"
"github.com/syncthing/syncthing/lib/protocol"
)
@@ -33,19 +37,19 @@ func indexDump() error {
name := nulString(key[1+4+4:])
fmt.Printf("[device] F:%d D:%d N:%q", folder, device, name)
var f protocol.FileInfo
err := f.Unmarshal(it.Value())
var f bep.FileInfo
err := proto.Unmarshal(it.Value(), &f)
if err != nil {
return err
}
fmt.Printf(" V:%v\n", f)
fmt.Printf(" V:%v\n", &f)
case db.KeyTypeGlobal:
folder := binary.BigEndian.Uint32(key[1:])
name := nulString(key[1+4:])
var flv db.VersionList
flv.Unmarshal(it.Value())
fmt.Printf("[global] F:%d N:%q V:%s\n", folder, name, flv)
var flv dbproto.VersionList
proto.Unmarshal(it.Value(), &flv)
fmt.Printf("[global] F:%d N:%q V:%s\n", folder, name, &flv)
case db.KeyTypeBlock:
folder := binary.BigEndian.Uint32(key[1:])
@@ -94,11 +98,11 @@ func indexDump() error {
case db.KeyTypeFolderMeta:
folder := binary.BigEndian.Uint32(key[1:])
fmt.Printf("[foldermeta] F:%d", folder)
var cs db.CountsSet
if err := cs.Unmarshal(it.Value()); err != nil {
var cs dbproto.CountsSet
if err := proto.Unmarshal(it.Value(), &cs); err != nil {
fmt.Printf(" (invalid)\n")
} else {
fmt.Printf(" V:%v\n", cs)
fmt.Printf(" V:%v\n", &cs)
}
case db.KeyTypeMiscData:
@@ -125,20 +129,20 @@ func indexDump() error {
case db.KeyTypeVersion:
fmt.Printf("[version] H:%x", key[1:])
var v protocol.Vector
err := v.Unmarshal(it.Value())
var v bep.Vector
err := proto.Unmarshal(it.Value(), &v)
if err != nil {
fmt.Printf(" (invalid)\n")
} else {
fmt.Printf(" V:%v\n", v)
fmt.Printf(" V:%v\n", &v)
}
case db.KeyTypePendingFolder:
device := binary.BigEndian.Uint32(key[1:])
folder := string(key[5:])
var of db.ObservedFolder
of.Unmarshal(it.Value())
fmt.Printf("[pendingFolder] D:%d F:%s V:%v\n", device, folder, of)
var of dbproto.ObservedFolder
proto.Unmarshal(it.Value(), &of)
fmt.Printf("[pendingFolder] D:%d F:%s V:%v\n", device, folder, &of)
case db.KeyTypePendingDevice:
device := "<invalid>"
@@ -146,9 +150,9 @@ func indexDump() error {
if err == nil {
device = dev.String()
}
var od db.ObservedDevice
od.Unmarshal(it.Value())
fmt.Printf("[pendingDevice] D:%v V:%v\n", device, od)
var od dbproto.ObservedDevice
proto.Unmarshal(it.Value(), &od)
fmt.Printf("[pendingDevice] D:%v V:%v\n", device, &od)
default:
fmt.Printf("[??? %d]\n %x\n %x\n", key[0], key, it.Value())

View File

@@ -7,9 +7,10 @@
package cli
import (
"cmp"
"encoding/binary"
"fmt"
"sort"
"slices"
"github.com/syncthing/syncthing/lib/db"
)
@@ -77,8 +78,8 @@ func indexDumpSize() error {
elems = append(elems, ele)
}
sort.Slice(elems, func(i, j int) bool {
return elems[i].size > elems[j].size
slices.SortFunc(elems, func(a, b sizedElement) int {
return cmp.Compare(b.size, a.size)
})
for _, ele := range elems {
fmt.Println(ele.key, ele.size)

View File

@@ -8,11 +8,16 @@ package cli
import (
"bytes"
"cmp"
"encoding/binary"
"errors"
"fmt"
"sort"
"slices"
"google.golang.org/protobuf/proto"
"github.com/syncthing/syncthing/internal/gen/bep"
"github.com/syncthing/syncthing/internal/gen/dbproto"
"github.com/syncthing/syncthing/lib/db"
"github.com/syncthing/syncthing/lib/protocol"
)
@@ -42,12 +47,12 @@ func indexCheck() (err error) {
folders := make(map[uint32]string)
devices := make(map[uint32]string)
deviceToIDs := make(map[string]uint32)
fileInfos := make(map[fileInfoKey]protocol.FileInfo)
globals := make(map[globalKey]db.VersionList)
fileInfos := make(map[fileInfoKey]*bep.FileInfo)
globals := make(map[globalKey]*dbproto.VersionList)
sequences := make(map[sequenceKey]string)
needs := make(map[globalKey]struct{})
blocklists := make(map[string]struct{})
versions := make(map[string]protocol.Vector)
versions := make(map[string]*bep.Vector)
usedBlocklists := make(map[string]struct{})
usedVersions := make(map[string]struct{})
var localDeviceKey uint32
@@ -74,26 +79,26 @@ func indexCheck() (err error) {
device := binary.BigEndian.Uint32(key[1+4:])
name := nulString(key[1+4+4:])
var f protocol.FileInfo
err := f.Unmarshal(it.Value())
var f bep.FileInfo
err := proto.Unmarshal(it.Value(), &f)
if err != nil {
fmt.Println("Unable to unmarshal FileInfo:", err)
success = false
continue
}
fileInfos[fileInfoKey{folder, device, name}] = f
fileInfos[fileInfoKey{folder, device, name}] = &f
case db.KeyTypeGlobal:
folder := binary.BigEndian.Uint32(key[1:])
name := nulString(key[1+4:])
var flv db.VersionList
if err := flv.Unmarshal(it.Value()); err != nil {
var flv dbproto.VersionList
if err := proto.Unmarshal(it.Value(), &flv); err != nil {
fmt.Println("Unable to unmarshal VersionList:", err)
success = false
continue
}
globals[globalKey{folder, name}] = flv
globals[globalKey{folder, name}] = &flv
case db.KeyTypeFolderIdx:
key := binary.BigEndian.Uint32(it.Key()[1:])
@@ -124,13 +129,13 @@ func indexCheck() (err error) {
case db.KeyTypeVersion:
hash := string(key[1:])
var v protocol.Vector
if err := v.Unmarshal(it.Value()); err != nil {
var v bep.Vector
if err := proto.Unmarshal(it.Value(), &v); err != nil {
fmt.Println("Unable to unmarshal Vector:", err)
success = false
continue
}
versions[hash] = v
versions[hash] = &v
}
}
@@ -203,11 +208,11 @@ func indexCheck() (err error) {
// Aggregate the ranges of missing sequence entries, print them
sort.Slice(missingSeq, func(a, b int) bool {
if missingSeq[a].folder != missingSeq[b].folder {
return missingSeq[a].folder < missingSeq[b].folder
slices.SortFunc(missingSeq, func(a, b sequenceKey) int {
if a.folder != b.folder {
return cmp.Compare(a.folder, b.folder)
}
return missingSeq[a].sequence < missingSeq[b].sequence
return cmp.Compare(a.sequence, b.sequence)
})
var folder uint32
@@ -248,25 +253,27 @@ func indexCheck() (err error) {
if fi.VersionHash != nil {
fiv = versions[string(fi.VersionHash)]
}
if !fiv.Equal(version) {
if !protocol.VectorFromWire(fiv).Equal(version) {
fmt.Printf("VersionList %q, folder %q, entry %d, FileInfo version mismatch, %v (VersionList) != %v (FileInfo)\n", gk.name, folder, i, version, fi.Version)
success = false
}
if fi.IsInvalid() != invalid {
fmt.Printf("VersionList %q, folder %q, entry %d, FileInfo invalid mismatch, %v (VersionList) != %v (FileInfo)\n", gk.name, folder, i, invalid, fi.IsInvalid())
ffi := protocol.FileInfoFromDB(fi)
if ffi.IsInvalid() != invalid {
fmt.Printf("VersionList %q, folder %q, entry %d, FileInfo invalid mismatch, %v (VersionList) != %v (FileInfo)\n", gk.name, folder, i, invalid, ffi.IsInvalid())
success = false
}
if fi.IsDeleted() != deleted {
fmt.Printf("VersionList %q, folder %q, entry %d, FileInfo deleted mismatch, %v (VersionList) != %v (FileInfo)\n", gk.name, folder, i, deleted, fi.IsDeleted())
if ffi.IsDeleted() != deleted {
fmt.Printf("VersionList %q, folder %q, entry %d, FileInfo deleted mismatch, %v (VersionList) != %v (FileInfo)\n", gk.name, folder, i, deleted, ffi.IsDeleted())
success = false
}
}
for i, fv := range vl.RawVersions {
for i, fv := range vl.Versions {
ver := protocol.VectorFromWire(fv.Version)
for _, device := range fv.Devices {
checkGlobal(i, device, fv.Version, false, fv.Deleted)
checkGlobal(i, device, ver, false, fv.Deleted)
}
for _, device := range fv.InvalidDevices {
checkGlobal(i, device, fv.Version, true, fv.Deleted)
checkGlobal(i, device, ver, true, fv.Deleted)
}
}
@@ -276,10 +283,10 @@ func indexCheck() (err error) {
if needsLocally(vl) {
_, ok := needs[gk]
if !ok {
fv, _ := vl.GetGlobal()
devB, _ := fv.FirstDevice()
fv, _ := vlGetGlobal(vl)
devB, _ := fvFirstDevice(fv)
dev := deviceToIDs[string(devB)]
fi := fileInfos[fileInfoKey{gk.folder, dev, gk.name}]
fi := protocol.FileInfoFromDB(fileInfos[fileInfoKey{gk.folder, dev, gk.name}])
if !fi.IsDeleted() && !fi.IsIgnored() {
fmt.Printf("Missing need entry for needed file %q, folder %q\n", gk.name, folder)
}
@@ -345,11 +352,84 @@ func indexCheck() (err error) {
return nil
}
func needsLocally(vl db.VersionList) bool {
gfv, gok := vl.GetGlobal()
func needsLocally(vl *dbproto.VersionList) bool {
gfv, gok := vlGetGlobal(vl)
if !gok { // That's weird, but we hardly need something non-existent
return false
}
fv, ok := vl.Get(protocol.LocalDeviceID[:])
return db.Need(gfv, ok, fv.Version)
fv, ok := vlGet(vl, protocol.LocalDeviceID[:])
return db.Need(gfv, ok, protocol.VectorFromWire(fv.Version))
}
// Get returns a FileVersion that contains the given device and whether it has
// been found at all.
func vlGet(vl *dbproto.VersionList, device []byte) (*dbproto.FileVersion, bool) {
_, i, _, ok := vlFindDevice(vl, device)
if !ok {
return &dbproto.FileVersion{}, false
}
return vl.Versions[i], true
}
// GetGlobal returns the current global FileVersion. The returned FileVersion
// may be invalid, if all FileVersions are invalid. Returns false only if
// VersionList is empty.
func vlGetGlobal(vl *dbproto.VersionList) (*dbproto.FileVersion, bool) {
i := vlFindGlobal(vl)
if i == -1 {
return nil, false
}
return vl.Versions[i], true
}
// findGlobal returns the first version that isn't invalid, or if all versions are
// invalid just the first version (i.e. 0) or -1, if there's no versions at all.
func vlFindGlobal(vl *dbproto.VersionList) int {
for i := range vl.Versions {
if !fvIsInvalid(vl.Versions[i]) {
return i
}
}
if len(vl.Versions) == 0 {
return -1
}
return 0
}
// findDevice returns whether the device is in InvalidVersions or Versions and
// in InvalidDevices or Devices (true for invalid), the positions in the version
// and device slices and whether it has been found at all.
func vlFindDevice(vl *dbproto.VersionList, device []byte) (bool, int, int, bool) {
for i, v := range vl.Versions {
if j := deviceIndex(v.Devices, device); j != -1 {
return false, i, j, true
}
if j := deviceIndex(v.InvalidDevices, device); j != -1 {
return true, i, j, true
}
}
return false, -1, -1, false
}
func deviceIndex(devices [][]byte, device []byte) int {
for i, dev := range devices {
if bytes.Equal(device, dev) {
return i
}
}
return -1
}
func fvFirstDevice(fv *dbproto.FileVersion) ([]byte, bool) {
if len(fv.Devices) != 0 {
return fv.Devices[0], true
}
if len(fv.InvalidDevices) != 0 {
return fv.InvalidDevices[0], true
}
return nil, false
}
func fvIsInvalid(fv *dbproto.FileVersion) bool {
return fv == nil || len(fv.Devices) == 0
}

View File

@@ -14,7 +14,7 @@ import (
"net/http"
"os"
"path/filepath"
"sort"
"slices"
"strings"
"time"
)
@@ -37,7 +37,9 @@ func uploadPanicLogs(ctx context.Context, urlBase, dir string) {
return
}
sort.Sort(sort.Reverse(sort.StringSlice(files)))
slices.SortFunc(files, func(a, b string) int {
return strings.Compare(b, a)
})
for _, file := range files {
if strings.Contains(file, ".reported.") {
// We've already sent this file. It'll be cleaned out at some

View File

@@ -10,6 +10,4 @@ import (
"github.com/syncthing/syncthing/lib/logger"
)
var (
l = logger.DefaultLogger.NewFacility("main", "Main package")
)
var l = logger.DefaultLogger.NewFacility("main", "Main package")

View File

@@ -17,6 +17,9 @@ import (
"os"
"path/filepath"
"google.golang.org/protobuf/proto"
"github.com/syncthing/syncthing/internal/gen/bep"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/fs"
"github.com/syncthing/syncthing/lib/osutil"
@@ -280,10 +283,11 @@ func loadEncryptedFileInfo(fd fs.File) (*protocol.FileInfo, error) {
return nil, err
}
var encFi protocol.FileInfo
if err := encFi.Unmarshal(trailer); err != nil {
var encFi bep.FileInfo
if err := proto.Unmarshal(trailer, &encFi); err != nil {
return nil, err
}
fi := protocol.FileInfoFromWire(&encFi)
return &encFi, nil
return &fi, nil
}

View File

@@ -23,14 +23,14 @@ import (
"path/filepath"
"regexp"
"runtime/pprof"
"sort"
"slices"
"strconv"
"strings"
"syscall"
"time"
"github.com/alecthomas/kong"
_ "github.com/syncthing/syncthing/lib/automaxprocs"
"github.com/gofrs/flock"
"github.com/thejerf/suture/v4"
"github.com/willabides/kongplete"
@@ -38,10 +38,10 @@ import (
"github.com/syncthing/syncthing/cmd/syncthing/cmdutil"
"github.com/syncthing/syncthing/cmd/syncthing/decrypt"
"github.com/syncthing/syncthing/cmd/syncthing/generate"
_ "github.com/syncthing/syncthing/lib/automaxprocs"
"github.com/syncthing/syncthing/lib/build"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/db"
"github.com/syncthing/syncthing/lib/db/backend"
"github.com/syncthing/syncthing/lib/dialer"
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/fs"
@@ -349,10 +349,12 @@ func (options serveOptions) Run() error {
return nil
}
// Ensure that our home directory exists.
if err := syncthing.EnsureDir(locations.GetBaseDir(locations.ConfigBaseDir), 0o700); err != nil {
l.Warnln("Failure on home directory:", err)
os.Exit(svcutil.ExitError.AsInt())
// Ensure that our config and data directories exist.
for _, loc := range []locations.BaseDirEnum{locations.ConfigBaseDir, locations.DataBaseDir} {
if err := syncthing.EnsureDir(locations.GetBaseDir(loc), 0o700); err != nil {
l.Warnln("Failed to ensure directory exists:", err)
os.Exit(svcutil.ExitError.AsInt())
}
}
if options.UpgradeTo != "" {
@@ -376,15 +378,18 @@ func (options serveOptions) Run() error {
if options.Upgrade {
release, err := checkUpgrade()
if err == nil {
// Use leveldb database locks to protect against concurrent upgrades
var ldb backend.Backend
ldb, err = syncthing.OpenDBBackend(locations.Get(locations.Database), config.TuningAuto)
lf := flock.New(locations.Get(locations.LockFile))
locked, err := lf.TryLock()
if err != nil {
l.Warnln("Upgrade:", err)
os.Exit(1)
} else if locked {
err = upgradeViaRest()
} else {
_ = ldb.Close()
err = upgrade.To(release)
}
_ = lf.Unlock()
_ = os.Remove(locations.Get(locations.LockFile))
}
if err != nil {
l.Warnln("Upgrade:", err)
@@ -438,7 +443,7 @@ func debugFacilities() string {
maxLen = len(name)
}
}
sort.Strings(names)
slices.Sort(names)
// Format the choices
b := new(bytes.Buffer)
@@ -544,6 +549,17 @@ func syncthingMain(options serveOptions) {
os.Exit(1)
}
// Ensure we are the only running instance
lf := flock.New(locations.Get(locations.LockFile))
locked, err := lf.TryLock()
if err != nil {
l.Warnln("Failed to acquire lock:", err)
os.Exit(1)
} else if !locked {
l.Warnln("Failed to acquire lock: is another Syncthing instance already running?")
os.Exit(1)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
@@ -562,6 +578,7 @@ func syncthingMain(options serveOptions) {
os.Exit(svcutil.ExitError.AsInt())
}
earlyService.Add(cfgWrapper)
config.RegisterInfoMetrics(cfgWrapper)
// Candidate builds should auto upgrade. Make sure the option is set,
// unless we are in a build where it's disabled or the STNOUPGRADE
@@ -624,9 +641,21 @@ func syncthingMain(options serveOptions) {
DBRecheckInterval: options.DebugDBRecheckInterval,
DBIndirectGCInterval: options.DebugDBIndirectGCInterval,
}
if options.Audit {
appOpts.AuditWriter = auditWriter(options.AuditFile)
if options.Audit || cfgWrapper.Options().AuditEnabled {
l.Infoln("Auditing is enabled.")
auditFile := cfgWrapper.Options().AuditFile
// Ignore config option if command-line option is set
if options.AuditFile != "" {
l.Debugln("Using the audit file from the command-line parameter.")
auditFile = options.AuditFile
}
appOpts.AuditWriter = auditWriter(auditFile)
}
if dur, err := time.ParseDuration(os.Getenv("STRECHECKDBEVERY")); err == nil {
appOpts.DBRecheckInterval = dur
}
@@ -680,6 +709,10 @@ func syncthingMain(options serveOptions) {
pprof.StopCPUProfile()
}
// Best effort remove lockfile, doesn't matter if it succeeds
_ = lf.Unlock()
_ = os.Remove(locations.Get(locations.LockFile))
os.Exit(int(status))
}
@@ -830,6 +863,10 @@ func initialAutoUpgradeCheck(misc *db.NamespacedKV) (upgrade.Release, error) {
if err != nil {
return upgrade.Release{}, err
}
if upgrade.CompareVersions(release.Tag, build.Version) == upgrade.MajorNewer {
return upgrade.Release{}, errors.New("higher major version")
}
if lastVersion, ok, err := misc.String(upgradeVersionKey); err == nil && ok && lastVersion == release.Tag {
// Only check time if we try to upgrade to the same release.
if lastTime, ok, err := misc.Time(upgradeTimeKey); err == nil && ok && time.Since(lastTime) < upgradeRetryInterval {

View File

@@ -64,7 +64,7 @@ func monitorMain(options serveOptions) {
fileDst, err = open(logFile)
}
if err != nil {
l.Warnln("Failed to setup logging to file, proceeding with logging to stdout only:", err)
l.Warnln("Failed to set up logging to file, proceeding with logging to stdout only:", err)
} else {
if build.IsWindows {
// Translate line breaks to Windows standard
@@ -238,19 +238,18 @@ func copyStderr(stderr io.Reader, dst io.Writer) {
return
}
if panicFd == nil {
dst.Write([]byte(line))
dst.Write([]byte(line))
if strings.HasPrefix(line, "panic:") || strings.HasPrefix(line, "fatal error:") {
panicFd, err = os.Create(locations.GetTimestamped(locations.PanicLog))
if err != nil {
l.Warnln("Create panic log:", err)
continue
}
if panicFd == nil && (strings.HasPrefix(line, "panic:") || strings.HasPrefix(line, "fatal error:")) {
panicFd, err = os.Create(locations.GetTimestamped(locations.PanicLog))
if err != nil {
l.Warnln("Create panic log:", err)
continue
}
l.Warnf("Panic detected, writing to \"%s\"", panicFd.Name())
if strings.Contains(line, "leveldb") && strings.Contains(line, "corrupt") {
l.Warnln(`
l.Warnf("Panic detected, writing to \"%s\"", panicFd.Name())
if strings.Contains(line, "leveldb") && strings.Contains(line, "corrupt") {
l.Warnln(`
*********************************************************************************
* Crash due to corrupt database. *
* *
@@ -263,22 +262,21 @@ func copyStderr(stderr io.Reader, dst io.Writer) {
* https://docs.syncthing.net/users/faq.html#my-syncthing-database-is-corrupt *
*********************************************************************************
`)
} else {
l.Warnln("Please check for existing issues with similar panic message at https://github.com/syncthing/syncthing/issues/")
l.Warnln("If no issue with similar panic message exists, please create a new issue with the panic log attached")
}
stdoutMut.Lock()
for _, line := range stdoutFirstLines {
panicFd.WriteString(line)
}
panicFd.WriteString("...\n")
for _, line := range stdoutLastLines {
panicFd.WriteString(line)
}
stdoutMut.Unlock()
} else {
l.Warnln("Please check for existing issues with similar panic message at https://github.com/syncthing/syncthing/issues/")
l.Warnln("If no issue with similar panic message exists, please create a new issue with the panic log attached")
}
stdoutMut.Lock()
for _, line := range stdoutFirstLines {
panicFd.WriteString(line)
}
panicFd.WriteString("...\n")
for _, line := range stdoutLastLines {
panicFd.WriteString(line)
}
stdoutMut.Unlock()
panicFd.WriteString("Panic at " + time.Now().Format(time.RFC3339) + "\n")
}

View File

@@ -140,7 +140,7 @@ func checkNotExist(t *testing.T, name string) {
func TestAutoClosedFile(t *testing.T) {
os.RemoveAll("_autoclose")
defer os.RemoveAll("_autoclose")
os.Mkdir("_autoclose", 0755)
os.Mkdir("_autoclose", 0o755)
file := filepath.FromSlash("_autoclose/tmp")
data := []byte("hello, world\n")

View File

@@ -26,3 +26,9 @@
darwin: "20"
linux: "2.6.32"
windows: "10.0"
- runtime: go1.24
requirements:
darwin: "20"
linux: "3.2"
windows: "10.0"

View File

@@ -2,7 +2,7 @@
Name=Start Syncthing
GenericName=File synchronization
Comment=Starts the main syncthing process in the background.
Exec=/usr/bin/syncthing serve --no-browser --logfile=default
Exec=syncthing serve --no-browser --logfile=default
Icon=syncthing
Terminal=false
Type=Application

View File

@@ -2,7 +2,7 @@
Name=Syncthing Web UI
GenericName=File synchronization UI
Comment=Opens Syncthing's Web UI in the default browser (Syncthing must already be started).
Exec=/usr/bin/syncthing --browser-only
Exec=syncthing --browser-only
Icon=syncthing
Terminal=false
Type=Application

80
go.mod
View File

@@ -1,100 +1,100 @@
module github.com/syncthing/syncthing
go 1.22.0
go 1.23.0
require (
github.com/AudriusButkevicius/recli v0.0.7-0.20220911121932-d000ce8fbf0f
github.com/alecthomas/kong v1.2.1
github.com/aws/aws-sdk-go v1.55.5
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1
github.com/alecthomas/kong v1.11.0
github.com/aws/aws-sdk-go v1.55.7
github.com/calmh/incontainer v1.0.0
github.com/calmh/xdr v1.1.0
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/getsentry/raven-go v0.2.0
github.com/go-ldap/ldap/v3 v3.4.8
github.com/go-ldap/ldap/v3 v3.4.11
github.com/gobwas/glob v0.2.3
github.com/gogo/protobuf v1.3.2
github.com/gofrs/flock v0.12.1
github.com/greatroar/blobloom v0.8.0
github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/jackpal/gateway v1.0.15
github.com/jackpal/gateway v1.0.16
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/maruel/panicparse/v2 v2.5.0
github.com/maxbrunsfeld/counterfeiter/v6 v6.11.2
github.com/maxmind/geoipupdate/v6 v6.1.0
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.20.4
github.com/puzpuzpuz/xsync/v3 v3.4.0
github.com/quic-go/quic-go v0.47.0
github.com/pierrec/lz4/v4 v4.1.22
github.com/prometheus/client_golang v1.22.0
github.com/puzpuzpuz/xsync/v3 v3.5.1
github.com/quic-go/quic-go v0.52.0
github.com/rabbitmq/amqp091-go v1.10.0
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475
github.com/shirou/gopsutil/v4 v4.24.8
github.com/syncthing/notify v0.0.0-20210616190510-c6b7342338d2
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9
github.com/shirou/gopsutil/v4 v4.25.4
github.com/syncthing/notify v0.0.0-20250528144937-c7027d4f7465
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/thejerf/suture/v4 v4.0.6
github.com/urfave/cli v1.22.16
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.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
go.uber.org/automaxprocs v1.6.0
golang.org/x/crypto v0.38.0
golang.org/x/net v0.40.0
golang.org/x/sys v0.33.0
golang.org/x/text v0.25.0
golang.org/x/time v0.11.0
golang.org/x/tools v0.33.0
google.golang.org/protobuf v1.36.6
sigs.k8s.io/yaml v1.4.0
)
require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/beorn7/perks v1.0.1 // indirect
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/coreos/go-semver v0.3.1 // 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.3 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // 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.12.1 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 // indirect
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 // 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/jmespath/go-jmespath v0.4.0 // indirect
github.com/klauspost/compress v1.17.9 // 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.20.2 // indirect
github.com/onsi/ginkgo/v2 v2.23.4 // 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.59.1 // indirect
github.com/prometheus/common v0.62.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/shoenig/go-m1cpu v0.1.6 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.9.0 // indirect
github.com/stretchr/testify v1.10.0 // indirect
github.com/tklauser/go-sysconf v0.3.14 // indirect
github.com/tklauser/numcpus v0.8.0 // indirect
github.com/tklauser/numcpus v0.9.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-20240909161429-701f63a606c0 // indirect
golang.org/x/mod v0.21.0 // indirect
golang.org/x/sync v0.8.0 // indirect
go.uber.org/mock v0.5.2 // indirect
golang.org/x/mod v0.24.0 // indirect
golang.org/x/sync v0.14.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

234
go.sum
View File

@@ -1,26 +1,38 @@
github.com/AudriusButkevicius/recli v0.0.7-0.20220911121932-d000ce8fbf0f h1:GmH5lT+moM7PbAJFBq57nH9WJ+wRnBXr/tyaYWbSAx8=
github.com/AudriusButkevicius/recli v0.0.7-0.20220911121932-d000ce8fbf0f/go.mod h1:Nhfib1j/VFnLrXL9cHgA+/n2O6P5THuWelOnbfPNd78=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 h1:OVoM452qUFBrX+URdH3VpR299ma4kfom0yB0URYky9g=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0/go.mod h1:kUjrAo8bgEwLeZ/CmHqNl3Z/kPm7y6FKfxxK0izYUg4=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.0 h1:LR0kAX9ykz8G4YgLCaRDVJ3+n43R8MneB5dTy2konZo=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.0/go.mod h1:DWAciXemNf++PQJLeXUB4HHH5OpsAh12HZnu2wXE1jA=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1 h1:lhZdRq7TIx0GJQvSyX2Si406vrYsov2FXGp/RnSEtcs=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1/go.mod h1:8cl44BDmi+effbARHMQjgOKA2AYvcohNm7KEt42mSV8=
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.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/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/kong v1.11.0 h1:y++1gI7jf8O7G7l4LZo5ASFhrhJvzc+WgF/arranEmM=
github.com/alecthomas/kong v1.11.0/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU=
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/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE=
github.com/aws/aws-sdk-go v1.55.7/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/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=
@@ -34,7 +46,8 @@ 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/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
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=
@@ -42,6 +55,8 @@ github.com/d4l3k/messagediff v1.2.1/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkE
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/ebitengine/purego v0.8.3 h1:K+0AjQp63JEZTEMZiwsI9g0+hAMNohwUOtY0RPGexmc=
github.com/ebitengine/purego v0.8.3/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=
@@ -50,11 +65,10 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs=
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk=
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-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-ldap/ldap/v3 v3.4.11 h1:4k0Yxweg+a3OyBLjdYn5OKglv18JNvfDykSoI8bW0gU=
github.com/go-ldap/ldap/v3 v3.4.11/go.mod h1:bY7t0FLK8OAVpp/vV6sSlpz3EQDGcQwc8pF0ujLgKvM=
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=
@@ -65,8 +79,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
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-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
@@ -82,17 +96,15 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
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/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 h1:c5FlPPgxOn7kJz3VoPLkQYQXGBS3EklQ4Zfi57uOuqQ=
github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 h1:gD0vax+4I+mAj+jEChEf25Ia07Jq7kYOFO5PPhAxFl4=
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
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=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/greatroar/blobloom v0.8.0 h1:I9RlEkfqK9/6f1v9mFmDYegDQ/x0mISCpiNpAm23Pt4=
github.com/greatroar/blobloom v0.8.0/go.mod h1:mjMJ1hh1wjGVfr93QIHJ6FfDNVrA0IELv8OvMHJxHKs=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -101,7 +113,6 @@ github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brv
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
@@ -110,8 +121,8 @@ github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUq
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jackpal/gateway v1.0.15 h1:yb4Gltgr8ApHWWnSyybnDL1vURbqw7ooo7IIL5VZSeg=
github.com/jackpal/gateway v1.0.15/go.mod h1:dbyEDcDhHUh9EmjB9ung81elMUZfG0SoNc2TfTbcj4c=
github.com/jackpal/gateway v1.0.16 h1:mTBRuHSW8qviVqX7kXnxKevqlfS/OA01ys6k6fxSX7w=
github.com/jackpal/gateway v1.0.16/go.mod h1:IOn1OUbso/cGYmnCBZbCEqhNCLSz0xxdtIpUpri5/nA=
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
@@ -134,29 +145,22 @@ github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4d
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/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
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/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
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=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/maxbrunsfeld/counterfeiter/v6 v6.8.1 h1:NicmruxkeqHjDv03SfSxqmaLuisddudfP3h5wdXFbhM=
github.com/maxbrunsfeld/counterfeiter/v6 v6.8.1/go.mod h1:eyp4DdUJAKkr9tvxR3jWhw2mDK7CWABMG5r9uyaKC7I=
github.com/maruel/panicparse/v2 v2.5.0 h1:yCtuS0FWjfd0RTYMXGpDvWcb0kINm8xJGu18/xMUh00=
github.com/maruel/panicparse/v2 v2.5.0/go.mod h1:DA2fDiBk63bKfBf4CVZP9gb4fuvzdPbLDsSI873hweQ=
github.com/maxbrunsfeld/counterfeiter/v6 v6.11.2 h1:yVCLo4+ACVroOEr4iFU1iH46Ldlzz2rTuu18Ra7M8sU=
github.com/maxbrunsfeld/counterfeiter/v6 v6.11.2/go.mod h1:VzB2VoMh1Y32/QqDfg9ZJYHj99oM4LiGtqPZydTiQSQ=
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/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=
@@ -171,20 +175,22 @@ 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.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4=
github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag=
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
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.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU=
github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
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.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/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -196,36 +202,32 @@ 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.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI=
github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
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.59.1 h1:LXb1quJHWm1P6wq/U824uxYi4Sg0oGvNeUm1z5dJoX0=
github.com/prometheus/common v0.59.1/go.mod h1:GpWM7dewqmVYcd7SmRaiWVe9SSqjf0UrwnYnpEZNuT0=
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
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/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/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/quic-go/quic-go v0.52.0 h1:/SlHrCRElyaU6MaEPKqKr9z83sBg2v4FLLvWM+Z47pA=
github.com/quic-go/quic-go v0.52.0/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ=
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=
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg=
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/riywo/loginshell v0.0.0-20200815045211-7d26008be1ab h1:ZjX6I48eZSFetPb41dHudEyVr5v953N15TsNZXlkcWY=
github.com/riywo/loginshell v0.0.0-20200815045211-7d26008be1ab/go.mod h1:/PfPXh0EntGc3QAAyUaviy4S9tzy4Zp0e2ilq4voC6E=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
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/v4 v4.24.8 h1:pVQjIenQkIhqO81mwTaXjTzOMT7d3TZkf43PlVFHENI=
github.com/shirou/gopsutil/v4 v4.24.8/go.mod h1:wE0OrJtj4dG+hYkxqDH3QiBICdKSf04/npcvLLc/oRg=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/shirou/gopsutil/v4 v4.25.4 h1:cdtFO363VEOOFrUCjZRh4XVJkb548lyF0q0uTeMqYPw=
github.com/shirou/gopsutil/v4 v4.25.4/go.mod h1:xbuxyoZj+UsgnZrENu3lQivsngRR5BdjbJwf2fv4szA=
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=
@@ -236,82 +238,59 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/syncthing/notify v0.0.0-20210616190510-c6b7342338d2 h1:F4snRP//nIuTTW9LYEzVH4HVwDG9T3M4t8y/2nqMbiY=
github.com/syncthing/notify v0.0.0-20210616190510-c6b7342338d2/go.mod h1:J0q59IWjLtpRIJulohwqEZvjzwOfTEPp8SVhDJl+y0Y=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/syncthing/notify v0.0.0-20250528144937-c7027d4f7465 h1:yhxdTGmFkAM2TFA65c3NgGwpnIkUM8oVqPX2e9S7IVg=
github.com/syncthing/notify v0.0.0-20250528144937-c7027d4f7465/go.mod h1:J0q59IWjLtpRIJulohwqEZvjzwOfTEPp8SVhDJl+y0Y=
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDdvS342BElfbETmL1Aiz3i2t0zfRj16Hs=
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/thejerf/suture/v4 v4.0.6 h1:QsuCEsCqb03xF9tPAsWAj8QOAJBgQI1c0VqJNaingg8=
github.com/thejerf/suture/v4 v4.0.6/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/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo=
github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI=
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=
github.com/urfave/cli v1.22.16 h1:MH0k6uJxdwdeWQTwhSO42Pwr4YLrNLwBtg1MRgTqPdQ=
github.com/urfave/cli v1.22.16/go.mod h1:EeJR6BKodywf4zciqrdw6hpCPk68JO9z5LazXZMn5Po=
github.com/vitrun/qart v0.0.0-20160531060029-bf64b92db6b0 h1:okhMind4q9H1OxF44gNegWkiP4H/gsTFLalHFa4OOUI=
github.com/vitrun/qart v0.0.0-20160531060029-bf64b92db6b0/go.mod h1:TTbGUfE+cXXceWtbTHq6lqcTvYPBKLNejBEbnUsQJtU=
github.com/willabides/kongplete v0.4.0 h1:eivXxkp5ud5+4+NVN9e4goxC5mSh3n1RHov+gsblM2g=
github.com/willabides/kongplete v0.4.0/go.mod h1:0P0jtWD9aTsqPSUAl4de35DLghrr57XcayPyvqSi2X8=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
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=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
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.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/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
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.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
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=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
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.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
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.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
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=
@@ -328,47 +307,28 @@ golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
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=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
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.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/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
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=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
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.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/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
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=
@@ -382,8 +342,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.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
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=

View File

@@ -224,7 +224,6 @@ code.ng-binding{
}
.well, .form-control[readonly="readonly"], .popover { /* read-only fields*/
color: #666 !important;
border-color: #444 !important;
background-color: #111 !important;
}
@@ -278,3 +277,17 @@ code.ng-binding{
.reception {
filter: invert(77%) sepia(0%) saturate(724%) hue-rotate(146deg) brightness(91%) contrast(85%);
}
/* Disabled checkbox panels */
.checkbox[disabled] {
background-color: #222222;
}
.checkbox[disabled] * {
color: #666666;
}
.checkbox[disabled] .help-block {
color: #666666 !important;
}

View File

@@ -228,7 +228,6 @@ code.ng-binding{
}
.well, .form-control[readonly="readonly"], .popover { /* read-only fields*/
color: #666 !important;
border-color: #424242 !important;
background-color: #3B3B3B !important;
}
@@ -289,4 +288,18 @@ code.ng-binding{
/* Remote Devices 'connection type'-icon color set to #aaa */
.reception {
filter: invert(77%) sepia(0%) saturate(724%) hue-rotate(146deg) brightness(91%) contrast(85%);
}
}
/* Disabled checkbox panels */
.checkbox[disabled] {
background-color: #3B3B3B;
}
.checkbox[disabled] * {
color: #999999;
}
.checkbox[disabled] .help-block {
color: #999999 !important;
}

View File

@@ -448,7 +448,6 @@ ul.three-columns li, ul.two-columns li {
}
@media (max-width:479px) {
nav .dropdown-toggle {
font-size: 1em;
}
@@ -456,13 +455,7 @@ ul.three-columns li, ul.two-columns li {
.navbar-nav .open .dropdown-menu > li > a {
padding: 12px 15px 12px 25px;
}
}
.tab-content {
padding-top: 10px;
}
@media (max-width: 419px) {
/* The selectors are build to target only the content of folder and device
panels as it would "destroy" e.g. out of sync or recent changes listings.
The !important is needed to override .visible-xs that sets display to a
@@ -513,6 +506,10 @@ ul.three-columns li, ul.two-columns li {
}
}
.tab-content {
padding-top: 10px;
}
.form-horizontal .form-group {
margin-bottom: 5px;
}

View File

@@ -2,7 +2,7 @@
"A device with that ID is already added.": "أضيف هذا الجهاز بالفعل.",
"A negative number of days doesn't make sense.": "لا يمكن استخدام قيمة سالبة لعدد الأيام.",
"A new major version may not be compatible with previous versions.": "الإصدار الجديد قد لا يتوافق مع الإصدارات السابقة.",
"API Key": "مفتاح API",
"API Key": "مفتاح واجهة برمجة التطبيقات \"API\"",
"About": "حول",
"Action": "إجراء",
"Actions": "الإجراءات",
@@ -27,6 +27,7 @@
"Allowed Networks": "الشبكات المسموح بها",
"Alphabetic": "أبجدية",
"Altered by ignoring deletes.": "تغير بتجاهل عمليات الحذف.",
"Always turned on when the folder type is \"{%foldertype%}\".": "مفعل دائمًا عندما يكون نوع المجلد هو \"{{foldertype}}\".",
"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?": "هل تريد الانتقال الى التصميم الجديد لتقرير الاستخدام المجهول ؟",
@@ -52,6 +53,7 @@
"Body:": "جسم:",
"Bugs": "أخطاء برمجية",
"Cancel": "إلغاء",
"Cannot be enabled when the folder type is \"{%foldertype%}\".": "لا يمكن تفعيله عندما يكون نوع المجلد هو \"{{foldertype}}\".",
"Changelog": "سجل التغيير",
"Clean out after": "نظف بعد",
"Cleaning Versions": "إصدارات نظيفة",
@@ -154,7 +156,7 @@
"Failed Items": "العناصر الفاشلة",
"Failed to load file versions.": "لم يُتَوَصَّل لنسخة الملف.",
"Failed to load ignore patterns.": "فشل التَّوَصُّل إلى مُرَشِّحات التجاهل.",
"Failed to setup, retrying": "فشل الإعداد، تجري المحاولة مرة أخرى",
"Failed to set up, retrying": "فشل الإعداد، تجري المحاولة مرة أخرى",
"Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": "يُتوقع فشل الاتصال بخوادم IPv6، إذا لم يكن IPv6 متاحا.",
"File Pull Order": "ترتيب استيراد الملفات",
"File Versioning": "إصدارات الملف",

View File

@@ -22,11 +22,12 @@
"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.": "Всички папки, споделени с устройството трябва да бъдат защитени с парола, така че данните да са недостъпни без нея.",
"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?": "Разрешавате ли анонимно отчитане на употребата?",
"Allowed Networks": "Разрешени мрежи",
"Alphabetic": "Азбучен ред",
"Altered by ignoring deletes.": "Промяна чрез пренебрегване на премахваните елементи.",
"Always turned on when the folder type is \"{%foldertype%}\".": "Винаги включено, когато вида на папката е „{{foldertype}}“.",
"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?": "Форматът на данните за анонимно отчитане на употреба е променен. Желаете ли да използвате него вместо стария?",
@@ -52,6 +53,7 @@
"Body:": "Съдържание:",
"Bugs": "Дефекти",
"Cancel": "Отказ",
"Cannot be enabled when the folder type is \"{%foldertype%}\".": "Не мож да бъде включено, ако вида на папката е „{{foldertype}}“.",
"Changelog": "Дневник на промените",
"Clean out after": "Почистване след",
"Cleaning Versions": "Почистване на версии",
@@ -111,7 +113,7 @@
"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).": "Изключва сравняването и синхронизацията на правата на файловете. Полезно за системи с липсващи или специфични права (като FAT, exFAT, Synology, Android).",
"Discard": "Отказване",
"Disconnected": "Не е свързано",
"Disconnected": "Няма връзка",
"Disconnected (Inactive)": "Не е свързано (неизползвано)",
"Disconnected (Unused)": "Не е свързано (неизползвано)",
"Discovered": "Открит",
@@ -134,8 +136,8 @@
"Edit Folder Defaults": "За нови папки",
"Editing {%path%}.": "Променяне на {{path}}.",
"Enable Crash Reporting": "Включване на доклад за срив",
"Enable NAT traversal": "Преминаване през NAT",
"Enable Relaying": "Препращане",
"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.": "Когато е отметнато разширените атрибути се изпращат към другите устройства, но получените разширени атрибути не се прилагат. Може да има значително неблагоприятно влияние върху производителността. Винаги е отметнато когато „Синхронизиране на разширени атрибути“ е отметнато.",
@@ -154,12 +156,12 @@
"Failed Items": "Елементи с грешка",
"Failed to load file versions.": "Грешка при зареждане на версии.",
"Failed to load ignore patterns.": "Грешка при зареждане на шаблони за пренебрегване.",
"Failed to setup, retrying": "Грешка при настройване, извършва се повторен опит",
"Failed to set up, retrying": "Грешка при настройване, извършва се повторен опит",
"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.": "Файловете биват преместени в папка .stversions при заменяне или изтриване от Syncthing.",
"Files are moved to date stamped versions in a .stversions directory when replaced or deleted by Syncthing.": "Когато Syncthing замени или изтрие файл той бива преместен в папката .stversions и преименуван чрез добавяне на датата и часа.",
"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": "Грешка при наблюдаване на файловата система",
@@ -205,7 +207,7 @@
"Ignored Folders": "Пренебрегнати папки",
"Ignored at": "Пренебрегнато на",
"Included Software": "Използван софтуер",
"Incoming Rate Limit (KiB/s)": "Ограничение при изтегляне (KiB/s)",
"Incoming Rate Limit (KiB/s)": "Ограничение при изтегляне (КиБ/с)",
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Неправилни настройки могат да повредят файлове и да попречат на синхронизирането.",
"Incorrect user name or password.": "Грешно потребителско име или парола.",
"Internally used paths:": "Вътрешно използвани пътища:",
@@ -215,7 +217,7 @@
"Inversion of the given condition (i.e. do not exclude)": "Обръща значението на условието (напр. да не се отхвърля)",
"Keep Versions": "Пазени версии",
"LDAP": "LDAP",
"Largest First": "Първо най-големи",
"Largest First": "Първо най-големите",
"Last 30 Days": "Последните 30 дена",
"Last 7 Days": "Последните 7 дена",
"Last Month": "Миналия месец",
@@ -261,7 +263,7 @@
"Never": "никога",
"New Device": "Ново устройство",
"New Folder": "Нова папка",
"Newest First": "Първо най-нови",
"Newest First": "Първо най-новите",
"No": "Не",
"No File Versioning": "Без пазене на версии",
"No files will be deleted as a result of this operation.": "В резултат на операцията няма да бъдат премахнати файлове.",
@@ -272,12 +274,12 @@
"Number of Connections": "Брой на връзките",
"OK": "Добре",
"Off": "Изключено",
"Oldest First": "Първо най-стари",
"Oldest First": "Първо най-старите",
"Optional descriptive label for the folder. Can be different on each device.": "Незадължително име на папката. Може да бъде различно на всяко устройство.",
"Options": "Настройки",
"Out of Sync": "Несинхронизирано",
"Out of Sync Items": "Несинхронизирани елементи",
"Outgoing Rate Limit (KiB/s)": "Ограничение при качване (KiB/s)",
"Outgoing Rate Limit (KiB/s)": "Ограничение при качване (КиБ/с)",
"Override": "Налагане",
"Override Changes": "Налагане на местни промени",
"Ownership": "Собственост",
@@ -307,11 +309,11 @@
"QUIC LAN": "QUIC LAN",
"QUIC WAN": "QUIC WAN",
"Quick guide to supported patterns": "Кратък наръчник на поддържаните шаблони",
"Random": "Произволен",
"Receive Encrypted": риема шифровани данни",
"Random": "В случаен ред",
"Receive Encrypted": олучава шифровани данни",
"Receive Only": "Само получава",
"Received data is already encrypted": "Получените данни вече са шифровани",
"Recent Changes": "Последни промени",
"Recent Changes": "Скорошни промени",
"Reduced by ignore patterns": "Наложени са шаблони за пренебрегване",
"Relay LAN": "Препращане по LAN",
"Relay WAN": "Препращане по WAN",
@@ -374,7 +376,7 @@
"Simple File Versioning": "Обикновени версии",
"Single level wildcard (matches within a directory only)": "Заместващ символ за едно ниво (съвпада само с папка)",
"Size": "Размер",
"Smallest First": "Първо най-малки",
"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:": "Някои от адресите, на които Syncthing очаква връзка не могат да бъдат настроени да получават входящи връзки:",
@@ -384,7 +386,7 @@
"Stable releases only": "Само стабилни версии",
"Staggered": "Разпределени",
"Staggered File Versioning": "Разпределени версии",
"Start Browser": "Отваряне в мрежов четец",
"Start Browser": "Отваряне на мрежов четец",
"Statistics": "Статистика",
"Stay logged in": "Оставане в системата",
"Stopped": "Спряна",
@@ -437,11 +439,11 @@
"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.": "Интервал, в секунди, на почистване на папката с версии. Нула изключва периодичното почистване.",
"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 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 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.": "Ограничението се прилага към общия трафик от всички връзки към това устройство.",
@@ -474,7 +476,7 @@
"Unexpected Items": "Неочаквани елементи",
"Unexpected items have been found in this folder.": "В папката са намерени неочаквани елементи.",
"Unignore": "Отменяне на пренебрегване",
"Unknown": "Неясно",
"Unknown": "Неизвестно",
"Unshared": "Несподелена",
"Unshared Devices": "Устройства, с които не е споделена",
"Unshared Folders": "Несподелени папки",
@@ -498,7 +500,7 @@
"Using a direct TCP connection over WAN": "Използване на директна свързаност с TCP през широкодостъпна мрежа",
"Version": "Издание",
"Versions": "Версии",
"Versions Path": ът до версиите",
"Versions Path": апка с версиите",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Версиите биват изтривани автоматично ако са по-стари от максималната възраст или надминават броя разрешени версии за определено време.",
"Waiting to Clean": "Изчаква за почистване",
"Waiting to Scan": "Изчаква за обхождане",

View File

@@ -154,7 +154,7 @@
"Failed Items": "Elements fallats",
"Failed to load file versions.": "No s'han pogut carregar les versions dels fitxers.",
"Failed to load ignore patterns.": "No s'han pogut carregar els patrons ignorats.",
"Failed to setup, retrying": "No s'ha pogut configurar, s'està tornant a provar",
"Failed to set up, retrying": "No s'ha pogut configurar, s'està tornant a provar",
"Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": "S'espera que no es pugui connectar als servidors IPv6 si no hi ha connectivitat IPv6.",
"File Pull Order": "Ordre d'agafar fitxers",
"File Versioning": "Versionat de Fitxers",

View File

@@ -150,7 +150,7 @@
"Failed Items": "Objectes fallits",
"Failed to load file versions.": "No s'han pogut carregar les versions dels fitxers.",
"Failed to load ignore patterns.": "No s'han pogut carregar els patrons ignorats.",
"Failed to setup, retrying": "Errada en la configuració, reintentant",
"Failed to set up, retrying": "Errada en la configuració, reintentant",
"Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": "És possible que es produïsca una fallada al connectar als servidors IPv6 si no hi ha connectivitat IPv6.",
"File Pull Order": "Ordre de fitxers del pull",
"File Versioning": "Versionat de fitxer",

View File

@@ -154,7 +154,7 @@
"Failed Items": "Nezdařené položky",
"Failed to load file versions.": "Nepodařilo se nahrát verze souboru.",
"Failed to load ignore patterns.": "Načtení vzorů ignorovaného se nezdařilo.",
"Failed to setup, retrying": "Nastavování se nezdařilo, zkouší se znovu",
"Failed to set up, retrying": "Nastavování se nezdařilo, zkouší se znovu",
"Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": "Je v pořádku, když se připojení k IPv6 serverům nezdaří, pokud není k dispozici IPv6 konektivita.",
"File Pull Order": "Pořadí stahování souborů",
"File Versioning": "Správa verzí souborů",
@@ -221,13 +221,14 @@
"Last seen": "Naposledy spatřen",
"Latest Change": "Poslední změna",
"Learn more": "Zjistěte více",
"Learn more at {%url%}": "Více na {{url}}",
"Limit": "Limit",
"Listener Failures": "Selhání při naslouchání",
"Listener Status": "Stav naslouchání",
"Listeners": "Naslouchající",
"Loading data...": "Načítání dat…",
"Loading...": "Načítání…",
"Local Additions": "Místní příbytky",
"Local Additions": "Místní přebytky",
"Local Discovery": "Místní objevování",
"Local State": "Místní status",
"Local State (Total)": "Místní status (Celkem)",
@@ -236,11 +237,16 @@
"Log File": "Soubor logů",
"Log In": "Přihlásit se",
"Log Out": "Odhlásit se",
"Log in to see paths information.": "Pro zobrazení informací o cestě se přihlaste.",
"Log in to see version information.": "Pro zobrazení informací o verzi se přihlaste.",
"Log tailing paused. Scroll to the bottom to continue.": "Zaznamenávání událostí pozastaveno. Sjeďte dolů pro pokračování.",
"Login failed, see Syncthing logs for details.": "Přihlášení selhalo, detaily najdete v Syncthing logu.",
"Logs": "Záznamy událostí",
"Major Upgrade": "Aktualizace hlavní verze",
"Mass actions": "Hromadné akce",
"Maximum Age": "Maximální časový limit",
"Maximum single entry size": "Maximální velikost jedné položky",
"Maximum total size": "Maximální celková velikost",
"Metadata Only": "Pouze metadata",
"Minimum Free Disk Space": "Minimální velikost volného místa na úložišti",
"Mod. Device": "Zařízení, které provedlo změnu",
@@ -410,15 +416,17 @@
"The folder path cannot be blank.": "Popis umístění složky nemůže zůstat nevyplně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.": "Jsou použity následující intervaly: za první hodinu jsou ponechány verze pro každých 30 sekund, za první den jsou ponechány verze pro každou hodinu, za prvních 30 dní jsou ponechány verze pro každý den a do nejvyššího nastaveného stáří jsou ponechány verze pro každý týden.",
"The following items could not be synchronized.": "Následující položky nemohly být synchronizovány.",
"The following items were changed locally.": "Tyto položky byly změněny lokálně",
"The following items were changed locally.": "Tyto položky byly změněny lokálně.",
"The following methods are used to discover other devices on the network and announce this device to be found by others:": "K objevování ostatních zařízení a oznamování tohoto zařízení se používají následující metody:",
"The following text will automatically be inserted into a new message.": "Následující text bude automaticky vložen do nové zprávy.",
"The following unexpected items were found.": "Byly nalezeny tyto neočekávané položky.",
"The interval must be a positive number of seconds.": "Interval musí být kladný počet sekund.",
"The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.": "Interval (v sekundách) pro spouštění čištění ve složce s verzemi. Nula pravidelné čištění vypíná.",
"The maximum age must be a number and cannot be blank.": "Nejvyšší stáří je třeba zadat v podobě čísla a nemůže být prázdné.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Maximální doba pro zachování verze (dny, zapsáním hodnoty 0 bude ponecháno navždy).",
"The number of days must be a number and cannot be blank.": "Je třeba, aby počet dní bylo číslo a nemůže zůstat nevyplněné.",
"The number of days to keep files in the trash can. Zero means forever.": "Počet dní, po který budou soubory uchovány v koši. Nula znamená navždy.",
"The number of connections must be a non-negative number.": "Počet spojení musí být nezáporné číslo.",
"The number of days must be a number and cannot be blank.": "Počet dní musí být číslo a nemůže zůstat nevyplněné.",
"The number of days to keep files in the trash can. Zero means forever.": "Počet dní, po které budou soubory uchovány v koši. Nula znamená navždy.",
"The number of old versions to keep, per file.": "Počet uchovávaných starších verzí každého ze souborů.",
"The number of versions must be a number and cannot be blank.": "Je třeba, aby počet verzí bylo číslo a nemůže zůstat nevyplněné.",
"The path cannot be blank.": "Popis umístění nemůže zůstat nevyplněný.",
@@ -501,6 +509,8 @@
"folder": "složka",
"full documentation": "úplná dokumentace",
"items": "položky",
"modified": "změněno",
"permit": "povolit",
"seconds": "sekund",
"theme": {
"name": {

View File

@@ -154,7 +154,7 @@
"Failed Items": "Mislykkede filer",
"Failed to load file versions.": "Fil versioner kunne ikke indlæses.",
"Failed to load ignore patterns.": "Ignorerings-mønstre kunne ikke indlæses.",
"Failed to setup, retrying": "Opsætning mislykkedes; prøver igen",
"Failed to set up, retrying": "Opsætning mislykkedes; prøver igen",
"Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": "Fejl i forbindelse med opkobling til IPv6-servere skal forventes, hvis der ikke er IPv6-forbindelse.",
"File Pull Order": "Hentningsrækkefølge for filer",
"File Versioning": "Filversionering",

View File

@@ -27,6 +27,7 @@
"Allowed Networks": "Erlaubte Netzwerke",
"Alphabetic": "Alphabetisch",
"Altered by ignoring deletes.": "Weicht ab, weil Löschungen ignoriert werden.",
"Always turned on when the folder type is \"{%foldertype%}\".": "Immer eingeschaltet, wenn der Ordnertyp „{{foldertype}}“ ist.",
"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.": "Die Versionierung erfolgt über einen externen Befehl. Er muss die Datei aus dem geteilten Ordner entfernen. Wenn der Pfad zur Anwendung Leerzeichen enthält, sollte er in Anführungszeichen gesetzt werden.",
"Anonymous Usage Reporting": "Anonymer Nutzungsbericht",
"Anonymous usage report format has changed. Would you like to move to the new format?": "Das Format des anonymen Nutzungsberichts hat sich geändert. Möchten Sie auf das neue Format umsteigen?",
@@ -52,6 +53,7 @@
"Body:": "Nachrichtentext:",
"Bugs": "Fehler",
"Cancel": "Abbrechen",
"Cannot be enabled when the folder type is \"{%foldertype%}\".": "Kann nicht aktiviert werden, wenn der Ordnertyp „{{foldertype}}“ ist.",
"Changelog": "Änderungsprotokoll",
"Clean out after": "Löschen nach",
"Cleaning Versions": "Versionen bereinigen",
@@ -64,7 +66,7 @@
"Configuration Directory": "Konfigurationsverzeichnis",
"Configuration File": "Konfigurationsdatei",
"Configured": "Konfiguriert",
"Connected (Unused)": "Verbunden (Nicht genutzt)",
"Connected (Unused)": "Verbunden (nicht genutzt)",
"Connection Error": "Verbindungsfehler",
"Connection Management": "Verbindungsverwaltung",
"Connection Type": "Verbindungstyp",
@@ -95,7 +97,7 @@
"Deselect folders to stop sharing with this device.": "Ordner abwählen, um sie nicht mehr für mit diesem Gerät zu teilen.",
"Device": "Gerät",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Gerät „{{name}}“ ({{device}} {{address}}) möchte sich verbinden. Gerät hinzufügen?",
"Device Certificate": "Geräte-Zertifikat",
"Device Certificate": "Gerätezertifikat",
"Device ID": "Gerätekennung",
"Device Identification": "Geräteidentifikation",
"Device Name": "Gerätename",
@@ -112,8 +114,8 @@
"Disables comparing and syncing file permissions. Useful on systems with nonexistent or custom permissions (e.g. FAT, exFAT, Synology, Android).": "Deaktiviert Vergleich und Synchronisierung der Dateiberechtigungen. Dies ist hilfreich für Dateisysteme ohne konfigurierbare Berechtigungsparameter (z. B. FAT, exFAT, Synology, Android).",
"Discard": "Verwerfen",
"Disconnected": "Getrennt",
"Disconnected (Inactive)": "Getrennt (Inaktiv)",
"Disconnected (Unused)": "Getrennt (Nicht genutzt)",
"Disconnected (Inactive)": "Getrennt (inaktiv)",
"Disconnected (Unused)": "Getrennt (nicht genutzt)",
"Discovered": "Ermittelt",
"Discovery": "Gerätesuche",
"Discovery Failures": "Gerätesuchfehler",
@@ -132,7 +134,7 @@
"Edit Device Defaults": "Gerätevorgaben bearbeiten",
"Edit Folder": "Ordner bearbeiten",
"Edit Folder Defaults": "Ordnervorgaben bearbeiten",
"Editing {%path%}.": "Bearbeite {{path}}.",
"Editing {%path%}.": "Bearbeiten von {{path}}.",
"Enable Crash Reporting": "Absturzmeldung aktivieren",
"Enable NAT traversal": "NAT-Durchdringung aktivieren",
"Enable Relaying": "Weiterleitung aktivieren",
@@ -142,7 +144,7 @@
"Enables sending ownership information to other devices, and applying incoming ownership information. Typically requires running with elevated privileges.": "Bewirkt das Senden der Besitzinformation an andere Geräte und das Anwenden empfangener Besitzinformation. Erfordert üblicherweise die Ausführung mit höheren Zugriffsrechten.",
"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.": "Bewirkt das Senden von Besitzinformation an andere Geräte, jedoch ohne empfangene Besitzinformation anzuwenden. Kann zu einem merklichen Leistungseinbruch führen. Immer aktiviert, wenn „Besitzinformation synchronisieren“ eingeschaltet ist.",
"Enter a non-negative number (e.g., \"2.35\") and select a unit. Percentages are as part of the total disk size.": "Geben Sie eine positive Zahl ein (z. B. „2.35“) und wählen Sie eine Einheit. Prozentsätze sind Teil der gesamten Festplattengröße.",
"Enter a non-privileged port number (1024 - 65535).": "Geben Sie eine nichtprivilegierte Portnummer ein (1024 - 65535).",
"Enter a non-privileged port number (1024 - 65535).": "Geben Sie eine nicht privilegierte Portnummer ein (1024 - 65535).",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Kommagetrennte Adressen („tcp://ip:port“, „tcp://host:port“) oder „dynamic“ eingeben, um die Adresse automatisch zu ermitteln.",
"Enter ignore patterns, one per line.": "Geben Sie Ignoriermuster ein, eines pro Zeile.",
"Enter up to three octal digits.": "Tragen Sie bis zu drei oktale Ziffern ein.",
@@ -154,17 +156,17 @@
"Failed Items": "Fehlgeschlagene Elemente",
"Failed to load file versions.": "Fehler beim Laden der Dateiversionen.",
"Failed to load ignore patterns.": "Fehler beim Laden der Ignoriermuster.",
"Failed to setup, retrying": "Fehler beim Einrichten, erneuter Versuch",
"Failed to set up, retrying": "Fehler beim Einrichten, erneuter Versuch",
"Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": "Ein Verbindungsfehler zu IPv6-Servern ist zu erwarten, wenn es keine IPv6-Konnektivität gibt.",
"File Pull Order": "Dateiübertragungsreihenfolge",
"File Versioning": "Dateiversionierung",
"Files are moved to .stversions directory when replaced or deleted by Syncthing.": "Dateien werden in den .stversions Ordner verschoben, wenn sie von Syncthing ersetzt oder gelöscht wurden.",
"Files are moved to date stamped versions in a .stversions directory when replaced or deleted by Syncthing.": "Dateien werden mit Datumsstempel versioniert und in den .stversions Ordner verschoben, wenn sie von Syncthing ersetzt oder gelöscht wurden.",
"Files are moved to .stversions directory when replaced or deleted by Syncthing.": "Dateien werden in den Ordner .stversions verschoben, wenn sie von Syncthing ersetzt oder gelöscht wurden.",
"Files are moved to date stamped versions in a .stversions directory when replaced or deleted by Syncthing.": "Dateien werden mit Datumsstempel versioniert und in den Ordner .stversions verschoben, wenn sie von Syncthing ersetzt oder gelöscht wurden.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Dateien sind auf diesem Gerät schreibgeschützt. Auf diesem Gerät durchgeführte Veränderungen werden aber auf den Rest des Verbunds übertragen.",
"Files are synchronized from the cluster, but any changes made locally will not be sent to other devices.": "Dateien werden vom Verbund synchronisiert, aber lokal vorgenommene Änderungen werden nicht an andere Geräte gesendet.",
"Filesystem Watcher Errors": "Fehler im Dateisystembeobachter",
"Filter by date": "Nach Datum sortieren",
"Filter by name": "Nach Name sortieren",
"Filter by name": "Nach Namen sortieren",
"Folder": "Ordner",
"Folder ID": "Ordnerkennung",
"Folder Label": "Ordnerbezeichnung",
@@ -182,9 +184,9 @@
"GUI Authentication Password": "Passwort für Zugang zur Benutzeroberfläche",
"GUI Authentication User": "Benutzername für Zugang zur Benutzeroberfläche",
"GUI Authentication: Set User and Password": "Authentifizierung für die Benutzeroberfläche: Geben Sie Benutzer und Passwort ein.",
"GUI Listen Address": "Addresse der Benutzeroberfläche",
"GUI Listen Address": "Adresse der Benutzeroberfläche",
"GUI Override Directory": "GUI-Ersatz-Verzeichnis",
"GUI Theme": "GUI Design",
"GUI Theme": "GUI-Design",
"General": "Allgemein",
"Generate": "Generieren",
"Global Discovery": "Globale Gerätesuche",
@@ -212,7 +214,7 @@
"Introduced By": "Verteilt von",
"Introducer": "Verteilergerät",
"Introduction": "Einführung",
"Inversion of the given condition (i.e. do not exclude)": "Umkehrung der angegebenen Bedingung (d. h. schließe nicht aus)",
"Inversion of the given condition (i.e. do not exclude)": "Umkehrung der angegebenen Bedingung (d. h. nicht ausschließen)",
"Keep Versions": "Versionen erhalten",
"LDAP": "LDAP",
"Largest First": "Größte zuerst",
@@ -223,7 +225,7 @@
"Last seen": "Zuletzt online",
"Latest Change": "Letzte Änderung",
"Learn more": "Mehr erfahren",
"Learn more at {%url%}": "Erfahre mehr unter {{url}}",
"Learn more at {%url%}": "Erfahren Sie mehr unter {{url}}",
"Limit": "Limit",
"Listener Failures": "Fehler bei Listener",
"Listener Status": "Status der Listener",
@@ -233,7 +235,7 @@
"Local Additions": "Lokal hinzugefügte Elemente",
"Local Discovery": "Lokale Gerätesuche",
"Local State": "Lokaler Status",
"Local State (Total)": "Lokaler Status (Gesamt)",
"Local State (Total)": "Lokaler Status (gesamt)",
"Locally Changed Items": "Lokal geänderte Elemente",
"Log": "Protokoll",
"Log File": "Protokolldatei",
@@ -283,23 +285,23 @@
"Ownership": "Besitzinformation",
"Password": "Passwort",
"Path": "Pfad",
"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": "Pfad zum Ordner auf dem lokalen Gerät. Ordner wird erzeugt, wenn er nicht existiert. Das Tilden-Zeichen (~) kann als Abkürzung benutzt werden für",
"Path where versions should be stored (leave empty for the default .stversions directory in the shared folder).": "Pfad in dem Versionen gespeichert werden sollen (leer lassen, wenn der Standard .stversions Ordner für den geteilten Ordner verwendet werden soll).",
"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": "Pfad zum Ordner auf dem lokalen Gerät. Ordner wird erzeugt, wenn er nicht existiert. Das Tilde-Zeichen (~) kann als Abkürzung verwendet werden für",
"Path where versions should be stored (leave empty for the default .stversions directory in the shared folder).": "Pfad, in dem Versionen gespeichert werden sollen (leer lassen für den Standardordner .stversions im freigegebenen Ordner).",
"Paths": "Pfade",
"Pause": "Pause",
"Pause All": "Alles pausieren",
"Paused": "Pausiert",
"Paused (Unused)": "Pausiert (Nicht genutzt)",
"Paused (Unused)": "Pausiert (nicht genutzt)",
"Pending changes": "Ausstehende Änderungen",
"Periodic scanning at given interval and disabled watching for changes": "Periodischer Scan im angegebenen Intervall und Überwachung von Änderungen deaktiviert",
"Periodic scanning at given interval and enabled watching for changes": "Periodischer Scan im angegebenen Intervall und Überwachung von Änderungen aktiviert",
"Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:": "Periodischer Scan im angegebenen Intervall, Überwachung von Änderungen fehlgeschlagen, erneuter Versuch jede Minute:",
"Permanently add it to the ignore list, suppressing further notifications.": "Permanent zur Ignorierliste hinzufügen, um weitere Benachrichtigungen zu unterdrücken.",
"Please consult the release notes before performing a major upgrade.": "Bitte lesen Sie die Veröffentlichungshinweise bevor Sie eine Hauptversionsaktualisierung installieren.",
"Please set a GUI Authentication User and Password in the Settings dialog.": "Bitte lege einen Benutzer und ein Passwort für die Benutzeroberfläche in den Einstellungen fest.",
"Please consult the release notes before performing a major upgrade.": "Bitte lesen Sie die Veröffentlichungshinweise, bevor Sie eine Hauptversionsaktualisierung installieren.",
"Please set a GUI Authentication User and Password in the Settings dialog.": "Bitte legen Sie in den Einstellungen einen Benutzer und ein Passwort für die Benutzeroberfläche fest.",
"Please wait": "Bitte warten",
"Prefix indicating that the file can be deleted if preventing directory removal": "Präfix, das anzeigt, dass die Datei gelöscht werden kann, wenn sie die Entfernung des Ordners verhindert",
"Prefix indicating that the pattern should be matched without case sensitivity": "Präfix, das anzeigt, dass das Muster ohne Beachtung der Groß- / Kleinschreibung übereinstimmen soll",
"Prefix indicating that the pattern should be matched without case sensitivity": "Präfix, das anzeigt, dass das Muster ohne Beachtung der Groß-/Kleinschreibung abgeglichen werden soll",
"Preparing to Sync": "Vorbereiten auf die Synchronisation",
"Preview": "Vorschau",
"Preview Usage Report": "Vorschau des Nutzungsberichts",
@@ -308,7 +310,7 @@
"QUIC WAN": "QUIC WAN",
"Quick guide to supported patterns": "Schnellanleitung zu den unterstützten Mustern",
"Random": "Zufall",
"Receive Encrypted": "Empfange verschlüsselt",
"Receive Encrypted": "Verschlüsselt empfangen",
"Receive Only": "Nur empfangen",
"Received data is already encrypted": "Empfangene Daten sind bereits verschlüsselt",
"Recent Changes": "Letzte Änderungen",
@@ -362,20 +364,20 @@
"Shared With": "Geteilt mit",
"Sharing": "Teilen",
"Show ID": "Eigene Kennung",
"Show QR": "Zeige QR Code",
"Show QR": "QR-Code anzeigen",
"Show detailed discovery status": "Status der Gerätesuche anzeigen",
"Show detailed listener status": "Detaillierten Listener-Status anzeigen",
"Show diff with previous version": "Unterschied zur vorherigen Version anzeigen",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Wird anstatt der Gerätekennung im Verbund-Status angezeigt. Wird als optionaler Standardname an andere Geräte bekannt gegeben.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Wird auf diesem Gerät als Gerätename angezeigt und an die anderen Geräte im Geräte-Verbund weitergegeben. Wenn kein Gerätename angegeben wird, wird der Name des entfernten Gerätes genommen.",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Wird anstelle der Gerätekennung im Verbundstatus angezeigt. Wird anderen Geräten als optionaler Standardname bekannt gegeben.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Wird anstelle der Gerätekennung im Verbundstatus angezeigt. Wird auf den Namen aktualisiert, den das Gerät anzeigt, wenn er leer bleibt.",
"Shutdown": "Herunterfahren",
"Shutdown Complete": "Vollständig Heruntergefahren",
"Shutdown Complete": "Vollständig heruntergefahren",
"Simple": "Einfach",
"Simple File Versioning": "Einfache Dateiversionierung",
"Single level wildcard (matches within a directory only)": "Einzelnes Maskenzeichen (wird für einen einzelnen Ordner verwendet)",
"Size": "Größe",
"Smallest First": "Kleinstes zuerst",
"Some discovery methods could not be established for finding other devices or announcing this device:": "Zum Auffinden anderer Geräte, oder um dieses Gerät anzukündigen, konnten manche Methoden nicht eingerichtet werden:",
"Some discovery methods could not be established for finding other devices or announcing this device:": "Einige Erkennungsmethoden zum Auffinden anderer Geräte oder zur Meldung dieses Geräts konnten nicht eingerichtet werden:",
"Some items could not be restored:": "Einige Elemente konnten nicht wiederhergestellt werden:",
"Some listening addresses could not be enabled to accept connections:": "An manchen Netzwerkadressen kann nicht gelauscht werden, um Verbindungen anzunehmen:",
"Source Code": "Quellcode",
@@ -388,7 +390,7 @@
"Statistics": "Statistiken",
"Stay logged in": "Angemeldet bleiben",
"Stopped": "Gestoppt",
"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.": "Speichert und synchronisiert nur verschlüsselte Daten. Ordner auf allen verbundenen Geräten müssen mit dem selben Passwort eingerichtet werden oder vom Typ „{{receiveEncrypted}}“ sein.",
"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.": "Speichert und synchronisiert nur verschlüsselte Daten. Ordner auf allen verbundenen Geräten müssen mit demselben Passwort eingerichtet werden oder vom Typ „{{receiveEncrypted}}“ sein.",
"Subject:": "Betreff:",
"Support": "Support",
"Support Bundle": "Supportpaket",
@@ -396,7 +398,7 @@
"Sync Ownership": "Besitzinformation synchronisieren",
"Sync Protocol Listen Addresses": "Adresse(n) für das Synchronisierungsprotokoll",
"Sync Status": "Status der Synchronisierung",
"Syncing": "Synchronisiere",
"Syncing": "Wird synchronisiert",
"Syncthing device ID for \"{%devicename%}\"": "Syncthing-Geräte-ID für „{{devicename}}“",
"Syncthing has been shut down.": "Syncthing wurde heruntergefahren.",
"Syncthing includes the following software or portions thereof:": "Syncthing enthält die folgende Software oder Teile von:",
@@ -409,24 +411,24 @@
"Syncthing is upgrading.": "Syncthing wird aktualisiert.",
"Syncthing now supports automatically reporting crashes to the developers. This feature is enabled by default.": "Syncthing unterstützt jetzt automatische Absturzberichte an die Entwickler. Diese Funktion ist standardmäßig aktiviert.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing scheint nicht erreichbar zu sein oder es gibt ein Problem mit der Internetverbindung. Erneuter Versuch …",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing scheint ein Problem mit der Verarbeitung Deiner Eingabe zu haben. Bitte lade die Seite neu oder führe einen Neustart durch, falls das Problem weiterhin besteht.",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing scheint ein Problem mit der Verarbeitung Ihrer Eingabe zu haben. Laden Sie bitte die Seite neu oder führen einen Neustart durch, falls das Problem weiterhin besteht.",
"TCP LAN": "TCP LAN",
"TCP WAN": "TCP WAN",
"Take me back": "Führe mich zurück",
"Take me back": "Führen Sie mich zurück",
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "Die GUI-Adresse wird durch Startoptionen überschrieben. Hier vorgenommene Änderungen werden nicht wirksam, solange die Überschreibung besteht.",
"The Syncthing Authors": "Die Syncthing-Autoren",
"The Syncthing admin interface is configured to allow remote access without a password.": "Die Syncthing-Oberfläche erlaubt mit den jetzigen Einstellungen einen Zugriff ohne Passwort.",
"The aggregated statistics are publicly available at the URL below.": "Die gesammelten Statistiken sind öffentlich unter der nachfolgenden URL verfügbar.",
"The cleanup interval cannot be blank.": "Das Bereinigungsintervall darf nicht leer sein.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Die Konfiguration wurde gespeichert, aber noch nicht aktiviert. Syncthing muss neugestartet werden, um die neue Konfiguration zu übernehmen.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Die Konfiguration wurde gespeichert, aber noch nicht aktiviert. Syncthing muss neu gestartet werden, um die neue Konfiguration zu übernehmen.",
"The device ID cannot be blank.": "Die Gerätekennung darf nicht leer sein.",
"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).": "Die hier einzutragende Gerätekennung kann im Dialog „Aktionen > Kennung anzeigen“ auf dem anderen Gerät gefunden werden. Leerzeichen und Bindestriche sind optional (werden ignoriert).",
"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.": "Der verschlüsselte Nutzungsbericht wird täglich gesendet. Er wird verwendet, um Statistiken über verwendete Betriebssysteme, Ordnergrößen und Programmversionen zu erstellen. Sollte der Bericht in Zukunft weitere Daten erfassen, wird dieses Fenster erneut angezeigt.",
"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.": "Die eingegebene Gerätekennung scheint nicht gültig zu sein. Es sollte eine 52 oder 56 stellige Zeichenkette aus Buchstaben und Nummern sein. Leerzeichen und Bindestriche sind optional.",
"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.": "Die eingegebene Gerätekennung scheint nicht gültig zu sein. Es sollte eine 52- oder 56-stellige Zeichenkette aus Buchstaben und Zahlen sein. Leerzeichen und Bindestriche sind optional.",
"The folder ID cannot be blank.": "Die Ordnerkennung darf nicht leer sein.",
"The folder ID must be unique.": "Die Ordnerkennung muss eindeutig sein.",
"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.": "Der Ordnerinhalt auf anderen Geräten wird überschrieben, um mit diesem Gerät identisch zu werden. Dateien, die hier nicht vorhanden sind, werden auf anderen Geräten gelöscht.",
"The folder content on this device will be overwritten to become identical with other devices. Files newly added here will be deleted.": "Der Ordnerinhalt auf diesem Gerät wird überschrieben, um mit anderen Geräten identisch zu werden. Dateien, die hier neu hinzugefügt wurden, werden gelöscht.",
"The folder content on this device will be overwritten to become identical with other devices. Files newly added here will be deleted.": "Der Ordnerinhalt auf diesem Gerät wird überschrieben, damit er mit anderen Geräten identisch ist. Dateien, die hier neu hinzugefügt wurden, werden gelöscht.",
"The folder path cannot be blank.": "Der Ordnerpfad darf nicht leer sein.",
"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.": "Es wird in folgenden Abständen versioniert: In der ersten Stunde wird alle 30 Sekunden eine Version behalten, am ersten Tag eine jede Stunde, in den ersten 30 Tagen eine jeden Tag. Danach wird bis zum angegebenen Höchstalter eine Version pro Woche behalten.",
"The following items could not be synchronized.": "Die folgenden Elemente konnten nicht synchronisiert werden.",
@@ -435,12 +437,12 @@
"The following text will automatically be inserted into a new message.": "Der folgende Text wird automatisch in eine neue Nachricht eingefügt.",
"The following unexpected items were found.": "Die folgenden unerwarteten Elemente wurden gefunden.",
"The interval must be a positive number of seconds.": "Das Intervall muss eine positive Zahl von Sekunden sein.",
"The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.": "Das Intervall, in Sekunden, zwischen den Bereinigungen im Versionsverzeichnis. Null um das regelmäßige Bereinigen zu deaktivieren.",
"The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.": "Das Intervall in Sekunden zwischen den Bereinigungen im Versionsverzeichnis. Null, um das regelmäßige Bereinigen zu deaktivieren.",
"The maximum age must be a number and cannot be blank.": "Das Höchstalter muss angegeben werden und eine Zahl sein.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Die längste Zeit, die alte Versionen vorgehalten werden (in Tagen) (0 um alte Versionen für immer zu behalten).",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Die längste Zeit, die alte Versionen vorgehalten werden (in Tagen) (0, um alte Versionen für immer zu behalten).",
"The number of connections must be a non-negative number.": "Die Anzahl der Verbindungen muss eine nicht-negative Zahl sein.",
"The number of days must be a number and cannot be blank.": "Die Anzahl der Tage muss eine Ganzzahl sein und darf nicht leer sein.",
"The number of days to keep files in the trash can. Zero means forever.": "Dauer in Tagen für welche die Dateien aufgehoben werden sollen. 0 bedeutet für immer.",
"The number of days to keep files in the trash can. Zero means forever.": "Die Anzahl der Tage, die Dateien im Papierkorb verbleiben sollen. Null bedeutet für immer.",
"The number of old versions to keep, per file.": "Anzahl der alten Versionen, die von jeder Datei behalten werden sollen.",
"The number of versions must be a number and cannot be blank.": "Die Anzahl von Versionen muss eine Ganzzahl und darf nicht leer sein.",
"The path cannot be blank.": "Der Pfad darf nicht leer sein.",
@@ -456,16 +458,16 @@
"This Device": "Dieses Gerät",
"This Month": "Dieser Monat",
"This can easily give hackers access to read and change any files on your computer.": "Dies kann dazu führen, dass Unberechtigte relativ einfach auf Ihre Dateien zugreifen und diese ändern können.",
"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.": "Dieses Gerät kann nicht automatisch andere Geräte auffinden, oder seine eigene Adresse bekannt geben, um von anderen gefunden zu werden. Nur Geräte mit statisch konfigurierten Adressen können sich verbinden.",
"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.": "Dieses Gerät kann nicht automatisch andere Geräte auffinden oder seine eigene Adresse bekannt geben, um von anderen gefunden zu werden. Nur Geräte mit statisch konfigurierten Adressen können sich verbinden.",
"This is a major version upgrade.": "Dies ist eine Hauptversionsaktualisierung.",
"This setting controls the free space required on the home (i.e., index database) disk.": "Diese Einstellung regelt den freien Speicherplatz, der für den Systemordner (d.h. Indexdatenbank) erforderlich ist.",
"This setting controls the free space required on the home (i.e., index database) disk.": "Diese Einstellung regelt den freien Speicherplatz, der für den Systemordner (d. h. Indexdatenbank) erforderlich ist.",
"Time": "Zeit",
"Time the item was last modified": "Zeit der letzten Änderung des Elements",
"To connect with the Syncthing device named \"{%devicename%}\", add a new remote device on your end with this ID:": "Um sich mit dem Syncthing-Gerät namens „{{devicename}}“ zu verbinden, fügen Sie ein neues Gerät mit dieser ID hinzu:",
"To permit a rule, have the checkbox checked. To deny a rule, leave it unchecked.": "Zum Erlauben einer Regel Häkchen setzen. Zum Verweigern einer Regel frei lassen.",
"Today": "Heute",
"Trash Can": "Papierkorb",
"Trash Can File Versioning": "Papierkorb Dateiversionierung",
"Trash Can File Versioning": "Papierkorb-Dateiversionierung",
"Type": "Typ",
"UNIX Permissions": "UNIX-Berechtigungen",
"Unavailable": "Nicht verfügbar",
@@ -491,7 +493,7 @@
"Use notifications from the filesystem to detect changed items.": "Benachrichtigungen des Dateisystems nutzen, um Änderungen zu erkennen.",
"User": "Benutzer",
"User Home": "Benutzer-Stammverzeichnis",
"Username/Password has not been set for the GUI authentication. Please consider setting it up.": "Benutzername / Passwort wurde für die Benutzeroberfläche nicht gesetzt. Bitte erwägen Sie dies einzurichten.",
"Username/Password has not been set for the GUI authentication. Please consider setting it up.": "Benutzername / Passwort wurde für die Benutzeroberfläche nicht gesetzt. Bitte erwägen Sie, dies einzurichten.",
"Using a QUIC connection over LAN": "Verwendet eine QUIC-Verbindung über LAN",
"Using a QUIC connection over WAN": "Verwendet eine QUIC-Verbindung über WAN",
"Using a direct TCP connection over LAN": "Verwendet eine direkte TCP-Verbindung über LAN",
@@ -512,8 +514,8 @@
"Watch for Changes": "Änderungen überwachen",
"Watching for Changes": "Überwachung von Änderungen",
"Watching for changes discovers most changes without periodic scanning.": "Das Überwachen von Änderungen entdeckt die meisten Änderungen ohne regelmäßiges Scannen.",
"When adding a new device, keep in mind that this device must be added on the other side too.": "Beachte beim Hinzufügen eines neuen Gerätes, dass dieses Gerät auch auf den anderen Geräten hinzugefügt werden muss.",
"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.": "Beachte bitte beim Hinzufügen eines neuen Ordners, dass die Ordnerkennung dazu verwendet wird, Ordner zwischen Geräten zu verbinden. Die Kennung muss also auf allen Geräten gleich sein, die Groß- und Kleinschreibung muss dabei beachtet werden.",
"When adding a new device, keep in mind that this device must be added on the other side too.": "Beachten Sie beim Hinzufügen eines neuen Gerätes, dass dieses Gerät auch auf den anderen Geräten hinzugefügt werden muss.",
"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.": "Beachten Sie bitte beim Hinzufügen eines neuen Ordners, dass die Ordnerkennung dazu verwendet wird, Ordner zwischen Geräten zu verbinden. Die Kennung muss also auf allen Geräten gleich sein, die Groß- und Kleinschreibung muss dabei beachtet werden.",
"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.": "Wenn auf beiden Geräten der Wert höher als eins eingestellt ist, versucht Syncthing, mehrere gleichzeitige Verbindungen herzustellen. Wenn die Werte unterschiedlich sind, wird der höchste Wert verwendet. Den Wert auf Null setzen, um Syncthing entscheiden zu lassen.",
"Yes": "Ja",
"Yesterday": "Gestern",
@@ -524,8 +526,8 @@
"You have no ignored devices.": "Sie haben keine ignorierten Geräte.",
"You have no ignored folders.": "Sie haben keine ignorierten Ordner.",
"You have unsaved changes. Do you really want to discard them?": "Sie haben nicht gespeicherte Änderungen. Wollen Sie diese wirklich verwerfen?",
"You must keep at least one version.": "Du musst mindestens eine Version behalten.",
"You should never add or change anything locally in a \"{%receiveEncrypted%}\" folder.": "Sie sollten nie etwas im „{{receiveEncrypted}}“ Ordner lokal ändern oder hinzufügen.",
"You must keep at least one version.": "Sie müssen zumindest eine Version behalten.",
"You should never add or change anything locally in a \"{%receiveEncrypted%}\" folder.": "Sie sollten nie etwas im Ordner „{{receiveEncrypted}}“ lokal ändern oder hinzufügen.",
"Your SMS app should open to let you choose the recipient and send it from your own number.": "Ihre SMS-App sollte sich öffnen, damit Sie den Empfänger auswählen und die Nachricht von Ihrer eigenen Nummer aus versenden können.",
"Your email app should open to let you choose the recipient and send it from your own address.": "Ihre E-Mail-App sollte sich öffnen, damit Sie den Empfänger auswählen und die Nachricht von Ihrer eigenen Adresse aus versenden können.",
"days": "Tage",

View File

@@ -154,7 +154,7 @@
"Failed Items": "Αρχεία που απέτυχαν",
"Failed to load file versions.": "Η φόρτωση των εκδόσεων αρχείων απέτυχε.",
"Failed to load ignore patterns.": "Αποτυχία φόρτωσης μοτίβων παράβλεψης.",
"Failed to setup, retrying": "Αποτυχία ενεργοποίησης, γίνεται νέα προσπάθεια",
"Failed to set up, retrying": "Αποτυχία ενεργοποίησης, γίνεται νέα προσπάθεια",
"Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": "Είναι φυσιολογική η αποτυχία σύνδεσης σε εξυπηρετητές IPv6 όταν δεν υπάρχει συνδεσιμότητα IPv6.",
"File Pull Order": "Σειρά με την οποία θα κατεβαίνουν τα αρχεία",
"File Versioning": "Τήρηση εκδόσεων αρχείων",

View File

@@ -150,7 +150,7 @@
"Failed Items": "Failed Items",
"Failed to load file versions.": "Failed to load file versions.",
"Failed to load ignore patterns.": "Failed to load ignore patterns.",
"Failed to setup, retrying": "Failed to setup, retrying",
"Failed to set up, retrying": "Failed to set up, retrying",
"Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": "Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.",
"File Pull Order": "File Pull Order",
"File Versioning": "File Versioning",

View File

@@ -27,6 +27,7 @@
"Allowed Networks": "Allowed Networks",
"Alphabetic": "Alphabetic",
"Altered by ignoring deletes.": "Altered by ignoring deletes.",
"Always turned on when the folder type is \"{%foldertype%}\".": "Always turned on when the folder type is \"{{foldertype}}\".",
"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.": "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 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?",
@@ -52,6 +53,7 @@
"Body:": "Body:",
"Bugs": "Bugs",
"Cancel": "Cancel",
"Cannot be enabled when the folder type is \"{%foldertype%}\".": "Cannot be enabled when the folder type is \"{{foldertype}}\".",
"Changelog": "Changelog",
"Clean out after": "Clean out after",
"Cleaning Versions": "Cleaning Versions",
@@ -154,7 +156,7 @@
"Failed Items": "Failed Items",
"Failed to load file versions.": "Failed to load file versions.",
"Failed to load ignore patterns.": "Failed to load ignore patterns.",
"Failed to setup, retrying": "Failed to setup, retrying",
"Failed to set up, retrying": "Failed to set up, retrying",
"Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": "Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.",
"File Pull Order": "File Pull Order",
"File Versioning": "File Versioning",

View File

@@ -27,6 +27,7 @@
"Allowed Networks": "Allowed Networks",
"Alphabetic": "Alphabetic",
"Altered by ignoring deletes.": "Altered by ignoring deletes.",
"Always turned on when the folder type is \"{%foldertype%}\".": "Always turned on when the folder type is \"{{foldertype}}\".",
"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.": "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 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?",
@@ -52,6 +53,7 @@
"Body:": "Body:",
"Bugs": "Bugs",
"Cancel": "Cancel",
"Cannot be enabled when the folder type is \"{%foldertype%}\".": "Cannot be enabled when the folder type is \"{{foldertype}}\".",
"Changelog": "Changelog",
"Clean out after": "Clean out after",
"Cleaning Versions": "Cleaning Versions",
@@ -154,7 +156,7 @@
"Failed Items": "Failed Items",
"Failed to load file versions.": "Failed to load file versions.",
"Failed to load ignore patterns.": "Failed to load ignore patterns.",
"Failed to setup, retrying": "Failed to setup, retrying",
"Failed to set up, retrying": "Failed to set up, retrying",
"Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": "Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.",
"File Pull Order": "File Pull Order",
"File Versioning": "File Versioning",

View File

@@ -106,7 +106,7 @@
"Error": "Eraro",
"External File Versioning": "Ekstera Versionado de Dosiero",
"Failed Items": "Malsukcesaj Eroj",
"Failed to setup, retrying": "Malsukcesis agordi, provante denove",
"Failed to set up, retrying": "Malsukcesis agordi, provante denove",
"Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": "Malsukceso por konekti al IPv6 serviloj atendante se ekzistas neniu IPv6 konektebleco.",
"File Pull Order": "Ordo por Tiri Dosieron",
"File Versioning": "Versionado de Dosieroj",

View File

@@ -11,7 +11,7 @@
"Add Device": "Agregar el dispositivo",
"Add Folder": "Agregar Carpeta",
"Add Remote Device": "Añadir un dispositivo remoto",
"Add devices from the introducer to our device list, for mutually shared folders.": "Añadir dispositivos desde el introductor a nuestra lista de dispositivos, para las carpetas compartidas mutuamente.",
"Add devices from the introducer to our device list, for mutually shared folders.": "Añadir dispositivos del presentador a nuestra lista de dispositivos para las carpetas compartidas mutuamente.",
"Add filter entry": "Añadir una entrada al filtro",
"Add ignore patterns": "Agregar patrones a ignorar",
"Add new folder?": "¿Agregar una carpeta nueva?",
@@ -29,7 +29,7 @@
"Altered by ignoring deletes.": "Alterado ignorando eliminaciones.",
"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.": "Un comando externo maneja las versiones. Tienes que eliminar el archivo de la carpeta compartida. Si la ruta a la aplicación contiene espacios, ésta debe estar entre comillas.",
"Anonymous Usage Reporting": "Informe anónimo de uso",
"Anonymous usage report format has changed. Would you like to move to the new format?": "El formato del informe de uso anónimo a cambiado. ¿Desearía usar el nuevo formato?",
"Anonymous usage report format has changed. Would you like to move to the new format?": "El formato del informe de uso anónimo a cambiado. ¿Le gustaría pasar al nuevo formato?",
"Applied to LAN": "Aplicado a la LAN",
"Apply": "Solicitar",
"Are you sure you want to override all remote changes?": "¿Está seguro(a) de que desea sobreescribir todos los cambios remotos?",
@@ -131,7 +131,7 @@
"Edit Device": "Editar Dispositivo",
"Edit Device Defaults": "Editar Valores Predeterminados del Dispositivo",
"Edit Folder": "Editar Carpeta",
"Edit Folder Defaults": "Editar Valores Predeterminados de la Carpeta",
"Edit Folder Defaults": "Editar Valores Predeterminados de las Carpeta",
"Editing {%path%}.": "Editando {{path}}.",
"Enable Crash Reporting": "Activar Informes de Fallos",
"Enable NAT traversal": "Permitir NAT transversal",
@@ -154,7 +154,7 @@
"Failed Items": "Elementos fallidos",
"Failed to load file versions.": "Error al cargar las versiones de los archivos.",
"Failed to load ignore patterns.": "No se pudieron cargar los patrones de ignorar.",
"Failed to setup, retrying": "Fallo en la configuración, reintentando",
"Failed to set up, retrying": "Fallo en la configuración, reintentando",
"Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": "Se espera un fallo al conectar a los servidores IPv6 si no hay conectividad IPv6.",
"File Pull Order": "Orden de Obtención de los Archivos",
"File Versioning": "Versionado de ficheros",
@@ -269,7 +269,7 @@
"No upgrades": "Sin actualizaciones",
"Not shared": "No Compartido(a)",
"Notice": "Aviso",
"Number of Connections": "Número de las conexiones",
"Number of Connections": "Número de conexiones",
"OK": "De acuerdo",
"Off": "Desactivado",
"Oldest First": "El más antiguo primero",
@@ -532,7 +532,7 @@
"deleted": "eliminado",
"deny": "denegar",
"directories": "directorios",
"file": "expediente",
"file": "fichero",
"files": "archivos",
"folder": "carpeta",
"full documentation": "Documentación completa",

View File

@@ -9,9 +9,15 @@
"Add Folder": "Lisa kaust",
"Add new folder?": "Lisa uus kaust?",
"Address": "Aadress",
"Addresses": "Aadressid",
"All Data": "Kõik andmed",
"All Time": "Kõik ajad",
"Allowed Networks": "Lubatud võrgud",
"Alphabetic": "Tähestikuline",
"Automatic upgrades": "Automaatsed uuendused",
"Be careful!": "Ettevaatust!",
"Cancel": "Loobu",
"Changelog": "Muudatuste nimekiri",
"Close": "Sulge",
"Configured": "Seadistatud",
"Connection Error": "Ühenduse viga",

View File

@@ -130,7 +130,7 @@
"External File Versioning": "Fitxategi bertsioen kanpoko kudeaketa",
"Failed Items": "Huts egin duten fitxategiak",
"Failed to load ignore patterns.": "Huts egin du baztertze ereduak kargatzean.",
"Failed to setup, retrying": "Konfigurazioan huts egitea, berriro saiatuz",
"Failed to set up, retrying": "Konfigurazioan huts egitea, berriro saiatuz",
"Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": "IPv6 zerbitzariei buruzko konexioak huts eginen du, IPv6 konektibitaterik ez bada",
"File Pull Order": "Fitxategiak berreskuratzeko ordena",
"File Versioning": "Fitxategiak zaintzeko metodoa",

View File

@@ -122,7 +122,7 @@
"External": "Ulkoinen",
"External File Versioning": "Ulkoinen tiedostoversionti",
"Failed Items": "Epäonnistuneet kohteet",
"Failed to setup, retrying": "Käyttöönotto epäonnistui, Yritetään uudelleen",
"Failed to set up, retrying": "Käyttöönotto epäonnistui, Yritetään uudelleen",
"Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": "Yhteys IPv6-palvelimiin todennäköisesti epäonnistuu, koska IPv6-yhteyksiä ei ole.",
"File Pull Order": "Tiedostojen noutojärjestys",
"File Versioning": "Tiedostoversiointi",

View File

@@ -1,7 +1,7 @@
{
"A device with that ID is already added.": "Nadagdag na ang device na may ganitong ID.",
"A device with that ID is already added.": "May naidagdag na device na may ganitong ding ID.",
"A negative number of days doesn't make sense.": "Walang saysay ang negatibong numero ng araw.",
"A new major version may not be compatible with previous versions.": "Maaring hindi compatible ang isang bagong major na beryson sa mga kasalukuyang bersyon.",
"A new major version may not be compatible with previous versions.": "Maaring hindi compatible ang isang bagong major na beryson sa mga nakaraang bersyon.",
"API Key": "API Key",
"About": "Tungkol sa",
"Action": "Aksyon",
@@ -27,6 +27,7 @@
"Allowed Networks": "Mga Pinapayagang Network",
"Alphabetic": "Alpabetiko",
"Altered by ignoring deletes.": "Binago sa pamamagitan ng hindi pagpansin sa mga pagtanggal.",
"Always turned on when the folder type is \"{%foldertype%}\".": "Palaging nakabukas kung ang uri ng folder ay nakatakda bilang \"{{foldertype}}\".",
"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.": "Pinapamahala ng external na command ang file versioning. Kailangan nitong tanggalin ang file mula sa binabahaging folder. Kung may mga space ang path sa application, kailangan itong i-quote.",
"Anonymous Usage Reporting": "Anonymous na Pag-uulat ng Paggamit",
"Anonymous usage report format has changed. Would you like to move to the new format?": "Nagbago ang pormat ng anonymous na ulat ng paggamit. Gusto mo bang lumipat sa bagong pormat?",
@@ -52,6 +53,7 @@
"Body:": "Body:",
"Bugs": "Mga Bug",
"Cancel": "Kanselahin",
"Cannot be enabled when the folder type is \"{%foldertype%}\".": "Hindi maaaring paganahin kapag ang uri ng folder ay \"{{foldertype}}\".",
"Changelog": "Mga Pagbabago",
"Clean out after": "Linisin pagkatapos",
"Cleaning Versions": "Mga Bersyon ng Paglinis",
@@ -154,7 +156,7 @@
"Failed Items": "Mga Nabigong Item",
"Failed to load file versions.": "Nabigong i-load ang mga bersyon ng file.",
"Failed to load ignore patterns.": "Nabigong i-load ang mga ignore pattern.",
"Failed to setup, retrying": "Nabigong i-set up, sinusubukan muli",
"Failed to set up, retrying": "Nabigong i-set up, sinusubukan muli",
"Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": "Inaasahan ang pagbigo sa pagkonekta sa mga IPv6 na server kapag walang konektibidad sa IPv6.",
"File Pull Order": "Order ng Pagkuha ng File",
"File Versioning": "File Versioning",
@@ -311,7 +313,7 @@
"Receive Encrypted": "Makatanggap Naka-Encrypt",
"Receive Only": "Makatanggap Lamang",
"Received data is already encrypted": "Naka-encrypt na ang natanggap na data",
"Recent Changes": "Mga Kamakilang Pagbabago",
"Recent Changes": "Mga Kamakailang Pagbabago",
"Reduced by ignore patterns": "Binabawasan ng mga ignore pattern",
"Relay LAN": "Relay na LAN",
"Relay WAN": "Relay na WAN",
@@ -403,7 +405,7 @@
"Syncthing is Free and Open Source Software licensed as MPL v2.0.": "Ang Syncthing ay Libre at Open Source na Software na nakalisensya sa 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.": "Ang Syncthing ay isang continuous na file synchronization na program. Sini-synchronize nito ang mga file sa pagitan ng dalawa o higit pang mga computer sa totoong oras, ligtas na protektado mula sa prying na mata. Ang iyong data ay iyong data at nararapat kang pumili kung saan sila ilalagay, kung binabahagi ito sa third party, at kung paano ito pinapadala sa Internet.",
"Syncthing is listening on the following network addresses for connection attempts from other devices:": "Nakikinig ang Syncthing sa mga sumusunod na network address para sa mga tangka sa koneksyon mula sa ibang device:",
"Syncthing is not listening for connection attempts from other devices on any address. Only outgoing connections from this device may work.": "Hindi nakikinig ang Syncthing sa mga tangka sa koneksyon mula sa ibang mga device sa anumang address. Ang mga palabas na koneksyon lamang ay maaring gumana.",
"Syncthing is not listening for connection attempts from other devices on any address. Only outgoing connections from this device may work.": "Hindi nakikinig ang Syncthing sa mga tangka sa koneksyon mula sa ibang mga device sa anumang address. Ang mga palabas na koneksyon lamang ay maaaring gumana.",
"Syncthing is restarting.": "Nagre-restart ang Syncthing.",
"Syncthing is saving changes.": "Nagse-save ng mga pagbabago ang Syncthing.",
"Syncthing is upgrading.": "Naga-upgrade ang Syncthing.",
@@ -417,17 +419,17 @@
"The Syncthing Authors": "Ang Mga Awtor ng Syncthing",
"The Syncthing admin interface is configured to allow remote access without a password.": "Naka-configure ang Syncthing admin interface na payagan ang remote access nang walang password.",
"The aggregated statistics are publicly available at the URL below.": "Available nang publiko ang pinagsama-samang istatistika sa URL sa ibaba.",
"The cleanup interval cannot be blank.": "Hindi maaring walang laman ang pagitan ng paglinis.",
"The cleanup interval cannot be blank.": "Hindi maaaring walang laman ang pagitan ng paglinis.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Na-save na ang configuration ngunit hindi naka-activate. Kailangang mag-restart ang Syncthing para i-activate ang bagong configuration.",
"The device ID cannot be blank.": "Hindi maaring walang laman ang Device ID.",
"The device ID cannot be blank.": "Hindi maaaring walang laman ang Device 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).": "Mahahanap ang device ID na ilalagay dito sa \"Mga Aksyon > Ipakita ang ID\" na dialog sa isa pang device. Opsyonal ang mga puwang at gitling (hindi pinapansin).",
"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.": "Araw-araw na pinapadala ang naka-encrypt na ulat sa paggamit. Ginagamit ito sa pag-track ng mga karaniwang platform, laki ng folder, at bersyon ng app. Kapag nabago ang tinakdang data ng ulat ipo-prompt kang muli ng dialog na ito.",
"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.": "Mukhang hindi angkop ang inilagay na device ID. Dapat itong 52 o 56 na character na string na binubuo ng mga titik at numero, na may mga puwang at gitling bilang opsyonal.",
"The folder ID cannot be blank.": "Hindi maaring walang laman ang folder ID.",
"The folder ID cannot be blank.": "Hindi maaaring walang laman ang folder ID.",
"The folder ID must be unique.": "Kailangang kakaiba ang folder 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.": "Io-overwrite ang nilalaman ng folder sa mga ibang device para maging magkapareho sa device na ito. Ang mga file na hindi nandito ay buburahin sa mga ibang device.",
"The folder content on this device will be overwritten to become identical with other devices. Files newly added here will be deleted.": "Io-overwrite ang nilalaman ng folder sa mga ibang device para maging magkapareho sa device na ito. Ang mga file na kamakilang dinagdag dito ay buburahin sa mga ibang device.",
"The folder path cannot be blank.": "Hindi maaring walang laman ang path ng folder.",
"The folder path cannot be blank.": "Hindi maaaring walang laman ang path ng folder.",
"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.": "Ang mga sumusunod na pagitan ay ginagamit: sa unang oras ang isang bersyon ay pinapanatili bawat 30 segundo, sa unang araw ang isang bersyon ay pinapanatili bawat oras, sa unang 30 araw ang isang bersyon ay pinapanatili bawat araw, hanggang sa pinakamataas na edad ang isang bersyon ay pinapanatili bawat linggo.",
"The following items could not be synchronized.": "Hindi ma-synchronize ang mga sumusunod na item.",
"The following items were changed locally.": "Binago ng lokal ang mga sumusunod na item.",
@@ -436,14 +438,14 @@
"The following unexpected items were found.": "Nahanap ang mga sumusunod na hindi inaasahang item.",
"The interval must be a positive number of seconds.": "Dapat positibong numero ng segundo ang pagitan.",
"The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.": "Ang pagitan, bilang segundo, para sa pagtakbo ng paglinis sa versions na direktoryo. Sero para i-disable ang periodical na paglinis.",
"The maximum age must be a number and cannot be blank.": "Dapat numero ang pinakamataas na edad at hindi maaring walang laman.",
"The maximum age must be a number and cannot be blank.": "Dapat numero ang pinakamataas na edad at hindi maaaring walang laman.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Ang pinakamataas na oras para panatilihin ang bersyon (bilang araw, itakda sa 0 para panatilihin ang mga bersyon magpakailanman).",
"The number of connections must be a non-negative number.": "Dapat hindi negatibong numero ang bilang ng mga koneksyon.",
"The number of days must be a number and cannot be blank.": "Dapat numero ang bilang ng araw at hindi maaring walang laman.",
"The number of days must be a number and cannot be blank.": "Dapat numero ang bilang ng araw at hindi maaaring walang laman.",
"The number of days to keep files in the trash can. Zero means forever.": "Ang bilang ng araw para panatilihin ang mga file sa basurahan. Ang sero ay ibig sabihin ay magpakailanman.",
"The number of old versions to keep, per file.": "Ang bilang ng mga lumang bersyon na dapat panatilihin, bawat file.",
"The number of versions must be a number and cannot be blank.": "Dapat numero ang bilang ng mga bersyon at hindi maaring walang laman.",
"The path cannot be blank.": "Hindi maaring walang laman ang path.",
"The number of versions must be a number and cannot be blank.": "Dapat numero ang bilang ng mga bersyon at hindi maaaring walang laman.",
"The path cannot be blank.": "Hindi maaaring walang laman ang path.",
"The rate limit is applied to the accumulated traffic of all connections to this device.": "Ina-apply ang rate limit sa naipon na traffic ng lahat ng mga koneksyon sa device na ito.",
"The rate limit must be a non-negative number (0: no limit)": "Dapat hindi negatibong numero ang rate limit (0: walang limitasyon)",
"The remote device has not accepted sharing this folder.": "Hindi tinanggap ng remote device ang pagbahagi ng folder na ito.",

View File

@@ -27,6 +27,7 @@
"Allowed Networks": "Réseaux autorisés",
"Alphabetic": "Alphabétique",
"Altered by ignoring deletes.": "Protégé par \"Ignore Delete\".",
"Always turned on when the folder type is \"{%foldertype%}\".": "Toujours activé pour le type de partage \"{{foldertype}}\".",
"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.": "Une commande externe gère les versions de fichiers. Il lui incombe de supprimer les fichiers du répertoire partagé. Si le chemin contient des espaces, il doit être spécifié entre guillemets.",
"Anonymous Usage Reporting": "Rapport anonyme de statistiques d'utilisation",
"Anonymous usage report format has changed. Would you like to move to the new format?": "Le format du rapport anonyme d'utilisation a changé. Voulez-vous passer au nouveau format ?",
@@ -52,6 +53,7 @@
"Body:": "Corps du message :",
"Bugs": "Bogues",
"Cancel": "Annuler",
"Cannot be enabled when the folder type is \"{%foldertype%}\".": "Ne peut être activé pour le type de partage \"{{foldertype}}\".",
"Changelog": "Historique des versions",
"Clean out after": "Conserver pendant",
"Cleaning Versions": "Purge des versions",
@@ -154,7 +156,7 @@
"Failed Items": "Éléments en échec",
"Failed to load file versions.": "Échec de chargement des versions de fichiers.",
"Failed to load ignore patterns.": "Échec du chargement des masques d'exclusions.",
"Failed to setup, retrying": "Échec, nouvel essai",
"Failed to set up, retrying": "Échec, nouvel essai",
"Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": "La connexion aux serveurs en IPv6 va échouer s'il n'y a pas de connectivité IPv6.",
"File Pull Order": "Ordre de récupération des fichiers",
"File Versioning": "Préservation des fichiers",

View File

@@ -146,7 +146,7 @@
"Error": "Flater",
"External File Versioning": "Ekstern ferzjebehear foar triemen",
"Failed Items": "Mislearre items",
"Failed to setup, retrying": "Ynskeakeljen mislearre, wurd no opnij besocht",
"Failed to set up, retrying": "Ynskeakeljen mislearre, wurd no opnij besocht",
"Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": "Mislearjen fan it ferbinen mei IPv6-tsjinners wurd ferwachte as der gjin stipe foar IPv6-ferbinings is.",
"File Pull Order": "Triemlûkfolchoarder",
"File Versioning": "Triemferzjebehear",

View File

@@ -27,6 +27,7 @@
"Allowed Networks": "Líonraí Ceadaithe",
"Alphabetic": "Aibítreach",
"Altered by ignoring deletes.": "Athraithe trí neamhaird a dhéanamh ar scriosadh.",
"Always turned on when the folder type is \"{%foldertype%}\".": "Cuirtear ar siúl i gcónaí é nuair is é \"{{foldertype}}\" an cineál fillteáin.",
"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?",
@@ -52,6 +53,7 @@
"Body:": "Comhlacht:",
"Bugs": "Fabhtanna",
"Cancel": "Cuir ar ceal",
"Cannot be enabled when the folder type is \"{%foldertype%}\".": "Ní féidir é a chumasú nuair is é \"{{foldertype}}\" an cineál fillteáin.",
"Changelog": "ChangelogName",
"Clean out after": "Glan amach tar éis",
"Cleaning Versions": "Leaganacha Glantacháin",
@@ -154,7 +156,7 @@
"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",
"Failed to set up, 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",

View File

@@ -6,6 +6,7 @@
"About": "Sobre",
"Action": "Acción",
"Actions": "Accións",
"Active filter rules": "Regras de filtrado activas",
"Add": "Engadir",
"Add Device": "Engadir dispositivo",
"Add Folder": "Engadir cartafol",
@@ -25,6 +26,7 @@
"Allow Anonymous Usage Reporting?": "Permitir o informe de uso anónimo?",
"Allowed Networks": "Redes permitidas",
"Alphabetic": "Alfabética",
"Altered by ignoring deletes.": "Cambiado por ignorar o borrado.",
"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.": "Un comando externo xestiona as versións. Ten que eliminar o ficheiro do cartafol compartido. Si a ruta ao aplicativo contén espazos, deberían ir acotados.",
"Anonymous Usage Reporting": "Informe anónimo de uso",
"Anonymous usage report format has changed. Would you like to move to the new format?": "O formato do informe de uso anónimo cambiou. Quere usar o novo formato?",
@@ -37,21 +39,26 @@
"Are you sure you want to restore {%count%} files?": "Está seguro de que desexa restaurar {{count}} ficheiros?",
"Are you sure you want to revert all local changes?": "Está seguro de que quere reverter todos os cambios locais?",
"Are you sure you want to upgrade?": "Está seguro de que desexa actualizar?",
"Authentication Required": "Autenticación Necesaria",
"Authors": "Autores",
"Auto Accept": "Aceptar automaticamente",
"Automatic Crash Reporting": "Informe Automático de Erros",
"Automatic upgrade now offers the choice between stable releases and release candidates.": "Agora a actualización automática permite escoller entre versións estables e versións candidatas.",
"Automatic upgrades": "Actualizacións automáticas",
"Automatic upgrades are always enabled for candidate releases.": "As actualizacións automáticas sempre están activadas para versións candidatas.",
"Automatically create or share folders that this device advertises at the default path.": "Crear ou compartir automaticamente os cartafoles na ruta predeterminada que este dispositivo anuncia.",
"Available debug logging facilities:": "Ferramentas de depuración dispoñibles:",
"Be careful!": "Teña coidado!",
"Body:": "Corpo:",
"Bugs": "Erros",
"Cancel": "Cancelar",
"Changelog": "Rexistro de cambios",
"Clean out after": "Limpar despois",
"Cleaning Versions": "Limpando Versións",
"Cleanup Interval": "Intervalo de Limpeza",
"Click to see full identification string and QR code.": "Faga clic para ver a cadea de identificación completa e o código QR.",
"Close": "Pechar",
"Command": "Orde",
"Comment, when used at the start of a line": "Comentar, cando se usa ao inicio dunha liña",
"Compression": "Compresión",
"Configuration Directory": "Directorio de Configuración",
@@ -69,13 +76,17 @@
"Copied!": "Copiado!",
"Copy": "Copiar",
"Copy failed! Try to select and copy manually.": "Fallou a copia! Probe a seleccionar e copiar manualmente.",
"Currently Shared With Devices": "Compartido actualmente cos dispositivos",
"Custom Range": "Rango personalizado",
"Danger!": "Perigo!",
"Database Location": "Localización da Base de Datos",
"Debugging Facilities": "Ferramentas de depuración",
"Default": "Predeterminado",
"Default Configuration": "Configuración Predeterminada",
"Default Device": "Dispositivo Predeterminado",
"Default Folder": "Cartafol Predeterminado",
"Default Ignore Patterns": "Patróns de Ignorado Predeterminados",
"Defaults": "Predeterminados",
"Delete": "Eliminar",
"Delete Unexpected Items": "Eliminar os Ítems Inesperados",
"Deleted {%file%}": "Eliminado {{file}}",
@@ -89,10 +100,16 @@
"Device Identification": "Identificación do dispositivo",
"Device Name": "Nome do dispositivo",
"Device Status": "Estado do dispositivo",
"Device is untrusted, enter encryption password": "Sen confianza no dispositivo, escribir contrasinal para cifrar",
"Device rate limits": "Límites do dispositivo",
"Device that last modified the item": "Dispositivo que modificou o elemento por última vez",
"Devices": "Dispositivos",
"Disable Crash Reporting": "Desactivar o Informe de Erros",
"Disabled": "Deshabilitado",
"Disabled periodic scanning and disabled watching for changes": "Desactivaronse o escaneo periódico e o control de cambios",
"Disabled periodic scanning and enabled watching for changes": "Desactivouse o escaneo periódico e activouse o control de cambios",
"Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:": "Desactivouse o escaneo periódico e fallou establecer a vixilancia de cambios, reintento cada minuto:",
"Disables comparing and syncing file permissions. Useful on systems with nonexistent or custom permissions (e.g. FAT, exFAT, Synology, Android).": "Desactiva a comparación e sincronización dos permisos dos ficheiros. Útil en sistemas que non teñen ou usan permisos personalizados (ex. FAT, exFAT, Synology, Android).",
"Discard": "Descartar",
"Disconnected": "Desconectado",
"Disconnected (Inactive)": "Desconectado (Inactivo)",
@@ -102,7 +119,9 @@
"Discovery Failures": "Erros de Descubrimento",
"Discovery Status": "Estado do Descubrimento",
"Dismiss": "Descartar",
"Do not add it to the ignore list, so this notification may recur.": "Non engadilo á lista de ignorados, para que esta notificación poida repetirse.",
"Do not restore": "Non restaurar",
"Do not restore all": "Non restablecer todo",
"Do you want to enable watching for changes for all your folders?": "Quere habilitar o control de cambios para todos os seus cartafois?",
"Documentation": "Documentación",
"Download Rate": "Velocidade de Descarga",
@@ -110,11 +129,17 @@
"Downloading": "Descargando",
"Edit": "Editar",
"Edit Device": "Editar o Dispositivo",
"Edit Device Defaults": "Editar predeterminados do dispositivo",
"Edit Folder": "Editar o Cartafol",
"Edit Folder Defaults": "Editar predeterminados dos cartafoles",
"Editing {%path%}.": "Editando {{path}}.",
"Enable Crash Reporting": "Activar informar dos fallos",
"Enable NAT traversal": "Habilitar o NAT traversal",
"Enable Relaying": "Habilitar Relevos",
"Enabled": "Habilitado",
"Enables sending extended attributes to other devices, and applying incoming extended attributes. May require running with elevated privileges.": "Activa o envío de atributos extendidos a outros dispositivos, e aplicar os atributos extendidos recibidos. Podería requerir a execución con privilexios elevados.",
"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.": "Activa o envío de atributos extendidos a outros dispositivos, pero non aplica atributos extendidos que se reciben. Isto podería afectar significativamente ao rendemento. Sempre está activado cando «Sincr Atributos Extendidos\" está activado.",
"Enables sending ownership information to other devices, and applying incoming ownership information. Typically requires running with elevated privileges.": "Activa o envío de información sobre a propiedade a outros dispositivos, e aplica a información sobre a propiedade cando se recibe. Normalmente require a execución con privilexios elevados.",
"Enter a non-negative number (e.g., \"2.35\") and select a unit. Percentages are as part of the total disk size.": "Introduza un número non negativo (por exemplo, \"2.35\") e seleccione unha unidade. As porcentaxes son como partes totais do tamaño do disco.",
"Enter a non-privileged port number (1024 - 65535).": "Introduza un número de porto non privilexiado (1024-65535).",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Introduza direccións separadas por comas (\"tcp://ip:porto\", \"tcp://host:porto\") ou \"dynamic\" para realizar o descubrimento automático da dirección.",
@@ -125,8 +150,14 @@
"Extended Attributes Filter": "Filtro de Atributos Estendidos",
"External": "Externo",
"External File Versioning": "Versionado de Fichiro Externo",
"Failed Items": "Elmentos fallados",
"Failed to load file versions.": "Fallou a carga das versións dos ficheiros.",
"Failed to load ignore patterns.": "Fallou a carga de patróns ignorados.",
"Failed to set up, retrying": "Fallou a configuración, reintentando",
"Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": "É de agardar o fallo ao conectar con servidores IPv6 se non hai conexión por IPv6.",
"File Pull Order": "Orde de Obtención de Arquivos",
"File Versioning": "Versionado de Ficheiros",
"Files are moved to .stversions directory when replaced or deleted by Syncthing.": "Os ficheiros móvense ao directorio .stversions cando se substitúen ou eleminan con Syncthing.",
"Filter by date": "FIltrar por data",
"Filter by name": "Filtrar por nome",
"Folder": "Cartafol",

View File

@@ -1,114 +1,555 @@
{
"A device with that ID is already added.": "כבר נוסף התקן עם המזהה הזה.",
"A negative number of days doesn't make sense.": "מספר שלילי של ימים אינו הגיוני.",
"A new major version may not be compatible with previous versions.": "ייתכן שגרסה עיקרית חדשה לא תהיה תואמת לגרסאות קודמות.",
"API Key": "מפתח API",
"About": "על אודות",
"About": "אודות",
"Action": "פעולה",
"Actions": "פעולות",
"Add": "הוספה",
"Add Device": "הוספת התקן",
"Add Folder": "הוספת תיקיה",
"Add Remote Device": "הוספת התקן מרוחק",
"Add new folder?": "להוסיף תיקיה חדשה?",
"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 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, כלומר ברירת מחדל חדשה של שעה). אתה יכול גם להגדיר אותו ידנית עבור כל תיקייה מאוחר יותר לאחר שבחרת 'לא'.",
"Address": "כתובת",
"Addresses": "כתובות",
"Advanced": "מתקדם",
"Advanced Configuration": "הגדרת תצורה מתקדמת",
"Advanced Configuration": "תצורה מתקדמת",
"All Data": "כל הנתונים",
"Allow Anonymous Usage Reporting?": "לאפשר דיווח שימוש בעילום שם?",
"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?": "לאפשר דיווח שימוש אנונימי?",
"Allowed Networks": "רשתות מורשות",
"Alphabetic": "אלפאבתי",
"Anonymous Usage Reporting": "דיווח שימוש בעילום שם",
"Are you sure you want to restore {%count%} files?": "האם אכן ברצונך לשחזר {{count}} קבצים?",
"Are you sure you want to upgrade?": "האם אכן ברצונך לשדרג?",
"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.": "פקודה חיצונית מטפלת בניהול הגרסאות. היא חייבת להסיר את הקובץ מהתיקייה המשותפת. אם הנתיב ליישום מכיל רווחים, יש לצטט אותו.",
"Anonymous Usage Reporting": "דיווח שימוש אנונימי",
"Anonymous usage report format has changed. Would you like to move to the new format?": "פורמט דוח שימוש אנונימי השתנה. האם ברצונך לעבור לפורמט החדש?",
"Applied to LAN": "הוחל על 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?": "האם אתה בטוח שברצונך לשדרג?",
"Authentication Required": "נדרש אימות",
"Authors": "מחברים",
"Auto Accept": "הסכמה אוטומטית",
"Automatic upgrades": "שדרוג אוטומטי",
"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.": "צור או שתף באופן אוטומטי תיקיות שההתקן הזה מפרסם בנתיב ברירת המחדל.",
"Available debug logging facilities:": "מתקני רישום דיבג זמינים:",
"Be careful!": "זהירות!",
"Bugs": "תקלות",
"Changelog": "רשימת שינויים",
"Close": "סגירה",
"Body:": "גוף:",
"Bugs": "באגים",
"Cancel": "ביטול",
"Changelog": "יומן שינויים",
"Clean out after": "נקה לאחר",
"Cleaning Versions": "מנקה גרסאות",
"Cleanup Interval": "פרק זמן לניקוי",
"Click to see full identification string and QR code.": "לחץ כדי לראות מחרוזת זיהוי מלאה וקוד QR.",
"Close": "סגור",
"Command": "פקודה",
"Comment, when used at the start of a line": "הערה, כאשר משתמשים בה בתחילת שורה",
"Compression": "דחיסה",
"Connection Error": קלת חיבור",
"Configuration Directory": יקיית תצורה",
"Configuration File": "קובץ תצורה",
"Configured": "מוגדר",
"Connected (Unused)": "מחובר (לא בשימוש)",
"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": "הועתק ממקום אחר",
"Copied from original": "הועתק מהמקור",
"Copied!": "הועתק!",
"Copy": "העתק",
"Copy failed! Try to select and copy manually.": "העתקה נכשלה! נסה לבחור ולהעתיק ידנית.",
"Currently Shared With Devices": "כרגע משותף עם התקנים",
"Custom Range": "טווח מותאם אישית",
"Danger!": "סכנה!",
"Database Location": "מיקום מסד נתונים",
"Debugging Facilities": "מתקני דיבוג",
"Default": "ברירת מחדל",
"Default Configuration": "תצורת ברירת מחדל",
"Default Device": "התקן ברירת מחדל",
"Default Folder": "תיקייה ברירת מחדל",
"Default Ignore Patterns": "דפוסי התעלמות ברירת מחדל",
"Defaults": "ברירות מחדל",
"Delete": "מחק",
"Delete Unexpected Items": "מחק פריטים בלתי צפויים",
"Deleted {%file%}": "{{file}} נמחק",
"Deselect All": "בטל את הבחירה בהכל",
"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}}\" ({{device}} ב־{{address}}) רוצה להתחבר. להוסיף התקן חדש?",
"Device Certificate": "תעודת התקן",
"Device ID": "מזהה התקן",
"Device Identification": "מזהה התקן",
"Device Identification": "זיהוי התקן",
"Device Name": "שם התקן",
"Device Status": "מצב התקן",
"Device is untrusted, enter encryption password": "ההתקן אינו מהימן, הזן סיסמת הצפנה",
"Device rate limits": "מגבלות קצב התקן",
"Device that last modified the item": "ההתקן ששינה את הפריט בפעם האחרונה",
"Devices": "התקנים",
"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:": "סריקה תקופתית הושבתה והגדרת מעקב אחר שינויים נכשלה, מנסה שוב כל דקה:",
"Disables comparing and syncing file permissions. Useful on systems with nonexistent or custom permissions (e.g. FAT, exFAT, Synology, Android).": "משבית השוואה וסנכרון של הרשאות קובץ. שימושי במערכות עם הרשאות שאינן קיימות או מותאמות אישית (למשל Android, Synology, exFAT, FAT).",
"Discard": "השלך",
"Disconnected": "מנותק",
"Disconnected (Inactive)": "מנותק (לא פעיל)",
"Disconnected (Unused)": "מנותק (לא בשימוש)",
"Discovered": "התגלה",
"Discovery": "גילוי",
"Discovery Failures": "כשלי גילוי",
"Discovery Status": "מצב גילוי",
"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?": "האם ברצונך להפעיל מעקב אחר שינויים עבור כל התיקיות שלך?",
"Documentation": "תיעוד",
"Download Rate": "קצב הורדה",
"Edit": "עריכה",
"Edit Device": "עריכת התקן",
"Edit Folder": "עריכה תיקיה",
"Downloaded": ורד",
"Downloading": "מוריד",
"Edit": "ערוך",
"Edit Device": "ערוך התקן",
"Edit Device Defaults": "ערוך ברירות מחדל של התקן",
"Edit Folder": "ערוך תיקייה",
"Edit Folder Defaults": "ערוך ברירות מחדל של תיקייה",
"Editing {%path%}.": "עורך {{path}}.",
"Enable Crash Reporting": "הפעל דיווח קריסה",
"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\" כדי לבצע גילוי אוטומטי של הכתובת.",
"Enter ignore patterns, one per line.": "הזן דפוסי התעלמות, אחד בכל שורה.",
"Enter up to three octal digits.": "הזן עד שלושה ספרות אוקטליות.",
"Error": "שגיאה",
"Folder": "תיקיה",
"Folder ID": "מזהה תיקיה",
"Folder Label": "כותרת תיקיה",
"Folder Path": "נתיב תיקיה",
"Folder Type": "סוג תיקיה",
"Extended Attributes": "תכונות מורחבות",
"Extended Attributes Filter": "מסנן תכונות מורחבות",
"External": "חיצוני",
"External File Versioning": "ניהול גרסאות קבצים חיצוני",
"Failed Items": "פריטים שנכשלו",
"Failed to load file versions.": "טעינת גרסאות קבצים נכשלה.",
"Failed to load ignore patterns.": "טעינת דפוסי התעלמות נכשלה.",
"Failed to set up, retrying": "ההגדרה נכשלה, מנסה שוב",
"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.": "הקבצים מועברים לתיקיית stversions. כאשר הם מוחלפים או נמחקים על ידי Syncthing.",
"Files are moved to date stamped versions in a .stversions directory when replaced or deleted by Syncthing.": "הקבצים מועברים לגרסאות עם חותמת תאריך בתיקיית stversions. כאשר הם מוחלפים או נמחקים על ידי Syncthing.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "הקבצים מוגנים מפני שינויים שנעשו בהתקנים אחרים, אבל שינויים שנעשו בהתקן זה יישלחו לשאר האשכול.",
"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": "סנן לפי שם",
"Folder": "תיקייה",
"Folder ID": "מזהה תיקייה",
"Folder Label": "תווית תיקייה",
"Folder Path": "נתיב תיקייה",
"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}}\" לאחר הוספת התיקייה. אתה צריך להסיר את התיקייה, למחוק או לפענח את הנתונים בדיסק, ולהוסיף את התיקייה שוב.",
"Folders": "תיקיות",
"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 / API HTTPS Certificate": "תעודת HTTPS עבור ממשק גרפי / תכנותי",
"GUI Authentication Password": "סיסמת אימות ממשק גרפי",
"GUI Authentication User": "משתמש אימות ממשק גרפי",
"GUI Authentication: Set User and Password": "אימות ממשק גרפי: הגדר משתמש וסיסמה",
"GUI Listen Address": "כתובת האזנה של ממשק גרפי",
"GUI Override Directory": "תיקיית מעקף ממשק גרפי",
"GUI Theme": "ערכת נושא של ממשק גרפי",
"General": "כללי",
"Generate": "צור",
"Global Discovery": "גילוי גלובלי",
"Global Discovery Servers": "שרתי גילוי גלובלי",
"Global State": "מצב גלובלי",
"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 ודרך כך לקבצים שלך, שקול להגדיר אימות.",
"Ignore": "התעלמות",
"Ignore Permissions": "התעלמות מהרשאות",
"Keep Versions": "שמירת גרסאות",
"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": "תיקיות בהתעלמות",
"Ignored at": "בהתעלמות אצל",
"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)": "היפוך של התנאי הנתון (כלומר אל תחריג)",
"Keep Versions": "שמור גרסאות",
"LDAP": "LDAP",
"Learn more": "למידע נוסף",
"Loading...": "כעת בטעינה...",
"Largest First": "הגדול ביותר תחילה",
"Last 30 Days": "30 הימים האחרונים",
"Last 7 Days": "7 הימים האחרונים",
"Last Month": "החודש האחרון",
"Last Scan": "הסריקה האחרונה",
"Last seen": "נראה לאחרונה",
"Latest Change": "השינוי העדכני ביותר",
"Learn more": "למד עוד",
"Learn more at {%url%}": "למד עוד באתר {{url}}",
"Limit": "מגבלה",
"Listener Failures": "כשלי מאזין",
"Listener Status": "מצב מאזין",
"Listeners": "מאזינים",
"Loading data...": "טוען נתונים...",
"Loading...": "טוען...",
"Local Additions": "הוספות מקומיות",
"Local Discovery": "גילוי מקומי",
"Local State": "מצב מקומי",
"Local State (Total)": "מצב מקומי (סה\"כ)",
"Locally Changed Items": "פריטים שהשתנו מקומית",
"Log": "יומן",
"Move to top of queue": "העברה לראש הרשימה",
"Log File": "קובץ יומן",
"Log In": "היכנס",
"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 לפרטים.",
"Logs": "יומנים",
"Major Upgrade": "שדרוג עיקרי",
"Mass actions": "פעולות המוניות",
"Maximum Age": "גיל מרבי",
"Maximum single entry size": "גודל ערך יחיד מרבי",
"Maximum total size": "גודל כולל מרבי",
"Metadata Only": "מטא-נתונים בלבד",
"Minimum Free Disk Space": "שטח דיסק פנוי מזערי",
"Mod. Device": "שנה התקן",
"Mod. Time": "זמן שינוי",
"More than a month ago": "לפני יותר מחודש",
"More than a week ago": "לפני יותר משבוע",
"More than a year ago": "לפני יותר משנה",
"Move to top of queue": "העבר לראש התור",
"Multi level wildcard (matches multiple directory levels)": "תו כללי רב רמות (תואם רמות תיקייה מרובות)",
"Never": "לעולם לא",
"New Device": "התקן חדש",
"New Folder": "תיקיה חדשה",
"New Folder": "תיקייה חדשה",
"Newest First": "החדש ביותר תחילה",
"No": "לא",
"No File Versioning": "אין ניהול גרסאות קבצים",
"No files will be deleted as a result of this operation.": "לא יימחקו קבצים כתוצאה מפעולה זו.",
"No rules set": "לא נקבעו כללים",
"No upgrades": "אין שדרוגים",
"Not shared": "לא בשיתוף",
"Notice": "הודעה",
"Number of Connections": "מספר חיבורים",
"OK": "אישור",
"Off": "כבוי",
"Oldest First": "הישן ביותר תחילה",
"Optional descriptive label for the folder. Can be different on each device.": "תווית תיאורית אופציונלית עבור התיקייה. יכולה להיות שונה בכל התקן.",
"Options": "אפשרויות",
"Out of Sync": "לא מסונכרן",
"Out of Sync Items": "פריטים לא מסונכרנים",
"Outgoing Rate Limit (KiB/s)": "מגבלת קצב יוצא (KiB/s)",
"Override": "דריסה",
"Override Changes": "דרוס שינויים",
"Ownership": "בעלות",
"Password": "סיסמה",
"Path": "נתיב",
"Pause": "השהיה",
"Pause All": "להשהות הכול",
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "נתיב לתיקייה במחשב המקומי. תיווצר אם אינה קיימת. תו הטילדה (~) יכול לשמש כקיצור דרך עבור",
"Path where versions should be stored (leave empty for the default .stversions directory in the shared folder).": "הנתיב שבו יש לאחסן גרסאות (השאר ריק עבור תיקיית stversion. ברירת המחדל בתיקייה המשותפת).",
"Paths": "נתיבים",
"Pause": "השהה",
"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.": "הוסף אותו לצמיתות לרשימת ההתעלמות, תוך העלמת התראות נוספות.",
"Please consult the release notes before performing a major upgrade.": "נא לבדוק את הערות השחרור לפני ביצוע שדרוג עיקרי.",
"Please set a GUI Authentication User and Password in the Settings dialog.": "נא להגדיר משתמש וסיסמה לאימות ממשק גרפי בשיח ההגדרות.",
"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": "מתכונן לסנכרון",
"Preview": "תצוגה מקדימה",
"Preview Usage Report": "תצוגה מקדימה של דוח שימוש",
"QR code": "קוד QR",
"QUIC LAN": "QUIC LAN",
"QUIC WAN": "QUIC WAN",
"Quick guide to supported patterns": "מדריך מהיר לדפוסים נתמכים",
"Random": "אקראי",
"Receive Encrypted": "קבלה מוצפנת",
"Receive Only": "קבלה בלבד",
"Received data is already encrypted": "הנתונים שהתקבלו כבר מוצפנים",
"Recent Changes": "שינויים אחרונים",
"Reduced by ignore patterns": "הופחת על ידי דפוסי התעלמות",
"Relay LAN": "ממסר LAN",
"Relay WAN": "ממסר WAN",
"Release Notes": "הערות שחרור",
"Release candidates contain the latest features and fixes. They are similar to the traditional bi-weekly Syncthing releases.": "מועמדים לשחרור מכילים את התכונות והתיקונים העדכניים ביותר. הם דומים לשחרורים הדו־שבועיים המסורתיים של Syncthing.",
"Remote Devices": "התקנים מרוחקים",
"Remove": "הסרה",
"Remote GUI": "ממשק גרפי מרוחק",
"Remove": "הסר",
"Remove Device": "הסר התקן",
"Remove Folder": "הסר תיקייה",
"Required identifier for the folder. Must be the same on all cluster devices.": "נדרש מזהה עבור התיקייה. חייב להיות אותו הדבר בכל התקני האשכול.",
"Rescan": "סריקה מחדש",
"Rescan All": "לסרוק הכול",
"Save": "שמירה",
"Scanning": "כעת בסריקה",
"Send & Receive": "שליחה וקבלה",
"Rescan All": "סרוק הכל מחדש",
"Rescans": "סריקות מחדש",
"Restart": "הפעלה מחדש",
"Restart Needed": "נדרשת הפעלה מחדש",
"Restarting": "מפעיל מחדש",
"Restore": "שחזר",
"Restore Versions": "שחזר גרסאות",
"Resume": "המשך",
"Resume All": "המשך הכל",
"Reused": "בשימוש חוזר",
"Revert": "בטל",
"Revert Local Changes": "בטל שינויים מקומיים",
"Save": "שמור",
"Saving changes": "שומר שינויים",
"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 latest version": "בחר את הגרסה העדכנית ביותר",
"Select oldest version": "בחר את הגרסה הישנה ביותר",
"Send & Receive": "שליחה & קבלה",
"Send Extended Attributes": "שלח תכונות מורחבות",
"Send Only": "שליחה בלבד",
"Share": יתוף",
"Share Folder": "שיתוף תיקיה",
"Share this folder?": "לשתף תיקיה זו?",
"Send Ownership": לח בעלות",
"Set Ignores on Added Folder": "הגדר התעלמויות על התיקייה שנוספה",
"Settings": "הגדרות",
"Share": "שתף",
"Share Folder": "שתף תיקייה",
"Share by Email": "שתף באמצעות דוא\"ל",
"Share by SMS": "שתף באמצעות מסרון",
"Share this folder?": "לשתף תיקייה זו?",
"Shared Folders": "תיקיות משותפות",
"Shared With": "משותף עם",
"Show ID": "הצגת מזהה",
"Sharing": "שיתוף",
"Show ID": "הצג מזהה",
"Show QR": "הצג 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.": "מוצג במקום מזהה ההתקן במצב האשכול. יפורסם להתקנים אחרים כשם ברירת מחדל אופציונלי.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "מוצג במקום מזהה ההתקן במצב האשכול. יעודכן לשם שההתקן מפרסם אם נותר ריק.",
"Shutdown": "כיבוי",
"Shutdown Complete": "הכיבוי הושלם",
"Simple": "פשוט",
"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:": "לא היה ניתן להפעיל כמה כתובות האזנה לקבלת חיבורים:",
"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 only": "שחרורים יציבים בלבד",
"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}}\".",
"Subject:": "נושא:",
"Support": "תמיכה",
"Syncing": "כעת בסנכרון",
"Support Bundle": "חבילת תמיכה",
"Sync Extended Attributes": "סנכרון תכונות מורחבות",
"Sync Ownership": "סנכרון בעלות",
"Sync Protocol Listen Addresses": "כתובות האזנה של פרוטוקול הסנכרון",
"Sync Status": "מצב סנכרון",
"Syncing": "מסנכרן",
"Syncthing device ID for \"{%devicename%}\"": "מזהה התקן Syncthing עבור \"{{devicename}}\"",
"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 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 LAN",
"TCP WAN": "TCP WAN",
"Take me back": "קח אותי בחזרה",
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "הכתובת של הממשק הגרפי נעקפת על ידי אפשרויות הפעלה. שינויים כאן לא ייכנסו לתוקף כל עוד המעקף נמצא במקום.",
"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.": "מזהה ההתקן אינו יכול להיות ריק.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "ניתן למצוא את מזהה ההתקן אותו יש להזין כאן בדו־השיח \"פעולות > הצג מזהה\" בהתקנים האחרים. רווחים ומקפים הם אופציונליים (מוזנחים).",
"The 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.": "מזהה ההתקן שהוזן לא נראה חוקי. הוא צריך להיות מחרוזת באורך 52 או 56 תווים המורכבת מאותיות ומספרים, כאשר רווחים ומקפים הם אופציונליים.",
"The folder ID cannot be blank.": "מזהה התיקייה לא יכול להיות ריק.",
"The folder ID must be unique.": "מזהה התיקייה חייב להיות ייחודי.",
"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.": "נעשה שימוש בפרקי הזמן הבאים: במשך השעה הראשונה נשמרת גרסה כל 30 שניות, במשך היום הראשון נשמרת גרסה כל שעה, במשך 30 הימים הראשונים נשמרת גרסה כל יום, עד הגיל המרבי נשמרת גרסה כל שבוע.",
"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.": "פרק הזמן, בשניות, להרצת ניקוי בתיקיית הגרסאות. אפס כדי להשבית ניקוי תקופתי.",
"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 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.": "אין התקנים לשיתוף תיקייה זו.",
"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": "התקן זה",
"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.": "הגדרה זו שולטת במקום הפנוי הנדרש בדיסק הבית (כלומר, אינדקס של מסד נתונים).",
"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:": "כדי להתחבר עם התקן Syncthing בשם \"{{devicename}}\", הוסף התקן מרוחק חדש בקצה שלך עם מזהה זה:",
"To permit a rule, have the checkbox checked. To deny a rule, leave it unchecked.": "כדי להתיר כלל, סמן את תיבת הסימון. כדי לשלול כלל, השאר אותה לא מסומנת.",
"Today": "היום",
"Trash Can": "פח אשפה",
"Trash Can File Versioning": "ניהול גרסאות קבצים עם פח אשפה",
"Type": "סוג",
"UNIX Permissions": "הרשאות UNIX",
"Unavailable": "לא זמין",
"Unavailable/Disabled by administrator or maintainer": "לא זמין/מושבת על ידי מנהל או מתחזק",
"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}} עודכן",
"Upgrade": "שדרוג",
"Upgrade To {%version%}": "שדרוג לגרסה {{version}}",
"Upgrading": "כעת בשדרוג",
"Upgrade To {%version%}": "שדרג ל־{{version}}",
"Upgrading": "משדרג",
"Upload Rate": "קצב העלאה",
"Uptime": "זמן פעולה",
"Use HTTPS for GUI": "שימוש ב־HTTP לממשק המשתמש",
"Usage reporting is always enabled for candidate releases.": "דיווח שימוש תמיד מופעל עבור מועמדים לשחרור.",
"Use HTTPS for GUI": "השתמש ב־HTTPS עבור ממשק גרפי",
"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.": "שם משתמש/סיסמה לא הוגדרו עבור אימות הממשק הגרפי. נא לשקול להגדיר אותם.",
"Using a QUIC connection over LAN": "משתמש בחיבור QUIC על גבי LAN",
"Using a QUIC connection over WAN": "משתמש בחיבור QUIC על גבי WAN",
"Using a direct TCP connection over LAN": "משתמש בחיבור TCP ישיר על גבי LAN",
"Using a direct TCP connection over WAN": "משתמש בחיבור TCP ישיר על גבי WAN",
"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": "ממתין לסנכרון",
"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}}, עליך לוודא שהוא מושבת.",
"Watch for Changes": "עקוב אחר שינויים",
"Watching for Changes": "מעקב אחר שינויים",
"Watching for changes discovers most changes without periodic scanning.": "מעקב אחר שינויים מגלה את רוב השינויים ללא סריקה תקופתית.",
"When adding a new device, keep in mind that this device must be added on the other side too.": "בעת הוספת התקן חדש, זכור כי יש להוסיף את ההתקן הזה גם בצד השני.",
"When adding a new 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.": "בעת הוספת תיקייה חדשה, זכור כי מזהה התיקייה משמש לקשירת תיקיות יחד בין התקנים. הם רגישים לרישיות וצריכים להתאים בדיוק בין כל ההתקנים.",
"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.": "כאשר מוגדר ליותר מאחד בשני ההתקנים, Syncthing ינסה להקים חיבורים בו-זמניים מרובים. אם הערכים שונים, ייעשה שימוש בערך הגבוה ביותר. הגדר לאפס כדי לתת ל־Syncthing להחליט.",
"Yes": "כן",
"You must keep at least one version.": "חובה לשמור גרסה אחת לפחות.",
"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 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.": "יישום הדוא\"ל שלך צריך להיפתח כדי לתת לך לבחור את הנמען ולשלוח את ההודעה מהכתובת שלך.",
"days": "ימים",
"deleted": "נמחק",
"deny": "לשלול",
"directories": "תיקיות",
"file": "קובץ",
"files": "קבצים",
"folder": "תיקייה",
"full documentation": "תיעוד מלא",
"items": "פריטים"
"items": "פריטים",
"modified": "שונה",
"permit": "היתר",
"seconds": "שניות",
"theme": {
"name": {
"black": "שחור",
"dark": "כהה",
"default": "ברירת מחדל",
"light": "בהיר"
}
},
"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}} עשוי להציג מחדש את ההתקן הזה."
}

View File

@@ -154,7 +154,7 @@
"Failed Items": "विफल वस्तुएं",
"Failed to load file versions.": "फाइल संस्करण लोड करने में विफल।",
"Failed to load ignore patterns.": "नजरअंदाज प्रतिमान लोड करने में विफल।",
"Failed to setup, retrying": "स्थापना करने में विफल, पुनः प्रयास किया जा रहा है",
"Failed to set up, retrying": "स्थापना करने में विफल, पुनः प्रयास किया जा रहा है",
"Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": "यदि IPv6 संयोजकता नहीं है तो IPv6 सर्वर से जुड़ने में विफलता अपेक्षित है।",
"File Pull Order": "फाइल खींचने का क्रम",
"File Versioning": "फाइल संस्करणीकरण",

View File

@@ -146,7 +146,7 @@
"Failed Items": "Hibás elemek",
"Failed to load file versions.": "Nem sikerült betölteni a fájlverziókat.",
"Failed to load ignore patterns.": "Nem sikerült betölteni a mellőzési mintákat.",
"Failed to setup, retrying": "Telepítés nem sikerült, újrapróbálkozás",
"Failed to set up, retrying": "Telepítés nem sikerült, újrapróbálkozás",
"Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": "Mivel nincs IPv6 kapcsolat, ezért várhatóan nem fog sikerülni IPv6-os szerverekhez csatlakozni.",
"File Pull Order": "Fájlküldési sorrend",
"File Versioning": "Fájlverzió-követés",

View File

@@ -146,7 +146,7 @@
"Failed Items": "Berkas yang gagal",
"Failed to load file versions.": "Gagal memuat versi berkas.",
"Failed to load ignore patterns.": "Gagal memuat pola pengabaian.",
"Failed to setup, retrying": "Gagal menyiapkan, mengulang",
"Failed to set up, retrying": "Gagal menyiapkan, mengulang",
"Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": "Gagal untuk menyambung ke server IPv6 itu disangka apabila tidak ada konektivitas IPv6.",
"File Pull Order": "Urutan Penarikan Berkas",
"File Versioning": "Pemversian Berkas",

View File

@@ -27,6 +27,7 @@
"Allowed Networks": "Reti Consentite",
"Alphabetic": "Alfabetico",
"Altered by ignoring deletes.": "Modificato ignorando le eliminazioni.",
"Always turned on when the folder type is \"{%foldertype%}\".": "Sempre attivato quando il tipo di cartella è \"{{foldertype}}\".",
"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.": "Il controllo versione è gestito da un comando esterno. Quest'ultimo deve rimuovere il file dalla cartella condivisa. Se il percorso dell'applicazione contiene spazi, deve essere indicato tra virgolette.",
"Anonymous Usage Reporting": "Statistiche Anonime di Utilizzo",
"Anonymous usage report format has changed. Would you like to move to the new format?": "Il formato delle statistiche anonime di utilizzo è cambiato. Vuoi passare al nuovo formato?",
@@ -52,6 +53,7 @@
"Body:": "Corpo:",
"Bugs": "Bug",
"Cancel": "Annulla",
"Cannot be enabled when the folder type is \"{%foldertype%}\".": "Non può essere abilitato se il tipo di cartella è \"{{foldertype}}\".",
"Changelog": "Registro modifiche",
"Clean out after": "Svuota dopo",
"Cleaning Versions": "Pulizia Versioni",
@@ -154,7 +156,7 @@
"Failed Items": "Elementi Errati",
"Failed to load file versions.": "Impossibile caricare le versioni dei file.",
"Failed to load ignore patterns.": "Impossibile caricare gli schemi di esclusione.",
"Failed to setup, retrying": "Configurazione fallita, riprovo",
"Failed to set up, retrying": "Configurazione fallita, riprovo",
"Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": "La connessione a server IPv6 fallisce se non c'è connettività IPv6.",
"File Pull Order": "Ordine Prelievo File",
"File Versioning": "Controllo Versione File",

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