Compare commits

...

41 Commits

Author SHA1 Message Date
Jakob Borg
025905fcdf chore: switch database engine to sqlite (fixes #9954) (#9965)
Switch the database from LevelDB to SQLite, for greater stability and
simpler code.

Co-authored-by: Tommy van der Vorst <tommy@pixelspark.nl>
Co-authored-by: bt90 <btom1990@googlemail.com>
2025-03-29 13:50:08 +01:00
Jakob Borg
b1c8f88a44 chore: remove weak hashing which does not pull its weight (#10005)
We've had weak/rolling hashing in the code for quite a while. It was a
popular request for a while, based on the belief that rsync does this
and we should too. However, the benefit is quite small; we save on
average about 0.8% of transferred blocks over the population as a whole:

<img width="974" alt="Screenshot 2025-03-28 at 17 09 02"
src="https://github.com/user-attachments/assets/bbe10dea-f85e-4043-9823-7cef1220b4a2"
/>

This would be fine if the cost was comparably low, however the downside
of attempting rolling hash matching is that we (by default) do a
complete file read on the destination in order to look for matches
before we starting pulling blocks for the file. For any larger file this
means a sometimes long, I/O-intensive pause before the file starts
syncing, for usually no benefit.

I propose we simply rip off the bandaid and save the effort.
2025-03-29 13:21:10 +01:00
Jakob Borg
1a25ae32ca chore: remove abandoned next-gen-gui experiment (#10004)
It didn't go anywhere, it adds no value where it is.
2025-03-29 13:20:35 +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
335 changed files with 9763 additions and 55886 deletions

View File

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

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,20 +3,16 @@ name: Build Syncthing
on:
pull_request:
push:
schedule:
# Run nightly build at 05:00 UTC
- cron: '00 05 * * *'
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
GOARM: "5"
GOMIPS: softfloat
# Avoid hilarious amounts of obscuring log output when running tests.
@@ -26,6 +22,8 @@ env:
BUILD_USER: builder
BUILD_HOST: github.syncthing.net
TAGS: "netgo osusergo sqlite_omit_load_extension"
# A note on actions and third party code... The actions under actions/ (like
# `uses: actions/checkout`) are maintained by GitHub, and we need to trust
# GitHub to maintain their code and infrastructure or we're in deep shit in
@@ -48,7 +46,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
@@ -87,6 +85,7 @@ jobs:
LOKI_USER: ${{ secrets.LOKI_USER }}
LOKI_PASSWORD: ${{ secrets.LOKI_PASSWORD }}
LOKI_LABELS: "go=${{ matrix.go }},runner=${{ matrix.runner }},repo=${{ github.repository }},ref=${{ github.ref }}"
CGO_ENABLED: "1"
#
# Meta checks for formatting, copyright, etc
@@ -127,6 +126,7 @@ jobs:
- package-cross
- package-source
- package-debian
- package-windows
- govulncheck
steps:
- uses: actions/checkout@v4
@@ -137,19 +137,8 @@ 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-') || startsWith(github.ref, 'refs/tags/v'))
environment: release
runs-on: windows-latest
runs-on: ubuntu-latest
steps:
- name: Set git to use LF
# Without this, the checkout will happen with CRLF line endings,
# which is fine for the source code but messes up tests that depend
# on data on disk being as expected. Ideally, those tests should be
# fixed, but not today.
run: |
git config --global core.autocrlf false
git config --global core.eol lf
- uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -161,17 +150,14 @@ jobs:
cache: false
check-latest: true
- name: Get actual Go version
run: |
go version
echo "GO_VERSION=$(go version | sed 's#^.*go##;s# .*##')" >> $GITHUB_ENV
- uses: mlugg/setup-zig@v1
- uses: actions/cache@v4
with:
path: |
~\AppData\Local\go-build
~\go\pkg\mod
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-package-${{ hashFiles('**/go.sum') }}
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-package-windows-${{ hashFiles('**/go.sum') }}
- name: Install dependencies
run: |
@@ -179,25 +165,73 @@ jobs:
- name: Create packages
run: |
$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
}
}
for tgt in syncthing stdiscosrv strelaysrv ; do
go run build.go -tags "${{env.TAGS}}" -goos windows -goarch amd64 -cc "zig cc -target x86_64-windows" zip $tgt
go run build.go -tags "${{env.TAGS}}" -goos windows -goarch 386 -cc "zig cc -target x86-windows" zip $tgt
go run build.go -tags "${{env.TAGS}}" -goos windows -goarch arm64 -cc "zig cc -target aarch64-windows" zip $tgt
# go run build.go -tags "${{env.TAGS}}" -goos windows -goarch arm -cc "zig cc -target thumb-windows" zip $tgt # failes with linker errors
done
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 }}
CGO_ENABLED: "1"
- name: Archive artifacts
uses: actions/upload-artifact@v4
with:
name: unsigned-packages-windows
path: "*.zip"
codesign-windows:
name: Codesign for Windows
if: github.repository_owner == 'syncthing' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/release' || startsWith(github.ref, 'refs/heads/release-') || 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: "*.zip"
path: "packages/*.zip"
#
# Linux
@@ -223,6 +257,8 @@ jobs:
go version
echo "GO_VERSION=$(go version | sed 's#^.*go##;s# .*##')" >> $GITHUB_ENV
- uses: mlugg/setup-zig@v1
- uses: actions/cache@v4
with:
path: |
@@ -232,14 +268,25 @@ jobs:
- name: Create packages
run: |
archs=$(go tool dist list | grep linux | sed 's#linux/##')
for goarch in $archs ; do
for tgt in syncthing stdiscosrv strelaysrv ; do
go run build.go -goarch "$goarch" tar "$tgt"
done
sudo apt-get install -y gcc-mips64-linux-gnuabi64 gcc-mips64el-linux-gnuabi64
for tgt in syncthing stdiscosrv strelaysrv ; do
go run build.go -tags "${{env.TAGS}}" -goos linux -goarch amd64 -cc "zig cc -target x86_64-linux-musl" tar "$tgt"
go run build.go -tags "${{env.TAGS}}" -goos linux -goarch 386 -cc "zig cc -target x86-linux-musl" tar "$tgt"
go run build.go -tags "${{env.TAGS}}" -goos linux -goarch arm -cc "zig cc -target arm-linux-musleabi" tar "$tgt"
go run build.go -tags "${{env.TAGS}}" -goos linux -goarch arm64 -cc "zig cc -target aarch64-linux-musl" tar "$tgt"
go run build.go -tags "${{env.TAGS}}" -goos linux -goarch mips -cc "zig cc -target mips-linux-musleabi" tar "$tgt"
go run build.go -tags "${{env.TAGS}}" -goos linux -goarch mipsle -cc "zig cc -target mipsel-linux-musleabi" tar "$tgt"
go run build.go -tags "${{env.TAGS}}" -goos linux -goarch mips64 -cc mips64-linux-gnuabi64-gcc tar "$tgt"
go run build.go -tags "${{env.TAGS}}" -goos linux -goarch mips64le -cc mips64el-linux-gnuabi64-gcc tar "$tgt"
go run build.go -tags "${{env.TAGS}}" -goos linux -goarch riscv64 -cc "zig cc -target riscv64-linux-musl" tar "$tgt"
go run build.go -tags "${{env.TAGS}}" -goos linux -goarch s390x -cc "zig cc -target s390x-linux-musl" tar "$tgt"
go run build.go -tags "${{env.TAGS}}" -goos linux -goarch loong64 -cc "zig cc -target loongarch64-linux-musl" tar "$tgt"
# go run build.go -tags "${{env.TAGS}}" -goos linux -goarch ppc64 -cc "zig cc -target powerpc64-linux-musl" tar "$tgt" # fails with linkmode not supported
go run build.go -tags "${{env.TAGS}}" -goos linux -goarch ppc64le -cc "zig cc -target powerpc64le-linux-musl" tar "$tgt"
done
env:
CGO_ENABLED: "0"
CGO_ENABLED: "1"
EXTRA_LDFLAGS: "-linkmode=external -extldflags=-static"
- name: Archive artifacts
uses: actions/upload-artifact@v4
@@ -257,6 +304,8 @@ jobs:
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-') || startsWith(github.ref, 'refs/tags/v'))
environment: release
env:
CODESIGN_IDENTITY: ${{ secrets.CODESIGN_IDENTITY }}
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
@@ -283,6 +332,7 @@ jobs:
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-package-${{ hashFiles('**/go.sum') }}
- name: Import signing certificate
if: env.CODESIGN_IDENTITY != ''
run: |
# Set up a run-specific keychain, making it available for the
# `codesign` tool.
@@ -310,7 +360,7 @@ jobs:
- name: Create package (amd64)
run: |
for tgt in syncthing stdiscosrv strelaysrv ; do
go run build.go -goarch amd64 zip "$tgt"
go run build.go -tags "${{env.TAGS}}" -goarch amd64 zip "$tgt"
done
env:
CGO_ENABLED: "1"
@@ -326,7 +376,7 @@ jobs:
EOT
chmod 755 xgo.sh
for tgt in syncthing stdiscosrv strelaysrv ; do
go run build.go -gocmd ./xgo.sh -goarch arm64 zip "$tgt"
go run build.go -tags "${{env.TAGS}}" -gocmd ./xgo.sh -goarch arm64 zip "$tgt"
done
env:
CGO_ENABLED: "1"
@@ -355,7 +405,7 @@ jobs:
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-') || startsWith(github.ref, 'refs/tags/v'))
if: github.repository_owner == 'syncthing' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/release' || startsWith(github.ref, 'refs/heads/release-') || startsWith(github.ref, 'refs/tags/v'))
environment: release
needs:
- package-macos
@@ -437,7 +487,7 @@ jobs:
goarch="${plat#*/}"
echo "::group ::$plat"
for tgt in syncthing stdiscosrv strelaysrv ; do
if ! go run build.go -goos "$goos" -goarch "$goarch" tar "$tgt" 2>/dev/null; then
if ! go run build.go -goos "$goos" -goarch "$goarch" tar "$tgt" ; then
echo "::warning ::Failed to build $tgt for $plat"
fi
done
@@ -499,10 +549,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-') || startsWith(github.ref, 'refs/tags/v'))
if: github.repository_owner == 'syncthing' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/release' || startsWith(github.ref, 'refs/heads/release-') || startsWith(github.ref, 'refs/tags/v'))
environment: release
needs:
- package-windows
- codesign-windows
- package-linux
- package-macos
- package-cross
@@ -617,6 +667,8 @@ jobs:
run: |
gem install fpm
- uses: mlugg/setup-zig@v1
- uses: actions/cache@v4
with:
path: |
@@ -624,15 +676,17 @@ jobs:
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-debian-${{ hashFiles('**/go.sum') }}
- name: Package for Debian
- name: Package for Debian (CGO)
run: |
for arch in amd64 i386 armhf armel arm64 ; do
for tgt in syncthing stdiscosrv strelaysrv ; do
go run build.go -no-upgrade -installsuffix=no-upgrade -goarch "$arch" deb "$tgt"
done
for tgt in syncthing stdiscosrv strelaysrv ; do
go run build.go -no-upgrade -installsuffix=no-upgrade -tags "${{env.TAGS}}" -goos linux -goarch amd64 -cc "zig cc -target x86_64-linux-musl" deb "$tgt"
go run build.go -no-upgrade -installsuffix=no-upgrade -tags "${{env.TAGS}}" -goos linux -goarch arm -cc "zig cc -target arm-linux-musleabi" deb "$tgt"
go run build.go -no-upgrade -installsuffix=no-upgrade -tags "${{env.TAGS}}" -goos linux -goarch arm64 -cc "zig cc -target aarch64-linux-musl" deb "$tgt"
done
env:
BUILD_USER: debian
CGO_ENABLED: "1"
EXTRA_LDFLAGS: "-linkmode=external -extldflags=-static"
- name: Archive artifacts
uses: actions/upload-artifact@v4
@@ -646,7 +700,7 @@ jobs:
publish-nightly:
name: Publish nightly build
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && startsWith(github.ref, 'refs/heads/release-nightly')
if: github.repository_owner == 'syncthing' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && startsWith(github.ref, 'refs/heads/release-nightly')
environment: release
needs:
- sign-for-upgrade
@@ -696,7 +750,7 @@ jobs:
publish-release-files:
name: Publish release files
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/release' || startsWith(github.ref, 'refs/tags/v'))
if: github.repository_owner == 'syncthing' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/release' || startsWith(github.ref, 'refs/tags/v'))
environment: release
needs:
- sign-for-upgrade
@@ -763,7 +817,7 @@ jobs:
publish-apt:
name: Publish APT
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/release' || startsWith(github.ref, 'refs/heads/release-') || startsWith(github.ref, 'refs/tags/v'))
if: github.repository_owner == 'syncthing' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/release' || startsWith(github.ref, 'refs/heads/release-') || startsWith(github.ref, 'refs/tags/v1'))
environment: release
needs:
- package-debian
@@ -790,7 +844,9 @@ jobs:
- name: Prepare packages
run: |
kind=stable
if [[ $VERSION == *-rc.[0-9] ]] ; then
if [[ $VERSION == v2* ]] ; then
kind=v2
elif [[ $VERSION == *-rc.[0-9] ]] ; then
kind=candidate
elif [[ $VERSION == *-* ]] ; then
kind=nightly
@@ -842,8 +898,10 @@ 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/main' || github.ref == 'refs/heads/release' || github.ref == 'refs/heads/infrastructure' || startsWith(github.ref, 'refs/heads/release-') || startsWith(github.ref, 'refs/tags/v'))
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
environment: docker
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
permissions:
contents: read
packages: write
@@ -856,13 +914,13 @@ jobs:
include:
- pkg: syncthing
dockerfile: Dockerfile
image: syncthing/syncthing
image: syncthing
- pkg: strelaysrv
dockerfile: Dockerfile.strelaysrv
image: syncthing/relaysrv
image: relaysrv
- pkg: stdiscosrv
dockerfile: Dockerfile.stdiscosrv
image: syncthing/discosrv
image: discosrv
steps:
- uses: actions/checkout@v4
with:
@@ -880,6 +938,8 @@ jobs:
go version
echo "GO_VERSION=$(go version | sed 's#^.*go##;s# .*##')" >> $GITHUB_ENV
- uses: mlugg/setup-zig@v1
- uses: actions/cache@v4
with:
path: |
@@ -887,33 +947,34 @@ jobs:
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-docker-${{ matrix.pkg }}-${{ hashFiles('**/go.sum') }}
- name: Build binaries
- name: Build binaries (CGO)
run: |
for arch in amd64 arm64 arm; do
go run build.go -goos linux -goarch "$arch" -no-upgrade build ${{ matrix.pkg }}
mv ${{ matrix.pkg }} ${{ matrix.pkg }}-linux-"$arch"
done
env:
CGO_ENABLED: "0"
BUILD_USER: docker
# amd64
go run build.go -goos linux -goarch amd64 -tags "${{env.TAGS}}" -cc "zig cc -target x86_64-linux-musl" -no-upgrade build ${{ matrix.pkg }}
mv ${{ matrix.pkg }} ${{ matrix.pkg }}-linux-amd64
- name: Check if we will be able to push images
run: |
if [[ "${{ secrets.DOCKERHUB_TOKEN }}" != "" ]]; then
echo "DOCKER_PUSH=true" >> $GITHUB_ENV;
fi
# arm64
go run build.go -goos linux -goarch arm64 -tags "${{env.TAGS}}" -cc "zig cc -target aarch64-linux-musl" -no-upgrade build ${{ matrix.pkg }}
mv ${{ matrix.pkg }} ${{ matrix.pkg }}-linux-arm64
# arm
go run build.go -goos linux -goarch arm -tags "${{env.TAGS}}" -cc "zig cc -target arm-linux-musleabi" -no-upgrade build ${{ matrix.pkg }}
mv ${{ matrix.pkg }} ${{ matrix.pkg }}-linux-arm
env:
CGO_ENABLED: "1"
BUILD_USER: docker
EXTRA_LDFLAGS: "-linkmode=external -extldflags=-static"
- name: Login to Docker Hub
uses: docker/login-action@v3
if: env.DOCKER_PUSH == 'true'
if: env.DOCKERHUB_USERNAME != ''
with:
registry: docker.io
username: ${{ secrets.DOCKERHUB_USERNAME }}
username: ${{ env.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 }}
@@ -926,18 +987,31 @@ jobs:
run: |
version=$(go run build.go version)
version=${version#v}
repo=ghcr.io/${{ github.repository_owner }}/${{ matrix.image }}
ref="${{github.ref_name}}"
ref=${ref//\//-} # slashes to dashes
# List of tags for ghcr.io
if [[ $version == @([0-9]|[0-9][0-9]).@([0-9]|[0-9][0-9]).@([0-9]|[0-9][0-9]) ]] ; then
echo Release version, pushing to :latest and version tags
major=${version%.*.*}
minor=${version%.*}
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
tags=$repo:$version,$repo:$major,$repo:$minor,$repo:latest
elif [[ $version == *-rc.@([0-9]|[0-9][0-9]) ]] ; then
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
tags=$repo:$version,$repo:rc
elif [[ $ref == "main" ]] ; then
tags=$repo:edge
else
echo Development version, pushing to :edge
tags=docker.io/${{ matrix.image }}:edge,ghcr.io/${{ matrix.image }}:edge
tags=$repo:$ref
fi
# If we have a Docker Hub secret, also push to there.
if [[ $DOCKERHUB_USERNAME != "" ]] ; then
dockerhubtags="${tags//ghcr.io\/syncthing/docker.io\/syncthing}"
tags="$tags,$dockerhubtags"
fi
echo Pushing to $tags
echo "DOCKER_TAGS=$tags" >> $GITHUB_ENV
echo "VERSION=$version" >> $GITHUB_ENV
@@ -947,8 +1021,8 @@ jobs:
context: .
file: ${{ matrix.dockerfile }}
platforms: linux/amd64,linux/arm64,linux/arm/7
push: ${{ env.DOCKER_PUSH == 'true' }}
tags: ${{ env.DOCKER_TAGS }}
push: true
labels: |
org.opencontainers.image.version=${{ env.VERSION }}
org.opencontainers.image.revision=${{ github.sha }}

1
.gitignore vendored
View File

@@ -17,5 +17,4 @@ deb
*.bz2
/repos
/proto/scripts/protoc-gen-gosyncthing
/gui/next-gen-gui
/compat.json

View File

@@ -3,6 +3,7 @@ linters:
disable:
- cyclop
- depguard
- err113
- exhaustive
- exhaustruct
- funlen
@@ -12,6 +13,7 @@ linters:
- gocognit
- goconst
- gocyclo
- godot
- godox
- gofmt
- goimports
@@ -21,15 +23,19 @@ linters:
- ireturn
- lll
- maintidx
- musttag
- nestif
- nlreturn
- nonamedreturns
- paralleltest
- prealloc
- protogetter
- scopelint
- tagalign
- tagliatelle
- testpackage
- varnamelen
- wrapcheck
- wsl
issues:

View File

@@ -42,7 +42,6 @@ approval_rules:
paths:
- ^[^/]+\.md
- ^\.policy\.yml
- ^\.github/
- ^LICENSE
requires:
count: 1

View File

@@ -97,6 +97,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>
@@ -229,6 +230,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>
@@ -287,6 +289,7 @@ 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>
Quentin Hibon <qh.public@yahoo.com>
Rahmi Pruitt <rjpruitt16@gmail.com>

176
build.go
View File

@@ -15,7 +15,6 @@ import (
"bytes"
"compress/flate"
"compress/gzip"
"encoding/base64"
"encoding/json"
"errors"
"flag"
@@ -39,27 +38,26 @@ import (
)
var (
goarch string
goos string
noupgrade bool
version string
goCmd string
race bool
debug = os.Getenv("BUILDDEBUG") != ""
extraTags string
installSuffix string
pkgdir string
cc string
run string
benchRun string
buildOut string
debugBinary bool
coverage bool
long bool
timeout = "120s"
longTimeout = "600s"
numVersions = 5
withNextGenGUI = os.Getenv("BUILD_NEXT_GEN_GUI") != ""
goarch string
goos string
noupgrade bool
version string
goCmd string
race bool
debug = os.Getenv("BUILDDEBUG") != ""
extraTags string
installSuffix string
pkgdir string
cc string
run string
benchRun string
buildOut string
debugBinary bool
coverage bool
long bool
timeout = "120s"
longTimeout = "600s"
numVersions = 5
)
type target struct {
@@ -290,10 +288,10 @@ func runCommand(cmd string, target target) {
build(target, tags)
case "test":
test(strings.Fields(extraTags), "github.com/syncthing/syncthing/lib/...", "github.com/syncthing/syncthing/cmd/...")
test(strings.Fields(extraTags), "github.com/syncthing/syncthing/internal/...", "github.com/syncthing/syncthing/lib/...", "github.com/syncthing/syncthing/cmd/...")
case "bench":
bench(strings.Fields(extraTags), "github.com/syncthing/syncthing/lib/...", "github.com/syncthing/syncthing/cmd/...")
bench(strings.Fields(extraTags), "github.com/syncthing/syncthing/internal/...", "github.com/syncthing/syncthing/lib/...", "github.com/syncthing/syncthing/cmd/...")
case "integration":
integration(false)
@@ -381,7 +379,6 @@ func parseFlags() {
flag.IntVar(&numVersions, "num-versions", numVersions, "Number of versions for changelog command")
flag.StringVar(&run, "run", "", "Specify which tests to run")
flag.StringVar(&benchRun, "bench", "", "Specify which benchmarks to run")
flag.BoolVar(&withNextGenGUI, "with-next-gen-gui", withNextGenGUI, "Also build 'newgui'")
flag.StringVar(&buildOut, "build-out", "", "Set the '-o' value for 'go build'")
flag.Parse()
}
@@ -454,10 +451,6 @@ func benchArgs() []string {
}
func install(target target, tags []string) {
if (target.name == "syncthing" || target.name == "") && !withNextGenGUI {
log.Println("Notice: Next generation GUI will not be built; see --with-next-gen-gui.")
}
lazyRebuildAssets()
tags = append(target.tags, tags...)
@@ -481,16 +474,12 @@ func install(target target, tags []string) {
defer shouldCleanupSyso(sysoPath)
}
args := []string{"install", "-v"}
args := []string{"install"}
args = appendParameters(args, tags, target.buildPkgs...)
runPrint(goCmd, args...)
}
func build(target target, tags []string) {
if (target.name == "syncthing" || target.name == "") && !withNextGenGUI {
log.Println("Notice: Next generation GUI will not be built; see --with-next-gen-gui.")
}
lazyRebuildAssets()
tags = append(target.tags, tags...)
@@ -513,7 +502,7 @@ func build(target target, tags []string) {
defer shouldCleanupSyso(sysoPath)
}
args := []string{"build", "-v"}
args := []string{"build"}
if buildOut != "" {
args = append(args, "-o", buildOut)
}
@@ -525,13 +514,6 @@ func setBuildEnvVars() {
os.Setenv("GOOS", goos)
os.Setenv("GOARCH", goarch)
os.Setenv("CC", cc)
if os.Getenv("CGO_ENABLED") == "" {
switch goos {
case "darwin", "solaris":
default:
os.Setenv("CGO_ENABLED", "0")
}
}
}
func appendParameters(args []string, tags []string, pkgs ...string) []string {
@@ -747,12 +729,9 @@ func shouldBuildSyso(dir string) (string, error) {
sysoPath := filepath.Join(dir, "cmd", "syncthing", "resource.syso")
// See https://github.com/josephspurrier/goversioninfo#command-line-flags
armOption := ""
if strings.Contains(goarch, "arm") {
armOption = "-arm=true"
}
if _, err := runError("goversioninfo", "-o", sysoPath, armOption); err != nil {
arm := strings.HasPrefix(goarch, "arm")
a64 := strings.Contains(goarch, "64")
if _, err := runError("goversioninfo", "-o", sysoPath, fmt.Sprintf("-arm=%v", arm), fmt.Sprintf("-64=%v", a64)); err != nil {
return "", errors.New("failed to create " + sysoPath + ": " + err.Error())
}
@@ -824,43 +803,11 @@ func lazyRebuildAssets() {
shouldRebuild := shouldRebuildAssets("lib/api/auto/gui.files.go", "gui") ||
shouldRebuildAssets("cmd/infra/strelaypoolsrv/auto/gui.files.go", "cmd/infra/strelaypoolsrv/gui")
if withNextGenGUI {
shouldRebuild = buildNextGenGUI() || shouldRebuild
}
if shouldRebuild {
rebuildAssets()
}
}
func buildNextGenGUI() bool {
// Check if we need to run the npm process, and if so also set the flag
// to rebuild Go assets afterwards. The index.html is regenerated every
// time by the build process. This assumes the new GUI ends up in
// next-gen-gui/dist/next-gen-gui.
if !shouldRebuildAssets("gui/next-gen-gui/index.html", "next-gen-gui") {
// The GUI is up to date.
return false
}
runPrintInDir("next-gen-gui", "npm", "install")
runPrintInDir("next-gen-gui", "npm", "run", "build", "--", "--prod", "--subresource-integrity")
rmr("gui/tech-ui")
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, 0o644); err != nil {
fmt.Println("copy:", err)
os.Exit(1)
}
}
return true
}
func shouldRebuildAssets(target, srcdir string) bool {
info, err := os.Stat(target)
if err != nil {
@@ -1345,10 +1292,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())
}
}
@@ -1372,70 +1316,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(0o600) // 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")

View File

@@ -72,7 +72,7 @@ func main() {
if *standardBlocks || blockSize < protocol.MinBlockSize {
blockSize = protocol.BlockSize(fi.Size())
}
bs, err := scanner.Blocks(context.TODO(), fd, blockSize, fi.Size(), nil, true)
bs, err := scanner.Blocks(context.TODO(), fd, blockSize, fi.Size(), nil)
if err != nil {
log.Fatal(err)
}

View File

@@ -41,5 +41,4 @@ func (p *profileCommand) Run(ctx Context) error {
type debugCommand struct {
File fileCommand `cmd:"" help:"Show information about a file (or directory/symlink)"`
Profile profileCommand `cmd:"" help:"Save a profile to help figuring out what Syncthing does"`
Index indexCommand `cmd:"" help:"Show information about the index (database)"`
}

View File

@@ -1,32 +0,0 @@
// Copyright (C) 2014 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 cli
import (
"github.com/alecthomas/kong"
)
type indexCommand struct {
Dump struct{} `cmd:"" help:"Print the entire db"`
DumpSize struct{} `cmd:"" help:"Print the db size of different categories of information"`
Check struct{} `cmd:"" help:"Check the database for inconsistencies"`
Account struct{} `cmd:"" help:"Print key and value size statistics per key type"`
}
func (*indexCommand) Run(kongCtx *kong.Context) error {
switch kongCtx.Selected().Name {
case "dump":
return indexDump()
case "dump-size":
return indexDumpSize()
case "check":
return indexCheck()
case "account":
return indexAccount()
}
return nil
}

View File

@@ -1,62 +0,0 @@
// Copyright (C) 2020 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 cli
import (
"fmt"
"os"
"text/tabwriter"
)
// indexAccount prints key and data size statistics per class
func indexAccount() error {
ldb, err := getDB()
if err != nil {
return err
}
it, err := ldb.NewPrefixIterator(nil)
if err != nil {
return err
}
var ksizes [256]int
var dsizes [256]int
var counts [256]int
var max [256]int
for it.Next() {
key := it.Key()
t := key[0]
ds := len(it.Value())
ks := len(key)
s := ks + ds
counts[t]++
ksizes[t] += ks
dsizes[t] += ds
if s > max[t] {
max[t] = s
}
}
tw := tabwriter.NewWriter(os.Stdout, 1, 1, 1, ' ', tabwriter.AlignRight)
toti, totds, totks := 0, 0, 0
for t := range ksizes {
if ksizes[t] > 0 {
// yes metric kilobytes 🤘
fmt.Fprintf(tw, "0x%02x:\t%d items,\t%d KB keys +\t%d KB data,\t%d B +\t%d B avg,\t%d B max\t\n", t, counts[t], ksizes[t]/1000, dsizes[t]/1000, ksizes[t]/counts[t], dsizes[t]/counts[t], max[t])
toti += counts[t]
totds += dsizes[t]
totks += ksizes[t]
}
}
fmt.Fprintf(tw, "Total\t%d items,\t%d KB keys +\t%d KB data.\t\n", toti, totks/1000, totds/1000)
tw.Flush()
return nil
}

View File

@@ -1,162 +0,0 @@
// Copyright (C) 2015 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
package cli
import (
"encoding/binary"
"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"
)
func indexDump() error {
ldb, err := getDB()
if err != nil {
return err
}
it, err := ldb.NewPrefixIterator(nil)
if err != nil {
return err
}
for it.Next() {
key := it.Key()
switch key[0] {
case db.KeyTypeDevice:
folder := binary.BigEndian.Uint32(key[1:])
device := binary.BigEndian.Uint32(key[1+4:])
name := nulString(key[1+4+4:])
fmt.Printf("[device] F:%d D:%d N:%q", folder, device, name)
var f bep.FileInfo
err := proto.Unmarshal(it.Value(), &f)
if err != nil {
return err
}
fmt.Printf(" V:%v\n", &f)
case db.KeyTypeGlobal:
folder := binary.BigEndian.Uint32(key[1:])
name := nulString(key[1+4:])
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:])
hash := key[1+4 : 1+4+32]
name := nulString(key[1+4+32:])
fmt.Printf("[block] F:%d H:%x N:%q I:%d\n", folder, hash, name, binary.BigEndian.Uint32(it.Value()))
case db.KeyTypeDeviceStatistic:
fmt.Printf("[dstat] K:%x V:%x\n", key, it.Value())
case db.KeyTypeFolderStatistic:
fmt.Printf("[fstat] K:%x V:%x\n", key, it.Value())
case db.KeyTypeVirtualMtime:
folder := binary.BigEndian.Uint32(key[1:])
name := nulString(key[1+4:])
val := it.Value()
var realTime, virtualTime time.Time
realTime.UnmarshalBinary(val[:len(val)/2])
virtualTime.UnmarshalBinary(val[len(val)/2:])
fmt.Printf("[mtime] F:%d N:%q R:%v V:%v\n", folder, name, realTime, virtualTime)
case db.KeyTypeFolderIdx:
key := binary.BigEndian.Uint32(key[1:])
fmt.Printf("[folderidx] K:%d V:%q\n", key, it.Value())
case db.KeyTypeDeviceIdx:
key := binary.BigEndian.Uint32(key[1:])
val := it.Value()
device := "<nil>"
if len(val) > 0 {
dev, err := protocol.DeviceIDFromBytes(val)
if err != nil {
device = fmt.Sprintf("<invalid %d bytes>", len(val))
} else {
device = dev.String()
}
}
fmt.Printf("[deviceidx] K:%d V:%s\n", key, device)
case db.KeyTypeIndexID:
device := binary.BigEndian.Uint32(key[1:])
folder := binary.BigEndian.Uint32(key[5:])
fmt.Printf("[indexid] D:%d F:%d I:%x\n", device, folder, it.Value())
case db.KeyTypeFolderMeta:
folder := binary.BigEndian.Uint32(key[1:])
fmt.Printf("[foldermeta] F:%d", folder)
var cs dbproto.CountsSet
if err := proto.Unmarshal(it.Value(), &cs); err != nil {
fmt.Printf(" (invalid)\n")
} else {
fmt.Printf(" V:%v\n", &cs)
}
case db.KeyTypeMiscData:
fmt.Printf("[miscdata] K:%q V:%q\n", key[1:], it.Value())
case db.KeyTypeSequence:
folder := binary.BigEndian.Uint32(key[1:])
seq := binary.BigEndian.Uint64(key[5:])
fmt.Printf("[sequence] F:%d S:%d V:%q\n", folder, seq, it.Value())
case db.KeyTypeNeed:
folder := binary.BigEndian.Uint32(key[1:])
file := string(key[5:])
fmt.Printf("[need] F:%d V:%q\n", folder, file)
case db.KeyTypeBlockList:
fmt.Printf("[blocklist] H:%x\n", key[1:])
case db.KeyTypeBlockListMap:
folder := binary.BigEndian.Uint32(key[1:])
hash := key[5:37]
fileName := string(key[37:])
fmt.Printf("[blocklistmap] F:%d H:%x N:%s\n", folder, hash, fileName)
case db.KeyTypeVersion:
fmt.Printf("[version] H:%x", key[1:])
var v bep.Vector
err := proto.Unmarshal(it.Value(), &v)
if err != nil {
fmt.Printf(" (invalid)\n")
} else {
fmt.Printf(" V:%v\n", &v)
}
case db.KeyTypePendingFolder:
device := binary.BigEndian.Uint32(key[1:])
folder := string(key[5:])
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>"
dev, err := protocol.DeviceIDFromBytes(key[1:])
if err == nil {
device = dev.String()
}
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())
}
}
return nil
}

View File

@@ -1,88 +0,0 @@
// Copyright (C) 2015 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
package cli
import (
"encoding/binary"
"fmt"
"sort"
"github.com/syncthing/syncthing/lib/db"
)
func indexDumpSize() error {
type sizedElement struct {
key string
size int
}
ldb, err := getDB()
if err != nil {
return err
}
it, err := ldb.NewPrefixIterator(nil)
if err != nil {
return err
}
var elems []sizedElement
for it.Next() {
var ele sizedElement
key := it.Key()
switch key[0] {
case db.KeyTypeDevice:
folder := binary.BigEndian.Uint32(key[1:])
device := binary.BigEndian.Uint32(key[1+4:])
name := nulString(key[1+4+4:])
ele.key = fmt.Sprintf("DEVICE:%d:%d:%s", folder, device, name)
case db.KeyTypeGlobal:
folder := binary.BigEndian.Uint32(key[1:])
name := nulString(key[1+4:])
ele.key = fmt.Sprintf("GLOBAL:%d:%s", folder, name)
case db.KeyTypeBlock:
folder := binary.BigEndian.Uint32(key[1:])
hash := key[1+4 : 1+4+32]
name := nulString(key[1+4+32:])
ele.key = fmt.Sprintf("BLOCK:%d:%x:%s", folder, hash, name)
case db.KeyTypeDeviceStatistic:
ele.key = fmt.Sprintf("DEVICESTATS:%s", key[1:])
case db.KeyTypeFolderStatistic:
ele.key = fmt.Sprintf("FOLDERSTATS:%s", key[1:])
case db.KeyTypeVirtualMtime:
ele.key = fmt.Sprintf("MTIME:%s", key[1:])
case db.KeyTypeFolderIdx:
id := binary.BigEndian.Uint32(key[1:])
ele.key = fmt.Sprintf("FOLDERIDX:%d", id)
case db.KeyTypeDeviceIdx:
id := binary.BigEndian.Uint32(key[1:])
ele.key = fmt.Sprintf("DEVICEIDX:%d", id)
default:
ele.key = fmt.Sprintf("UNKNOWN:%x", key)
}
ele.size = len(it.Value())
elems = append(elems, ele)
}
sort.Slice(elems, func(i, j int) bool {
return elems[i].size > elems[j].size
})
for _, ele := range elems {
fmt.Println(ele.key, ele.size)
}
return nil
}

View File

@@ -1,434 +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 cli
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"sort"
"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"
)
type fileInfoKey struct {
folder uint32
device uint32
name string
}
type globalKey struct {
folder uint32
name string
}
type sequenceKey struct {
folder uint32
sequence uint64
}
func indexCheck() (err error) {
ldb, err := getDB()
if err != nil {
return err
}
folders := make(map[uint32]string)
devices := make(map[uint32]string)
deviceToIDs := make(map[string]uint32)
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]*bep.Vector)
usedBlocklists := make(map[string]struct{})
usedVersions := make(map[string]struct{})
var localDeviceKey uint32
success := true
defer func() {
if err == nil {
if success {
fmt.Println("Index check completed successfully.")
} else {
err = errors.New("Inconsistencies found in the index")
}
}
}()
it, err := ldb.NewPrefixIterator(nil)
if err != nil {
return err
}
for it.Next() {
key := it.Key()
switch key[0] {
case db.KeyTypeDevice:
folder := binary.BigEndian.Uint32(key[1:])
device := binary.BigEndian.Uint32(key[1+4:])
name := nulString(key[1+4+4:])
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
case db.KeyTypeGlobal:
folder := binary.BigEndian.Uint32(key[1:])
name := nulString(key[1+4:])
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
case db.KeyTypeFolderIdx:
key := binary.BigEndian.Uint32(it.Key()[1:])
folders[key] = string(it.Value())
case db.KeyTypeDeviceIdx:
key := binary.BigEndian.Uint32(it.Key()[1:])
devices[key] = string(it.Value())
deviceToIDs[string(it.Value())] = key
if bytes.Equal(it.Value(), protocol.LocalDeviceID[:]) {
localDeviceKey = key
}
case db.KeyTypeSequence:
folder := binary.BigEndian.Uint32(key[1:])
seq := binary.BigEndian.Uint64(key[5:])
val := it.Value()
sequences[sequenceKey{folder, seq}] = string(val[9:])
case db.KeyTypeNeed:
folder := binary.BigEndian.Uint32(key[1:])
name := nulString(key[1+4:])
needs[globalKey{folder, name}] = struct{}{}
case db.KeyTypeBlockList:
hash := string(key[1:])
blocklists[hash] = struct{}{}
case db.KeyTypeVersion:
hash := string(key[1:])
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
}
}
if localDeviceKey == 0 {
fmt.Println("Missing key for local device in device index (bailing out)")
success = false
return
}
var missingSeq []sequenceKey
for fk, fi := range fileInfos {
if fk.name != fi.Name {
fmt.Printf("Mismatching FileInfo name, %q (key) != %q (actual)\n", fk.name, fi.Name)
success = false
}
folder := folders[fk.folder]
if folder == "" {
fmt.Printf("Unknown folder ID %d for FileInfo %q\n", fk.folder, fk.name)
success = false
continue
}
if devices[fk.device] == "" {
fmt.Printf("Unknown device ID %d for FileInfo %q, folder %q\n", fk.folder, fk.name, folder)
success = false
}
if fk.device == localDeviceKey {
sk := sequenceKey{fk.folder, uint64(fi.Sequence)}
name, ok := sequences[sk]
if !ok {
fmt.Printf("Sequence entry missing for FileInfo %q, folder %q, seq %d\n", fi.Name, folder, fi.Sequence)
missingSeq = append(missingSeq, sk)
success = false
continue
}
if name != fi.Name {
fmt.Printf("Sequence entry refers to wrong name, %q (seq) != %q (FileInfo), folder %q, seq %d\n", name, fi.Name, folder, fi.Sequence)
success = false
}
}
if len(fi.Blocks) == 0 && len(fi.BlocksHash) != 0 {
key := string(fi.BlocksHash)
if _, ok := blocklists[key]; !ok {
fmt.Printf("Missing block list for file %q, block list hash %x\n", fi.Name, fi.BlocksHash)
success = false
} else {
usedBlocklists[key] = struct{}{}
}
}
if fi.VersionHash != nil {
key := string(fi.VersionHash)
if _, ok := versions[key]; !ok {
fmt.Printf("Missing version vector for file %q, version hash %x\n", fi.Name, fi.VersionHash)
success = false
} else {
usedVersions[key] = struct{}{}
}
}
_, ok := globals[globalKey{fk.folder, fk.name}]
if !ok {
fmt.Printf("Missing global for file %q\n", fi.Name)
success = false
continue
}
}
// 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
}
return missingSeq[a].sequence < missingSeq[b].sequence
})
var folder uint32
var startSeq, prevSeq uint64
for _, sk := range missingSeq {
if folder != sk.folder || sk.sequence != prevSeq+1 {
if folder != 0 {
fmt.Printf("Folder %d missing %d sequence entries: #%d - #%d\n", folder, prevSeq-startSeq+1, startSeq, prevSeq)
}
startSeq = sk.sequence
folder = sk.folder
}
prevSeq = sk.sequence
}
if folder != 0 {
fmt.Printf("Folder %d missing %d sequence entries: #%d - #%d\n", folder, prevSeq-startSeq+1, startSeq, prevSeq)
}
for gk, vl := range globals {
folder := folders[gk.folder]
if folder == "" {
fmt.Printf("Unknown folder ID %d for VersionList %q\n", gk.folder, gk.name)
success = false
}
checkGlobal := func(i int, device []byte, version protocol.Vector, invalid, deleted bool) {
dev, ok := deviceToIDs[string(device)]
if !ok {
fmt.Printf("VersionList %q, folder %q refers to unknown device %q\n", gk.name, folder, device)
success = false
}
fi, ok := fileInfos[fileInfoKey{gk.folder, dev, gk.name}]
if !ok {
fmt.Printf("VersionList %q, folder %q, entry %d refers to unknown FileInfo\n", gk.name, folder, i)
success = false
}
fiv := fi.Version
if fi.VersionHash != nil {
fiv = versions[string(fi.VersionHash)]
}
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
}
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 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.Versions {
ver := protocol.VectorFromWire(fv.Version)
for _, device := range fv.Devices {
checkGlobal(i, device, ver, false, fv.Deleted)
}
for _, device := range fv.InvalidDevices {
checkGlobal(i, device, ver, true, fv.Deleted)
}
}
// If we need this file we should have a need entry for it. False
// positives from needsLocally for deleted files, where we might
// legitimately lack an entry if we never had it, and ignored files.
if needsLocally(vl) {
_, ok := needs[gk]
if !ok {
fv, _ := vlGetGlobal(vl)
devB, _ := fvFirstDevice(fv)
dev := deviceToIDs[string(devB)]
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)
}
}
}
}
seenSeq := make(map[fileInfoKey]uint64)
for sk, name := range sequences {
folder := folders[sk.folder]
if folder == "" {
fmt.Printf("Unknown folder ID %d for sequence entry %d, %q\n", sk.folder, sk.sequence, name)
success = false
continue
}
if prev, ok := seenSeq[fileInfoKey{folder: sk.folder, name: name}]; ok {
fmt.Printf("Duplicate sequence entry for %q, folder %q, seq %d (prev %d)\n", name, folder, sk.sequence, prev)
success = false
}
seenSeq[fileInfoKey{folder: sk.folder, name: name}] = sk.sequence
fi, ok := fileInfos[fileInfoKey{sk.folder, localDeviceKey, name}]
if !ok {
fmt.Printf("Missing FileInfo for sequence entry %d, folder %q, %q\n", sk.sequence, folder, name)
success = false
continue
}
if fi.Sequence != int64(sk.sequence) {
fmt.Printf("Sequence mismatch for %q, folder %q, %d (key) != %d (FileInfo)\n", name, folder, sk.sequence, fi.Sequence)
success = false
}
}
for nk := range needs {
folder := folders[nk.folder]
if folder == "" {
fmt.Printf("Unknown folder ID %d for need entry %q\n", nk.folder, nk.name)
success = false
continue
}
vl, ok := globals[nk]
if !ok {
fmt.Printf("Missing global for need entry %q, folder %q\n", nk.name, folder)
success = false
continue
}
if !needsLocally(vl) {
fmt.Printf("Need entry for file we don't need, %q, folder %q\n", nk.name, folder)
success = false
}
}
if d := len(blocklists) - len(usedBlocklists); d > 0 {
fmt.Printf("%d block list entries out of %d needs GC\n", d, len(blocklists))
}
if d := len(versions) - len(usedVersions); d > 0 {
fmt.Printf("%d version entries out of %d needs GC\n", d, len(versions))
}
return nil
}
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 := 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

@@ -17,8 +17,6 @@ import (
"path/filepath"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/db/backend"
"github.com/syncthing/syncthing/lib/locations"
)
func responseToBArray(response *http.Response) ([]byte, error) {
@@ -133,10 +131,6 @@ func prettyPrintResponse(response *http.Response) error {
return prettyPrintJSON(data)
}
func getDB() (backend.Backend, error) {
return backend.OpenLevelDBRO(locations.Get(locations.Database))
}
func nulString(bs []byte) string {
for i := range bs {
if bs[i] == 0 {

View File

@@ -238,7 +238,7 @@ func (c *CLI) decryptFile(encFi *protocol.FileInfo, plainFi *protocol.FileInfo,
}
// Verify the hash against the plaintext block info
if !scanner.Validate(dec, plainBlock.Hash, 0) {
if !scanner.Validate(dec, plainBlock.Hash) {
// The block decrypted correctly but fails the hash check. This
// is odd and unexpected, but it it's still a valid block from
// the source. The file might have changed while we pulled it?

View File

@@ -22,6 +22,7 @@ import (
"path"
"path/filepath"
"regexp"
"runtime"
"runtime/pprof"
"sort"
"strconv"
@@ -30,6 +31,7 @@ import (
"time"
"github.com/alecthomas/kong"
"github.com/gofrs/flock"
"github.com/thejerf/suture/v4"
"github.com/willabides/kongplete"
@@ -37,11 +39,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/internal/db"
_ "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"
@@ -139,42 +140,41 @@ var entrypoint struct {
// serveOptions are the options for the `syncthing serve` command.
type serveOptions struct {
cmdutil.CommonOptions
AllowNewerConfig bool `help:"Allow loading newer than current config version"`
Audit bool `help:"Write events to audit file"`
AuditFile string `name:"auditfile" placeholder:"PATH" help:"Specify audit file (use \"-\" for stdout, \"--\" for stderr)"`
BrowserOnly bool `help:"Open GUI in browser"`
DataDir string `name:"data" placeholder:"PATH" env:"STDATADIR" help:"Set data directory (database and logs)"`
DeviceID bool `help:"Show the device ID"`
GenerateDir string `name:"generate" placeholder:"PATH" help:"Generate key and config in specified dir, then exit"` // DEPRECATED: replaced by subcommand!
GUIAddress string `name:"gui-address" placeholder:"URL" help:"Override GUI address (e.g. \"http://192.0.2.42:8443\")"`
GUIAPIKey string `name:"gui-apikey" placeholder:"API-KEY" help:"Override GUI API key"`
LogFile string `name:"logfile" default:"${logFile}" placeholder:"PATH" help:"Log file name (see below)"`
LogFlags int `name:"logflags" default:"${logFlags}" placeholder:"BITS" help:"Select information in log line prefix (see below)"`
LogMaxFiles int `placeholder:"N" default:"${logMaxFiles}" name:"log-max-old-files" help:"Number of old files to keep (zero to keep only current)"`
LogMaxSize int `placeholder:"BYTES" default:"${logMaxSize}" help:"Maximum size of any file (zero to disable log rotation)"`
NoBrowser bool `help:"Do not start browser"`
NoRestart bool `env:"STNORESTART" help:"Do not restart Syncthing when exiting due to API/GUI command, upgrade, or crash"`
NoUpgrade bool `env:"STNOUPGRADE" help:"Disable automatic upgrades"`
Paths bool `help:"Show configuration paths"`
Paused bool `help:"Start with all devices and folders paused"`
Unpaused bool `help:"Start with all devices and folders unpaused"`
Upgrade bool `help:"Perform upgrade"`
UpgradeCheck bool `help:"Check for available upgrade"`
UpgradeTo string `placeholder:"URL" help:"Force upgrade directly from specified URL"`
Verbose bool `help:"Print verbose log output"`
Version bool `help:"Show version"`
AllowNewerConfig bool `help:"Allow loading newer than current config version"`
Audit bool `help:"Write events to audit file"`
AuditFile string `name:"auditfile" placeholder:"PATH" help:"Specify audit file (use \"-\" for stdout, \"--\" for stderr)"`
BrowserOnly bool `help:"Open GUI in browser"`
DataDir string `name:"data" placeholder:"PATH" env:"STDATADIR" help:"Set data directory (database and logs)"`
DeviceID bool `help:"Show the device ID"`
GenerateDir string `name:"generate" placeholder:"PATH" help:"Generate key and config in specified dir, then exit"` // DEPRECATED: replaced by subcommand!
GUIAddress string `name:"gui-address" placeholder:"URL" help:"Override GUI address (e.g. \"http://192.0.2.42:8443\")"`
GUIAPIKey string `name:"gui-apikey" placeholder:"API-KEY" help:"Override GUI API key"`
LogFile string `name:"logfile" default:"${logFile}" placeholder:"PATH" help:"Log file name (see below)"`
LogFlags int `name:"logflags" default:"${logFlags}" placeholder:"BITS" help:"Select information in log line prefix (see below)"`
LogMaxFiles int `placeholder:"N" default:"${logMaxFiles}" name:"log-max-old-files" help:"Number of old files to keep (zero to keep only current)"`
LogMaxSize int `placeholder:"BYTES" default:"${logMaxSize}" help:"Maximum size of any file (zero to disable log rotation)"`
NoBrowser bool `help:"Do not start browser"`
NoRestart bool `env:"STNORESTART" help:"Do not restart Syncthing when exiting due to API/GUI command, upgrade, or crash"`
NoUpgrade bool `env:"STNOUPGRADE" help:"Disable automatic upgrades"`
Paths bool `help:"Show configuration paths"`
Paused bool `help:"Start with all devices and folders paused"`
Unpaused bool `help:"Start with all devices and folders unpaused"`
Upgrade bool `help:"Perform upgrade"`
UpgradeCheck bool `help:"Check for available upgrade"`
UpgradeTo string `placeholder:"URL" help:"Force upgrade directly from specified URL"`
Verbose bool `help:"Print verbose log output"`
Version bool `help:"Show version"`
DBMaintenanceInterval time.Duration `env:"STDBMAINTINTERVAL" help:"Database maintenance interval" default:"8h"`
// Debug options below
DebugDBIndirectGCInterval time.Duration `env:"STGCINDIRECTEVERY" help:"Database indirection GC interval"`
DebugDBRecheckInterval time.Duration `env:"STRECHECKDBEVERY" help:"Database metadata recalculation interval"`
DebugGUIAssetsDir string `placeholder:"PATH" help:"Directory to load GUI assets from" env:"STGUIASSETS"`
DebugPerfStats bool `env:"STPERFSTATS" help:"Write running performance statistics to perf-$pid.csv (Unix only)"`
DebugProfileBlock bool `env:"STBLOCKPROFILE" help:"Write block profiles to block-$pid-$timestamp.pprof every 20 seconds"`
DebugProfileCPU bool `help:"Write a CPU profile to cpu-$pid.pprof on exit" env:"STCPUPROFILE"`
DebugProfileHeap bool `env:"STHEAPPROFILE" help:"Write heap profiles to heap-$pid-$timestamp.pprof each time heap usage increases"`
DebugProfilerListen string `placeholder:"ADDR" env:"STPROFILER" help:"Network profiler listen address"`
DebugResetDatabase bool `name:"reset-database" help:"Reset the database, forcing a full rescan and resync"`
DebugResetDeltaIdxs bool `name:"reset-deltas" help:"Reset delta index IDs, forcing a full index exchange"`
DebugGUIAssetsDir string `placeholder:"PATH" help:"Directory to load GUI assets from" env:"STGUIASSETS"`
DebugPerfStats bool `env:"STPERFSTATS" help:"Write running performance statistics to perf-$pid.csv (Unix only)"`
DebugProfileBlock bool `env:"STBLOCKPROFILE" help:"Write block profiles to block-$pid-$timestamp.pprof every 20 seconds"`
DebugProfileCPU bool `help:"Write a CPU profile to cpu-$pid.pprof on exit" env:"STCPUPROFILE"`
DebugProfileHeap bool `env:"STHEAPPROFILE" help:"Write heap profiles to heap-$pid-$timestamp.pprof each time heap usage increases"`
DebugProfilerListen string `placeholder:"ADDR" env:"STPROFILER" help:"Network profiler listen address"`
DebugResetDatabase bool `name:"reset-database" help:"Reset the database, forcing a full rescan and resync"`
DebugResetDeltaIdxs bool `name:"reset-deltas" help:"Reset delta index IDs, forcing a full index exchange"`
// Internal options, not shown to users
InternalRestarting bool `env:"STRESTART" hidden:"1"`
@@ -376,13 +376,14 @@ 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.CertFile))
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)
}
}
@@ -544,6 +545,17 @@ func syncthingMain(options serveOptions) {
os.Exit(1)
}
// Ensure we are the only running instance
lf := flock.New(locations.Get(locations.CertFile))
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()
@@ -580,8 +592,12 @@ func syncthingMain(options serveOptions) {
})
}
dbFile := locations.Get(locations.Database)
ldb, err := syncthing.OpenDBBackend(dbFile, cfgWrapper.Options().DatabaseTuning)
if err := syncthing.TryMigrateDatabase(); err != nil {
l.Warnln("Failed to migrate old-style database:", err)
os.Exit(1)
}
sdb, err := syncthing.OpenDatabase(locations.Get(locations.Database))
if err != nil {
l.Warnln("Error opening database:", err)
os.Exit(1)
@@ -590,11 +606,11 @@ func syncthingMain(options serveOptions) {
// Check if auto-upgrades is possible, and if yes, and it's enabled do an initial
// upgrade immediately. The auto-upgrade routine can only be started
// later after App is initialised.
autoUpgradePossible := autoUpgradePossible(options)
if autoUpgradePossible && cfgWrapper.Options().AutoUpgradeEnabled() {
// try to do upgrade directly and log the error if relevant.
release, err := initialAutoUpgradeCheck(db.NewMiscDataNamespace(ldb))
miscDB := db.NewMiscDB(sdb)
release, err := initialAutoUpgradeCheck(miscDB)
if err == nil {
err = upgrade.To(release)
}
@@ -605,7 +621,7 @@ func syncthingMain(options serveOptions) {
l.Infoln("Initial automatic upgrade:", err)
}
} else {
l.Infof("Upgraded to %q, exiting now.", release.Tag)
l.Infof("Upgraded to %q, should exit now.", release.Tag)
os.Exit(svcutil.ExitUpgrade.AsInt())
}
}
@@ -617,24 +633,17 @@ func syncthingMain(options serveOptions) {
}
appOpts := syncthing.Options{
NoUpgrade: options.NoUpgrade,
ProfilerAddr: options.DebugProfilerListen,
ResetDeltaIdxs: options.DebugResetDeltaIdxs,
Verbose: options.Verbose,
DBRecheckInterval: options.DebugDBRecheckInterval,
DBIndirectGCInterval: options.DebugDBIndirectGCInterval,
NoUpgrade: options.NoUpgrade,
ProfilerAddr: options.DebugProfilerListen,
ResetDeltaIdxs: options.DebugResetDeltaIdxs,
Verbose: options.Verbose,
DBMaintenanceInterval: options.DBMaintenanceInterval,
}
if options.Audit {
appOpts.AuditWriter = auditWriter(options.AuditFile)
}
if dur, err := time.ParseDuration(os.Getenv("STRECHECKDBEVERY")); err == nil {
appOpts.DBRecheckInterval = dur
}
if dur, err := time.ParseDuration(os.Getenv("STGCINDIRECTEVERY")); err == nil {
appOpts.DBIndirectGCInterval = dur
}
app, err := syncthing.New(cfgWrapper, ldb, evLogger, cert, appOpts)
app, err := syncthing.New(cfgWrapper, sdb, evLogger, cert, appOpts)
if err != nil {
l.Warnln("Failed to start Syncthing:", err)
os.Exit(svcutil.ExitError.AsInt())
@@ -680,6 +689,7 @@ func syncthingMain(options serveOptions) {
pprof.StopCPUProfile()
}
runtime.KeepAlive(lf) // ensure lock is still held to this point
os.Exit(int(status))
}
@@ -821,7 +831,7 @@ func autoUpgrade(cfg config.Wrapper, app *syncthing.App, evLogger events.Logger)
}
}
func initialAutoUpgradeCheck(misc *db.NamespacedKV) (upgrade.Release, error) {
func initialAutoUpgradeCheck(misc *db.Typed) (upgrade.Release, error) {
if last, ok, err := misc.Time(upgradeCheckKey); err == nil && ok && time.Since(last) < upgradeCheckInterval {
return upgrade.Release{}, errTooEarlyUpgradeCheck
}
@@ -830,6 +840,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

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

64
go.mod
View File

@@ -1,51 +1,53 @@
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.6.0
github.com/aws/aws-sdk-go v1.55.5
github.com/alecthomas/kong v1.9.0
github.com/aws/aws-sdk-go v1.55.6
github.com/calmh/incontainer v1.0.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.10
github.com/gobwas/glob v0.2.3
github.com/greatroar/blobloom v0.8.0
github.com/gofrs/flock v0.12.1
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/jmoiron/sqlx v1.4.0
github.com/julienschmidt/httprouter v1.3.0
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/maruel/panicparse/v2 v2.4.0
github.com/maxbrunsfeld/counterfeiter/v6 v6.8.1
github.com/mattn/go-sqlite3 v1.14.24
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.5
github.com/puzpuzpuz/xsync/v3 v3.4.0
github.com/quic-go/quic-go v0.48.2
github.com/pierrec/lz4/v4 v4.1.22
github.com/prometheus/client_golang v1.21.1
github.com/puzpuzpuz/xsync/v3 v3.5.1
github.com/quic-go/quic-go v0.50.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.11
github.com/syncthing/notify v0.0.0-20210616190510-c6b7342338d2
github.com/shirou/gopsutil/v4 v4.25.2
github.com/syncthing/notify v0.0.0-20250207082249-f0fa8f99c2bc
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d
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.6.0
golang.org/x/crypto v0.30.0
golang.org/x/net v0.32.0
golang.org/x/sys v0.28.0
golang.org/x/text v0.21.0
golang.org/x/time v0.8.0
golang.org/x/tools v0.28.0
google.golang.org/protobuf v1.35.2
golang.org/x/crypto v0.36.0
golang.org/x/net v0.37.0
golang.org/x/sys v0.31.0
golang.org/x/text v0.23.0
golang.org/x/time v0.11.0
golang.org/x/tools v0.31.0
google.golang.org/protobuf v1.36.5
modernc.org/sqlite v1.36.0
sigs.k8s.io/yaml v1.4.0
)
@@ -57,12 +59,12 @@ require (
github.com/cespare/xxhash/v2 v2.3.0 // 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.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.8.2 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/gofrs/flock v0.12.1 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/pprof v0.0.0-20241009165004-a3522334989c // indirect
github.com/google/uuid v1.6.0 // indirect
@@ -71,7 +73,9 @@ require (
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/nxadm/tail v1.4.11 // indirect
github.com/onsi/ginkgo/v2 v2.20.2 // indirect
github.com/oschwald/maxminddb-golang v1.13.1 // indirect
@@ -80,20 +84,24 @@ require (
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.60.0 // indirect
github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/riywo/loginshell v0.0.0-20200815045211-7d26008be1ab // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.9.0 // indirect
github.com/stretchr/testify v1.10.0 // indirect
github.com/tklauser/go-sysconf v0.3.14 // 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
go.uber.org/mock v0.5.0 // indirect
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect
golang.org/x/mod v0.22.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/mod v0.24.0 // indirect
golang.org/x/sync v0.12.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.61.13 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.8.2 // indirect
)
// https://github.com/gobwas/glob/pull/55

174
go.sum
View File

@@ -1,3 +1,5 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
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/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
@@ -5,14 +7,14 @@ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzS
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.6.0 h1:mwOzbdMR7uv2vul9J0FU3GYxE7ls/iX1ieMg5WIM6gE=
github.com/alecthomas/kong v1.6.0/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU=
github.com/alecthomas/kong v1.9.0 h1:Wgg0ll5Ys7xDnpgYBuBn/wPeLGAuK0NvYmEcisJgrIs=
github.com/alecthomas/kong v1.9.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.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk=
github.com/aws/aws-sdk-go v1.55.6/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=
@@ -29,8 +31,6 @@ github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d h1:S2NE3iHSwP0XV
github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chmduquesne/rollinghash v4.0.0+incompatible h1:hnREQO+DXjqIw3rUTzWN7/+Dpw+N5Um8zpKV0JOEgbo=
github.com/chmduquesne/rollinghash v4.0.0+incompatible/go.mod h1:Uc2I36RRfTAf7Dge82bi3RU0OQUmXT9iweIcPqvr8A0=
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=
@@ -41,8 +41,10 @@ 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.1 h1:sdRKd6plj7KYW33EH5As6YKfe8m9zbN9JMrOjNVF/BE=
github.com/ebitengine/purego v0.8.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
github.com/ebitengine/purego v0.8.2/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=
@@ -51,16 +53,17 @@ 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-ldap/ldap/v3 v3.4.10 h1:ot/iwPOhfpNVgB1o+AVXljizWZ9JTp7YF5oeyONmcJU=
github.com/go-ldap/ldap/v3 v3.4.10/go.mod h1:JXh4Uxgi40P6E9rdsYqpUtbW46D9UTjJ9QSwGRznplY=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
@@ -82,8 +85,9 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.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-20241009165004-a3522334989c h1:NDovD0SMpBYXlE1zJmS1q55vWB/fUQBcPAqAboZSccA=
github.com/google/pprof v0.0.0-20241009165004-a3522334989c/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
@@ -91,8 +95,6 @@ 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=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -108,8 +110,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=
@@ -128,6 +130,8 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
@@ -140,18 +144,27 @@ 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.4.0 h1:yQKMIbQ0DKfinzVkTkcUzQyQ60UCiNnYfR7PWwTs2VI=
github.com/maruel/panicparse/v2 v2.4.0/go.mod h1:nOY2OKe8csO3F3SA5+hsxot05JLgukrF54B9x88fVp4=
github.com/maxbrunsfeld/counterfeiter/v6 v6.8.1 h1:NicmruxkeqHjDv03SfSxqmaLuisddudfP3h5wdXFbhM=
github.com/maxbrunsfeld/counterfeiter/v6 v6.8.1/go.mod h1:eyp4DdUJAKkr9tvxR3jWhw2mDK7CWABMG5r9uyaKC7I=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
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/miscreant/miscreant.go v0.0.0-20200214223636-26d376326b75 h1:cUVxyR+UfmdEAZGJ8IiKld1O0dbGotEnkMolG5hfMSY=
github.com/miscreant/miscreant.go v0.0.0-20200214223636-26d376326b75/go.mod h1:pBbZyGwC5i16IBkjVKoy/sznA8jPD/K9iedwe1ESE6w=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY=
@@ -168,14 +181,14 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J
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.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw=
github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
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/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=
@@ -187,22 +200,24 @@ 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.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk=
github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
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.60.0 h1:+V9PAREWNvJMAuJ1x1BaWl9dewMW4YrHZQbx0sJNllA=
github.com/prometheus/common v0.60.0/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw=
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.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE=
github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs=
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.50.0 h1:3H/ld1pa3CYhkcc20TPIyG1bNsdhn9qZBGN3b9/UyUo=
github.com/quic-go/quic-go v0.50.0/go.mod h1:Vim6OmUvlYdwBhXP9ZVrtGmCMWa3wEqhq3NgYrI8b4E=
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/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
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=
@@ -211,8 +226,8 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8=
github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM=
github.com/shirou/gopsutil/v4 v4.24.11 h1:WaU9xqGFKvFfsUv94SXcUPD7rCkU0vr/asVdQOBZNj8=
github.com/shirou/gopsutil/v4 v4.24.11/go.mod h1:s4D/wg+ag4rG0WO7AiTj2BeYCRhym0vM7DHbZRxnIT8=
github.com/shirou/gopsutil/v4 v4.25.2 h1:NMscG3l2CqtWFS86kj3vP7soOczqrQYIEhO/pMvvQkk=
github.com/shirou/gopsutil/v4 v4.25.2/go.mod h1:34gBYJzyqCDT11b6bMHP0XCvWeU3J61XRT7a2EmCRTA=
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=
@@ -225,10 +240,11 @@ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1F
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-20250207082249-f0fa8f99c2bc h1:xc3UfSFlH/X5hRw3h21RF6WXnRUYKmGRx06FEaVxfkM=
github.com/syncthing/notify v0.0.0-20250207082249-f0fa8f99c2bc/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.6 h1:QsuCEsCqb03xF9tPAsWAj8QOAJBgQI1c0VqJNaingg8=
@@ -252,24 +268,29 @@ 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.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
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.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
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.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY=
golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
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.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
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=
@@ -284,17 +305,23 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
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.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
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.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI=
golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
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-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.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.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=
@@ -318,35 +345,46 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.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.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
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.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
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/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
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.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
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-20201224043029-2b0845dc783e/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.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
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=
@@ -360,8 +398,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.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
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=
@@ -377,5 +415,29 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0=
modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.23.16 h1:Z2N+kk38b7SfySC1ZkpGLN2vthNJP1+ZzGZIlH7uBxo=
modernc.org/ccgo/v4 v4.23.16/go.mod h1:nNma8goMTY7aQZQNTyN9AIoJfxav4nvTnvKThAeMDdo=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.6.3 h1:aJVhcqAte49LF+mGveZ5KPlsp4tdGdAOT4sipJXADjw=
modernc.org/gc/v2 v2.6.3/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/libc v1.61.13 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8=
modernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI=
modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.36.0 h1:EQXNRn4nIS+gfsKeUTymHIz1waxuv5BzU7558dHSfH8=
modernc.org/sqlite v1.36.0/go.mod h1:7MPwH7Z6bREicF9ZVUR78P1IKuxfZ8mRIDHD0iD+8TU=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=

View File

@@ -64,7 +64,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 +95,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 +112,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 +132,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 +142,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.",
@@ -158,13 +158,13 @@
"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 +182,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 +212,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 +223,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 +233,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 +283,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 +308,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 +362,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 +388,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 +396,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 +409,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 +435,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 +456,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 +491,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 +512,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 +524,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

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

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

@@ -403,7 +403,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 +417,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 +436,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

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

@@ -58,6 +58,7 @@
"Configured": "設定値",
"Connected (Unused)": "接続中 (未使用)",
"Connection Error": "接続エラー",
"Connection Management": "接続管理",
"Connection Type": "接続種別",
"Connections": "接続",
"Connections via relays might be rate limited by the relay": "中継サーバー経由の通信では、中継サーバーによる帯域制限が行われる場合があります",
@@ -86,7 +87,7 @@
"Device ID": "デバイスID",
"Device Identification": "デバイスID",
"Device Name": "デバイス名",
"Device rate limits": "デバイス速度制限",
"Device rate limits": "デバイス帯域制限",
"Device that last modified the item": "項目を最後に変更したデバイス",
"Devices": "デバイス",
"Disable Crash Reporting": "クラッシュレポートを無効にする",
@@ -220,6 +221,7 @@
"No upgrades": "アップグレードしない",
"Not shared": "非共有",
"Notice": "通知",
"Number of Connections": "接続数",
"OK": "OK",
"Off": "オフ",
"Oldest First": "古い順",
@@ -368,6 +370,7 @@
"The number of old versions to keep, per file.": "ファイルごとに古いバージョンをいくつ保持するかを指定します。",
"The number of versions must be a number and cannot be blank.": "保持するバージョン数は数値を指定してください。空欄にはできません。",
"The path cannot be blank.": "パスを入力してください。",
"The rate limit 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以上で指定して下さい。 (0で無制限)",
"The remote device has not accepted sharing this folder.": "接続先デバイスはこのフォルダーの共有を承諾していません。",
"The remote device has paused this folder.": "接続先デバイスはこのフォルダーを一時停止中です。",
@@ -427,6 +430,7 @@
"Watching for changes discovers most changes without periodic scanning.": "変更の監視は、定期スキャンを行わずにほとんどの変更を検出できます。",
"When adding a new device, keep in mind that this device must be added on the other side too.": "新しいデバイスを追加する際は、相手側デバイスにもこのデバイスを追加してください。",
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "新しいフォルダーを追加する際、フォルダーIDはデバイス間でフォルダーの対応づけに使われることに注意してください。フォルダーIDは大文字と小文字が区別され、共有するすべてのデバイスの間で完全に一致しなくてはなりません。",
"When set to more than one on both devices, Syncthing will attempt to establish multiple concurrent connections. If the values differ, the highest will be used. Set to zero to let Syncthing decide.": "両方のデバイスで1より高い値に設定すると、Syncthingは複数同時接続を確立しようとします。値が異なる場合は、最も高い値が使用されます。Syncthingに決定させる場合は、「0」を設定します。",
"Yes": "はい",
"Yesterday": "昨日",
"You can also select one of these nearby devices:": "近くに検出された以下のデバイスの一つを選択できます。",

View File

@@ -6,11 +6,14 @@
"About": "Apie programą",
"Action": "Veiksmas",
"Actions": "Veiksmai",
"Active filter rules": "Aktyvios filtro taisyklės",
"Add": "Pridėti",
"Add Device": "Pridėti įrenginį",
"Add Folder": "Pridėti aplanką",
"Add Remote Device": "Pridėti nuotolinį įrenginį",
"Add devices from the introducer to our device list, for mutually shared folders.": "Pridėti įrenginius iš supažindintojo į mūsų įrenginių sąrašą, siekiant abipusiškai bendrinti aplankus.",
"Add filter entry": "Pridėti filtro įrašą",
"Add ignore patterns": "Pridėti ignoravimo šablonų",
"Add new folder?": "Pridėti naują aplanką?",
"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.": "Pilnas nuskaitymo iš naujo intervalas bus papildomai padidintas (60 kartų, t.y. nauja numatytoji 1 val. reikšmė). Taip pat vėliau, pasirinkę Ne, galite jį konfigūruoti rankiniu būdu kiekvienam atskiram aplankui.",
"Address": "Adresas",
@@ -45,6 +48,7 @@
"Automatically create or share folders that this device advertises at the default path.": "Automatiškai sukurti ar bendrinti aplankus, kuriuos šis įrenginys skelbia numatytajame kelyje.",
"Available debug logging facilities:": "Prieinamos derinimo registravimo priemonės:",
"Be careful!": "Būkite atsargūs!",
"Body:": "Turinys:",
"Bugs": "Klaidos",
"Cancel": "Atsisakyti",
"Changelog": "Keitinių žurnalas",
@@ -63,6 +67,7 @@
"Connection Management": "Ryšių valdymas",
"Connection Type": "Ryšio tipas",
"Connections": "Ryšiai",
"Connections via relays might be rate limited by the relay": "Ryšiai per tarpinius mazgus gali būti ribojami pagal užklausų skaičių per laiko vienetą",
"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.": "Pastoviai stebėti pakeitimus dabar galima Syncthing viduje. Tai aptiks pakeitimus jūsų diske ir paleis nuskaitymą tik modifikuotuose keliuose. Pranašumas yra tas, kad pakeitimai sklis greičiau ir reikės mažiau pilnų nuskaitymų.",
"Copied from elsewhere": "Nukopijuota iš kitur",
"Copied from original": "Nukopijuota iš originalo",

View File

@@ -1,2 +1,3 @@
{
"A device with that ID is already added.": "อุปกรณ์ที่เป็น ID นั้นๆ ได้ถูกเพิ่มเข้าไปแล้ว"
}

View File

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

View File

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

View File

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

View File

@@ -251,6 +251,9 @@
<select class="form-control" ng-if="currentFolder.type == 'sendonly'" disabled>
<option value="disabled" translate>Disabled</option>
</select>
<p class="help-block" ng-if="currentFolder.type === 'sendonly'" translate translate-value-foldertype="{{'Send Only' | translate}}">
Cannot be enabled when the folder type is "{%foldertype%}".
</p>
</div>
</div>
@@ -282,6 +285,9 @@
<p translate class="help-block">
Disables comparing and syncing file permissions. Useful on systems with nonexistent or custom permissions (e.g. FAT, exFAT, Synology, Android).
</p>
<p class="help-block" ng-if="currentFolder._recvEnc" translate translate-value-foldertype="{{'Receive Encrypted' | translate}}">
Always turned on when the folder type is "{%foldertype%}".
</p>
</div>
</div>
@@ -297,12 +303,18 @@
<p translate class="help-block">
Enables sending ownership information to other devices, and applying incoming ownership information. Typically requires running with elevated privileges.
</p>
<p class="help-block" ng-if="has(['sendonly', 'receiveencrypted'], currentFolder.type)" translate translate-value-foldertype="{{currentFolder.type === 'sendonly' ? ('Send Only' | translate) : currentFolder.type === 'receiveencrypted' ? ('Receive Encrypted' | translate) : ''}}">
Cannot be enabled when the folder type is "{%foldertype%}".
</p>
<label>
<input type="checkbox" ng-disabled="currentFolder.type == 'receiveonly' || currentFolder.type == 'receiveencrypted' || currentFolder.syncOwnership" ng-checked="currentFolder.sendOwnership || currentFolder.syncOwnership" ng-model="currentFolder.sendOwnership" /> <span translate>Send Ownership</span>
</label>
<p translate class="help-block">
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.
</p>
<p class="help-block" ng-if="has(['receiveonly', 'receiveencrypted'], currentFolder.type)" translate translate-value-foldertype="{{currentFolder.type === 'receiveonly' ? ('Receive Only' | translate) : currentFolder.type === 'receiveencrypted' ? ('Receive Encrypted' | translate) : ''}}">
Cannot be enabled when the folder type is "{%foldertype%}".
</p>
</div>
<div class="col-md-6 form-group">
<p>
@@ -315,12 +327,18 @@
<p translate class="help-block">
Enables sending extended attributes to other devices, and applying incoming extended attributes. May require running with elevated privileges.
</p>
<p class="help-block" ng-if="has(['sendonly', 'receiveencrypted'], currentFolder.type)" translate translate-value-foldertype="{{currentFolder.type === 'sendonly' ? ('Send Only' | translate) : currentFolder.type === 'receiveencrypted' ? ('Receive Encrypted' | translate) : ''}}">
Cannot be enabled when the folder type is "{%foldertype%}".
</p>
<label>
<input type="checkbox" ng-disabled="currentFolder.type == 'receiveonly' || currentFolder.type == 'receiveencrypted' || currentFolder.syncXattrs" ng-checked="currentFolder.sendXattrs || currentFolder.syncXattrs" ng-model="currentFolder.sendXattrs" /> <span translate>Send Extended Attributes</span>
</label>
<p translate class="help-block">
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.
</p>
<p class="help-block" ng-if="has(['receiveonly', 'receiveencrypted'], currentFolder.type)" translate translate-value-foldertype="{{currentFolder.type === 'receiveonly' ? ('Receive Only' | translate) : currentFolder.type === 'receiveencrypted' ? ('Receive Encrypted' | translate) : ''}}">
Cannot be enabled when the folder type is "{%foldertype%}".
</p>
</div>
</div>

73
internal/db/counts.go Normal file
View File

@@ -0,0 +1,73 @@
// Copyright (C) 2014 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 db
import (
"fmt"
"strings"
"github.com/syncthing/syncthing/lib/protocol"
)
type Counts struct {
Files int
Directories int
Symlinks int
Deleted int
Bytes int64
Sequence int64 // zero for the global state
DeviceID protocol.DeviceID // device ID for remote devices, or special values for local/global
LocalFlags uint32 // the local flag for this count bucket
}
func (c Counts) Add(other Counts) Counts {
return Counts{
Files: c.Files + other.Files,
Directories: c.Directories + other.Directories,
Symlinks: c.Symlinks + other.Symlinks,
Deleted: c.Deleted + other.Deleted,
Bytes: c.Bytes + other.Bytes,
Sequence: c.Sequence + other.Sequence,
DeviceID: protocol.EmptyDeviceID,
LocalFlags: c.LocalFlags | other.LocalFlags,
}
}
func (c Counts) TotalItems() int {
return c.Files + c.Directories + c.Symlinks + c.Deleted
}
func (c Counts) String() string {
var flags strings.Builder
if c.LocalFlags&protocol.FlagLocalNeeded != 0 {
flags.WriteString("Need")
}
if c.LocalFlags&protocol.FlagLocalIgnored != 0 {
flags.WriteString("Ignored")
}
if c.LocalFlags&protocol.FlagLocalMustRescan != 0 {
flags.WriteString("Rescan")
}
if c.LocalFlags&protocol.FlagLocalReceiveOnly != 0 {
flags.WriteString("Recvonly")
}
if c.LocalFlags&protocol.FlagLocalUnsupported != 0 {
flags.WriteString("Unsupported")
}
if c.LocalFlags != 0 {
flags.WriteString(fmt.Sprintf("(%x)", c.LocalFlags))
}
if flags.Len() == 0 {
flags.WriteString("---")
}
return fmt.Sprintf("{Device:%v, Files:%d, Dirs:%d, Symlinks:%d, Del:%d, Bytes:%d, Seq:%d, Flags:%s}", c.DeviceID, c.Files, c.Directories, c.Symlinks, c.Deleted, c.Bytes, c.Sequence, flags.String())
}
// Equal compares the numbers only, not sequence/dev/flags.
func (c Counts) Equal(o Counts) bool {
return c.Files == o.Files && c.Directories == o.Directories && c.Symlinks == o.Symlinks && c.Deleted == o.Deleted && c.Bytes == o.Bytes
}

123
internal/db/interface.go Normal file
View File

@@ -0,0 +1,123 @@
// Copyright (C) 2025 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 db // import "github.com/syncthing/syncthing/internal/db/sqlite"
import (
"iter"
"time"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/thejerf/suture/v4"
)
type DB interface {
Service(maintenanceInterval time.Duration) suture.Service
// Basics
Update(folder string, device protocol.DeviceID, fs []protocol.FileInfo) error
Close() error
// Single files
GetDeviceFile(folder string, device protocol.DeviceID, file string) (protocol.FileInfo, bool, error)
GetGlobalAvailability(folder, file string) ([]protocol.DeviceID, error)
GetGlobalFile(folder string, file string) (protocol.FileInfo, bool, error)
// File iterators
//
// n.b. there is a slight inconsistency in the return types where some
// return a FileInfo iterator and some a FileMetadata iterator. The
// latter is more lightweight, and the discrepancy depends on how the
// functions tend to be used. We can introduce more variations as
// required.
AllGlobalFiles(folder string) (iter.Seq[FileMetadata], func() error)
AllGlobalFilesPrefix(folder string, prefix string) (iter.Seq[FileMetadata], func() error)
AllLocalBlocksWithHash(hash []byte) (iter.Seq[BlockMapEntry], func() error)
AllLocalFiles(folder string, device protocol.DeviceID) (iter.Seq[protocol.FileInfo], func() error)
AllLocalFilesBySequence(folder string, device protocol.DeviceID, startSeq int64, limit int) (iter.Seq[protocol.FileInfo], func() error)
AllLocalFilesWithPrefix(folder string, device protocol.DeviceID, prefix string) (iter.Seq[protocol.FileInfo], func() error)
AllLocalFilesWithBlocksHash(folder string, h []byte) (iter.Seq[FileMetadata], func() error)
AllLocalFilesWithBlocksHashAnyFolder(h []byte) (iter.Seq2[string, FileMetadata], func() error)
AllNeededGlobalFiles(folder string, device protocol.DeviceID, order config.PullOrder, limit, offset int) (iter.Seq[protocol.FileInfo], func() error)
// Cleanup
DropAllFiles(folder string, device protocol.DeviceID) error
DropDevice(device protocol.DeviceID) error
DropFilesNamed(folder string, device protocol.DeviceID, names []string) error
DropFolder(folder string) error
// Various metadata
GetDeviceSequence(folder string, device protocol.DeviceID) (int64, error)
ListFolders() ([]string, error)
ListDevicesForFolder(folder string) ([]protocol.DeviceID, error)
RemoteSequences(folder string) (map[protocol.DeviceID]int64, error)
// Counts
CountGlobal(folder string) (Counts, error)
CountLocal(folder string, device protocol.DeviceID) (Counts, error)
CountNeed(folder string, device protocol.DeviceID) (Counts, error)
CountReceiveOnlyChanged(folder string) (Counts, error)
// Index IDs
DropAllIndexIDs() error
GetIndexID(folder string, device protocol.DeviceID) (protocol.IndexID, error)
SetIndexID(folder string, device protocol.DeviceID, id protocol.IndexID) error
// MtimeFS
DeleteMtime(folder, name string) error
GetMtime(folder, name string) (ondisk, virtual time.Time)
PutMtime(folder, name string, ondisk, virtual time.Time) error
KV
}
// Generic KV store
type KV interface {
GetKV(key string) ([]byte, error)
PutKV(key string, val []byte) error
DeleteKV(key string) error
PrefixKV(prefix string) (iter.Seq[KeyValue], func() error)
}
type BlockMapEntry struct {
BlocklistHash []byte
Offset int64
BlockIndex int
Size int
}
type KeyValue struct {
Key string
Value []byte
}
type FileMetadata struct {
Name string
Sequence int64
ModNanos int64
Size int64
LocalFlags int64
Type protocol.FileInfoType
Deleted bool
Invalid bool
}
func (f *FileMetadata) ModTime() time.Time {
return time.Unix(0, f.ModNanos)
}
func (f *FileMetadata) IsReceiveOnlyChanged() bool {
return f.LocalFlags&protocol.FlagLocalReceiveOnly != 0
}
func (f *FileMetadata) IsDirectory() bool {
return f.Type == protocol.FileInfoTypeDirectory
}
func (f *FileMetadata) ShouldConflict() bool {
return f.LocalFlags&protocol.LocalConflictFlags != 0
}

229
internal/db/metrics.go Normal file
View File

@@ -0,0 +1,229 @@
// Copyright (C) 2025 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 db
import (
"iter"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/protocol"
)
var (
metricCurrentOperations = promauto.NewGaugeVec(prometheus.GaugeOpts{
Namespace: "syncthing",
Subsystem: "db",
Name: "operations_current",
}, []string{"folder", "operation"})
metricTotalOperationSeconds = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: "syncthing",
Subsystem: "db",
Name: "operation_seconds_total",
Help: "Total time spent in database operations, per folder and operation",
}, []string{"folder", "operation"})
metricTotalOperationsCount = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: "syncthing",
Subsystem: "db",
Name: "operations_total",
Help: "Total number of database operations, per folder and operation",
}, []string{"folder", "operation"})
metricTotalFilesUpdatedCount = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: "syncthing",
Subsystem: "db",
Name: "files_updated_total",
Help: "Total number of files updated",
}, []string{"folder"})
)
func MetricsWrap(db DB) DB {
return metricsDB{db}
}
type metricsDB struct {
DB
}
func (m metricsDB) account(folder, op string) func() {
t0 := time.Now()
metricCurrentOperations.WithLabelValues(folder, op).Inc()
return func() {
if dur := time.Since(t0).Seconds(); dur > 0 {
metricTotalOperationSeconds.WithLabelValues(folder, op).Add(dur)
}
metricTotalOperationsCount.WithLabelValues(folder, op).Inc()
metricCurrentOperations.WithLabelValues(folder, op).Dec()
}
}
func (m metricsDB) AllLocalFilesWithBlocksHash(folder string, h []byte) (iter.Seq[FileMetadata], func() error) {
defer m.account(folder, "AllLocalFilesWithBlocksHash")()
return m.DB.AllLocalFilesWithBlocksHash(folder, h)
}
func (m metricsDB) AllLocalFilesWithBlocksHashAnyFolder(h []byte) (iter.Seq2[string, FileMetadata], func() error) {
defer m.account("-", "AllLocalFilesWithBlocksHashAnyFolder")()
return m.DB.AllLocalFilesWithBlocksHashAnyFolder(h)
}
func (m metricsDB) AllGlobalFiles(folder string) (iter.Seq[FileMetadata], func() error) {
defer m.account(folder, "AllGlobalFiles")()
return m.DB.AllGlobalFiles(folder)
}
func (m metricsDB) AllGlobalFilesPrefix(folder string, prefix string) (iter.Seq[FileMetadata], func() error) {
defer m.account(folder, "AllGlobalFilesPrefix")()
return m.DB.AllGlobalFilesPrefix(folder, prefix)
}
func (m metricsDB) AllLocalFiles(folder string, device protocol.DeviceID) (iter.Seq[protocol.FileInfo], func() error) {
defer m.account(folder, "AllLocalFiles")()
return m.DB.AllLocalFiles(folder, device)
}
func (m metricsDB) AllLocalFilesWithPrefix(folder string, device protocol.DeviceID, prefix string) (iter.Seq[protocol.FileInfo], func() error) {
defer m.account(folder, "AllLocalFilesPrefix")()
return m.DB.AllLocalFilesWithPrefix(folder, device, prefix)
}
func (m metricsDB) AllLocalFilesBySequence(folder string, device protocol.DeviceID, startSeq int64, limit int) (iter.Seq[protocol.FileInfo], func() error) {
defer m.account(folder, "AllLocalFilesBySequence")()
return m.DB.AllLocalFilesBySequence(folder, device, startSeq, limit)
}
func (m metricsDB) AllNeededGlobalFiles(folder string, device protocol.DeviceID, order config.PullOrder, limit, offset int) (iter.Seq[protocol.FileInfo], func() error) {
defer m.account(folder, "AllNeededGlobalFiles")()
return m.DB.AllNeededGlobalFiles(folder, device, order, limit, offset)
}
func (m metricsDB) GetGlobalAvailability(folder, file string) ([]protocol.DeviceID, error) {
defer m.account(folder, "GetGlobalAvailability")()
return m.DB.GetGlobalAvailability(folder, file)
}
func (m metricsDB) AllLocalBlocksWithHash(hash []byte) (iter.Seq[BlockMapEntry], func() error) {
defer m.account("-", "AllLocalBlocksWithHash")()
return m.DB.AllLocalBlocksWithHash(hash)
}
func (m metricsDB) Close() error {
defer m.account("-", "Close")()
return m.DB.Close()
}
func (m metricsDB) ListDevicesForFolder(folder string) ([]protocol.DeviceID, error) {
defer m.account(folder, "ListDevicesForFolder")()
return m.DB.ListDevicesForFolder(folder)
}
func (m metricsDB) RemoteSequences(folder string) (map[protocol.DeviceID]int64, error) {
defer m.account(folder, "RemoteSequences")()
return m.DB.RemoteSequences(folder)
}
func (m metricsDB) DropAllFiles(folder string, device protocol.DeviceID) error {
defer m.account(folder, "DropAllFiles")()
return m.DB.DropAllFiles(folder, device)
}
func (m metricsDB) DropDevice(device protocol.DeviceID) error {
defer m.account("-", "DropDevice")()
return m.DB.DropDevice(device)
}
func (m metricsDB) DropFilesNamed(folder string, device protocol.DeviceID, names []string) error {
defer m.account(folder, "DropFilesNamed")()
return m.DB.DropFilesNamed(folder, device, names)
}
func (m metricsDB) DropFolder(folder string) error {
defer m.account(folder, "DropFolder")()
return m.DB.DropFolder(folder)
}
func (m metricsDB) DropAllIndexIDs() error {
defer m.account("-", "IndexIDDropAll")()
return m.DB.DropAllIndexIDs()
}
func (m metricsDB) ListFolders() ([]string, error) {
defer m.account("-", "ListFolders")()
return m.DB.ListFolders()
}
func (m metricsDB) GetGlobalFile(folder string, file string) (protocol.FileInfo, bool, error) {
defer m.account(folder, "GetGlobalFile")()
return m.DB.GetGlobalFile(folder, file)
}
func (m metricsDB) CountGlobal(folder string) (Counts, error) {
defer m.account(folder, "CountGlobal")()
return m.DB.CountGlobal(folder)
}
func (m metricsDB) GetIndexID(folder string, device protocol.DeviceID) (protocol.IndexID, error) {
defer m.account(folder, "IndexIDGet")()
return m.DB.GetIndexID(folder, device)
}
func (m metricsDB) GetDeviceFile(folder string, device protocol.DeviceID, file string) (protocol.FileInfo, bool, error) {
defer m.account(folder, "GetDeviceFile")()
return m.DB.GetDeviceFile(folder, device, file)
}
func (m metricsDB) CountLocal(folder string, device protocol.DeviceID) (Counts, error) {
defer m.account(folder, "CountLocal")()
return m.DB.CountLocal(folder, device)
}
func (m metricsDB) CountNeed(folder string, device protocol.DeviceID) (Counts, error) {
defer m.account(folder, "CountNeed")()
return m.DB.CountNeed(folder, device)
}
func (m metricsDB) CountReceiveOnlyChanged(folder string) (Counts, error) {
defer m.account(folder, "CountReceiveOnlyChanged")()
return m.DB.CountReceiveOnlyChanged(folder)
}
func (m metricsDB) GetDeviceSequence(folder string, device protocol.DeviceID) (int64, error) {
defer m.account(folder, "GetDeviceSequence")()
return m.DB.GetDeviceSequence(folder, device)
}
func (m metricsDB) SetIndexID(folder string, device protocol.DeviceID, id protocol.IndexID) error {
defer m.account(folder, "IndexIDSet")()
return m.DB.SetIndexID(folder, device, id)
}
func (m metricsDB) Update(folder string, device protocol.DeviceID, fs []protocol.FileInfo) error {
defer m.account(folder, "Update")()
defer metricTotalFilesUpdatedCount.WithLabelValues(folder).Add(float64(len(fs)))
return m.DB.Update(folder, device, fs)
}
func (m metricsDB) GetKV(key string) ([]byte, error) {
defer m.account("-", "GetKV")()
return m.DB.GetKV(key)
}
func (m metricsDB) PutKV(key string, val []byte) error {
defer m.account("-", "PutKV")()
return m.DB.PutKV(key, val)
}
func (m metricsDB) DeleteKV(key string) error {
defer m.account("-", "DeleteKV")()
return m.DB.DeleteKV(key)
}
func (m metricsDB) PrefixKV(prefix string) (iter.Seq[KeyValue], func() error) {
defer m.account("-", "PrefixKV")()
return m.DB.PrefixKV(prefix)
}

View File

@@ -8,6 +8,7 @@ package db
import (
"fmt"
"strings"
"time"
"google.golang.org/protobuf/proto"
@@ -17,11 +18,19 @@ import (
"github.com/syncthing/syncthing/lib/protocol"
)
type ObservedDB struct {
kv KV
}
func NewObservedDB(kv KV) *ObservedDB {
return &ObservedDB{kv: kv}
}
type ObservedFolder struct {
Time time.Time
Label string
ReceiveEncrypted bool
RemoteEncrypted bool
Time time.Time `json:"time"`
Label string `json:"label"`
ReceiveEncrypted bool `json:"receiveEncrypted"`
RemoteEncrypted bool `json:"remoteEncrypted"`
}
func (o *ObservedFolder) toWire() *dbproto.ObservedFolder {
@@ -41,9 +50,9 @@ func (o *ObservedFolder) fromWire(w *dbproto.ObservedFolder) {
}
type ObservedDevice struct {
Time time.Time
Name string
Address string
Time time.Time `json:"time"`
Name string `json:"name"`
Address string `json:"address"`
}
func (o *ObservedDevice) fromWire(w *dbproto.ObservedDevice) {
@@ -52,39 +61,42 @@ func (o *ObservedDevice) fromWire(w *dbproto.ObservedDevice) {
o.Address = w.GetAddress()
}
func (db *Lowlevel) AddOrUpdatePendingDevice(device protocol.DeviceID, name, address string) error {
key := db.keyer.GeneratePendingDeviceKey(nil, device[:])
func (db *ObservedDB) AddOrUpdatePendingDevice(device protocol.DeviceID, name, address string) error {
key := "device/" + device.String()
od := &dbproto.ObservedDevice{
Time: timestamppb.New(time.Now().Truncate(time.Second)),
Name: name,
Address: address,
}
return db.Put(key, mustMarshal(od))
return db.kv.PutKV(key, mustMarshal(od))
}
func (db *Lowlevel) RemovePendingDevice(device protocol.DeviceID) error {
key := db.keyer.GeneratePendingDeviceKey(nil, device[:])
return db.Delete(key)
func (db *ObservedDB) RemovePendingDevice(device protocol.DeviceID) error {
key := "device/" + device.String()
return db.kv.DeleteKV(key)
}
// PendingDevices enumerates all entries. Invalid ones are dropped from the database
// after a warning log message, as a side-effect.
func (db *Lowlevel) PendingDevices() (map[protocol.DeviceID]ObservedDevice, error) {
iter, err := db.NewPrefixIterator([]byte{KeyTypePendingDevice})
if err != nil {
return nil, err
}
defer iter.Release()
func (db *ObservedDB) PendingDevices() (map[protocol.DeviceID]ObservedDevice, error) {
res := make(map[protocol.DeviceID]ObservedDevice)
for iter.Next() {
keyDev := db.keyer.DeviceFromPendingDeviceKey(iter.Key())
deviceID, err := protocol.DeviceIDFromBytes(keyDev)
it, errFn := db.kv.PrefixKV("device/")
for kv := range it {
_, keyDev, ok := strings.Cut(kv.Key, "/")
if !ok {
if err := db.kv.DeleteKV(kv.Key); err != nil {
return nil, fmt.Errorf("delete invalid pending device: %w", err)
}
continue
}
deviceID, err := protocol.DeviceIDFromString(keyDev)
var protoD dbproto.ObservedDevice
var od ObservedDevice
if err != nil {
goto deleteKey
}
if err = proto.Unmarshal(iter.Value(), &protoD); err != nil {
if err = proto.Unmarshal(kv.Value, &protoD); err != nil {
goto deleteKey
}
od.fromWire(&protoD)
@@ -94,52 +106,37 @@ func (db *Lowlevel) PendingDevices() (map[protocol.DeviceID]ObservedDevice, erro
// Deleting invalid entries is the only possible "repair" measure and
// appropriate for the importance of pending entries. They will come back
// soon if still relevant.
l.Infof("Invalid pending device entry, deleting from database: %x", iter.Key())
if err := db.Delete(iter.Key()); err != nil {
return nil, err
if err := db.kv.DeleteKV(kv.Key); err != nil {
return nil, fmt.Errorf("delete invalid pending device: %w", err)
}
}
return res, nil
return res, errFn()
}
func (db *Lowlevel) AddOrUpdatePendingFolder(id string, of ObservedFolder, device protocol.DeviceID) error {
key, err := db.keyer.GeneratePendingFolderKey(nil, device[:], []byte(id))
if err != nil {
return err
}
return db.Put(key, mustMarshal(of.toWire()))
func (db *ObservedDB) AddOrUpdatePendingFolder(id string, of ObservedFolder, device protocol.DeviceID) error {
key := "folder/" + device.String() + "/" + id
return db.kv.PutKV(key, mustMarshal(of.toWire()))
}
// RemovePendingFolderForDevice removes entries for specific folder / device combinations.
func (db *Lowlevel) RemovePendingFolderForDevice(id string, device protocol.DeviceID) error {
key, err := db.keyer.GeneratePendingFolderKey(nil, device[:], []byte(id))
if err != nil {
return err
}
return db.Delete(key)
func (db *ObservedDB) RemovePendingFolderForDevice(id string, device protocol.DeviceID) error {
key := "folder/" + device.String() + "/" + id
return db.kv.DeleteKV(key)
}
// RemovePendingFolder removes all entries matching a specific folder ID.
func (db *Lowlevel) RemovePendingFolder(id string) error {
iter, err := db.NewPrefixIterator([]byte{KeyTypePendingFolder})
if err != nil {
return fmt.Errorf("creating iterator: %w", err)
}
defer iter.Release()
var iterErr error
for iter.Next() {
if id != string(db.keyer.FolderFromPendingFolderKey(iter.Key())) {
func (db *ObservedDB) RemovePendingFolder(id string) error {
it, errFn := db.kv.PrefixKV("folder/")
for kv := range it {
parts := strings.Split(kv.Key, "/")
if len(parts) != 3 || parts[2] != id {
continue
}
if err = db.Delete(iter.Key()); err != nil {
if iterErr != nil {
l.Debugf("Repeat error removing pending folder: %v", err)
} else {
iterErr = err
}
if err := db.kv.DeleteKV(kv.Key); err != nil {
return fmt.Errorf("delete pending folder: %w", err)
}
}
return iterErr
return errFn()
}
// Consolidated information about a pending folder
@@ -147,41 +144,37 @@ type PendingFolder struct {
OfferedBy map[protocol.DeviceID]ObservedFolder `json:"offeredBy"`
}
func (db *Lowlevel) PendingFolders() (map[string]PendingFolder, error) {
func (db *ObservedDB) PendingFolders() (map[string]PendingFolder, error) {
return db.PendingFoldersForDevice(protocol.EmptyDeviceID)
}
// PendingFoldersForDevice enumerates only entries matching the given device ID, unless it
// is EmptyDeviceID. Invalid ones are dropped from the database after a info log
// message, as a side-effect.
func (db *Lowlevel) PendingFoldersForDevice(device protocol.DeviceID) (map[string]PendingFolder, error) {
var err error
prefixKey := []byte{KeyTypePendingFolder}
func (db *ObservedDB) PendingFoldersForDevice(device protocol.DeviceID) (map[string]PendingFolder, error) {
prefix := "folder/"
if device != protocol.EmptyDeviceID {
prefixKey, err = db.keyer.GeneratePendingFolderKey(nil, device[:], nil)
if err != nil {
return nil, err
}
prefix += device.String() + "/"
}
iter, err := db.NewPrefixIterator(prefixKey)
if err != nil {
return nil, err
}
defer iter.Release()
res := make(map[string]PendingFolder)
for iter.Next() {
keyDev, ok := db.keyer.DeviceFromPendingFolderKey(iter.Key())
deviceID, err := protocol.DeviceIDFromBytes(keyDev)
it, errFn := db.kv.PrefixKV(prefix)
for kv := range it {
parts := strings.Split(kv.Key, "/")
if len(parts) != 3 {
continue
}
keyDev := parts[1]
deviceID, err := protocol.DeviceIDFromString(keyDev)
var protoF dbproto.ObservedFolder
var of ObservedFolder
var folderID string
if !ok || err != nil {
if err != nil {
goto deleteKey
}
if folderID = string(db.keyer.FolderFromPendingFolderKey(iter.Key())); len(folderID) < 1 {
if folderID = parts[2]; len(folderID) < 1 {
goto deleteKey
}
if err = proto.Unmarshal(iter.Value(), &protoF); err != nil {
if err = proto.Unmarshal(kv.Value, &protoF); err != nil {
goto deleteKey
}
if _, ok := res[folderID]; !ok {
@@ -196,10 +189,17 @@ func (db *Lowlevel) PendingFoldersForDevice(device protocol.DeviceID) (map[strin
// Deleting invalid entries is the only possible "repair" measure and
// appropriate for the importance of pending entries. They will come back
// soon if still relevant.
l.Infof("Invalid pending folder entry, deleting from database: %x", iter.Key())
if err := db.Delete(iter.Key()); err != nil {
return nil, err
if err := db.kv.DeleteKV(kv.Key); err != nil {
return nil, fmt.Errorf("delete invalid pending folder: %w", err)
}
}
return res, nil
return res, errFn()
}
func mustMarshal(m proto.Message) []byte {
bs, err := proto.Marshal(m)
if err != nil {
panic(err)
}
return bs
}

View File

@@ -108,29 +108,8 @@ type Iterator interface {
// is empty for a db in memory.
type Backend interface {
Reader
Writer
NewReadTransaction() (ReadTransaction, error)
NewWriteTransaction(hooks ...CommitHook) (WriteTransaction, error)
Close() error
Compact() error
Location() string
}
type Tuning int
const (
// N.b. these constants must match those in lib/config.Tuning!
TuningAuto Tuning = iota
TuningSmall
TuningLarge
)
func Open(path string, tuning Tuning) (Backend, error) {
return OpenLevelDB(path, tuning)
}
func OpenMemory() Backend {
return OpenLevelDBMemory()
}
var (

View File

@@ -0,0 +1,113 @@
// 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 backend
import (
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/iterator"
"github.com/syndtr/goleveldb/leveldb/util"
)
// leveldbBackend implements Backend on top of a leveldb
type leveldbBackend struct {
ldb *leveldb.DB
closeWG *closeWaitGroup
location string
}
func newLeveldbBackend(ldb *leveldb.DB, location string) *leveldbBackend {
return &leveldbBackend{
ldb: ldb,
closeWG: &closeWaitGroup{},
location: location,
}
}
func (b *leveldbBackend) NewReadTransaction() (ReadTransaction, error) {
return b.newSnapshot()
}
func (b *leveldbBackend) newSnapshot() (leveldbSnapshot, error) {
rel, err := newReleaser(b.closeWG)
if err != nil {
return leveldbSnapshot{}, err
}
snap, err := b.ldb.GetSnapshot()
if err != nil {
rel.Release()
return leveldbSnapshot{}, wrapLeveldbErr(err)
}
return leveldbSnapshot{
snap: snap,
rel: rel,
}, nil
}
func (b *leveldbBackend) Close() error {
b.closeWG.CloseWait()
return wrapLeveldbErr(b.ldb.Close())
}
func (b *leveldbBackend) Get(key []byte) ([]byte, error) {
val, err := b.ldb.Get(key, nil)
return val, wrapLeveldbErr(err)
}
func (b *leveldbBackend) NewPrefixIterator(prefix []byte) (Iterator, error) {
return &leveldbIterator{b.ldb.NewIterator(util.BytesPrefix(prefix), nil)}, nil
}
func (b *leveldbBackend) NewRangeIterator(first, last []byte) (Iterator, error) {
return &leveldbIterator{b.ldb.NewIterator(&util.Range{Start: first, Limit: last}, nil)}, nil
}
func (b *leveldbBackend) Location() string {
return b.location
}
// leveldbSnapshot implements backend.ReadTransaction
type leveldbSnapshot struct {
snap *leveldb.Snapshot
rel *releaser
}
func (l leveldbSnapshot) Get(key []byte) ([]byte, error) {
val, err := l.snap.Get(key, nil)
return val, wrapLeveldbErr(err)
}
func (l leveldbSnapshot) NewPrefixIterator(prefix []byte) (Iterator, error) {
return l.snap.NewIterator(util.BytesPrefix(prefix), nil), nil
}
func (l leveldbSnapshot) NewRangeIterator(first, last []byte) (Iterator, error) {
return l.snap.NewIterator(&util.Range{Start: first, Limit: last}, nil), nil
}
func (l leveldbSnapshot) Release() {
l.snap.Release()
l.rel.Release()
}
type leveldbIterator struct {
iterator.Iterator
}
func (it *leveldbIterator) Error() error {
return wrapLeveldbErr(it.Iterator.Error())
}
// wrapLeveldbErr wraps errors so that the backend package can recognize them
func wrapLeveldbErr(err error) error {
switch err {
case leveldb.ErrClosed:
return errClosed
case leveldb.ErrNotFound:
return errNotFound
}
return err
}

View File

@@ -0,0 +1,32 @@
// 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 backend
import (
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/opt"
)
const dbMaxOpenFiles = 100
// OpenLevelDBRO attempts to open the database at the given location, read
// only.
func OpenLevelDBRO(location string) (Backend, error) {
opts := &opt.Options{
OpenFilesCacheCapacity: dbMaxOpenFiles,
ReadOnly: true,
}
ldb, err := open(location, opts)
if err != nil {
return nil, err
}
return newLeveldbBackend(ldb, location), nil
}
func open(location string, opts *opt.Options) (*leveldb.DB, error) {
return leveldb.OpenFile(location, opts)
}

View File

@@ -4,7 +4,7 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
package db
package olddb
import (
"encoding/binary"

View File

@@ -0,0 +1,70 @@
// Copyright (C) 2014 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 olddb
import (
"encoding/binary"
"time"
"github.com/syncthing/syncthing/internal/db/olddb/backend"
)
// deprecatedLowlevel is the lowest level database interface. It has a very simple
// purpose: hold the actual backend database, and the in-memory state
// that belong to that database. In the same way that a single on disk
// database can only be opened once, there should be only one deprecatedLowlevel for
// any given backend.
type deprecatedLowlevel struct {
backend.Backend
folderIdx *smallIndex
deviceIdx *smallIndex
keyer keyer
}
func NewLowlevel(backend backend.Backend) (*deprecatedLowlevel, error) {
// Only log restarts in debug mode.
db := &deprecatedLowlevel{
Backend: backend,
folderIdx: newSmallIndex(backend, []byte{KeyTypeFolderIdx}),
deviceIdx: newSmallIndex(backend, []byte{KeyTypeDeviceIdx}),
}
db.keyer = newDefaultKeyer(db.folderIdx, db.deviceIdx)
return db, nil
}
// ListFolders returns the list of folders currently in the database
func (db *deprecatedLowlevel) ListFolders() []string {
return db.folderIdx.Values()
}
func (db *deprecatedLowlevel) IterateMtimes(fn func(folder, name string, ondisk, virtual time.Time) error) error {
it, err := db.NewPrefixIterator([]byte{KeyTypeVirtualMtime})
if err != nil {
return err
}
defer it.Release()
for it.Next() {
key := it.Key()[1:]
folderID, ok := db.folderIdx.Val(binary.BigEndian.Uint32(key))
if !ok {
continue
}
name := key[4:]
val := it.Value()
var ondisk, virtual time.Time
if err := ondisk.UnmarshalBinary(val[:len(val)/2]); err != nil {
continue
}
if err := virtual.UnmarshalBinary(val[len(val)/2:]); err != nil {
continue
}
if err := fn(string(folderID), string(name), ondisk, virtual); err != nil {
return err
}
}
return it.Error()
}

67
internal/db/olddb/set.go Normal file
View File

@@ -0,0 +1,67 @@
// Copyright (C) 2014 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 db provides a set type to track local/remote files with newness
// checks. We must do a certain amount of normalization in here. We will get
// fed paths with either native or wire-format separators and encodings
// depending on who calls us. We transform paths to wire-format (NFC and
// slashes) on the way to the database, and transform to native format
// (varying separator and encoding) on the way back out.
package olddb
import (
"github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/protocol"
)
type deprecatedFileSet struct {
folder string
db *deprecatedLowlevel
}
// The Iterator is called with either a protocol.FileInfo or a
// FileInfoTruncated (depending on the method) and returns true to
// continue iteration, false to stop.
type Iterator func(f protocol.FileInfo) bool
func NewFileSet(folder string, db *deprecatedLowlevel) (*deprecatedFileSet, error) {
s := &deprecatedFileSet{
folder: folder,
db: db,
}
return s, nil
}
type Snapshot struct {
folder string
t readOnlyTransaction
}
func (s *deprecatedFileSet) Snapshot() (*Snapshot, error) {
t, err := s.db.newReadOnlyTransaction()
if err != nil {
return nil, err
}
return &Snapshot{
folder: s.folder,
t: t,
}, nil
}
func (s *Snapshot) Release() {
s.t.close()
}
func (s *Snapshot) WithHaveSequence(startSeq int64, fn Iterator) error {
return s.t.withHaveSequence([]byte(s.folder), startSeq, nativeFileIterator(fn))
}
func nativeFileIterator(fn Iterator) Iterator {
return func(fi protocol.FileInfo) bool {
fi.Name = osutil.NativeFilename(fi.Name)
return fn(fi)
}
}

View File

@@ -4,13 +4,13 @@
// 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 db
package olddb
import (
"encoding/binary"
"sort"
"github.com/syncthing/syncthing/lib/db/backend"
"github.com/syncthing/syncthing/internal/db/olddb/backend"
"github.com/syncthing/syncthing/lib/sync"
)
@@ -74,23 +74,7 @@ func (i *smallIndex) ID(val []byte) (uint32, error) {
return id, nil
}
id := i.nextID
i.nextID++
valStr := string(val)
i.val2id[valStr] = id
i.id2val[id] = valStr
key := make([]byte, len(i.prefix)+8) // prefix plus uint32 id
copy(key, i.prefix)
binary.BigEndian.PutUint32(key[len(i.prefix):], id)
if err := i.db.Put(key, val); err != nil {
i.mut.Unlock()
return 0, err
}
i.mut.Unlock()
return id, nil
panic("missing ID")
}
// Val returns the value for the given index number, or (nil, false) if there
@@ -106,33 +90,6 @@ func (i *smallIndex) Val(id uint32) ([]byte, bool) {
return []byte(val), true
}
func (i *smallIndex) Delete(val []byte) error {
i.mut.Lock()
defer i.mut.Unlock()
// Check the reverse mapping to get the ID for the value.
if id, ok := i.val2id[string(val)]; ok {
// Generate the corresponding database key.
key := make([]byte, len(i.prefix)+8) // prefix plus uint32 id
copy(key, i.prefix)
binary.BigEndian.PutUint32(key[len(i.prefix):], id)
// Put an empty value into the database. This indicates that the
// entry does not exist any more and prevents the ID from being
// reused in the future.
if err := i.db.Put(key, []byte{}); err != nil {
return err
}
// Delete reverse mapping.
delete(i.id2val, id)
}
// Delete forward mapping.
delete(i.val2id, string(val))
return nil
}
// Values returns the set of values in the index
func (i *smallIndex) Values() []string {
// In principle this method should return [][]byte because all the other

View File

@@ -0,0 +1,193 @@
// Copyright (C) 2014 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 olddb
import (
"fmt"
"google.golang.org/protobuf/proto"
"github.com/syncthing/syncthing/internal/db/olddb/backend"
"github.com/syncthing/syncthing/internal/gen/bep"
"github.com/syncthing/syncthing/internal/gen/dbproto"
"github.com/syncthing/syncthing/lib/protocol"
)
// A readOnlyTransaction represents a database snapshot.
type readOnlyTransaction struct {
backend.ReadTransaction
keyer keyer
}
func (db *deprecatedLowlevel) newReadOnlyTransaction() (readOnlyTransaction, error) {
tran, err := db.NewReadTransaction()
if err != nil {
return readOnlyTransaction{}, err
}
return db.readOnlyTransactionFromBackendTransaction(tran), nil
}
func (db *deprecatedLowlevel) readOnlyTransactionFromBackendTransaction(tran backend.ReadTransaction) readOnlyTransaction {
return readOnlyTransaction{
ReadTransaction: tran,
keyer: db.keyer,
}
}
func (t readOnlyTransaction) close() {
t.Release()
}
func (t readOnlyTransaction) getFileByKey(key []byte) (protocol.FileInfo, bool, error) {
f, ok, err := t.getFileTrunc(key, false)
if err != nil || !ok {
return protocol.FileInfo{}, false, err
}
return f, true, nil
}
func (t readOnlyTransaction) getFileTrunc(key []byte, trunc bool) (protocol.FileInfo, bool, error) {
bs, err := t.Get(key)
if backend.IsNotFound(err) {
return protocol.FileInfo{}, false, nil
}
if err != nil {
return protocol.FileInfo{}, false, err
}
f, err := t.unmarshalTrunc(bs, trunc)
if backend.IsNotFound(err) {
return protocol.FileInfo{}, false, nil
}
if err != nil {
return protocol.FileInfo{}, false, err
}
return f, true, nil
}
func (t readOnlyTransaction) unmarshalTrunc(bs []byte, trunc bool) (protocol.FileInfo, error) {
if trunc {
var bfi dbproto.FileInfoTruncated
err := proto.Unmarshal(bs, &bfi)
if err != nil {
return protocol.FileInfo{}, err
}
if err := t.fillTruncated(&bfi); err != nil {
return protocol.FileInfo{}, err
}
return protocol.FileInfoFromDBTruncated(&bfi), nil
}
var bfi bep.FileInfo
err := proto.Unmarshal(bs, &bfi)
if err != nil {
return protocol.FileInfo{}, err
}
if err := t.fillFileInfo(&bfi); err != nil {
return protocol.FileInfo{}, err
}
return protocol.FileInfoFromDB(&bfi), nil
}
type blocksIndirectionError struct {
err error
}
func (e *blocksIndirectionError) Error() string {
return fmt.Sprintf("filling Blocks: %v", e.err)
}
func (e *blocksIndirectionError) Unwrap() error {
return e.err
}
// fillFileInfo follows the (possible) indirection of blocks and version
// vector and fills it out.
func (t readOnlyTransaction) fillFileInfo(fi *bep.FileInfo) error {
var key []byte
if len(fi.Blocks) == 0 && len(fi.BlocksHash) != 0 {
// The blocks list is indirected and we need to load it.
key = t.keyer.GenerateBlockListKey(key, fi.BlocksHash)
bs, err := t.Get(key)
if err != nil {
return &blocksIndirectionError{err}
}
var bl dbproto.BlockList
if err := proto.Unmarshal(bs, &bl); err != nil {
return err
}
fi.Blocks = bl.Blocks
}
if len(fi.VersionHash) != 0 {
key = t.keyer.GenerateVersionKey(key, fi.VersionHash)
bs, err := t.Get(key)
if err != nil {
return fmt.Errorf("filling Version: %w", err)
}
var v bep.Vector
if err := proto.Unmarshal(bs, &v); err != nil {
return err
}
fi.Version = &v
}
return nil
}
// fillTruncated follows the (possible) indirection of version vector and
// fills it.
func (t readOnlyTransaction) fillTruncated(fi *dbproto.FileInfoTruncated) error {
var key []byte
if len(fi.VersionHash) == 0 {
return nil
}
key = t.keyer.GenerateVersionKey(key, fi.VersionHash)
bs, err := t.Get(key)
if err != nil {
return err
}
var v bep.Vector
if err := proto.Unmarshal(bs, &v); err != nil {
return err
}
fi.Version = &v
return nil
}
func (t *readOnlyTransaction) withHaveSequence(folder []byte, startSeq int64, fn Iterator) error {
first, err := t.keyer.GenerateSequenceKey(nil, folder, startSeq)
if err != nil {
return err
}
last, err := t.keyer.GenerateSequenceKey(nil, folder, maxInt64)
if err != nil {
return err
}
dbi, err := t.NewRangeIterator(first, last)
if err != nil {
return err
}
defer dbi.Release()
for dbi.Next() {
f, ok, err := t.getFileByKey(dbi.Value())
if err != nil {
return err
}
if !ok {
continue
}
if !fn(f) {
return nil
}
}
return dbi.Error()
}

77
internal/db/sqlite/db.go Normal file
View File

@@ -0,0 +1,77 @@
// Copyright (C) 2025 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 sqlite
import (
"sync"
"time"
"github.com/jmoiron/sqlx"
"github.com/syncthing/syncthing/internal/db"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/thejerf/suture/v4"
)
type DB struct {
sql *sqlx.DB
localDeviceIdx int64
updateLock sync.Mutex
statementsMut sync.RWMutex
statements map[string]*sqlx.Stmt
tplInput map[string]any
}
var _ db.DB = (*DB)(nil)
func (s *DB) Close() error {
s.updateLock.Lock()
s.statementsMut.Lock()
defer s.updateLock.Unlock()
defer s.statementsMut.Unlock()
for _, stmt := range s.statements {
stmt.Close()
}
return wrap(s.sql.Close())
}
func (s *DB) Service(maintenanceInterval time.Duration) suture.Service {
return newService(s, maintenanceInterval)
}
func (s *DB) ListFolders() ([]string, error) {
var res []string
err := s.stmt(`
SELECT folder_id FROM folders
ORDER BY folder_id
`).Select(&res)
return res, wrap(err)
}
func (s *DB) ListDevicesForFolder(folder string) ([]protocol.DeviceID, error) {
var res []string
err := s.stmt(`
SELECT d.device_id FROM counts s
INNER JOIN folders o ON o.idx = s.folder_idx
INNER JOIN devices d ON d.idx = s.device_idx
WHERE o.folder_id = ? AND s.count > 0 AND s.device_idx != {{.LocalDeviceIdx}}
GROUP BY d.device_id
ORDER BY d.device_id
`).Select(&res, folder)
if err != nil {
return nil, wrap(err)
}
devs := make([]protocol.DeviceID, len(res))
for i, s := range res {
devs[i], err = protocol.DeviceIDFromString(s)
if err != nil {
return nil, wrap(err)
}
}
return devs, nil
}

View File

@@ -0,0 +1,243 @@
// Copyright (C) 2025 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 sqlite
import (
"context"
"fmt"
"testing"
"time"
"github.com/syncthing/syncthing/internal/timeutil"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/rand"
)
var globalFi protocol.FileInfo
func BenchmarkUpdate(b *testing.B) {
db, err := OpenTemp()
if err != nil {
b.Fatal(err)
}
b.Cleanup(func() {
if err := db.Close(); err != nil {
b.Fatal(err)
}
})
svc := db.Service(time.Hour).(*Service)
fs := make([]protocol.FileInfo, 100)
seed := 0
size := 10000
for size < 200_000 {
t0 := time.Now()
if err := svc.periodic(context.Background()); err != nil {
b.Fatal(err)
}
b.Log("garbage collect in", time.Since(t0))
for {
local, err := db.CountLocal(folderID, protocol.LocalDeviceID)
if err != nil {
b.Fatal(err)
}
if local.Files >= size {
break
}
fs := make([]protocol.FileInfo, 1000)
for i := range fs {
fs[i] = genFile(rand.String(24), 64, 0)
}
if err := db.Update(folderID, protocol.LocalDeviceID, fs); err != nil {
b.Fatal(err)
}
}
b.Run(fmt.Sprintf("Insert100Loc@%d", size), func(b *testing.B) {
for range b.N {
for i := range fs {
fs[i] = genFile(rand.String(24), 64, 0)
}
if err := db.Update(folderID, protocol.LocalDeviceID, fs); err != nil {
b.Fatal(err)
}
}
b.ReportMetric(float64(b.N)*100.0/b.Elapsed().Seconds(), "files/s")
})
b.Run(fmt.Sprintf("RepBlocks100@%d", size), func(b *testing.B) {
for range b.N {
for i := range fs {
fs[i].Blocks = genBlocks(fs[i].Name, seed, 64)
fs[i].Version = fs[i].Version.Update(42)
}
seed++
if err := db.Update(folderID, protocol.LocalDeviceID, fs); err != nil {
b.Fatal(err)
}
}
b.ReportMetric(float64(b.N)*100.0/b.Elapsed().Seconds(), "files/s")
})
b.Run(fmt.Sprintf("RepSame100@%d", size), func(b *testing.B) {
for range b.N {
for i := range fs {
fs[i].Version = fs[i].Version.Update(42)
}
if err := db.Update(folderID, protocol.LocalDeviceID, fs); err != nil {
b.Fatal(err)
}
}
b.ReportMetric(float64(b.N)*100.0/b.Elapsed().Seconds(), "files/s")
})
b.Run(fmt.Sprintf("Insert100Rem@%d", size), func(b *testing.B) {
for range b.N {
for i := range fs {
fs[i].Blocks = genBlocks(fs[i].Name, seed, 64)
fs[i].Version = fs[i].Version.Update(42)
fs[i].Sequence = timeutil.StrictlyMonotonicNanos()
}
if err := db.Update(folderID, protocol.DeviceID{42}, fs); err != nil {
b.Fatal(err)
}
}
b.ReportMetric(float64(b.N)*100.0/b.Elapsed().Seconds(), "files/s")
})
b.Run(fmt.Sprintf("GetGlobal100@%d", size), func(b *testing.B) {
for range b.N {
for i := range fs {
_, ok, err := db.GetGlobalFile(folderID, fs[i].Name)
if err != nil {
b.Fatal(err)
}
if !ok {
b.Fatal("should exist")
}
}
}
b.ReportMetric(float64(b.N)*100.0/b.Elapsed().Seconds(), "files/s")
})
b.Run(fmt.Sprintf("LocalSequenced@%d", size), func(b *testing.B) {
count := 0
for range b.N {
cur, err := db.GetDeviceSequence(folderID, protocol.LocalDeviceID)
if err != nil {
b.Fatal(err)
}
it, errFn := db.AllLocalFilesBySequence(folderID, protocol.LocalDeviceID, cur-100, 0)
for f := range it {
count++
globalFi = f
}
if err := errFn(); err != nil {
b.Fatal(err)
}
}
b.ReportMetric(float64(count)/b.Elapsed().Seconds(), "files/s")
})
b.Run(fmt.Sprintf("GetDeviceSequenceLoc@%d", size), func(b *testing.B) {
for range b.N {
_, err := db.GetDeviceSequence(folderID, protocol.LocalDeviceID)
if err != nil {
b.Fatal(err)
}
}
})
b.Run(fmt.Sprintf("GetDeviceSequenceRem@%d", size), func(b *testing.B) {
for range b.N {
_, err := db.GetDeviceSequence(folderID, protocol.DeviceID{42})
if err != nil {
b.Fatal(err)
}
}
})
b.Run(fmt.Sprintf("RemoteNeed@%d", size), func(b *testing.B) {
count := 0
for range b.N {
it, errFn := db.AllNeededGlobalFiles(folderID, protocol.DeviceID{42}, config.PullOrderAlphabetic, 0, 0)
for f := range it {
count++
globalFi = f
}
if err := errFn(); err != nil {
b.Fatal(err)
}
}
b.ReportMetric(float64(count)/b.Elapsed().Seconds(), "files/s")
})
b.Run(fmt.Sprintf("LocalNeed100Largest@%d", size), func(b *testing.B) {
count := 0
for range b.N {
it, errFn := db.AllNeededGlobalFiles(folderID, protocol.LocalDeviceID, config.PullOrderLargestFirst, 100, 0)
for f := range it {
globalFi = f
count++
}
if err := errFn(); err != nil {
b.Fatal(err)
}
}
b.ReportMetric(float64(count)/b.Elapsed().Seconds(), "files/s")
})
size <<= 1
}
}
func TestBenchmarkDropAllRemote(t *testing.T) {
if testing.Short() {
t.Skip("slow test")
}
db, err := OpenTemp()
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := db.Close(); err != nil {
t.Fatal(err)
}
})
fs := make([]protocol.FileInfo, 1000)
seq := 0
for {
local, err := db.CountLocal(folderID, protocol.LocalDeviceID)
if err != nil {
t.Fatal(err)
}
if local.Files >= 15_000 {
break
}
for i := range fs {
seq++
fs[i] = genFile(rand.String(24), 64, seq)
}
if err := db.Update(folderID, protocol.DeviceID{42}, fs); err != nil {
t.Fatal(err)
}
if err := db.Update(folderID, protocol.LocalDeviceID, fs); err != nil {
t.Fatal(err)
}
}
t0 := time.Now()
if err := db.DropAllFiles(folderID, protocol.DeviceID{42}); err != nil {
t.Fatal(err)
}
d := time.Since(t0)
t.Log("drop all took", d)
}

View File

@@ -0,0 +1,137 @@
// Copyright (C) 2025 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 sqlite
import (
"github.com/syncthing/syncthing/internal/db"
"github.com/syncthing/syncthing/lib/protocol"
)
type countsRow struct {
Type protocol.FileInfoType
Count int
Size int64
Deleted bool
LocalFlags int64 `db:"local_flags"`
}
func (s *DB) CountLocal(folder string, device protocol.DeviceID) (db.Counts, error) {
var res []countsRow
if err := s.stmt(`
SELECT s.type, s.count, s.size, s.local_flags, s.deleted FROM counts s
INNER JOIN folders o ON o.idx = s.folder_idx
INNER JOIN devices d ON d.idx = s.device_idx
WHERE o.folder_id = ? AND d.device_id = ? AND s.local_flags & {{.FlagLocalIgnored}} = 0
`).Select(&res, folder, device.String()); err != nil {
return db.Counts{}, wrap(err)
}
return summarizeCounts(res), nil
}
func (s *DB) CountNeed(folder string, device protocol.DeviceID) (db.Counts, error) {
if device == protocol.LocalDeviceID {
return s.needSizeLocal(folder)
}
return s.needSizeRemote(folder, device)
}
func (s *DB) CountGlobal(folder string) (db.Counts, error) {
// Exclude ignored and receive-only changed files from the global count
// (legacy expectation? it's a bit weird since those files can in fact
// be global and you can get them with GetGlobal etc.)
var res []countsRow
err := s.stmt(`
SELECT s.type, s.count, s.size, s.local_flags, s.deleted FROM counts s
INNER JOIN folders o ON o.idx = s.folder_idx
WHERE o.folder_id = ? AND s.local_flags & {{.FlagLocalGlobal}} != 0 AND s.local_flags & {{or .FlagLocalReceiveOnly .FlagLocalIgnored}} = 0
`).Select(&res, folder)
if err != nil {
return db.Counts{}, wrap(err)
}
return summarizeCounts(res), nil
}
func (s *DB) CountReceiveOnlyChanged(folder string) (db.Counts, error) {
var res []countsRow
err := s.stmt(`
SELECT s.type, s.count, s.size, s.local_flags, s.deleted FROM counts s
INNER JOIN folders o ON o.idx = s.folder_idx
WHERE o.folder_id = ? AND local_flags & {{.FlagLocalReceiveOnly}} != 0
`).Select(&res, folder)
if err != nil {
return db.Counts{}, wrap(err)
}
return summarizeCounts(res), nil
}
func (s *DB) needSizeLocal(folder string) (db.Counts, error) {
// The need size for the local device is the sum of entries with the
// need bit set.
var res []countsRow
err := s.stmt(`
SELECT s.type, s.count, s.size, s.local_flags, s.deleted FROM counts s
INNER JOIN folders o ON o.idx = s.folder_idx
WHERE o.folder_id = ? AND s.local_flags & {{.FlagLocalNeeded}} != 0
`).Select(&res, folder)
if err != nil {
return db.Counts{}, wrap(err)
}
return summarizeCounts(res), nil
}
func (s *DB) needSizeRemote(folder string, device protocol.DeviceID) (db.Counts, error) {
var res []countsRow
// See neededGlobalFilesRemote for commentary as that is the same query without summing
if err := s.stmt(`
SELECT g.type, count(*) as count, sum(g.size) as size, g.local_flags, g.deleted FROM files g
INNER JOIN folders o ON o.idx = g.folder_idx
WHERE o.folder_id = ? AND g.local_flags & {{.FlagLocalGlobal}} != 0 AND NOT g.deleted AND NOT g.invalid AND NOT EXISTS (
SELECT 1 FROM FILES f
INNER JOIN devices d ON d.idx = f.device_idx
WHERE f.name = g.name AND f.version = g.version AND f.folder_idx = g.folder_idx AND d.device_id = ?
)
GROUP BY g.type, g.local_flags, g.deleted
UNION ALL
SELECT g.type, count(*) as count, sum(g.size) as size, g.local_flags, g.deleted FROM files g
INNER JOIN folders o ON o.idx = g.folder_idx
WHERE o.folder_id = ? AND g.local_flags & {{.FlagLocalGlobal}} != 0 AND g.deleted AND NOT g.invalid AND EXISTS (
SELECT 1 FROM FILES f
INNER JOIN devices d ON d.idx = f.device_idx
WHERE f.name = g.name AND f.folder_idx = g.folder_idx AND d.device_id = ? AND NOT f.deleted
)
GROUP BY g.type, g.local_flags, g.deleted
`).Select(&res, folder, device.String(),
folder, device.String()); err != nil {
return db.Counts{}, wrap(err)
}
return summarizeCounts(res), nil
}
func summarizeCounts(res []countsRow) db.Counts {
c := db.Counts{
DeviceID: protocol.LocalDeviceID,
}
for _, r := range res {
switch {
case r.Deleted:
c.Deleted += r.Count
case r.Type == protocol.FileInfoTypeFile:
c.Files += r.Count
c.Bytes += r.Size
case r.Type == protocol.FileInfoTypeDirectory:
c.Directories += r.Count
c.Bytes += r.Size
case r.Type == protocol.FileInfoTypeSymlink:
c.Symlinks += r.Count
c.Bytes += r.Size
}
}
return c
}

View File

@@ -0,0 +1,189 @@
// Copyright (C) 2025 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 sqlite
import (
"database/sql"
"errors"
"fmt"
"iter"
"github.com/syncthing/syncthing/internal/db"
"github.com/syncthing/syncthing/internal/itererr"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/protocol"
)
func (s *DB) GetGlobalFile(folder string, file string) (protocol.FileInfo, bool, error) {
file = osutil.NormalizedFilename(file)
var ind indirectFI
err := s.stmt(`
SELECT fi.fiprotobuf, bl.blprotobuf FROM fileinfos fi
INNER JOIN files f on fi.sequence = f.sequence
LEFT JOIN blocklists bl ON bl.blocklist_hash = f.blocklist_hash
INNER JOIN folders o ON o.idx = f.folder_idx
WHERE o.folder_id = ? AND f.name = ? AND f.local_flags & {{.FlagLocalGlobal}} != 0
`).Get(&ind, folder, file)
if errors.Is(err, sql.ErrNoRows) {
return protocol.FileInfo{}, false, nil
}
if err != nil {
return protocol.FileInfo{}, false, wrap(err)
}
fi, err := ind.FileInfo()
if err != nil {
return protocol.FileInfo{}, false, wrap(err)
}
return fi, true, nil
}
func (s *DB) GetGlobalAvailability(folder, file string) ([]protocol.DeviceID, error) {
file = osutil.NormalizedFilename(file)
var devStrs []string
err := s.stmt(`
SELECT d.device_id FROM files f
INNER JOIN devices d ON d.idx = f.device_idx
INNER JOIN folders o ON o.idx = f.folder_idx
INNER JOIN files g ON f.folder_idx = g.folder_idx AND g.version = f.version AND g.name = f.name
WHERE o.folder_id = ? AND g.name = ? AND g.local_flags & {{.FlagLocalGlobal}} != 0 AND f.device_idx != {{.LocalDeviceIdx}}
ORDER BY d.device_id
`).Select(&devStrs, folder, file)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
if err != nil {
return nil, wrap(err)
}
devs := make([]protocol.DeviceID, 0, len(devStrs))
for _, s := range devStrs {
d, err := protocol.DeviceIDFromString(s)
if err != nil {
return nil, wrap(err)
}
devs = append(devs, d)
}
return devs, nil
}
func (s *DB) AllGlobalFiles(folder string) (iter.Seq[db.FileMetadata], func() error) {
it, errFn := iterStructs[db.FileMetadata](s.stmt(`
SELECT f.sequence, f.name, f.type, f.modified as modnanos, f.size, f.deleted, f.invalid, f.local_flags as localflags FROM files f
INNER JOIN folders o ON o.idx = f.folder_idx
WHERE o.folder_id = ? AND f.local_flags & {{.FlagLocalGlobal}} != 0
ORDER BY f.name
`).Queryx(folder))
return itererr.Map(it, errFn, func(m db.FileMetadata) (db.FileMetadata, error) {
m.Name = osutil.NativeFilename(m.Name)
return m, nil
})
}
func (s *DB) AllGlobalFilesPrefix(folder string, prefix string) (iter.Seq[db.FileMetadata], func() error) {
if prefix == "" {
return s.AllGlobalFiles(folder)
}
prefix = osutil.NormalizedFilename(prefix)
end := prefixEnd(prefix)
it, errFn := iterStructs[db.FileMetadata](s.stmt(`
SELECT f.sequence, f.name, f.type, f.modified as modnanos, f.size, f.deleted, f.invalid, f.local_flags as localflags FROM files f
INNER JOIN folders o ON o.idx = f.folder_idx
WHERE o.folder_id = ? AND f.name >= ? AND f.name < ? AND f.local_flags & {{.FlagLocalGlobal}} != 0
ORDER BY f.name
`).Queryx(folder, prefix, end))
return itererr.Map(it, errFn, func(m db.FileMetadata) (db.FileMetadata, error) {
m.Name = osutil.NativeFilename(m.Name)
return m, nil
})
}
func (s *DB) AllNeededGlobalFiles(folder string, device protocol.DeviceID, order config.PullOrder, limit, offset int) (iter.Seq[protocol.FileInfo], func() error) {
var selectOpts string
switch order {
case config.PullOrderRandom:
selectOpts = "ORDER BY RANDOM()"
case config.PullOrderAlphabetic:
selectOpts = "ORDER BY g.name ASC"
case config.PullOrderSmallestFirst:
selectOpts = "ORDER BY g.size ASC"
case config.PullOrderLargestFirst:
selectOpts = "ORDER BY g.size DESC"
case config.PullOrderOldestFirst:
selectOpts = "ORDER BY g.modified ASC"
case config.PullOrderNewestFirst:
selectOpts = "ORDER BY g.modified DESC"
}
if limit > 0 {
selectOpts += fmt.Sprintf(" LIMIT %d", limit)
}
if offset > 0 {
selectOpts += fmt.Sprintf(" OFFSET %d", offset)
}
if device == protocol.LocalDeviceID {
return s.neededGlobalFilesLocal(folder, selectOpts)
}
return s.neededGlobalFilesRemote(folder, device, selectOpts)
}
func (s *DB) neededGlobalFilesLocal(folder, selectOpts string) (iter.Seq[protocol.FileInfo], func() error) {
// Select all the non-ignored files with the need bit set.
it, errFn := iterStructs[indirectFI](s.stmt(`
SELECT fi.fiprotobuf, bl.blprotobuf, g.name, g.size, g.modified FROM fileinfos fi
INNER JOIN files g on fi.sequence = g.sequence
LEFT JOIN blocklists bl ON bl.blocklist_hash = g.blocklist_hash
INNER JOIN folders o ON o.idx = g.folder_idx
WHERE o.folder_id = ? AND g.local_flags & {{.FlagLocalIgnored}} = 0 AND g.local_flags & {{.FlagLocalNeeded}} != 0
` + selectOpts).Queryx(folder))
return itererr.Map(it, errFn, indirectFI.FileInfo)
}
func (s *DB) neededGlobalFilesRemote(folder string, device protocol.DeviceID, selectOpts string) (iter.Seq[protocol.FileInfo], func() error) {
// Select:
//
// - all the valid, non-deleted global files that don't have a corresponding
// remote file with the same version.
//
// - all the valid, deleted global files that have a corresponding non-deleted
// remote file (of any version)
it, errFn := iterStructs[indirectFI](s.stmt(`
SELECT fi.fiprotobuf, bl.blprotobuf, g.name, g.size, g.modified FROM fileinfos fi
INNER JOIN files g on fi.sequence = g.sequence
LEFT JOIN blocklists bl ON bl.blocklist_hash = g.blocklist_hash
INNER JOIN folders o ON o.idx = g.folder_idx
WHERE o.folder_id = ? AND g.local_flags & {{.FlagLocalGlobal}} != 0 AND NOT g.deleted AND NOT g.invalid AND NOT EXISTS (
SELECT 1 FROM FILES f
INNER JOIN devices d ON d.idx = f.device_idx
WHERE f.name = g.name AND f.version = g.version AND f.folder_idx = g.folder_idx AND d.device_id = ?
)
UNION ALL
SELECT fi.fiprotobuf, bl.blprotobuf, g.name, g.size, g.modified FROM fileinfos fi
INNER JOIN files g on fi.sequence = g.sequence
LEFT JOIN blocklists bl ON bl.blocklist_hash = g.blocklist_hash
INNER JOIN folders o ON o.idx = g.folder_idx
WHERE o.folder_id = ? AND g.local_flags & {{.FlagLocalGlobal}} != 0 AND g.deleted AND NOT g.invalid AND EXISTS (
SELECT 1 FROM FILES f
INNER JOIN devices d ON d.idx = f.device_idx
WHERE f.name = g.name AND f.folder_idx = g.folder_idx AND d.device_id = ? AND NOT f.deleted
)
`+selectOpts).Queryx(
folder, device.String(),
folder, device.String(),
))
return itererr.Map(it, errFn, indirectFI.FileInfo)
}

View File

@@ -0,0 +1,493 @@
// Copyright (C) 2025 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 sqlite
import (
"slices"
"testing"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/protocol"
)
func TestNeed(t *testing.T) {
t.Helper()
db, err := OpenTemp()
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := db.Close(); err != nil {
t.Fatal(err)
}
})
// Some local files
var v protocol.Vector
baseV := v.Update(1)
newerV := baseV.Update(42)
files := []protocol.FileInfo{
genFile("test1", 1, 0), // remote need
genFile("test2", 2, 0), // local need
genFile("test3", 3, 0), // global
}
files[0].Version = baseV
files[1].Version = baseV
files[2].Version = newerV
err = db.Update(folderID, protocol.LocalDeviceID, files)
if err != nil {
t.Fatal(err)
}
// Some remote files
remote := []protocol.FileInfo{
genFile("test2", 2, 100), // global
genFile("test3", 3, 101), // remote need
genFile("test4", 4, 102), // local need
}
remote[0].Version = newerV
remote[1].Version = baseV
remote[2].Version = newerV
err = db.Update(folderID, protocol.DeviceID{42}, remote)
if err != nil {
t.Fatal(err)
}
// A couple are needed locally
localNeed := fiNames(mustCollect[protocol.FileInfo](t)(db.AllNeededGlobalFiles(folderID, protocol.LocalDeviceID, config.PullOrderAlphabetic, 0, 0)))
if !slices.Equal(localNeed, []string{"test2", "test4"}) {
t.Log(localNeed)
t.Fatal("bad local need")
}
// Another couple are needed remotely
remoteNeed := fiNames(mustCollect[protocol.FileInfo](t)(db.AllNeededGlobalFiles(folderID, protocol.DeviceID{42}, config.PullOrderAlphabetic, 0, 0)))
if !slices.Equal(remoteNeed, []string{"test1", "test3"}) {
t.Log(remoteNeed)
t.Fatal("bad remote need")
}
}
func TestDropRecalcsGlobal(t *testing.T) {
// When we drop a device we may get a new global
t.Parallel()
t.Run("DropAllFiles", func(t *testing.T) {
t.Parallel()
testDropWithDropper(t, func(t *testing.T, db *DB) {
t.Helper()
if err := db.DropAllFiles(folderID, protocol.DeviceID{42}); err != nil {
t.Fatal(err)
}
})
})
t.Run("DropDevice", func(t *testing.T) {
t.Parallel()
testDropWithDropper(t, func(t *testing.T, db *DB) {
t.Helper()
if err := db.DropDevice(protocol.DeviceID{42}); err != nil {
t.Fatal(err)
}
})
})
t.Run("DropFilesNamed", func(t *testing.T) {
t.Parallel()
testDropWithDropper(t, func(t *testing.T, db *DB) {
t.Helper()
if err := db.DropFilesNamed(folderID, protocol.DeviceID{42}, []string{"test1", "test42"}); err != nil {
t.Fatal(err)
}
})
})
}
func testDropWithDropper(t *testing.T, dropper func(t *testing.T, db *DB)) {
t.Helper()
db, err := OpenTemp()
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := db.Close(); err != nil {
t.Fatal(err)
}
})
// Some local files
err = db.Update(folderID, protocol.LocalDeviceID, []protocol.FileInfo{
genFile("test1", 1, 0),
genFile("test2", 2, 0),
})
if err != nil {
t.Fatal(err)
}
// Some remote files
remote := []protocol.FileInfo{
genFile("test1", 3, 0),
}
remote[0].Version = remote[0].Version.Update(42)
err = db.Update(folderID, protocol.DeviceID{42}, remote)
if err != nil {
t.Fatal(err)
}
// Remote test1 wins as the global, verify.
count, err := db.CountGlobal(folderID)
if err != nil {
t.Fatal(err)
}
if count.Bytes != (2+3)*128<<10 {
t.Log(count)
t.Fatal("bad global size to begin with")
}
if g, ok, err := db.GetGlobalFile(folderID, "test1"); err != nil || !ok {
t.Fatal("missing global to begin with")
} else if g.Size != 3*128<<10 {
t.Fatal("remote test1 should be the global")
}
// Now remove that remote device
dropper(t, db)
// Our test1 should now be the global
count, err = db.CountGlobal(folderID)
if err != nil {
t.Fatal(err)
}
if count.Bytes != (1+2)*128<<10 {
t.Log(count)
t.Fatal("bad global size after drop")
}
if g, ok, err := db.GetGlobalFile(folderID, "test1"); err != nil || !ok {
t.Fatal("missing global after drop")
} else if g.Size != 1*128<<10 {
t.Fatal("local test1 should be the global")
}
}
func TestNeedDeleted(t *testing.T) {
t.Parallel()
db, err := OpenTemp()
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := db.Close(); err != nil {
t.Fatal(err)
}
})
// Some local files
err = db.Update(folderID, protocol.LocalDeviceID, []protocol.FileInfo{
genFile("test1", 1, 0),
genFile("test2", 2, 0),
})
if err != nil {
t.Fatal(err)
}
// A remote deleted file
remote := []protocol.FileInfo{
genFile("test1", 1, 101),
}
remote[0].SetDeleted(42)
err = db.Update(folderID, protocol.DeviceID{42}, remote)
if err != nil {
t.Fatal(err)
}
// We need the one deleted file
s, err := db.CountNeed(folderID, protocol.LocalDeviceID)
if err != nil {
t.Fatal(err)
}
if s.Bytes != 0 || s.Deleted != 1 {
t.Log(s)
t.Error("bad need")
}
}
func TestDontNeedIgnored(t *testing.T) {
t.Parallel()
db, err := OpenTemp()
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := db.Close(); err != nil {
t.Fatal(err)
}
})
// A remote file
files := []protocol.FileInfo{
genFile("test1", 1, 103),
}
err = db.Update(folderID, protocol.DeviceID{42}, files)
if err != nil {
t.Fatal(err)
}
// Which we've ignored locally
files[0].SetIgnored()
err = db.Update(folderID, protocol.LocalDeviceID, files)
if err != nil {
t.Fatal(err)
}
// We don't need it
s, err := db.CountNeed(folderID, protocol.LocalDeviceID)
if err != nil {
t.Fatal(err)
}
if s.Bytes != 0 || s.Files != 0 {
t.Log(s)
t.Error("bad need")
}
// It shouldn't show up in the need list
names := mustCollect[protocol.FileInfo](t)(db.AllNeededGlobalFiles(folderID, protocol.LocalDeviceID, config.PullOrderAlphabetic, 0, 0))
if len(names) != 0 {
t.Log(names)
t.Error("need no files")
}
}
func TestRemoveDontNeedLocalIgnored(t *testing.T) {
t.Parallel()
db, err := OpenTemp()
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := db.Close(); err != nil {
t.Fatal(err)
}
})
// A local ignored file
file := genFile("test1", 1, 103)
file.SetIgnored()
files := []protocol.FileInfo{file}
err = db.Update(folderID, protocol.LocalDeviceID, files)
if err != nil {
t.Fatal(err)
}
// Which the remote doesn't have (no update)
// They don't need it
s, err := db.CountNeed(folderID, protocol.DeviceID{42})
if err != nil {
t.Fatal(err)
}
if s.Bytes != 0 || s.Files != 0 {
t.Log(s)
t.Error("bad need")
}
// It shouldn't show up in their need list
names := mustCollect[protocol.FileInfo](t)(db.AllNeededGlobalFiles(folderID, protocol.DeviceID{42}, config.PullOrderAlphabetic, 0, 0))
if len(names) != 0 {
t.Log(names)
t.Error("need no files")
}
}
func TestLocalDontNeedDeletedMissing(t *testing.T) {
t.Parallel()
db, err := OpenTemp()
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := db.Close(); err != nil {
t.Fatal(err)
}
})
// A remote deleted file
file := genFile("test1", 1, 103)
file.SetDeleted(42)
files := []protocol.FileInfo{file}
err = db.Update(folderID, protocol.DeviceID{42}, files)
if err != nil {
t.Fatal(err)
}
// Which we don't have (no local update)
// We don't need it
s, err := db.CountNeed(folderID, protocol.LocalDeviceID)
if err != nil {
t.Fatal(err)
}
if s.Bytes != 0 || s.Files != 0 || s.Deleted != 0 {
t.Log(s)
t.Error("bad need")
}
// It shouldn't show up in the need list
names := mustCollect[protocol.FileInfo](t)(db.AllNeededGlobalFiles(folderID, protocol.LocalDeviceID, config.PullOrderAlphabetic, 0, 0))
if len(names) != 0 {
t.Log(names)
t.Error("need no files")
}
}
func TestRemoteDontNeedDeletedMissing(t *testing.T) {
t.Parallel()
db, err := OpenTemp()
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := db.Close(); err != nil {
t.Fatal(err)
}
})
// A local deleted file
file := genFile("test1", 1, 103)
file.SetDeleted(42)
files := []protocol.FileInfo{file}
err = db.Update(folderID, protocol.LocalDeviceID, files)
if err != nil {
t.Fatal(err)
}
// Which the remote doesn't have (no local update)
// They don't need it
s, err := db.CountNeed(folderID, protocol.DeviceID{42})
if err != nil {
t.Fatal(err)
}
if s.Bytes != 0 || s.Files != 0 || s.Deleted != 0 {
t.Log(s)
t.Error("bad need")
}
// It shouldn't show up in their need list
names := mustCollect[protocol.FileInfo](t)(db.AllNeededGlobalFiles(folderID, protocol.DeviceID{42}, config.PullOrderAlphabetic, 0, 0))
if len(names) != 0 {
t.Log(names)
t.Error("need no files")
}
}
func TestNeedRemoteSymlinkAndDir(t *testing.T) {
t.Parallel()
db, err := OpenTemp()
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := db.Close(); err != nil {
t.Fatal(err)
}
})
// Two remote "specials", a symlink and a directory
var v protocol.Vector
v.Update(1)
files := []protocol.FileInfo{
{Name: "sym", Type: protocol.FileInfoTypeSymlink, Sequence: 100, Version: v, Blocks: genBlocks("symlink", 0, 1)},
{Name: "dir", Type: protocol.FileInfoTypeDirectory, Sequence: 101, Version: v},
}
err = db.Update(folderID, protocol.DeviceID{42}, files)
if err != nil {
t.Fatal(err)
}
// We need them
s, err := db.CountNeed(folderID, protocol.LocalDeviceID)
if err != nil {
t.Fatal(err)
}
if s.Directories != 1 || s.Symlinks != 1 {
t.Log(s)
t.Error("bad need")
}
// They should be in the need list
names := mustCollect[protocol.FileInfo](t)(db.AllNeededGlobalFiles(folderID, protocol.LocalDeviceID, config.PullOrderAlphabetic, 0, 0))
if len(names) != 2 {
t.Log(names)
t.Error("bad need")
}
}
func TestNeedPagination(t *testing.T) {
t.Parallel()
db, err := OpenTemp()
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := db.Close(); err != nil {
t.Fatal(err)
}
})
// Several remote files
var v protocol.Vector
v.Update(1)
files := []protocol.FileInfo{
genFile("test0", 1, 100),
genFile("test1", 1, 101),
genFile("test2", 1, 102),
genFile("test3", 1, 103),
genFile("test4", 1, 104),
genFile("test5", 1, 105),
genFile("test6", 1, 106),
genFile("test7", 1, 107),
genFile("test8", 1, 108),
genFile("test9", 1, 109),
}
err = db.Update(folderID, protocol.DeviceID{42}, files)
if err != nil {
t.Fatal(err)
}
// We should get the first two
names := fiNames(mustCollect[protocol.FileInfo](t)(db.AllNeededGlobalFiles(folderID, protocol.LocalDeviceID, config.PullOrderAlphabetic, 2, 0)))
if !slices.Equal(names, []string{"test0", "test1"}) {
t.Log(names)
t.Error("bad need")
}
// We should get the next three
names = fiNames(mustCollect[protocol.FileInfo](t)(db.AllNeededGlobalFiles(folderID, protocol.LocalDeviceID, config.PullOrderAlphabetic, 3, 2)))
if !slices.Equal(names, []string{"test2", "test3", "test4"}) {
t.Log(names)
t.Error("bad need")
}
// We should get the last five
names = fiNames(mustCollect[protocol.FileInfo](t)(db.AllNeededGlobalFiles(folderID, protocol.LocalDeviceID, config.PullOrderAlphabetic, 5, 5)))
if !slices.Equal(names, []string{"test5", "test6", "test7", "test8", "test9"}) {
t.Log(names)
t.Error("bad need")
}
}

View File

@@ -0,0 +1,163 @@
// Copyright (C) 2025 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 sqlite
import (
"database/sql"
"encoding/hex"
"errors"
"fmt"
"github.com/syncthing/syncthing/internal/itererr"
"github.com/syncthing/syncthing/lib/protocol"
)
func (s *DB) GetIndexID(folder string, device protocol.DeviceID) (protocol.IndexID, error) {
// Try a fast read-only query to begin with. If it does not find the ID
// we'll do the full thing under a lock.
var indexID string
if err := s.stmt(`
SELECT i.index_id FROM indexids i
INNER JOIN folders o ON o.idx = i.folder_idx
INNER JOIN devices d ON d.idx = i.device_idx
WHERE o.folder_id = ? AND d.device_id = ?
`).Get(&indexID, folder, device.String()); err == nil && indexID != "" {
idx, err := indexIDFromHex(indexID)
return idx, wrap(err, "select")
}
if device != protocol.LocalDeviceID {
// For non-local devices we do not create the index ID, so return
// zero anyway if we don't have one.
return 0, nil
}
s.updateLock.Lock()
defer s.updateLock.Unlock()
// We are now operating only for the local device ID
folderIdx, err := s.folderIdxLocked(folder)
if err != nil {
return 0, wrap(err)
}
if err := s.stmt(`
SELECT index_id FROM indexids WHERE folder_idx = ? AND device_idx = {{.LocalDeviceIdx}}
`).Get(&indexID, folderIdx); err != nil && !errors.Is(err, sql.ErrNoRows) {
return 0, wrap(err, "select local")
}
if indexID == "" {
// Generate a new index ID. Some trickiness in the query as we need
// to find the max sequence of local files if there already exist
// any.
id := protocol.NewIndexID()
if _, err := s.stmt(`
INSERT INTO indexids (folder_idx, device_idx, index_id, sequence)
SELECT ?, {{.LocalDeviceIdx}}, ?, COALESCE(MAX(sequence), 0) FROM files
WHERE folder_idx = ? AND device_idx = {{.LocalDeviceIdx}}
ON CONFLICT DO UPDATE SET index_id = ?
`).Exec(folderIdx, indexIDToHex(id), folderIdx, indexIDToHex(id)); err != nil {
return 0, wrap(err, "insert")
}
return id, nil
}
return indexIDFromHex(indexID)
}
func (s *DB) SetIndexID(folder string, device protocol.DeviceID, id protocol.IndexID) error {
s.updateLock.Lock()
defer s.updateLock.Unlock()
folderIdx, err := s.folderIdxLocked(folder)
if err != nil {
return wrap(err, "folder idx")
}
deviceIdx, err := s.deviceIdxLocked(device)
if err != nil {
return wrap(err, "device idx")
}
if _, err := s.stmt(`
INSERT OR REPLACE INTO indexids (folder_idx, device_idx, index_id, sequence) values (?, ?, ?, 0)
`).Exec(folderIdx, deviceIdx, indexIDToHex(id)); err != nil {
return wrap(err, "insert")
}
return nil
}
func (s *DB) DropAllIndexIDs() error {
s.updateLock.Lock()
defer s.updateLock.Unlock()
_, err := s.stmt(`DELETE FROM indexids`).Exec()
return wrap(err)
}
func (s *DB) GetDeviceSequence(folder string, device protocol.DeviceID) (int64, error) {
var res sql.NullInt64
err := s.stmt(`
SELECT sequence FROM indexids i
INNER JOIN folders o ON o.idx = i.folder_idx
INNER JOIN devices d ON d.idx = i.device_idx
WHERE o.folder_id = ? AND d.device_id = ?
`).Get(&res, folder, device.String())
if errors.Is(err, sql.ErrNoRows) {
return 0, nil
}
if err != nil {
return 0, wrap(err)
}
if !res.Valid {
return 0, nil
}
return res.Int64, nil
}
func (s *DB) RemoteSequences(folder string) (map[protocol.DeviceID]int64, error) {
type row struct {
Device string
Seq int64
}
it, errFn := iterStructs[row](s.stmt(`
SELECT d.device_id AS device, i.sequence AS seq FROM indexids i
INNER JOIN folders o ON o.idx = i.folder_idx
INNER JOIN devices d ON d.idx = i.device_idx
WHERE o.folder_id = ? AND i.device_idx != {{.LocalDeviceIdx}}
`).Queryx(folder))
res := make(map[protocol.DeviceID]int64)
for row, err := range itererr.Zip(it, errFn) {
if err != nil {
return nil, wrap(err)
}
dev, err := protocol.DeviceIDFromString(row.Device)
if err != nil {
return nil, wrap(err, "device ID")
}
res[dev] = row.Seq
}
return res, nil
}
func indexIDFromHex(s string) (protocol.IndexID, error) {
bs, err := hex.DecodeString(s)
if err != nil {
return 0, fmt.Errorf("indexIDFromHex: %q: %w", s, err)
}
var id protocol.IndexID
if err := id.Unmarshal(bs); err != nil {
return 0, fmt.Errorf("indexIDFromHex: %q: %w", s, err)
}
return id, nil
}
func indexIDToHex(i protocol.IndexID) string {
bs, _ := i.Marshal()
return hex.EncodeToString(bs)
}

View File

@@ -0,0 +1,81 @@
// Copyright (C) 2025 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 sqlite
import (
"testing"
"github.com/syncthing/syncthing/lib/protocol"
)
func TestIndexIDs(t *testing.T) {
t.Parallel()
db, err := OpenTemp()
if err != nil {
t.Fatal()
}
t.Cleanup(func() {
if err := db.Close(); err != nil {
t.Fatal(err)
}
})
t.Run("LocalDeviceID", func(t *testing.T) {
t.Parallel()
localID, err := db.GetIndexID("foo", protocol.LocalDeviceID)
if err != nil {
t.Fatal(err)
}
if localID == 0 {
t.Fatal("should have been generated")
}
again, err := db.GetIndexID("foo", protocol.LocalDeviceID)
if err != nil {
t.Fatal(err)
}
if again != localID {
t.Fatal("should get same again")
}
other, err := db.GetIndexID("bar", protocol.LocalDeviceID)
if err != nil {
t.Fatal(err)
}
if other == localID {
t.Fatal("should not get same for other folder")
}
})
t.Run("OtherDeviceID", func(t *testing.T) {
t.Parallel()
localID, err := db.GetIndexID("foo", protocol.DeviceID{42})
if err != nil {
t.Fatal(err)
}
if localID != 0 {
t.Fatal("should have been zero")
}
newID := protocol.NewIndexID()
if err := db.SetIndexID("foo", protocol.DeviceID{42}, newID); err != nil {
t.Fatal(err)
}
again, err := db.GetIndexID("foo", protocol.DeviceID{42})
if err != nil {
t.Fatal(err)
}
if again != newID {
t.Log(again, newID)
t.Fatal("should get the ID we set")
}
})
}

View File

@@ -0,0 +1,78 @@
// Copyright (C) 2025 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 sqlite
import (
"iter"
"github.com/jmoiron/sqlx"
"github.com/syncthing/syncthing/internal/db"
)
func (s *DB) GetKV(key string) ([]byte, error) {
var val []byte
if err := s.stmt(`
SELECT value FROM kv
WHERE key = ?
`).Get(&val, key); err != nil {
return nil, wrap(err)
}
return val, nil
}
func (s *DB) PutKV(key string, val []byte) error {
s.updateLock.Lock()
defer s.updateLock.Unlock()
_, err := s.stmt(`
INSERT OR REPLACE INTO kv (key, value)
VALUES (?, ?)
`).Exec(key, val)
return wrap(err)
}
func (s *DB) DeleteKV(key string) error {
s.updateLock.Lock()
defer s.updateLock.Unlock()
_, err := s.stmt(`
DELETE FROM kv WHERE key = ?
`).Exec(key)
return wrap(err)
}
func (s *DB) PrefixKV(prefix string) (iter.Seq[db.KeyValue], func() error) {
var rows *sqlx.Rows
var err error
if prefix == "" {
rows, err = s.stmt(`SELECT key, value FROM kv`).Queryx()
} else {
end := prefixEnd(prefix)
rows, err = s.stmt(`
SELECT key, value FROM kv
WHERE key >= ? AND key < ?
`).Queryx(prefix, end)
}
if err != nil {
return func(_ func(db.KeyValue) bool) {}, func() error { return err }
}
return func(yield func(db.KeyValue) bool) {
defer rows.Close()
for rows.Next() {
var key string
var val []byte
if err = rows.Scan(&key, &val); err != nil {
return
}
if !yield(db.KeyValue{Key: key, Value: val}) {
return
}
}
err = rows.Err()
}, func() error {
return err
}
}

View File

@@ -0,0 +1,126 @@
// Copyright (C) 2025 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 sqlite
import (
"database/sql"
"errors"
"fmt"
"iter"
"github.com/syncthing/syncthing/internal/db"
"github.com/syncthing/syncthing/internal/itererr"
"github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/protocol"
)
func (s *DB) GetDeviceFile(folder string, device protocol.DeviceID, file string) (protocol.FileInfo, bool, error) {
file = osutil.NormalizedFilename(file)
var ind indirectFI
err := s.stmt(`
SELECT fi.fiprotobuf, bl.blprotobuf FROM fileinfos fi
INNER JOIN files f on fi.sequence = f.sequence
LEFT JOIN blocklists bl ON bl.blocklist_hash = f.blocklist_hash
INNER JOIN devices d ON f.device_idx = d.idx
INNER JOIN folders o ON f.folder_idx = o.idx
WHERE o.folder_id = ? AND d.device_id = ? AND f.name = ?
`).Get(&ind, folder, device.String(), file)
if errors.Is(err, sql.ErrNoRows) {
return protocol.FileInfo{}, false, nil
}
if err != nil {
return protocol.FileInfo{}, false, wrap(err)
}
fi, err := ind.FileInfo()
if err != nil {
return protocol.FileInfo{}, false, wrap(err, "indirect")
}
return fi, true, nil
}
func (s *DB) AllLocalFiles(folder string, device protocol.DeviceID) (iter.Seq[protocol.FileInfo], func() error) {
it, errFn := iterStructs[indirectFI](s.stmt(`
SELECT fi.fiprotobuf, bl.blprotobuf FROM fileinfos fi
INNER JOIN files f on fi.sequence = f.sequence
LEFT JOIN blocklists bl ON bl.blocklist_hash = f.blocklist_hash
INNER JOIN folders o ON o.idx = f.folder_idx
INNER JOIN devices d ON d.idx = f.device_idx
WHERE o.folder_id = ? AND d.device_id = ?
`).Queryx(folder, device.String()))
return itererr.Map(it, errFn, indirectFI.FileInfo)
}
func (s *DB) AllLocalFilesBySequence(folder string, device protocol.DeviceID, startSeq int64, limit int) (iter.Seq[protocol.FileInfo], func() error) {
var limitStr string
if limit > 0 {
limitStr = fmt.Sprintf(" LIMIT %d", limit)
}
it, errFn := iterStructs[indirectFI](s.stmt(`
SELECT fi.fiprotobuf, bl.blprotobuf FROM fileinfos fi
INNER JOIN files f on fi.sequence = f.sequence
LEFT JOIN blocklists bl ON bl.blocklist_hash = f.blocklist_hash
INNER JOIN folders o ON o.idx = f.folder_idx
INNER JOIN devices d ON d.idx = f.device_idx
WHERE o.folder_id = ? AND d.device_id = ? AND f.sequence >= ?
ORDER BY f.sequence`+limitStr).Queryx(
folder, device.String(), startSeq))
return itererr.Map(it, errFn, indirectFI.FileInfo)
}
func (s *DB) AllLocalFilesWithPrefix(folder string, device protocol.DeviceID, prefix string) (iter.Seq[protocol.FileInfo], func() error) {
if prefix == "" {
return s.AllLocalFiles(folder, device)
}
prefix = osutil.NormalizedFilename(prefix)
end := prefixEnd(prefix)
it, errFn := iterStructs[indirectFI](s.sql.Queryx(`
SELECT fi.fiprotobuf, bl.blprotobuf FROM fileinfos fi
INNER JOIN files f on fi.sequence = f.sequence
LEFT JOIN blocklists bl ON bl.blocklist_hash = f.blocklist_hash
INNER JOIN folders o ON o.idx = f.folder_idx
INNER JOIN devices d ON d.idx = f.device_idx
WHERE o.folder_id = ? AND d.device_id = ? AND f.name >= ? AND f.name < ?
`, folder, device.String(), prefix, end))
return itererr.Map(it, errFn, indirectFI.FileInfo)
}
func (s *DB) AllLocalFilesWithBlocksHash(folder string, h []byte) (iter.Seq[db.FileMetadata], func() error) {
return iterStructs[db.FileMetadata](s.stmt(`
SELECT f.sequence, f.name, f.type, f.modified as modnanos, f.size, f.deleted, f.invalid, f.local_flags as localflags FROM files f
INNER JOIN folders o ON o.idx = f.folder_idx
WHERE o.folder_id = ? AND f.device_idx = {{.LocalDeviceIdx}} AND f.blocklist_hash = ?
`).Queryx(folder, h))
}
func (s *DB) AllLocalFilesWithBlocksHashAnyFolder(h []byte) (iter.Seq2[string, db.FileMetadata], func() error) {
type row struct {
FolderID string `db:"folder_id"`
db.FileMetadata
}
it, errFn := iterStructs[row](s.stmt(`
SELECT o.folder_id, f.sequence, f.name, f.type, f.modified as modnanos, f.size, f.deleted, f.invalid, f.local_flags as localflags FROM files f
INNER JOIN folders o ON o.idx = f.folder_idx
WHERE f.device_idx = {{.LocalDeviceIdx}} AND f.blocklist_hash = ?
`).Queryx(h))
return itererr.Map2(it, errFn, func(r row) (string, db.FileMetadata, error) {
return r.FolderID, r.FileMetadata, nil
})
}
func (s *DB) AllLocalBlocksWithHash(hash []byte) (iter.Seq[db.BlockMapEntry], func() error) {
// We involve the files table in this select because deletion of blocks
// & blocklists is deferred (garbage collected) while the files list is
// not. This filters out blocks that are in fact deleted.
return iterStructs[db.BlockMapEntry](s.stmt(`
SELECT f.blocklist_hash as blocklisthash, b.idx as blockindex, b.offset, b.size FROM files f
LEFT JOIN blocks b ON f.blocklist_hash = b.blocklist_hash
WHERE f.device_idx = {{.LocalDeviceIdx}} AND b.hash = ?
`).Queryx(hash))
}

View File

@@ -0,0 +1,202 @@
// Copyright (C) 2025 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 sqlite
import (
"testing"
"github.com/syncthing/syncthing/internal/db"
"github.com/syncthing/syncthing/internal/itererr"
"github.com/syncthing/syncthing/lib/protocol"
)
func TestBlocks(t *testing.T) {
t.Parallel()
db, err := OpenTemp()
if err != nil {
t.Fatal()
}
t.Cleanup(func() {
if err := db.Close(); err != nil {
t.Fatal(err)
}
})
files := []protocol.FileInfo{
{
Name: "file1",
Blocks: []protocol.BlockInfo{
{Hash: []byte{1, 2, 3}, Offset: 0, Size: 42},
{Hash: []byte{2, 3, 4}, Offset: 42, Size: 42},
{Hash: []byte{3, 4, 5}, Offset: 84, Size: 42},
},
},
{
Name: "file2",
Blocks: []protocol.BlockInfo{
{Hash: []byte{2, 3, 4}, Offset: 0, Size: 42},
{Hash: []byte{3, 4, 5}, Offset: 42, Size: 42},
{Hash: []byte{4, 5, 6}, Offset: 84, Size: 42},
},
},
}
if err := db.Update("test", protocol.LocalDeviceID, files); err != nil {
t.Fatal(err)
}
// Search for blocks
vals, err := itererr.Collect(db.AllLocalBlocksWithHash([]byte{1, 2, 3}))
if err != nil {
t.Fatal(err)
}
if len(vals) != 1 {
t.Log(vals)
t.Fatal("expected one hit")
} else if vals[0].BlockIndex != 0 || vals[0].Offset != 0 || vals[0].Size != 42 {
t.Log(vals[0])
t.Fatal("bad entry")
}
// Get FileInfos for those blocks
found := 0
it, errFn := db.AllLocalFilesWithBlocksHashAnyFolder(vals[0].BlocklistHash)
for folder, fileInfo := range it {
if folder != folderID {
t.Fatal("should be same folder")
}
if fileInfo.Name != "file1" {
t.Fatal("should be file1")
}
found++
}
if err := errFn(); err != nil {
t.Fatal(err)
}
if found != 1 {
t.Fatal("should find one file")
}
// Get the other blocks
vals, err = itererr.Collect(db.AllLocalBlocksWithHash([]byte{3, 4, 5}))
if err != nil {
t.Fatal(err)
}
if len(vals) != 2 {
t.Log(vals)
t.Fatal("expected two hits")
}
// if vals[0].Index != 2 || vals[0].Offset != 84 || vals[0].Size != 42 {
// t.Log(vals[0])
// t.Fatal("bad entry 1")
// }
// if vals[1].Index != 1 || vals[1].Offset != 42 || vals[1].Size != 42 {
// t.Log(vals[1])
// t.Fatal("bad entry 2")
// }
}
func TestBlocksDeleted(t *testing.T) {
t.Parallel()
sdb, err := OpenTemp()
if err != nil {
t.Fatal()
}
t.Cleanup(func() {
if err := sdb.Close(); err != nil {
t.Fatal(err)
}
})
// Insert a file
file := genFile("foo", 1, 0)
if err := sdb.Update(folderID, protocol.LocalDeviceID, []protocol.FileInfo{file}); err != nil {
t.Fatal()
}
// We should find one entry for the block hash
search := file.Blocks[0].Hash
es := mustCollect[db.BlockMapEntry](t)(sdb.AllLocalBlocksWithHash(search))
if len(es) != 1 {
t.Fatal("expected one hit")
}
// Update the file with a new block hash
file.Blocks = genBlocks("foo", 42, 1)
if err := sdb.Update(folderID, protocol.LocalDeviceID, []protocol.FileInfo{file}); err != nil {
t.Fatal()
}
// Searching for the old hash should yield no hits
if hits := mustCollect[db.BlockMapEntry](t)(sdb.AllLocalBlocksWithHash(search)); len(hits) != 0 {
t.Log(hits)
t.Error("expected no hits")
}
// Searching for the new hash should yield one hits
if hits := mustCollect[db.BlockMapEntry](t)(sdb.AllLocalBlocksWithHash(file.Blocks[0].Hash)); len(hits) != 1 {
t.Log(hits)
t.Error("expected one hit")
}
}
func TestRemoteSequence(t *testing.T) {
t.Parallel()
sdb, err := OpenTemp()
if err != nil {
t.Fatal()
}
t.Cleanup(func() {
if err := sdb.Close(); err != nil {
t.Fatal(err)
}
})
// Insert a local file
file := genFile("foo", 1, 0)
if err := sdb.Update(folderID, protocol.LocalDeviceID, []protocol.FileInfo{file}); err != nil {
t.Fatal()
}
// Insert several remote files
file = genFile("foo1", 1, 42)
if err := sdb.Update(folderID, protocol.DeviceID{42}, []protocol.FileInfo{file}); err != nil {
t.Fatal()
}
if err := sdb.Update(folderID, protocol.DeviceID{43}, []protocol.FileInfo{file}); err != nil {
t.Fatal()
}
file = genFile("foo2", 1, 43)
if err := sdb.Update(folderID, protocol.DeviceID{43}, []protocol.FileInfo{file}); err != nil {
t.Fatal()
}
if err := sdb.Update(folderID, protocol.DeviceID{44}, []protocol.FileInfo{file}); err != nil {
t.Fatal()
}
file = genFile("foo3", 1, 44)
if err := sdb.Update(folderID, protocol.DeviceID{44}, []protocol.FileInfo{file}); err != nil {
t.Fatal()
}
// Verify remote sequences
seqs, err := sdb.RemoteSequences(folderID)
if err != nil {
t.Fatal(err)
}
if len(seqs) != 3 || seqs[protocol.DeviceID{42}] != 42 ||
seqs[protocol.DeviceID{43}] != 43 ||
seqs[protocol.DeviceID{44}] != 44 {
t.Log(seqs)
t.Error("bad seqs")
}
}

View File

@@ -0,0 +1,54 @@
// Copyright (C) 2025 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 sqlite
import (
"time"
)
func (s *DB) GetMtime(folder, name string) (ondisk, virtual time.Time) {
var res struct {
Ondisk int64
Virtual int64
}
if err := s.stmt(`
SELECT m.ondisk, m.virtual FROM mtimes m
INNER JOIN folders o ON o.idx = m.folder_idx
WHERE o.folder_id = ? AND m.name = ?
`).Get(&res, folder, name); err != nil {
return time.Time{}, time.Time{}
}
return time.Unix(0, res.Ondisk), time.Unix(0, res.Virtual)
}
func (s *DB) PutMtime(folder, name string, ondisk, virtual time.Time) error {
s.updateLock.Lock()
defer s.updateLock.Unlock()
folderIdx, err := s.folderIdxLocked(folder)
if err != nil {
return wrap(err)
}
_, err = s.stmt(`
INSERT OR REPLACE INTO mtimes (folder_idx, name, ondisk, virtual)
VALUES (?, ?, ?, ?)
`).Exec(folderIdx, name, ondisk.UnixNano(), virtual.UnixNano())
return wrap(err)
}
func (s *DB) DeleteMtime(folder, name string) error {
s.updateLock.Lock()
defer s.updateLock.Unlock()
folderIdx, err := s.folderIdxLocked(folder)
if err != nil {
return wrap(err)
}
_, err = s.stmt(`
DELETE FROM mtimes
WHERE folder_idx = ? AND name = ?
`).Exec(folderIdx, name)
return wrap(err)
}

View File

@@ -0,0 +1,54 @@
// Copyright (C) 2025 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 sqlite
import (
"testing"
"time"
)
func TestMtimePairs(t *testing.T) {
t.Parallel()
db, err := OpenTemp()
if err != nil {
t.Fatal()
}
t.Cleanup(func() {
if err := db.Close(); err != nil {
t.Fatal(err)
}
})
t0 := time.Now().Truncate(time.Second)
t1 := t0.Add(1234567890)
// Set a pair
if err := db.PutMtime("foo", "bar", t0, t1); err != nil {
t.Fatal(err)
}
// Check it
gt0, gt1 := db.GetMtime("foo", "bar")
if !gt0.Equal(t0) || !gt1.Equal(t1) {
t.Log(t0, gt0)
t.Log(t1, gt1)
t.Log("bad times")
}
// Delete it
if err := db.DeleteMtime("foo", "bar"); err != nil {
t.Fatal(err)
}
// Check it
gt0, gt1 = db.GetMtime("foo", "bar")
if !gt0.IsZero() || !gt1.IsZero() {
t.Log(gt0, gt1)
t.Log("bad times")
}
}

View File

@@ -0,0 +1,203 @@
// Copyright (C) 2025 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 sqlite
import (
"database/sql"
"os"
"path/filepath"
"strconv"
"strings"
"text/template"
"github.com/jmoiron/sqlx"
"github.com/syncthing/syncthing/lib/build"
"github.com/syncthing/syncthing/lib/protocol"
)
const maxDBConns = 128
func Open(path string) (*DB, error) {
// Open the database with options to enable foreign keys and recursive
// triggers (needed for the delete+insert triggers on row replace).
sqlDB, err := sqlx.Open(dbDriver, "file:"+path+"?"+commonOptions)
if err != nil {
return nil, wrap(err)
}
sqlDB.SetMaxOpenConns(maxDBConns)
if _, err := sqlDB.Exec(`PRAGMA journal_mode = WAL`); err != nil {
return nil, wrap(err, "PRAGMA journal_mode")
}
if _, err := sqlDB.Exec(`PRAGMA optimize = 0x10002`); err != nil {
// https://www.sqlite.org/pragma.html#pragma_optimize
return nil, wrap(err, "PRAGMA optimize")
}
if _, err := sqlDB.Exec(`PRAGMA journal_size_limit = 6144000`); err != nil {
// https://www.powersync.com/blog/sqlite-optimizations-for-ultra-high-performance
return nil, wrap(err, "PRAGMA journal_size_limit")
}
return openCommon(sqlDB)
}
// Open the database with options suitable for the migration inserts. This
// is not a safe mode of operation for normal processing, use only for bulk
// inserts with a close afterwards.
func OpenForMigration(path string) (*DB, error) {
sqlDB, err := sqlx.Open(dbDriver, "file:"+path+"?"+commonOptions)
if err != nil {
return nil, wrap(err, "open")
}
sqlDB.SetMaxOpenConns(1)
if _, err := sqlDB.Exec(`PRAGMA foreign_keys = 0`); err != nil {
return nil, wrap(err, "PRAGMA foreign_keys")
}
if _, err := sqlDB.Exec(`PRAGMA journal_mode = OFF`); err != nil {
return nil, wrap(err, "PRAGMA journal_mode")
}
if _, err := sqlDB.Exec(`PRAGMA synchronous = 0`); err != nil {
return nil, wrap(err, "PRAGMA synchronous")
}
return openCommon(sqlDB)
}
func OpenTemp() (*DB, error) {
// SQLite has a memory mode, but it works differently with concurrency
// compared to what we need with the WAL mode. So, no memory databases
// for now.
dir, err := os.MkdirTemp("", "syncthing-db")
if err != nil {
return nil, wrap(err)
}
path := filepath.Join(dir, "db")
l.Debugln("Test DB in", path)
return Open(path)
}
func openCommon(sqlDB *sqlx.DB) (*DB, error) {
if _, err := sqlDB.Exec(`PRAGMA auto_vacuum = INCREMENTAL`); err != nil {
return nil, wrap(err, "PRAGMA auto_vacuum")
}
if _, err := sqlDB.Exec(`PRAGMA default_temp_store = MEMORY`); err != nil {
return nil, wrap(err, "PRAGMA default_temp_store")
}
if _, err := sqlDB.Exec(`PRAGMA temp_store = MEMORY`); err != nil {
return nil, wrap(err, "PRAGMA temp_store")
}
db := &DB{
sql: sqlDB,
statements: make(map[string]*sqlx.Stmt),
}
if err := db.runScripts("sql/schema/*"); err != nil {
return nil, wrap(err)
}
ver, _ := db.getAppliedSchemaVersion()
if ver.SchemaVersion > 0 {
filter := func(scr string) bool {
scr = filepath.Base(scr)
nstr, _, ok := strings.Cut(scr, "-")
if !ok {
return false
}
n, err := strconv.ParseInt(nstr, 10, 32)
if err != nil {
return false
}
return int(n) > ver.SchemaVersion
}
if err := db.runScripts("sql/migrations/*", filter); err != nil {
return nil, wrap(err)
}
}
// Touch device IDs that should always exist and have a low index
// numbers, and will never change
db.localDeviceIdx, _ = db.deviceIdxLocked(protocol.LocalDeviceID)
// Set the current schema version, if not already set
if err := db.setAppliedSchemaVersion(currentSchemaVersion); err != nil {
return nil, wrap(err)
}
db.tplInput = map[string]any{
"FlagLocalUnsupported": protocol.FlagLocalUnsupported,
"FlagLocalIgnored": protocol.FlagLocalIgnored,
"FlagLocalMustRescan": protocol.FlagLocalMustRescan,
"FlagLocalReceiveOnly": protocol.FlagLocalReceiveOnly,
"FlagLocalGlobal": protocol.FlagLocalGlobal,
"FlagLocalNeeded": protocol.FlagLocalNeeded,
"LocalDeviceIdx": db.localDeviceIdx,
"SyncthingVersion": build.LongVersion,
}
return db, nil
}
var tplFuncs = template.FuncMap{
"or": func(vs ...int) int {
v := vs[0]
for _, ov := range vs[1:] {
v |= ov
}
return v
},
}
// stmt returns a prepared statement for the given SQL string, after
// applying local template expansions. The statement is cached.
func (s *DB) stmt(tpl string) stmt {
tpl = strings.TrimSpace(tpl)
// Fast concurrent lookup of cached statement
s.statementsMut.RLock()
stmt, ok := s.statements[tpl]
s.statementsMut.RUnlock()
if ok {
return stmt
}
// On miss, take the full lock, check again
s.statementsMut.Lock()
defer s.statementsMut.Unlock()
stmt, ok = s.statements[tpl]
if ok {
return stmt
}
// Apply template expansions
var sb strings.Builder
compTpl := template.Must(template.New("tpl").Funcs(tplFuncs).Parse(tpl))
if err := compTpl.Execute(&sb, s.tplInput); err != nil {
panic("bug: bad template: " + err.Error())
}
// Prepare and cache
stmt, err := s.sql.Preparex(sb.String())
if err != nil {
return failedStmt{err}
}
s.statements[tpl] = stmt
return stmt
}
type stmt interface {
Exec(args ...any) (sql.Result, error)
Get(dest any, args ...any) error
Queryx(args ...any) (*sqlx.Rows, error)
Select(dest any, args ...any) error
}
type failedStmt struct {
err error
}
func (f failedStmt) Exec(_ ...any) (sql.Result, error) { return nil, f.err }
func (f failedStmt) Get(_ any, _ ...any) error { return f.err }
func (f failedStmt) Queryx(_ ...any) (*sqlx.Rows, error) { return nil, f.err }
func (f failedStmt) Select(_ any, _ ...any) error { return f.err }

View File

@@ -0,0 +1,18 @@
// Copyright (C) 2025 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
//go:build cgo
package sqlite
import (
_ "github.com/mattn/go-sqlite3" // register sqlite3 database driver
)
const (
dbDriver = "sqlite3"
commonOptions = "_fk=true&_rt=true&_cache_size=-65536&_sync=1&_txlock=immediate"
)

View File

@@ -0,0 +1,23 @@
// Copyright (C) 2025 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
//go:build !cgo && !wazero
package sqlite
import (
"github.com/syncthing/syncthing/lib/build"
_ "modernc.org/sqlite" // register sqlite database driver
)
const (
dbDriver = "sqlite"
commonOptions = "_pragma=foreign_keys(1)&_pragma=recursive_triggers(1)&_pragma=cache_size(-65536)&_pragma=synchronous(1)"
)
func init() {
build.AddTag("modernc-sqlite")
}

View File

@@ -0,0 +1,44 @@
// Copyright (C) 2025 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 sqlite
import "github.com/jmoiron/sqlx"
type txPreparedStmts struct {
*sqlx.Tx
stmts map[string]*sqlx.Stmt
}
func (p *txPreparedStmts) Preparex(query string) (*sqlx.Stmt, error) {
if p.stmts == nil {
p.stmts = make(map[string]*sqlx.Stmt)
}
stmt, ok := p.stmts[query]
if ok {
return stmt, nil
}
stmt, err := p.Tx.Preparex(query)
if err != nil {
return nil, wrap(err)
}
p.stmts[query] = stmt
return stmt, nil
}
func (p *txPreparedStmts) Commit() error {
for _, s := range p.stmts {
s.Close()
}
return p.Tx.Commit()
}
func (p *txPreparedStmts) Rollback() error {
for _, s := range p.stmts {
s.Close()
}
return p.Tx.Rollback()
}

View File

@@ -0,0 +1,88 @@
// Copyright (C) 2025 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 sqlite
import (
"embed"
"io/fs"
"strings"
"time"
"github.com/syncthing/syncthing/lib/build"
)
const currentSchemaVersion = 1
//go:embed sql/**
var embedded embed.FS
func (s *DB) runScripts(glob string, filter ...func(s string) bool) error {
scripts, err := fs.Glob(embedded, glob)
if err != nil {
return wrap(err)
}
tx, err := s.sql.Begin()
if err != nil {
return wrap(err)
}
defer tx.Rollback() //nolint:errcheck
nextScript:
for _, scr := range scripts {
for _, fn := range filter {
if !fn(scr) {
l.Debugln("Skipping script", scr)
continue nextScript
}
}
l.Debugln("Executing script", scr)
bs, err := fs.ReadFile(embedded, scr)
if err != nil {
return wrap(err, scr)
}
// SQLite requires one statement per exec, so we split the init
// files on lines containing only a semicolon and execute them
// separately. We require it on a separate line because there are
// also statement-internal semicolons in the triggers.
for _, stmt := range strings.Split(string(bs), "\n;") {
if _, err := tx.Exec(stmt); err != nil {
return wrap(err, stmt)
}
}
}
return wrap(tx.Commit())
}
type schemaVersion struct {
SchemaVersion int
AppliedAt int64
SyncthingVersion string
}
func (s *schemaVersion) AppliedTime() time.Time {
return time.Unix(0, s.AppliedAt)
}
func (s *DB) setAppliedSchemaVersion(ver int) error {
_, err := s.stmt(`
INSERT OR IGNORE INTO schemamigrations (schema_version, applied_at, syncthing_version)
VALUES (?, ?, ?)
`).Exec(ver, time.Now().UnixNano(), build.LongVersion)
return wrap(err)
}
func (s *DB) getAppliedSchemaVersion() (schemaVersion, error) {
var v schemaVersion
err := s.stmt(`
SELECT schema_version as schemaversion, applied_at as appliedat, syncthing_version as syncthingversion FROM schemamigrations
ORDER BY schema_version DESC
LIMIT 1
`).Get(&v)
return v, wrap(err)
}

View File

@@ -0,0 +1,141 @@
// Copyright (C) 2025 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 sqlite
import (
"context"
"time"
"github.com/syncthing/syncthing/internal/db"
)
const (
internalMetaPrefix = "dbsvc"
lastMaintKey = "lastMaint"
)
type Service struct {
sdb *DB
maintenanceInterval time.Duration
internalMeta *db.Typed
}
func newService(sdb *DB, maintenanceInterval time.Duration) *Service {
return &Service{
sdb: sdb,
maintenanceInterval: maintenanceInterval,
internalMeta: db.NewTyped(sdb, internalMetaPrefix),
}
}
func (s *Service) Serve(ctx context.Context) error {
// Run periodic maintenance
// Figure out when we last ran maintenance and schedule accordingly. If
// it was never, do it now.
lastMaint, _, _ := s.internalMeta.Time(lastMaintKey)
nextMaint := lastMaint.Add(s.maintenanceInterval)
wait := time.Until(nextMaint)
if wait < 0 {
wait = time.Minute
}
l.Debugln("Next periodic run in", wait)
timer := time.NewTimer(wait)
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-timer.C:
}
if err := s.periodic(ctx); err != nil {
return wrap(err)
}
timer.Reset(s.maintenanceInterval)
l.Debugln("Next periodic run in", s.maintenanceInterval)
_ = s.internalMeta.PutTime(lastMaintKey, time.Now())
}
}
func (s *Service) periodic(ctx context.Context) error {
t0 := time.Now()
l.Debugln("Periodic start")
s.sdb.updateLock.Lock()
defer s.sdb.updateLock.Unlock()
t1 := time.Now()
defer func() { l.Debugln("Periodic done in", time.Since(t1), "+", t1.Sub(t0)) }()
if err := s.garbageCollectBlocklistsAndBlocksLocked(ctx); err != nil {
return wrap(err)
}
_, _ = s.sdb.sql.ExecContext(ctx, `ANALYZE`)
_, _ = s.sdb.sql.ExecContext(ctx, `PRAGMA optimize`)
_, _ = s.sdb.sql.ExecContext(ctx, `PRAGMA incremental_vacuum`)
_, _ = s.sdb.sql.ExecContext(ctx, `PRAGMA wal_checkpoint(TRUNCATE)`)
return nil
}
func (s *Service) garbageCollectBlocklistsAndBlocksLocked(ctx context.Context) error {
// Remove all blocklists not referred to by any files and, by extension,
// any blocks not referred to by a blocklist. This is an expensive
// operation when run normally, especially if there are a lot of blocks
// to collect.
//
// We make this orders of magnitude faster by disabling foreign keys for
// the transaction and doing the cleanup manually. This requires using
// an explicit connection and disabling foreign keys before starting the
// transaction. We make sure to clean up on the way out.
conn, err := s.sdb.sql.Connx(ctx)
if err != nil {
return wrap(err)
}
defer conn.Close()
if _, err := conn.ExecContext(ctx, `PRAGMA foreign_keys = 0`); err != nil {
return wrap(err)
}
defer func() { //nolint:contextcheck
_, _ = conn.ExecContext(context.Background(), `PRAGMA foreign_keys = 1`)
}()
tx, err := conn.BeginTxx(ctx, nil)
if err != nil {
return wrap(err)
}
defer tx.Rollback() //nolint:errcheck
if res, err := tx.ExecContext(ctx, `
DELETE FROM blocklists
WHERE NOT EXISTS (
SELECT 1 FROM files WHERE files.blocklist_hash = blocklists.blocklist_hash
)`); err != nil {
return wrap(err, "delete blocklists")
} else if shouldDebug() {
rows, err := res.RowsAffected()
l.Debugln("Blocklist GC:", rows, err)
}
if res, err := tx.ExecContext(ctx, `
DELETE FROM blocks
WHERE NOT EXISTS (
SELECT 1 FROM blocklists WHERE blocklists.blocklist_hash = blocks.blocklist_hash
)`); err != nil {
return wrap(err, "delete blocks")
} else if shouldDebug() {
rows, err := res.RowsAffected()
l.Debugln("Blocks GC:", rows, err)
}
return wrap(tx.Commit())
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,549 @@
// Copyright (C) 2025 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 sqlite
import (
"cmp"
"context"
"fmt"
"runtime"
"slices"
"strings"
"github.com/jmoiron/sqlx"
"github.com/syncthing/syncthing/internal/gen/dbproto"
"github.com/syncthing/syncthing/internal/itererr"
"github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/sliceutil"
"google.golang.org/protobuf/proto"
)
func (s *DB) Update(folder string, device protocol.DeviceID, fs []protocol.FileInfo) error {
s.updateLock.Lock()
defer s.updateLock.Unlock()
folderIdx, err := s.folderIdxLocked(folder)
if err != nil {
return wrap(err)
}
deviceIdx, err := s.deviceIdxLocked(device)
if err != nil {
return wrap(err)
}
tx, err := s.sql.BeginTxx(context.Background(), nil)
if err != nil {
return wrap(err)
}
defer tx.Rollback() //nolint:errcheck
txp := &txPreparedStmts{Tx: tx}
//nolint:sqlclosecheck
insertFileStmt, err := txp.Preparex(`
INSERT OR REPLACE INTO files (folder_idx, device_idx, remote_sequence, name, type, modified, size, version, deleted, invalid, local_flags, blocklist_hash)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
RETURNING sequence
`)
if err != nil {
return wrap(err, "prepare insert file")
}
//nolint:sqlclosecheck
insertFileInfoStmt, err := txp.Preparex(`
INSERT INTO fileinfos (sequence, fiprotobuf)
VALUES (?, ?)
`)
if err != nil {
return wrap(err, "prepare insert fileinfo")
}
//nolint:sqlclosecheck
insertBlockListStmt, err := txp.Preparex(`
INSERT OR IGNORE INTO blocklists (blocklist_hash, blprotobuf)
VALUES (?, ?)
`)
if err != nil {
return wrap(err, "prepare insert blocklist")
}
var prevRemoteSeq int64
for i, f := range fs {
f.Name = osutil.NormalizedFilename(f.Name)
var blockshash *[]byte
if len(f.Blocks) > 0 {
f.BlocksHash = protocol.BlocksHash(f.Blocks)
blockshash = &f.BlocksHash
} else {
f.BlocksHash = nil
}
if f.Type == protocol.FileInfoTypeDirectory {
f.Size = 128 // synthetic directory size
}
// Insert the file.
//
// If it is a remote file, set remote_sequence otherwise leave it at
// null. Returns the new local sequence.
var remoteSeq *int64
if device != protocol.LocalDeviceID {
if i > 0 && f.Sequence == prevRemoteSeq {
return fmt.Errorf("duplicate remote sequence number %d", prevRemoteSeq)
}
prevRemoteSeq = f.Sequence
remoteSeq = &f.Sequence
}
var localSeq int64
if err := insertFileStmt.Get(&localSeq, folderIdx, deviceIdx, remoteSeq, f.Name, f.Type, f.ModTime().UnixNano(), f.Size, f.Version.String(), f.IsDeleted(), f.IsInvalid(), f.LocalFlags, blockshash); err != nil {
return wrap(err, "insert file")
}
if len(f.Blocks) > 0 {
// Indirect the block list
blocks := sliceutil.Map(f.Blocks, protocol.BlockInfo.ToWire)
bs, err := proto.Marshal(&dbproto.BlockList{Blocks: blocks})
if err != nil {
return wrap(err, "marshal blocklist")
}
if _, err := insertBlockListStmt.Exec(f.BlocksHash, bs); err != nil {
return wrap(err, "insert blocklist")
}
if device == protocol.LocalDeviceID {
// Insert all blocks
if err := s.insertBlocksLocked(txp, f.BlocksHash, f.Blocks); err != nil {
return wrap(err, "insert blocks")
}
}
f.Blocks = nil
}
// Insert the fileinfo
if device == protocol.LocalDeviceID {
f.Sequence = localSeq
}
bs, err := proto.Marshal(f.ToWire(true))
if err != nil {
return wrap(err, "marshal fileinfo")
}
if _, err := insertFileInfoStmt.Exec(localSeq, bs); err != nil {
return wrap(err, "insert fileinfo")
}
// Update global and need
if err := s.recalcGlobalForFile(txp, folderIdx, f.Name); err != nil {
return wrap(err)
}
}
return wrap(tx.Commit())
}
func (s *DB) DropFolder(folder string) error {
s.updateLock.Lock()
defer s.updateLock.Unlock()
_, err := s.stmt(`
DELETE FROM folders
WHERE folder_id = ?
`).Exec(folder)
return wrap(err)
}
func (s *DB) DropDevice(device protocol.DeviceID) error {
if device == protocol.LocalDeviceID {
panic("bug: cannot drop local device")
}
s.updateLock.Lock()
defer s.updateLock.Unlock()
deviceIdx, err := s.deviceIdxLocked(device)
if err != nil {
return wrap(err)
}
tx, err := s.sql.BeginTxx(context.Background(), nil)
if err != nil {
return wrap(err)
}
defer tx.Rollback() //nolint:errcheck
txp := &txPreparedStmts{Tx: tx}
// Find all folders where the device is involved
var folderIdxs []int64
if err := tx.Select(&folderIdxs, `
SELECT folder_idx
FROM counts
WHERE device_idx = ? AND count > 0
GROUP BY folder_idx
`, deviceIdx); err != nil {
return wrap(err)
}
// Drop the device, which cascades to delete all files etc for it
if _, err := tx.Exec(`DELETE FROM devices WHERE device_id = ?`, device.String()); err != nil {
return wrap(err)
}
// Recalc the globals for all affected folders
for _, idx := range folderIdxs {
if err := s.recalcGlobalForFolder(txp, idx); err != nil {
return wrap(err)
}
}
return wrap(tx.Commit())
}
func (s *DB) DropAllFiles(folder string, device protocol.DeviceID) error {
s.updateLock.Lock()
defer s.updateLock.Unlock()
// This is a two part operation, first dropping all the files and then
// recalculating the global state for the entire folder.
folderIdx, err := s.folderIdxLocked(folder)
if err != nil {
return wrap(err)
}
deviceIdx, err := s.deviceIdxLocked(device)
if err != nil {
return wrap(err)
}
tx, err := s.sql.BeginTxx(context.Background(), nil)
if err != nil {
return wrap(err)
}
defer tx.Rollback() //nolint:errcheck
txp := &txPreparedStmts{Tx: tx}
// Drop all the file entries
result, err := tx.Exec(`
DELETE FROM files
WHERE folder_idx = ? AND device_idx = ?
`, folderIdx, deviceIdx)
if err != nil {
return wrap(err)
}
if n, err := result.RowsAffected(); err == nil && n == 0 {
// The delete affected no rows, so we don't need to redo the entire
// global/need calculation.
return wrap(tx.Commit())
}
// Recalc global for the entire folder
if err := s.recalcGlobalForFolder(txp, folderIdx); err != nil {
return wrap(err)
}
return wrap(tx.Commit())
}
func (s *DB) DropFilesNamed(folder string, device protocol.DeviceID, names []string) error {
for i := range names {
names[i] = osutil.NormalizedFilename(names[i])
}
s.updateLock.Lock()
defer s.updateLock.Unlock()
folderIdx, err := s.folderIdxLocked(folder)
if err != nil {
return wrap(err)
}
deviceIdx, err := s.deviceIdxLocked(device)
if err != nil {
return wrap(err)
}
tx, err := s.sql.BeginTxx(context.Background(), nil)
if err != nil {
return wrap(err)
}
defer tx.Rollback() //nolint:errcheck
txp := &txPreparedStmts{Tx: tx}
// Drop the named files
query, args, err := sqlx.In(`
DELETE FROM files
WHERE folder_idx = ? AND device_idx = ? AND name IN (?)
`, folderIdx, deviceIdx, names)
if err != nil {
return wrap(err)
}
if _, err := tx.Exec(query, args...); err != nil {
return wrap(err)
}
// Recalc globals for the named files
for _, name := range names {
if err := s.recalcGlobalForFile(txp, folderIdx, name); err != nil {
return wrap(err)
}
}
return wrap(tx.Commit())
}
func (*DB) insertBlocksLocked(tx *txPreparedStmts, blocklistHash []byte, blocks []protocol.BlockInfo) error {
if len(blocks) == 0 {
return nil
}
bs := make([]map[string]any, len(blocks))
for i, b := range blocks {
bs[i] = map[string]any{
"hash": b.Hash,
"blocklist_hash": blocklistHash,
"idx": i,
"offset": b.Offset,
"size": b.Size,
}
}
_, err := tx.NamedExec(`
INSERT OR IGNORE INTO blocks (hash, blocklist_hash, idx, offset, size)
VALUES (:hash, :blocklist_hash, :idx, :offset, :size)
`, bs)
return wrap(err)
}
func (s *DB) recalcGlobalForFolder(txp *txPreparedStmts, folderIdx int64) error {
// Select files where there is no global, those are the ones we need to
// recalculate.
//nolint:sqlclosecheck
namesStmt, err := txp.Preparex(`
SELECT f.name FROM files f
WHERE f.folder_idx = ? AND NOT EXISTS (
SELECT 1 FROM files g
WHERE g.folder_idx = ? AND g.name = f.name AND g.local_flags & ? != 0
)
GROUP BY name
`)
if err != nil {
return wrap(err)
}
rows, err := namesStmt.Queryx(folderIdx, folderIdx, protocol.FlagLocalGlobal)
if err != nil {
return wrap(err)
}
defer rows.Close()
for rows.Next() {
var name string
if err := rows.Scan(&name); err != nil {
return wrap(err)
}
if err := s.recalcGlobalForFile(txp, folderIdx, name); err != nil {
return wrap(err)
}
}
return wrap(rows.Err())
}
func (s *DB) recalcGlobalForFile(txp *txPreparedStmts, folderIdx int64, file string) error {
//nolint:sqlclosecheck
selStmt, err := txp.Preparex(`
SELECT name, folder_idx, device_idx, sequence, modified, version, deleted, invalid, local_flags FROM files
WHERE folder_idx = ? AND name = ?
`)
if err != nil {
return wrap(err)
}
es, err := itererr.Collect(iterStructs[fileRow](selStmt.Queryx(folderIdx, file)))
if err != nil {
return wrap(err)
}
if len(es) == 0 {
// shouldn't happen
return nil
}
// Sort the entries; the global entry is at the head of the list
slices.SortFunc(es, fileRow.Compare)
// The global version is the first one in the list that is not invalid,
// or just the first one in the list if all are invalid.
var global fileRow
globIdx := slices.IndexFunc(es, func(e fileRow) bool { return !e.Invalid })
if globIdx < 0 {
globIdx = 0
}
global = es[globIdx]
// We "have" the file if the position in the list of versions is at the
// global version or better, or if the version is the same as the global
// file (we might be further down the list due to invalid flags), or if
// the global is deleted and we don't have it at all...
localIdx := slices.IndexFunc(es, func(e fileRow) bool { return e.DeviceIdx == s.localDeviceIdx })
hasLocal := localIdx >= 0 && localIdx <= globIdx || // have a better or equal version
localIdx >= 0 && es[localIdx].Version.Equal(global.Version.Vector) || // have an equal version but invalid/ignored
localIdx < 0 && global.Deleted // missing it, but the global is also deleted
// Set the global flag on the global entry. Set the need flag if the
// local device needs this file, unless it's invalid.
global.LocalFlags |= protocol.FlagLocalGlobal
if hasLocal || global.Invalid {
global.LocalFlags &= ^protocol.FlagLocalNeeded
} else {
global.LocalFlags |= protocol.FlagLocalNeeded
}
//nolint:sqlclosecheck
upStmt, err := txp.Prepare(`
UPDATE files SET local_flags = ?
WHERE folder_idx = ? AND device_idx = ? AND sequence = ?
`)
if err != nil {
return wrap(err)
}
if _, err := upStmt.Exec(global.LocalFlags, global.FolderIdx, global.DeviceIdx, global.Sequence); err != nil {
return wrap(err)
}
// Clear the need and global flags on all other entries
//nolint:sqlclosecheck
upStmt, err = txp.Prepare(`
UPDATE files SET local_flags = local_flags & ?
WHERE folder_idx = ? AND name = ? AND sequence != ? AND local_flags & ? != 0
`)
if err != nil {
return wrap(err)
}
if _, err := upStmt.Exec(^(protocol.FlagLocalNeeded | protocol.FlagLocalGlobal), folderIdx, global.Name, global.Sequence, protocol.FlagLocalNeeded|protocol.FlagLocalGlobal); err != nil {
return wrap(err)
}
return nil
}
func (s *DB) folderIdxLocked(folderID string) (int64, error) {
if _, err := s.stmt(`
INSERT OR IGNORE INTO folders(folder_id)
VALUES (?)
`).Exec(folderID); err != nil {
return 0, wrap(err)
}
var idx int64
if err := s.stmt(`
SELECT idx FROM folders
WHERE folder_id = ?
`).Get(&idx, folderID); err != nil {
return 0, wrap(err)
}
return idx, nil
}
func (s *DB) deviceIdxLocked(deviceID protocol.DeviceID) (int64, error) {
devStr := deviceID.String()
if _, err := s.stmt(`
INSERT OR IGNORE INTO devices(device_id)
VALUES (?)
`).Exec(devStr); err != nil {
return 0, wrap(err)
}
var idx int64
if err := s.stmt(`
SELECT idx FROM devices
WHERE device_id = ?
`).Get(&idx, devStr); err != nil {
return 0, wrap(err)
}
return idx, nil
}
// wrap returns the error wrapped with the calling function name and
// optional extra context strings as prefix. A nil error wraps to nil.
func wrap(err error, context ...string) error {
if err == nil {
return nil
}
prefix := "error"
pc, _, _, ok := runtime.Caller(1)
details := runtime.FuncForPC(pc)
if ok && details != nil {
prefix = strings.ToLower(details.Name())
if dotIdx := strings.LastIndex(prefix, "."); dotIdx > 0 {
prefix = prefix[dotIdx+1:]
}
}
if len(context) > 0 {
for i := range context {
context[i] = strings.TrimSpace(context[i])
}
extra := strings.Join(context, ", ")
return fmt.Errorf("%s (%s): %w", prefix, extra, err)
}
return fmt.Errorf("%s: %w", prefix, err)
}
type fileRow struct {
Name string
Version dbVector
FolderIdx int64 `db:"folder_idx"`
DeviceIdx int64 `db:"device_idx"`
Sequence int64
Modified int64
Size int64
LocalFlags int64 `db:"local_flags"`
Deleted bool
Invalid bool
}
func (e fileRow) Compare(other fileRow) int {
// From FileInfo.WinsConflict
vc := e.Version.Vector.Compare(other.Version.Vector)
switch vc {
case protocol.Equal:
if e.Invalid != other.Invalid {
if e.Invalid {
return 1
}
return -1
}
// Compare the device ID index, lower is better. This is only
// deterministic to the extent that LocalDeviceID will always be the
// lowest one, order between remote devices is random (and
// irrelevant).
return cmp.Compare(e.DeviceIdx, other.DeviceIdx)
case protocol.Greater: // we are newer
return -1
case protocol.Lesser: // we are older
return 1
case protocol.ConcurrentGreater, protocol.ConcurrentLesser: // there is a conflict
if e.Invalid != other.Invalid {
if e.Invalid { // we are invalid, we lose
return 1
}
return -1 // they are invalid, we win
}
if e.Deleted != other.Deleted {
if e.Deleted { // we are deleted, we lose
return 1
}
return -1 // they are deleted, we win
}
if d := cmp.Compare(e.Modified, other.Modified); d != 0 {
return -d // positive d means we were newer, so we win (negative return)
}
if vc == protocol.ConcurrentGreater {
return -1 // we have a better device ID, we win
}
return 1 // they win
default:
return 0
}
}

View File

@@ -1,17 +1,15 @@
// Copyright (C) 2014 The Syncthing Authors.
// Copyright (C) 2025 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 db
package sqlite
import (
"github.com/syncthing/syncthing/lib/logger"
)
var l = logger.DefaultLogger.NewFacility("db", "The database layer")
var l = logger.DefaultLogger.NewFacility("sqlite", "SQLite database")
func shouldDebug() bool {
return l.ShouldDebug("db")
}
func shouldDebug() bool { return l.ShouldDebug("sqlite") }

View File

@@ -0,0 +1,8 @@
These SQL scripts are embedded in the binary.
Scripts in `schema/` are run at every startup, in alphanumerical order.
Scripts in `migrations/` are run when a migration is needed; the must begin
with a number that equals the schema version that results from that
migration. Migrations are not run on initial database creation, so the
scripts in `schema/` should create the latest version.

View File

@@ -0,0 +1,7 @@
-- Copyright (C) 2025 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/.
-- The next migration should be number two.

View File

@@ -0,0 +1,19 @@
-- Copyright (C) 2025 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/.
-- folders map folder IDs as used by Syncthing to database folder indexes
CREATE TABLE IF NOT EXISTS folders (
idx INTEGER NOT NULL PRIMARY KEY,
folder_id TEXT NOT NULL UNIQUE COLLATE BINARY
) STRICT
;
-- devices map device IDs as used by Syncthing to database device indexes
CREATE TABLE IF NOT EXISTS devices (
idx INTEGER NOT NULL PRIMARY KEY,
device_id TEXT NOT NULL UNIQUE COLLATE BINARY
) STRICT
;

View File

@@ -0,0 +1,14 @@
-- Copyright (C) 2025 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/.
-- Schema migrations hold the list of historical migrations applied
CREATE TABLE IF NOT EXISTS schemamigrations (
schema_version INTEGER NOT NULL,
applied_at INTEGER NOT NULL, -- unix nanos
syncthing_version TEXT NOT NULL COLLATE BINARY,
PRIMARY KEY(schema_version)
) STRICT
;

View File

@@ -0,0 +1,62 @@
-- Copyright (C) 2025 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/.
-- Files
--
-- The files table contains all files announced by any device. Files present
-- on this device are filed under the LocalDeviceID, not the actual current
-- device ID, for simplicity, consistency and portability. One announced
-- version of each file is considered the "global" version - the latest one,
-- that all other devices strive to replicate. This instance gets the Global
-- flag bit set. There may be other identical instances of this file
-- announced by other devices, but only one onstance gets the Global flag;
-- this simplifies accounting. If the current device has the Global version,
-- the LocalDeviceID instance of the file is the one that has the Global
-- bit.
--
-- If the current device does not have that version of the file it gets the
-- Need bit set. Only Global files announced by another device can have the
-- Need bit. This allows for very efficient lookup of files needing handling
-- on this device, which is a common query.
CREATE TABLE IF NOT EXISTS files (
folder_idx INTEGER NOT NULL,
device_idx INTEGER NOT NULL, -- actual device ID or LocalDeviceID
sequence INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, -- our local database sequence, for each and every entry
remote_sequence INTEGER, -- remote device's sequence number, null for local or synthetic entries
name TEXT NOT NULL COLLATE BINARY,
type INTEGER NOT NULL, -- protocol.FileInfoType
modified INTEGER NOT NULL, -- Unix nanos
size INTEGER NOT NULL,
version TEXT NOT NULL COLLATE BINARY,
deleted INTEGER NOT NULL, -- boolean
invalid INTEGER NOT NULL, -- boolean
local_flags INTEGER NOT NULL,
blocklist_hash BLOB, -- null when there are no blocks
FOREIGN KEY(device_idx) REFERENCES devices(idx) ON DELETE CASCADE,
FOREIGN KEY(folder_idx) REFERENCES folders(idx) ON DELETE CASCADE
) STRICT
;
-- FileInfos store the actual protobuf object. We do this separately to keep
-- the files rows smaller and more efficient.
CREATE TABLE IF NOT EXISTS fileinfos (
sequence INTEGER NOT NULL PRIMARY KEY, -- our local database sequence from the files table
fiprotobuf BLOB NOT NULL,
FOREIGN KEY(sequence) REFERENCES files(sequence) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED
) STRICT
;
-- There can be only one file per folder, device, and remote sequence number
CREATE UNIQUE INDEX IF NOT EXISTS files_remote_sequence ON files (folder_idx, device_idx, remote_sequence)
WHERE remote_sequence IS NOT NULL
;
-- There can be only one file per folder, device, and name
CREATE UNIQUE INDEX IF NOT EXISTS files_device_name ON files (folder_idx, device_idx, name)
;
-- We want to be able to look up & iterate files based on just folder and name
CREATE INDEX IF NOT EXISTS files_name_only ON files (folder_idx, name)
;
-- We want to be able to look up & iterate files based on blocks hash
CREATE INDEX IF NOT EXISTS files_blocklist_hash_only ON files (blocklist_hash, device_idx, folder_idx) WHERE blocklist_hash IS NOT NULL
;

View File

@@ -0,0 +1,24 @@
-- Copyright (C) 2025 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/.
-- indexids holds the index ID and maximum sequence for a given device and folder
CREATE TABLE IF NOT EXISTS indexids (
device_idx INTEGER NOT NULL,
folder_idx INTEGER NOT NULL,
index_id TEXT NOT NULL COLLATE BINARY,
sequence INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY(device_idx, folder_idx),
FOREIGN KEY(folder_idx) REFERENCES folders(idx) ON DELETE CASCADE,
FOREIGN KEY(device_idx) REFERENCES devices(idx) ON DELETE CASCADE
) STRICT, WITHOUT ROWID
;
CREATE TRIGGER IF NOT EXISTS indexids_seq AFTER INSERT ON files
BEGIN
INSERT INTO indexids (folder_idx, device_idx, index_id, sequence)
VALUES (NEW.folder_idx, NEW.device_idx, "", COALESCE(NEW.remote_sequence, NEW.sequence))
ON CONFLICT DO UPDATE SET sequence = COALESCE(NEW.remote_sequence, NEW.sequence);
END
;

View File

@@ -0,0 +1,53 @@
-- Copyright (C) 2025 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/.
-- Counts
--
-- Counts and sizes are maintained for each device, folder, type, flag bits
-- combination.
CREATE TABLE IF NOT EXISTS counts (
folder_idx INTEGER NOT NULL,
device_idx INTEGER NOT NULL,
type INTEGER NOT NULL,
local_flags INTEGER NOT NULL,
count INTEGER NOT NULL,
size INTEGER NOT NULL,
deleted INTEGER NOT NULL, -- boolean
PRIMARY KEY(folder_idx, device_idx, type, local_flags, deleted),
FOREIGN KEY(device_idx) REFERENCES devices(idx) ON DELETE CASCADE,
FOREIGN KEY(folder_idx) REFERENCES folders(idx) ON DELETE CASCADE
) STRICT, WITHOUT ROWID
;
--- Maintain counts when files are added and removed using triggers
CREATE TRIGGER IF NOT EXISTS counts_insert AFTER INSERT ON files
BEGIN
INSERT INTO counts (folder_idx, device_idx, type, local_flags, count, size, deleted)
VALUES (NEW.folder_idx, NEW.device_idx, NEW.type, NEW.local_flags, 1, NEW.size, NEW.deleted)
ON CONFLICT DO UPDATE SET count = count + 1, size = size + NEW.size;
END
;
CREATE TRIGGER IF NOT EXISTS counts_delete AFTER DELETE ON files
BEGIN
UPDATE counts SET count = count - 1, size = size - OLD.size
WHERE folder_idx = OLD.folder_idx AND device_idx = OLD.device_idx AND type = OLD.type AND local_flags = OLD.local_flags AND deleted = OLD.deleted;
END
;
CREATE TRIGGER IF NOT EXISTS counts_update AFTER UPDATE OF local_flags ON files
WHEN NEW.local_flags != OLD.local_flags
BEGIN
INSERT INTO counts (folder_idx, device_idx, type, local_flags, count, size, deleted)
VALUES (NEW.folder_idx, NEW.device_idx, NEW.type, NEW.local_flags, 1, NEW.size, NEW.deleted)
ON CONFLICT DO UPDATE SET count = count + 1, size = size + NEW.size;
UPDATE counts SET count = count - 1, size = size - OLD.size
WHERE folder_idx = OLD.folder_idx AND device_idx = OLD.device_idx AND type = OLD.type AND local_flags = OLD.local_flags AND deleted = OLD.deleted;
END
;
DROP TRIGGER IF EXISTS counts_update_add -- tmp migration
;
DROP TRIGGER IF EXISTS counts_update_del -- tmp migration
;

View File

@@ -0,0 +1,34 @@
-- Copyright (C) 2025 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/.
-- Block lists
--
-- The block lists are extracted from FileInfos and stored separately. This
-- reduces the database size by reusing the same block list entry for all
-- devices announcing the same file. Doing it for all block lists instead of
-- using a size cutoff simplifies queries. Block lists are garbage collected
-- "manually", not using a trigger as that was too performance impacting.
CREATE TABLE IF NOT EXISTS blocklists (
blocklist_hash BLOB NOT NULL PRIMARY KEY,
blprotobuf BLOB NOT NULL
) STRICT
;
-- Blocks
--
-- For all local files we store the blocks individually for quick lookup. A
-- given block can exist in multiple blocklists and at multiple offsets in a
-- blocklist.
CREATE TABLE IF NOT EXISTS blocks (
hash BLOB NOT NULL,
blocklist_hash BLOB NOT NULL,
idx INTEGER NOT NULL,
offset INTEGER NOT NULL,
size INTEGER NOT NULL,
PRIMARY KEY (hash, blocklist_hash, idx),
FOREIGN KEY(blocklist_hash) REFERENCES blocklists(blocklist_hash) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED
) STRICT
;

View File

@@ -0,0 +1,16 @@
-- Copyright (C) 2025 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/.
--- Backing for the MtimeFS
CREATE TABLE IF NOT EXISTS mtimes (
folder_idx INTEGER NOT NULL,
name TEXT NOT NULL,
ondisk INTEGER NOT NULL, -- unix nanos
virtual INTEGER NOT NULL, -- unix nanos
PRIMARY KEY(folder_idx, name),
FOREIGN KEY(folder_idx) REFERENCES folders(idx) ON DELETE CASCADE
) STRICT, WITHOUT ROWID
;

View File

@@ -0,0 +1,13 @@
-- Copyright (C) 2025 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/.
--- Simple KV store. This backs the "miscDB" we use for certain minor pieces
-- of data.
CREATE TABLE IF NOT EXISTS kv (
key TEXT NOT NULL PRIMARY KEY COLLATE BINARY,
value BLOB NOT NULL
) STRICT
;

117
internal/db/sqlite/util.go Normal file
View File

@@ -0,0 +1,117 @@
// Copyright (C) 2025 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 sqlite
import (
"database/sql/driver"
"errors"
"iter"
"github.com/jmoiron/sqlx"
"github.com/syncthing/syncthing/internal/gen/bep"
"github.com/syncthing/syncthing/internal/gen/dbproto"
"github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/protocol"
"google.golang.org/protobuf/proto"
)
// iterStructs returns an iterator over the given struct type by scanning
// the SQL rows. `rows` is closed when the iterator exits.
func iterStructs[T any](rows *sqlx.Rows, err error) (iter.Seq[T], func() error) {
if err != nil {
return func(_ func(T) bool) {}, func() error { return err }
}
var retErr error
return func(yield func(T) bool) {
defer rows.Close()
for rows.Next() {
v := new(T)
if err := rows.StructScan(v); err != nil {
retErr = err
break
}
if cleanuper, ok := any(v).(interface{ cleanup() }); ok {
cleanuper.cleanup()
}
if !yield(*v) {
return
}
}
if err := rows.Err(); err != nil && retErr == nil {
retErr = err
}
}, func() error { return retErr }
}
// dbVector is a wrapper that allows protocol.Vector values to be serialized
// to and from the database.
type dbVector struct { //nolint:recvcheck
protocol.Vector
}
func (v dbVector) Value() (driver.Value, error) {
return v.String(), nil
}
func (v *dbVector) Scan(value any) error {
str, ok := value.(string)
if !ok {
return errors.New("not a string")
}
if str == "" {
v.Vector = protocol.Vector{}
return nil
}
vec, err := protocol.VectorFromString(str)
if err != nil {
return wrap(err)
}
v.Vector = vec
return nil
}
// indirectFI constructs a FileInfo from separate marshalled FileInfo and
// BlockList bytes.
type indirectFI struct {
Name string // not used, must be present as dest for Need iterator
FiProtobuf []byte
BlProtobuf []byte
Size int64 // not used
Modified int64 // not used
}
func (i indirectFI) FileInfo() (protocol.FileInfo, error) {
var fi bep.FileInfo
if err := proto.Unmarshal(i.FiProtobuf, &fi); err != nil {
return protocol.FileInfo{}, wrap(err, "unmarshal fileinfo")
}
if len(i.BlProtobuf) > 0 {
var bl dbproto.BlockList
if err := proto.Unmarshal(i.BlProtobuf, &bl); err != nil {
return protocol.FileInfo{}, wrap(err, "unmarshal blocklist")
}
fi.Blocks = bl.Blocks
}
fi.Name = osutil.NativeFilename(fi.Name)
return protocol.FileInfoFromDB(&fi), nil
}
func prefixEnd(s string) string {
if s == "" {
panic("bug: cannot represent end prefix for empty string")
}
bs := []byte(s)
for i := len(bs) - 1; i >= 0; i-- {
if bs[i] < 0xff {
bs[i]++
break
}
}
return string(bs)
}

140
internal/db/typed.go Normal file
View File

@@ -0,0 +1,140 @@
// Copyright (C) 2014 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 db
import (
"database/sql"
"encoding/binary"
"errors"
"time"
)
// Typed is a simple key-value store using a specific namespace within a
// lower level KV.
type Typed struct {
db KV
prefix string
}
func NewMiscDB(db KV) *Typed {
return NewTyped(db, "misc")
}
// NewTyped returns a new typed key-value store that lives in the namespace
// specified by the prefix.
func NewTyped(db KV, prefix string) *Typed {
return &Typed{
db: db,
prefix: prefix,
}
}
// PutInt64 stores a new int64. Any existing value (even if of another type)
// is overwritten.
func (n *Typed) PutInt64(key string, val int64) error {
var valBs [8]byte
binary.BigEndian.PutUint64(valBs[:], uint64(val))
return n.db.PutKV(n.prefixedKey(key), valBs[:])
}
// Int64 returns the stored value interpreted as an int64 and a boolean that
// is false if no value was stored at the key.
func (n *Typed) Int64(key string) (int64, bool, error) {
valBs, err := n.db.GetKV(n.prefixedKey(key))
if err != nil {
return 0, false, filterNotFound(err)
}
val := binary.BigEndian.Uint64(valBs)
return int64(val), true, nil
}
// PutTime stores a new time.Time. Any existing value (even if of another
// type) is overwritten.
func (n *Typed) PutTime(key string, val time.Time) error {
valBs, _ := val.MarshalBinary() // never returns an error
return n.db.PutKV(n.prefixedKey(key), valBs)
}
// Time returns the stored value interpreted as a time.Time and a boolean
// that is false if no value was stored at the key.
func (n *Typed) Time(key string) (time.Time, bool, error) {
var t time.Time
valBs, err := n.db.GetKV(n.prefixedKey(key))
if err != nil {
return t, false, filterNotFound(err)
}
err = t.UnmarshalBinary(valBs)
return t, err == nil, err
}
// PutString stores a new string. Any existing value (even if of another type)
// is overwritten.
func (n *Typed) PutString(key, val string) error {
return n.db.PutKV(n.prefixedKey(key), []byte(val))
}
// String returns the stored value interpreted as a string and a boolean that
// is false if no value was stored at the key.
func (n *Typed) String(key string) (string, bool, error) {
valBs, err := n.db.GetKV(n.prefixedKey(key))
if err != nil {
return "", false, filterNotFound(err)
}
return string(valBs), true, nil
}
// PutBytes stores a new byte slice. Any existing value (even if of another type)
// is overwritten.
func (n *Typed) PutBytes(key string, val []byte) error {
return n.db.PutKV(n.prefixedKey(key), val)
}
// Bytes returns the stored value as a raw byte slice and a boolean that
// is false if no value was stored at the key.
func (n *Typed) Bytes(key string) ([]byte, bool, error) {
valBs, err := n.db.GetKV(n.prefixedKey(key))
if err != nil {
return nil, false, filterNotFound(err)
}
return valBs, true, nil
}
// PutBool stores a new boolean. Any existing value (even if of another type)
// is overwritten.
func (n *Typed) PutBool(key string, val bool) error {
if val {
return n.db.PutKV(n.prefixedKey(key), []byte{0x0})
}
return n.db.PutKV(n.prefixedKey(key), []byte{0x1})
}
// Bool returns the stored value as a boolean and a boolean that
// is false if no value was stored at the key.
func (n *Typed) Bool(key string) (bool, bool, error) {
valBs, err := n.db.GetKV(n.prefixedKey(key))
if err != nil {
return false, false, filterNotFound(err)
}
return valBs[0] == 0x0, true, nil
}
// Delete deletes the specified key. It is allowed to delete a nonexistent
// key.
func (n *Typed) Delete(key string) error {
return n.db.DeleteKV(n.prefixedKey(key))
}
func (n *Typed) prefixedKey(key string) string {
return n.prefix + "/" + key
}
func filterNotFound(err error) error {
if errors.Is(err, sql.ErrNoRows) {
return nil
}
return err
}

115
internal/db/typed_test.go Normal file
View File

@@ -0,0 +1,115 @@
// Copyright (C) 2014 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 db_test
import (
"testing"
"time"
"github.com/syncthing/syncthing/internal/db"
"github.com/syncthing/syncthing/internal/db/sqlite"
)
func TestNamespacedInt(t *testing.T) {
t.Parallel()
ldb, err := sqlite.OpenTemp()
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
ldb.Close()
})
n1 := db.NewTyped(ldb, "foo")
n2 := db.NewTyped(ldb, "bar")
t.Run("Int", func(t *testing.T) {
t.Parallel()
// Key is missing to start with
if v, ok, err := n1.Int64("testint"); err != nil {
t.Error("Unexpected error:", err)
} else if v != 0 || ok {
t.Errorf("Incorrect return v %v != 0 || ok %v != false", v, ok)
}
if err := n1.PutInt64("testint", 42); err != nil {
t.Fatal(err)
}
// It should now exist in n1
if v, ok, err := n1.Int64("testint"); err != nil {
t.Error("Unexpected error:", err)
} else if v != 42 || !ok {
t.Errorf("Incorrect return v %v != 42 || ok %v != true", v, ok)
}
// ... but not in n2, which is in a different namespace
if v, ok, err := n2.Int64("testint"); err != nil {
t.Error("Unexpected error:", err)
} else if v != 0 || ok {
t.Errorf("Incorrect return v %v != 0 || ok %v != false", v, ok)
}
if err := n1.Delete("testint"); err != nil {
t.Fatal(err)
}
// It should no longer exist
if v, ok, err := n1.Int64("testint"); err != nil {
t.Error("Unexpected error:", err)
} else if v != 0 || ok {
t.Errorf("Incorrect return v %v != 0 || ok %v != false", v, ok)
}
})
t.Run("Time", func(t *testing.T) {
t.Parallel()
if v, ok, err := n1.Time("testtime"); err != nil {
t.Error("Unexpected error:", err)
} else if !v.IsZero() || ok {
t.Errorf("Incorrect return v %v != %v || ok %v != false", v, time.Time{}, ok)
}
now := time.Now()
if err := n1.PutTime("testtime", now); err != nil {
t.Fatal(err)
}
if v, ok, err := n1.Time("testtime"); err != nil {
t.Error("Unexpected error:", err)
} else if !v.Equal(now) || !ok {
t.Errorf("Incorrect return v %v != %v || ok %v != true", v, now, ok)
}
})
t.Run("String", func(t *testing.T) {
t.Parallel()
if v, ok, err := n1.String("teststring"); err != nil {
t.Error("Unexpected error:", err)
} else if v != "" || ok {
t.Errorf("Incorrect return v %q != \"\" || ok %v != false", v, ok)
}
if err := n1.PutString("teststring", "yo"); err != nil {
t.Fatal(err)
}
if v, ok, err := n1.String("teststring"); err != nil {
t.Error("Unexpected error:", err)
} else if v != "yo" || !ok {
t.Errorf("Incorrect return v %q != \"yo\" || ok %v != true", v, ok)
}
})
}

View File

@@ -877,7 +877,7 @@ type FileInfo struct {
Version *Vector `protobuf:"bytes,9,opt,name=version,proto3" json:"version,omitempty"`
Sequence int64 `protobuf:"varint,10,opt,name=sequence,proto3" json:"sequence,omitempty"`
Blocks []*BlockInfo `protobuf:"bytes,16,rep,name=blocks,proto3" json:"blocks,omitempty"`
SymlinkTarget string `protobuf:"bytes,17,opt,name=symlink_target,json=symlinkTarget,proto3" json:"symlink_target,omitempty"`
SymlinkTarget []byte `protobuf:"bytes,17,opt,name=symlink_target,json=symlinkTarget,proto3" json:"symlink_target,omitempty"`
BlocksHash []byte `protobuf:"bytes,18,opt,name=blocks_hash,json=blocksHash,proto3" json:"blocks_hash,omitempty"`
Encrypted []byte `protobuf:"bytes,19,opt,name=encrypted,proto3" json:"encrypted,omitempty"`
Type FileInfoType `protobuf:"varint,2,opt,name=type,proto3,enum=bep.FileInfoType" json:"type,omitempty"`
@@ -983,11 +983,11 @@ func (x *FileInfo) GetBlocks() []*BlockInfo {
return nil
}
func (x *FileInfo) GetSymlinkTarget() string {
func (x *FileInfo) GetSymlinkTarget() []byte {
if x != nil {
return x.SymlinkTarget
}
return ""
return nil
}
func (x *FileInfo) GetBlocksHash() []byte {
@@ -1093,10 +1093,9 @@ type BlockInfo struct {
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Hash []byte `protobuf:"bytes,3,opt,name=hash,proto3" json:"hash,omitempty"`
Offset int64 `protobuf:"varint,1,opt,name=offset,proto3" json:"offset,omitempty"`
Size int32 `protobuf:"varint,2,opt,name=size,proto3" json:"size,omitempty"`
WeakHash uint32 `protobuf:"varint,4,opt,name=weak_hash,json=weakHash,proto3" json:"weak_hash,omitempty"`
Hash []byte `protobuf:"bytes,3,opt,name=hash,proto3" json:"hash,omitempty"`
Offset int64 `protobuf:"varint,1,opt,name=offset,proto3" json:"offset,omitempty"`
Size int32 `protobuf:"varint,2,opt,name=size,proto3" json:"size,omitempty"`
}
func (x *BlockInfo) Reset() {
@@ -1150,13 +1149,6 @@ func (x *BlockInfo) GetSize() int32 {
return 0
}
func (x *BlockInfo) GetWeakHash() uint32 {
if x != nil {
return x.WeakHash
}
return 0
}
type Vector struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -1578,7 +1570,6 @@ type Request struct {
Size int32 `protobuf:"varint,5,opt,name=size,proto3" json:"size,omitempty"`
Hash []byte `protobuf:"bytes,6,opt,name=hash,proto3" json:"hash,omitempty"`
FromTemporary bool `protobuf:"varint,7,opt,name=from_temporary,json=fromTemporary,proto3" json:"from_temporary,omitempty"`
WeakHash uint32 `protobuf:"varint,8,opt,name=weak_hash,json=weakHash,proto3" json:"weak_hash,omitempty"`
BlockNo int32 `protobuf:"varint,9,opt,name=block_no,json=blockNo,proto3" json:"block_no,omitempty"`
}
@@ -1661,13 +1652,6 @@ func (x *Request) GetFromTemporary() bool {
return false
}
func (x *Request) GetWeakHash() uint32 {
if x != nil {
return x.WeakHash
}
return 0
}
func (x *Request) GetBlockNo() int32 {
if x != nil {
return x.BlockNo
@@ -2047,7 +2031,7 @@ var file_bep_bep_proto_rawDesc = []byte{
0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x62, 0x65, 0x70, 0x2e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x49, 0x6e,
0x66, 0x6f, 0x52, 0x06, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x79,
0x6d, 0x6c, 0x69, 0x6e, 0x6b, 0x5f, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x18, 0x11, 0x20, 0x01,
0x28, 0x09, 0x52, 0x0d, 0x73, 0x79, 0x6d, 0x6c, 0x69, 0x6e, 0x6b, 0x54, 0x61, 0x72, 0x67, 0x65,
0x28, 0x0c, 0x52, 0x0d, 0x73, 0x79, 0x6d, 0x6c, 0x69, 0x6e, 0x6b, 0x54, 0x61, 0x72, 0x67, 0x65,
0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x5f, 0x68, 0x61, 0x73, 0x68,
0x18, 0x12, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x48, 0x61,
0x73, 0x68, 0x12, 0x1c, 0x0a, 0x09, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x18,
@@ -2079,158 +2063,155 @@ var file_bep_bep_proto_rawDesc = []byte{
0x76, 0x61, 0x6c, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x69, 0x6e, 0x76,
0x61, 0x6c, 0x69, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x6e, 0x6f, 0x5f, 0x70, 0x65, 0x72, 0x6d, 0x69,
0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x6e, 0x6f,
0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x68, 0x0a, 0x09, 0x42,
0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x51, 0x0a, 0x09, 0x42,
0x6c, 0x6f, 0x63, 0x6b, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68,
0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x16, 0x0a, 0x06,
0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x6f, 0x66,
0x66, 0x73, 0x65, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01,
0x28, 0x05, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x77, 0x65, 0x61, 0x6b,
0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x77, 0x65, 0x61,
0x6b, 0x48, 0x61, 0x73, 0x68, 0x22, 0x32, 0x0a, 0x06, 0x56, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12,
0x28, 0x0a, 0x08, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28,
0x0b, 0x32, 0x0c, 0x2e, 0x62, 0x65, 0x70, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x52,
0x08, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x73, 0x22, 0x2f, 0x0a, 0x07, 0x43, 0x6f, 0x75,
0x6e, 0x74, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04,
0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20,
0x01, 0x28, 0x04, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0xfd, 0x01, 0x0a, 0x0c, 0x50,
0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x44, 0x61, 0x74, 0x61, 0x12, 0x21, 0x0a, 0x04, 0x75,
0x6e, 0x69, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x62, 0x65, 0x70, 0x2e,
0x55, 0x6e, 0x69, 0x78, 0x44, 0x61, 0x74, 0x61, 0x52, 0x04, 0x75, 0x6e, 0x69, 0x78, 0x12, 0x2a,
0x0a, 0x07, 0x77, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32,
0x10, 0x2e, 0x62, 0x65, 0x70, 0x2e, 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x73, 0x44, 0x61, 0x74,
0x61, 0x52, 0x07, 0x77, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x73, 0x12, 0x24, 0x0a, 0x05, 0x6c, 0x69,
0x6e, 0x75, 0x78, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x62, 0x65, 0x70, 0x2e,
0x58, 0x61, 0x74, 0x74, 0x72, 0x44, 0x61, 0x74, 0x61, 0x52, 0x05, 0x6c, 0x69, 0x6e, 0x75, 0x78,
0x12, 0x26, 0x0a, 0x06, 0x64, 0x61, 0x72, 0x77, 0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b,
0x32, 0x0e, 0x2e, 0x62, 0x65, 0x70, 0x2e, 0x58, 0x61, 0x74, 0x74, 0x72, 0x44, 0x61, 0x74, 0x61,
0x52, 0x06, 0x64, 0x61, 0x72, 0x77, 0x69, 0x6e, 0x12, 0x28, 0x0a, 0x07, 0x66, 0x72, 0x65, 0x65,
0x62, 0x73, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x62, 0x65, 0x70, 0x2e,
0x58, 0x61, 0x74, 0x74, 0x72, 0x44, 0x61, 0x74, 0x61, 0x52, 0x07, 0x66, 0x72, 0x65, 0x65, 0x62,
0x73, 0x64, 0x12, 0x26, 0x0a, 0x06, 0x6e, 0x65, 0x74, 0x62, 0x73, 0x64, 0x18, 0x06, 0x20, 0x01,
0x28, 0x05, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x4a, 0x04, 0x08, 0x04, 0x10, 0x05, 0x22, 0x32,
0x0a, 0x06, 0x56, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12, 0x28, 0x0a, 0x08, 0x63, 0x6f, 0x75, 0x6e,
0x74, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x62, 0x65, 0x70,
0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x52, 0x08, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x65,
0x72, 0x73, 0x22, 0x2f, 0x0a, 0x07, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x12, 0x0e, 0x0a,
0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a,
0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x76, 0x61,
0x6c, 0x75, 0x65, 0x22, 0xfd, 0x01, 0x0a, 0x0c, 0x50, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d,
0x44, 0x61, 0x74, 0x61, 0x12, 0x21, 0x0a, 0x04, 0x75, 0x6e, 0x69, 0x78, 0x18, 0x01, 0x20, 0x01,
0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x62, 0x65, 0x70, 0x2e, 0x55, 0x6e, 0x69, 0x78, 0x44, 0x61, 0x74,
0x61, 0x52, 0x04, 0x75, 0x6e, 0x69, 0x78, 0x12, 0x2a, 0x0a, 0x07, 0x77, 0x69, 0x6e, 0x64, 0x6f,
0x77, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x62, 0x65, 0x70, 0x2e, 0x57,
0x69, 0x6e, 0x64, 0x6f, 0x77, 0x73, 0x44, 0x61, 0x74, 0x61, 0x52, 0x07, 0x77, 0x69, 0x6e, 0x64,
0x6f, 0x77, 0x73, 0x12, 0x24, 0x0a, 0x05, 0x6c, 0x69, 0x6e, 0x75, 0x78, 0x18, 0x03, 0x20, 0x01,
0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x62, 0x65, 0x70, 0x2e, 0x58, 0x61, 0x74, 0x74, 0x72, 0x44, 0x61,
0x74, 0x61, 0x52, 0x06, 0x6e, 0x65, 0x74, 0x62, 0x73, 0x64, 0x22, 0x6c, 0x0a, 0x08, 0x55, 0x6e,
0x69, 0x78, 0x44, 0x61, 0x74, 0x61, 0x12, 0x1d, 0x0a, 0x0a, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f,
0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6f, 0x77, 0x6e, 0x65,
0x72, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x6e,
0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x67, 0x72, 0x6f, 0x75, 0x70,
0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28,
0x05, 0x52, 0x03, 0x75, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x67, 0x69, 0x64, 0x18, 0x04, 0x20,
0x01, 0x28, 0x05, 0x52, 0x03, 0x67, 0x69, 0x64, 0x22, 0x52, 0x0a, 0x0b, 0x57, 0x69, 0x6e, 0x64,
0x6f, 0x77, 0x73, 0x44, 0x61, 0x74, 0x61, 0x12, 0x1d, 0x0a, 0x0a, 0x6f, 0x77, 0x6e, 0x65, 0x72,
0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6f, 0x77, 0x6e,
0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x24, 0x0a, 0x0e, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f,
0x69, 0x73, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c,
0x6f, 0x77, 0x6e, 0x65, 0x72, 0x49, 0x73, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x22, 0x2f, 0x0a, 0x09,
0x58, 0x61, 0x74, 0x74, 0x72, 0x44, 0x61, 0x74, 0x61, 0x12, 0x22, 0x0a, 0x06, 0x78, 0x61, 0x74,
0x74, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x62, 0x65, 0x70, 0x2e,
0x58, 0x61, 0x74, 0x74, 0x72, 0x52, 0x06, 0x78, 0x61, 0x74, 0x74, 0x72, 0x73, 0x22, 0x31, 0x0a,
0x05, 0x58, 0x61, 0x74, 0x74, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01,
0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61,
0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65,
0x22, 0xe4, 0x01, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02,
0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x12, 0x16, 0x0a, 0x06,
0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f,
0x6c, 0x64, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01,
0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x66, 0x66, 0x73,
0x65, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74,
0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04,
0x73, 0x69, 0x7a, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x06, 0x20, 0x01,
0x28, 0x0c, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x25, 0x0a, 0x0e, 0x66, 0x72, 0x6f, 0x6d,
0x5f, 0x74, 0x65, 0x6d, 0x70, 0x6f, 0x72, 0x61, 0x72, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08,
0x52, 0x0d, 0x66, 0x72, 0x6f, 0x6d, 0x54, 0x65, 0x6d, 0x70, 0x6f, 0x72, 0x61, 0x72, 0x79, 0x12,
0x1b, 0x0a, 0x09, 0x77, 0x65, 0x61, 0x6b, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x08, 0x20, 0x01,
0x28, 0x0d, 0x52, 0x08, 0x77, 0x65, 0x61, 0x6b, 0x48, 0x61, 0x73, 0x68, 0x12, 0x19, 0x0a, 0x08,
0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x6e, 0x6f, 0x18, 0x09, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07,
0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x4e, 0x6f, 0x22, 0x52, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52,
0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28,
0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x22, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18,
0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0e, 0x2e, 0x62, 0x65, 0x70, 0x2e, 0x45, 0x72, 0x72, 0x6f,
0x72, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x22, 0x65, 0x0a, 0x10, 0x44,
0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x12,
0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x12, 0x39, 0x0a, 0x07, 0x75, 0x70, 0x64, 0x61, 0x74,
0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x62, 0x65, 0x70, 0x2e, 0x46,
0x74, 0x61, 0x52, 0x05, 0x6c, 0x69, 0x6e, 0x75, 0x78, 0x12, 0x26, 0x0a, 0x06, 0x64, 0x61, 0x72,
0x77, 0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x62, 0x65, 0x70, 0x2e,
0x58, 0x61, 0x74, 0x74, 0x72, 0x44, 0x61, 0x74, 0x61, 0x52, 0x06, 0x64, 0x61, 0x72, 0x77, 0x69,
0x6e, 0x12, 0x28, 0x0a, 0x07, 0x66, 0x72, 0x65, 0x65, 0x62, 0x73, 0x64, 0x18, 0x05, 0x20, 0x01,
0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x62, 0x65, 0x70, 0x2e, 0x58, 0x61, 0x74, 0x74, 0x72, 0x44, 0x61,
0x74, 0x61, 0x52, 0x07, 0x66, 0x72, 0x65, 0x65, 0x62, 0x73, 0x64, 0x12, 0x26, 0x0a, 0x06, 0x6e,
0x65, 0x74, 0x62, 0x73, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x62, 0x65,
0x70, 0x2e, 0x58, 0x61, 0x74, 0x74, 0x72, 0x44, 0x61, 0x74, 0x61, 0x52, 0x06, 0x6e, 0x65, 0x74,
0x62, 0x73, 0x64, 0x22, 0x6c, 0x0a, 0x08, 0x55, 0x6e, 0x69, 0x78, 0x44, 0x61, 0x74, 0x61, 0x12,
0x1d, 0x0a, 0x0a, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20,
0x01, 0x28, 0x09, 0x52, 0x09, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1d,
0x0a, 0x0a, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01,
0x28, 0x09, 0x52, 0x09, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a,
0x03, 0x75, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x03, 0x75, 0x69, 0x64, 0x12,
0x10, 0x0a, 0x03, 0x67, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x03, 0x67, 0x69,
0x64, 0x22, 0x52, 0x0a, 0x0b, 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x73, 0x44, 0x61, 0x74, 0x61,
0x12, 0x1d, 0x0a, 0x0a, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01,
0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x12,
0x24, 0x0a, 0x0e, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x5f, 0x69, 0x73, 0x5f, 0x67, 0x72, 0x6f, 0x75,
0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x49, 0x73,
0x47, 0x72, 0x6f, 0x75, 0x70, 0x22, 0x2f, 0x0a, 0x09, 0x58, 0x61, 0x74, 0x74, 0x72, 0x44, 0x61,
0x74, 0x61, 0x12, 0x22, 0x0a, 0x06, 0x78, 0x61, 0x74, 0x74, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03,
0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x62, 0x65, 0x70, 0x2e, 0x58, 0x61, 0x74, 0x74, 0x72, 0x52, 0x06,
0x78, 0x61, 0x74, 0x74, 0x72, 0x73, 0x22, 0x31, 0x0a, 0x05, 0x58, 0x61, 0x74, 0x74, 0x72, 0x12,
0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e,
0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01,
0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0xcd, 0x01, 0x0a, 0x07, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
0x05, 0x52, 0x02, 0x69, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18,
0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x12, 0x12, 0x0a,
0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d,
0x65, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28,
0x03, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a,
0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x12, 0x0a,
0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x68, 0x61, 0x73,
0x68, 0x12, 0x25, 0x0a, 0x0e, 0x66, 0x72, 0x6f, 0x6d, 0x5f, 0x74, 0x65, 0x6d, 0x70, 0x6f, 0x72,
0x61, 0x72, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x66, 0x72, 0x6f, 0x6d, 0x54,
0x65, 0x6d, 0x70, 0x6f, 0x72, 0x61, 0x72, 0x79, 0x12, 0x19, 0x0a, 0x08, 0x62, 0x6c, 0x6f, 0x63,
0x6b, 0x5f, 0x6e, 0x6f, 0x18, 0x09, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x62, 0x6c, 0x6f, 0x63,
0x6b, 0x4e, 0x6f, 0x4a, 0x04, 0x08, 0x08, 0x10, 0x09, 0x22, 0x52, 0x0a, 0x08, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
0x05, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20,
0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x22, 0x0a, 0x04, 0x63, 0x6f, 0x64,
0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0e, 0x2e, 0x62, 0x65, 0x70, 0x2e, 0x45, 0x72,
0x72, 0x6f, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x22, 0x65, 0x0a,
0x10, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73,
0x73, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28,
0x09, 0x52, 0x06, 0x66, 0x6f, 0x6c, 0x64, 0x65, 0x72, 0x12, 0x39, 0x0a, 0x07, 0x75, 0x70, 0x64,
0x61, 0x74, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x62, 0x65, 0x70,
0x2e, 0x46, 0x69, 0x6c, 0x65, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x50, 0x72, 0x6f,
0x67, 0x72, 0x65, 0x73, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x07, 0x75, 0x70, 0x64,
0x61, 0x74, 0x65, 0x73, 0x22, 0xe5, 0x01, 0x0a, 0x1a, 0x46, 0x69, 0x6c, 0x65, 0x44, 0x6f, 0x77,
0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x55, 0x70, 0x64,
0x61, 0x74, 0x65, 0x12, 0x44, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x79,
0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x23, 0x2e, 0x62, 0x65, 0x70, 0x2e, 0x46,
0x69, 0x6c, 0x65, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x50, 0x72, 0x6f, 0x67, 0x72,
0x65, 0x73, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x07, 0x75, 0x70, 0x64, 0x61, 0x74,
0x65, 0x73, 0x22, 0xe5, 0x01, 0x0a, 0x1a, 0x46, 0x69, 0x6c, 0x65, 0x44, 0x6f, 0x77, 0x6e, 0x6c,
0x6f, 0x61, 0x64, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74,
0x65, 0x12, 0x44, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65,
0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x23, 0x2e, 0x62, 0x65, 0x70, 0x2e, 0x46, 0x69, 0x6c,
0x65, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73,
0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x52, 0x0a, 0x75, 0x70, 0x64,
0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18,
0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x25, 0x0a, 0x07, 0x76,
0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x62,
0x65, 0x70, 0x2e, 0x56, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69,
0x6f, 0x6e, 0x12, 0x27, 0x0a, 0x0d, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x69, 0x6e, 0x64, 0x65,
0x78, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x05, 0x42, 0x02, 0x10, 0x00, 0x52, 0x0c, 0x62,
0x6c, 0x6f, 0x63, 0x6b, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x62,
0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x52,
0x09, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x53, 0x69, 0x7a, 0x65, 0x22, 0x06, 0x0a, 0x04, 0x50, 0x69,
0x6e, 0x67, 0x22, 0x1f, 0x0a, 0x05, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x72,
0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x61,
0x73, 0x6f, 0x6e, 0x2a, 0xed, 0x01, 0x0a, 0x0b, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x54,
0x79, 0x70, 0x65, 0x12, 0x1f, 0x0a, 0x1b, 0x4d, 0x45, 0x53, 0x53, 0x41, 0x47, 0x45, 0x5f, 0x54,
0x59, 0x50, 0x45, 0x5f, 0x43, 0x4c, 0x55, 0x53, 0x54, 0x45, 0x52, 0x5f, 0x43, 0x4f, 0x4e, 0x46,
0x49, 0x47, 0x10, 0x00, 0x12, 0x16, 0x0a, 0x12, 0x4d, 0x45, 0x53, 0x53, 0x41, 0x47, 0x45, 0x5f,
0x54, 0x59, 0x50, 0x45, 0x5f, 0x49, 0x4e, 0x44, 0x45, 0x58, 0x10, 0x01, 0x12, 0x1d, 0x0a, 0x19,
0x4d, 0x45, 0x53, 0x53, 0x41, 0x47, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x49, 0x4e, 0x44,
0x45, 0x58, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x10, 0x02, 0x12, 0x18, 0x0a, 0x14, 0x4d,
0x45, 0x53, 0x53, 0x41, 0x47, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x52, 0x45, 0x51, 0x55,
0x45, 0x53, 0x54, 0x10, 0x03, 0x12, 0x19, 0x0a, 0x15, 0x4d, 0x45, 0x53, 0x53, 0x41, 0x47, 0x45,
0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x52, 0x45, 0x53, 0x50, 0x4f, 0x4e, 0x53, 0x45, 0x10, 0x04,
0x12, 0x22, 0x0a, 0x1e, 0x4d, 0x45, 0x53, 0x53, 0x41, 0x47, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45,
0x5f, 0x44, 0x4f, 0x57, 0x4e, 0x4c, 0x4f, 0x41, 0x44, 0x5f, 0x50, 0x52, 0x4f, 0x47, 0x52, 0x45,
0x53, 0x53, 0x10, 0x05, 0x12, 0x15, 0x0a, 0x11, 0x4d, 0x45, 0x53, 0x53, 0x41, 0x47, 0x45, 0x5f,
0x54, 0x59, 0x50, 0x45, 0x5f, 0x50, 0x49, 0x4e, 0x47, 0x10, 0x06, 0x12, 0x16, 0x0a, 0x12, 0x4d,
0x45, 0x53, 0x53, 0x41, 0x47, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x43, 0x4c, 0x4f, 0x53,
0x45, 0x10, 0x07, 0x2a, 0x4f, 0x0a, 0x12, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x43, 0x6f,
0x6d, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x18, 0x4d, 0x45, 0x53,
0x65, 0x73, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x52, 0x0a, 0x75,
0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d,
0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x25, 0x0a,
0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0b,
0x2e, 0x62, 0x65, 0x70, 0x2e, 0x56, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x07, 0x76, 0x65, 0x72,
0x73, 0x69, 0x6f, 0x6e, 0x12, 0x27, 0x0a, 0x0d, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x69, 0x6e,
0x64, 0x65, 0x78, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x05, 0x42, 0x02, 0x10, 0x00, 0x52,
0x0c, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x73, 0x12, 0x1d, 0x0a,
0x0a, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28,
0x05, 0x52, 0x09, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x53, 0x69, 0x7a, 0x65, 0x22, 0x06, 0x0a, 0x04,
0x50, 0x69, 0x6e, 0x67, 0x22, 0x1f, 0x0a, 0x05, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x12, 0x16, 0x0a,
0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72,
0x65, 0x61, 0x73, 0x6f, 0x6e, 0x2a, 0xed, 0x01, 0x0a, 0x0b, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67,
0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1f, 0x0a, 0x1b, 0x4d, 0x45, 0x53, 0x53, 0x41, 0x47, 0x45,
0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x43, 0x4c, 0x55, 0x53, 0x54, 0x45, 0x52, 0x5f, 0x43, 0x4f,
0x4e, 0x46, 0x49, 0x47, 0x10, 0x00, 0x12, 0x16, 0x0a, 0x12, 0x4d, 0x45, 0x53, 0x53, 0x41, 0x47,
0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x49, 0x4e, 0x44, 0x45, 0x58, 0x10, 0x01, 0x12, 0x1d,
0x0a, 0x19, 0x4d, 0x45, 0x53, 0x53, 0x41, 0x47, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x49,
0x4e, 0x44, 0x45, 0x58, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x10, 0x02, 0x12, 0x18, 0x0a,
0x14, 0x4d, 0x45, 0x53, 0x53, 0x41, 0x47, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x52, 0x45,
0x51, 0x55, 0x45, 0x53, 0x54, 0x10, 0x03, 0x12, 0x19, 0x0a, 0x15, 0x4d, 0x45, 0x53, 0x53, 0x41,
0x47, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x52, 0x45, 0x53, 0x50, 0x4f, 0x4e, 0x53, 0x45,
0x10, 0x04, 0x12, 0x22, 0x0a, 0x1e, 0x4d, 0x45, 0x53, 0x53, 0x41, 0x47, 0x45, 0x5f, 0x54, 0x59,
0x50, 0x45, 0x5f, 0x44, 0x4f, 0x57, 0x4e, 0x4c, 0x4f, 0x41, 0x44, 0x5f, 0x50, 0x52, 0x4f, 0x47,
0x52, 0x45, 0x53, 0x53, 0x10, 0x05, 0x12, 0x15, 0x0a, 0x11, 0x4d, 0x45, 0x53, 0x53, 0x41, 0x47,
0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x50, 0x49, 0x4e, 0x47, 0x10, 0x06, 0x12, 0x16, 0x0a,
0x12, 0x4d, 0x45, 0x53, 0x53, 0x41, 0x47, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x43, 0x4c,
0x4f, 0x53, 0x45, 0x10, 0x07, 0x2a, 0x4f, 0x0a, 0x12, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
0x43, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x18, 0x4d,
0x45, 0x53, 0x53, 0x41, 0x47, 0x45, 0x5f, 0x43, 0x4f, 0x4d, 0x50, 0x52, 0x45, 0x53, 0x53, 0x49,
0x4f, 0x4e, 0x5f, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x1b, 0x0a, 0x17, 0x4d, 0x45, 0x53,
0x53, 0x41, 0x47, 0x45, 0x5f, 0x43, 0x4f, 0x4d, 0x50, 0x52, 0x45, 0x53, 0x53, 0x49, 0x4f, 0x4e,
0x5f, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x1b, 0x0a, 0x17, 0x4d, 0x45, 0x53, 0x53, 0x41,
0x47, 0x45, 0x5f, 0x43, 0x4f, 0x4d, 0x50, 0x52, 0x45, 0x53, 0x53, 0x49, 0x4f, 0x4e, 0x5f, 0x4c,
0x5a, 0x34, 0x10, 0x01, 0x2a, 0x56, 0x0a, 0x0b, 0x43, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, 0x73,
0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x14, 0x43, 0x4f, 0x4d, 0x50, 0x52, 0x45, 0x53, 0x53, 0x49,
0x4f, 0x4e, 0x5f, 0x4d, 0x45, 0x54, 0x41, 0x44, 0x41, 0x54, 0x41, 0x10, 0x00, 0x12, 0x15, 0x0a,
0x11, 0x43, 0x4f, 0x4d, 0x50, 0x52, 0x45, 0x53, 0x53, 0x49, 0x4f, 0x4e, 0x5f, 0x4e, 0x45, 0x56,
0x45, 0x52, 0x10, 0x01, 0x12, 0x16, 0x0a, 0x12, 0x43, 0x4f, 0x4d, 0x50, 0x52, 0x45, 0x53, 0x53,
0x49, 0x4f, 0x4e, 0x5f, 0x41, 0x4c, 0x57, 0x41, 0x59, 0x53, 0x10, 0x02, 0x2a, 0xb0, 0x01, 0x0a,
0x0c, 0x46, 0x69, 0x6c, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x54, 0x79, 0x70, 0x65, 0x12, 0x17, 0x0a,
0x13, 0x46, 0x49, 0x4c, 0x45, 0x5f, 0x49, 0x4e, 0x46, 0x4f, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f,
0x46, 0x49, 0x4c, 0x45, 0x10, 0x00, 0x12, 0x1c, 0x0a, 0x18, 0x46, 0x49, 0x4c, 0x45, 0x5f, 0x49,
0x4e, 0x46, 0x4f, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x44, 0x49, 0x52, 0x45, 0x43, 0x54, 0x4f,
0x52, 0x59, 0x10, 0x01, 0x12, 0x23, 0x0a, 0x1b, 0x46, 0x49, 0x4c, 0x45, 0x5f, 0x49, 0x4e, 0x46,
0x4f, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x53, 0x59, 0x4d, 0x4c, 0x49, 0x4e, 0x4b, 0x5f, 0x46,
0x49, 0x4c, 0x45, 0x10, 0x02, 0x1a, 0x02, 0x08, 0x01, 0x12, 0x28, 0x0a, 0x20, 0x46, 0x49, 0x4c,
0x45, 0x5f, 0x49, 0x4e, 0x46, 0x4f, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x53, 0x59, 0x4d, 0x4c,
0x49, 0x4e, 0x4b, 0x5f, 0x44, 0x49, 0x52, 0x45, 0x43, 0x54, 0x4f, 0x52, 0x59, 0x10, 0x03, 0x1a,
0x02, 0x08, 0x01, 0x12, 0x1a, 0x0a, 0x16, 0x46, 0x49, 0x4c, 0x45, 0x5f, 0x49, 0x4e, 0x46, 0x4f,
0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x53, 0x59, 0x4d, 0x4c, 0x49, 0x4e, 0x4b, 0x10, 0x04, 0x2a,
0x76, 0x0a, 0x09, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x17, 0x0a, 0x13,
0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x43, 0x4f, 0x44, 0x45, 0x5f, 0x4e, 0x4f, 0x5f, 0x45, 0x52,
0x52, 0x4f, 0x52, 0x10, 0x00, 0x12, 0x16, 0x0a, 0x12, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x43,
0x4f, 0x44, 0x45, 0x5f, 0x47, 0x45, 0x4e, 0x45, 0x52, 0x49, 0x43, 0x10, 0x01, 0x12, 0x1b, 0x0a,
0x17, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x43, 0x4f, 0x44, 0x45, 0x5f, 0x4e, 0x4f, 0x5f, 0x53,
0x55, 0x43, 0x48, 0x5f, 0x46, 0x49, 0x4c, 0x45, 0x10, 0x02, 0x12, 0x1b, 0x0a, 0x17, 0x45, 0x52,
0x52, 0x4f, 0x52, 0x5f, 0x43, 0x4f, 0x44, 0x45, 0x5f, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44,
0x5f, 0x46, 0x49, 0x4c, 0x45, 0x10, 0x03, 0x2a, 0x7e, 0x0a, 0x1e, 0x46, 0x69, 0x6c, 0x65, 0x44,
0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x55,
0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x2d, 0x0a, 0x29, 0x46, 0x49, 0x4c,
0x45, 0x5f, 0x44, 0x4f, 0x57, 0x4e, 0x4c, 0x4f, 0x41, 0x44, 0x5f, 0x50, 0x52, 0x4f, 0x47, 0x52,
0x45, 0x53, 0x53, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f,
0x41, 0x50, 0x50, 0x45, 0x4e, 0x44, 0x10, 0x00, 0x12, 0x2d, 0x0a, 0x29, 0x46, 0x49, 0x4c, 0x45,
0x5f, 0x44, 0x4f, 0x57, 0x4e, 0x4c, 0x4f, 0x41, 0x44, 0x5f, 0x50, 0x52, 0x4f, 0x47, 0x52, 0x45,
0x53, 0x53, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x46,
0x4f, 0x52, 0x47, 0x45, 0x54, 0x10, 0x01, 0x42, 0x70, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x2e, 0x62,
0x65, 0x70, 0x42, 0x08, 0x42, 0x65, 0x70, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x2f,
0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x79, 0x6e, 0x63, 0x74,
0x68, 0x69, 0x6e, 0x67, 0x2f, 0x73, 0x79, 0x6e, 0x63, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x2f, 0x69,
0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x62, 0x65, 0x70, 0xa2,
0x02, 0x03, 0x42, 0x58, 0x58, 0xaa, 0x02, 0x03, 0x42, 0x65, 0x70, 0xca, 0x02, 0x03, 0x42, 0x65,
0x70, 0xe2, 0x02, 0x0f, 0x42, 0x65, 0x70, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64,
0x61, 0x74, 0x61, 0xea, 0x02, 0x03, 0x42, 0x65, 0x70, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x33,
0x5f, 0x4c, 0x5a, 0x34, 0x10, 0x01, 0x2a, 0x56, 0x0a, 0x0b, 0x43, 0x6f, 0x6d, 0x70, 0x72, 0x65,
0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x14, 0x43, 0x4f, 0x4d, 0x50, 0x52, 0x45, 0x53,
0x53, 0x49, 0x4f, 0x4e, 0x5f, 0x4d, 0x45, 0x54, 0x41, 0x44, 0x41, 0x54, 0x41, 0x10, 0x00, 0x12,
0x15, 0x0a, 0x11, 0x43, 0x4f, 0x4d, 0x50, 0x52, 0x45, 0x53, 0x53, 0x49, 0x4f, 0x4e, 0x5f, 0x4e,
0x45, 0x56, 0x45, 0x52, 0x10, 0x01, 0x12, 0x16, 0x0a, 0x12, 0x43, 0x4f, 0x4d, 0x50, 0x52, 0x45,
0x53, 0x53, 0x49, 0x4f, 0x4e, 0x5f, 0x41, 0x4c, 0x57, 0x41, 0x59, 0x53, 0x10, 0x02, 0x2a, 0xb0,
0x01, 0x0a, 0x0c, 0x46, 0x69, 0x6c, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x54, 0x79, 0x70, 0x65, 0x12,
0x17, 0x0a, 0x13, 0x46, 0x49, 0x4c, 0x45, 0x5f, 0x49, 0x4e, 0x46, 0x4f, 0x5f, 0x54, 0x59, 0x50,
0x45, 0x5f, 0x46, 0x49, 0x4c, 0x45, 0x10, 0x00, 0x12, 0x1c, 0x0a, 0x18, 0x46, 0x49, 0x4c, 0x45,
0x5f, 0x49, 0x4e, 0x46, 0x4f, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x44, 0x49, 0x52, 0x45, 0x43,
0x54, 0x4f, 0x52, 0x59, 0x10, 0x01, 0x12, 0x23, 0x0a, 0x1b, 0x46, 0x49, 0x4c, 0x45, 0x5f, 0x49,
0x4e, 0x46, 0x4f, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x53, 0x59, 0x4d, 0x4c, 0x49, 0x4e, 0x4b,
0x5f, 0x46, 0x49, 0x4c, 0x45, 0x10, 0x02, 0x1a, 0x02, 0x08, 0x01, 0x12, 0x28, 0x0a, 0x20, 0x46,
0x49, 0x4c, 0x45, 0x5f, 0x49, 0x4e, 0x46, 0x4f, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x53, 0x59,
0x4d, 0x4c, 0x49, 0x4e, 0x4b, 0x5f, 0x44, 0x49, 0x52, 0x45, 0x43, 0x54, 0x4f, 0x52, 0x59, 0x10,
0x03, 0x1a, 0x02, 0x08, 0x01, 0x12, 0x1a, 0x0a, 0x16, 0x46, 0x49, 0x4c, 0x45, 0x5f, 0x49, 0x4e,
0x46, 0x4f, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x53, 0x59, 0x4d, 0x4c, 0x49, 0x4e, 0x4b, 0x10,
0x04, 0x2a, 0x76, 0x0a, 0x09, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x17,
0x0a, 0x13, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x43, 0x4f, 0x44, 0x45, 0x5f, 0x4e, 0x4f, 0x5f,
0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x00, 0x12, 0x16, 0x0a, 0x12, 0x45, 0x52, 0x52, 0x4f, 0x52,
0x5f, 0x43, 0x4f, 0x44, 0x45, 0x5f, 0x47, 0x45, 0x4e, 0x45, 0x52, 0x49, 0x43, 0x10, 0x01, 0x12,
0x1b, 0x0a, 0x17, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x43, 0x4f, 0x44, 0x45, 0x5f, 0x4e, 0x4f,
0x5f, 0x53, 0x55, 0x43, 0x48, 0x5f, 0x46, 0x49, 0x4c, 0x45, 0x10, 0x02, 0x12, 0x1b, 0x0a, 0x17,
0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x43, 0x4f, 0x44, 0x45, 0x5f, 0x49, 0x4e, 0x56, 0x41, 0x4c,
0x49, 0x44, 0x5f, 0x46, 0x49, 0x4c, 0x45, 0x10, 0x03, 0x2a, 0x7e, 0x0a, 0x1e, 0x46, 0x69, 0x6c,
0x65, 0x44, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73,
0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x2d, 0x0a, 0x29, 0x46,
0x49, 0x4c, 0x45, 0x5f, 0x44, 0x4f, 0x57, 0x4e, 0x4c, 0x4f, 0x41, 0x44, 0x5f, 0x50, 0x52, 0x4f,
0x47, 0x52, 0x45, 0x53, 0x53, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x5f, 0x54, 0x59, 0x50,
0x45, 0x5f, 0x41, 0x50, 0x50, 0x45, 0x4e, 0x44, 0x10, 0x00, 0x12, 0x2d, 0x0a, 0x29, 0x46, 0x49,
0x4c, 0x45, 0x5f, 0x44, 0x4f, 0x57, 0x4e, 0x4c, 0x4f, 0x41, 0x44, 0x5f, 0x50, 0x52, 0x4f, 0x47,
0x52, 0x45, 0x53, 0x53, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45,
0x5f, 0x46, 0x4f, 0x52, 0x47, 0x45, 0x54, 0x10, 0x01, 0x42, 0x70, 0x0a, 0x07, 0x63, 0x6f, 0x6d,
0x2e, 0x62, 0x65, 0x70, 0x42, 0x08, 0x42, 0x65, 0x70, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01,
0x5a, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x79, 0x6e,
0x63, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x2f, 0x73, 0x79, 0x6e, 0x63, 0x74, 0x68, 0x69, 0x6e, 0x67,
0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x62, 0x65,
0x70, 0xa2, 0x02, 0x03, 0x42, 0x58, 0x58, 0xaa, 0x02, 0x03, 0x42, 0x65, 0x70, 0xca, 0x02, 0x03,
0x42, 0x65, 0x70, 0xe2, 0x02, 0x0f, 0x42, 0x65, 0x70, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74,
0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x03, 0x42, 0x65, 0x70, 0x62, 0x06, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x33,
}
var (

View File

@@ -34,7 +34,7 @@ type FileInfoTruncated struct {
ModifiedBy uint64 `protobuf:"varint,12,opt,name=modified_by,json=modifiedBy,proto3" json:"modified_by,omitempty"`
Version *bep.Vector `protobuf:"bytes,9,opt,name=version,proto3" json:"version,omitempty"`
Sequence int64 `protobuf:"varint,10,opt,name=sequence,proto3" json:"sequence,omitempty"`
SymlinkTarget string `protobuf:"bytes,17,opt,name=symlink_target,json=symlinkTarget,proto3" json:"symlink_target,omitempty"`
SymlinkTarget []byte `protobuf:"bytes,17,opt,name=symlink_target,json=symlinkTarget,proto3" json:"symlink_target,omitempty"`
BlocksHash []byte `protobuf:"bytes,18,opt,name=blocks_hash,json=blocksHash,proto3" json:"blocks_hash,omitempty"`
Encrypted []byte `protobuf:"bytes,19,opt,name=encrypted,proto3" json:"encrypted,omitempty"`
Type bep.FileInfoType `protobuf:"varint,2,opt,name=type,proto3,enum=bep.FileInfoType" json:"type,omitempty"`
@@ -133,11 +133,11 @@ func (x *FileInfoTruncated) GetSequence() int64 {
return 0
}
func (x *FileInfoTruncated) GetSymlinkTarget() string {
func (x *FileInfoTruncated) GetSymlinkTarget() []byte {
if x != nil {
return x.SymlinkTarget
}
return ""
return nil
}
func (x *FileInfoTruncated) GetBlocksHash() []byte {
@@ -760,7 +760,7 @@ var file_dbproto_structs_proto_rawDesc = []byte{
0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63,
0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x73, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63,
0x65, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x79, 0x6d, 0x6c, 0x69, 0x6e, 0x6b, 0x5f, 0x74, 0x61, 0x72,
0x67, 0x65, 0x74, 0x18, 0x11, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x73, 0x79, 0x6d, 0x6c, 0x69,
0x67, 0x65, 0x74, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0d, 0x73, 0x79, 0x6d, 0x6c, 0x69,
0x6e, 0x6b, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x62, 0x6c, 0x6f, 0x63,
0x6b, 0x73, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x12, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x62,
0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x48, 0x61, 0x73, 0x68, 0x12, 0x1c, 0x0a, 0x09, 0x65, 0x6e, 0x63,

View File

@@ -0,0 +1,83 @@
// Copyright (C) 2025 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 itererr
import "iter"
// Collect returns a slice of the items from the iterator, plus the error if
// any.
func Collect[T any](it iter.Seq[T], errFn func() error) ([]T, error) {
var s []T
for v := range it {
s = append(s, v)
}
return s, errFn()
}
// Zip interleaves the iterator value with the error. The iteration ends
// after a non-nil error.
func Zip[T any](it iter.Seq[T], errFn func() error) iter.Seq2[T, error] {
return func(yield func(T, error) bool) {
for v := range it {
if !yield(v, nil) {
break
}
}
if err := errFn(); err != nil {
var zero T
yield(zero, err)
}
}
}
// Map returns a new iterator by applying the map function, while respecting
// the error function. Additionally, the map function can return an error if
// its own.
func Map[A, B any](i iter.Seq[A], errFn func() error, mapFn func(A) (B, error)) (iter.Seq[B], func() error) {
var retErr error
return func(yield func(B) bool) {
for v := range i {
mapped, err := mapFn(v)
if err != nil {
retErr = err
return
}
if !yield(mapped) {
return
}
}
}, func() error {
if prevErr := errFn(); prevErr != nil {
return prevErr
}
return retErr
}
}
// Map returns a new iterator by applying the map function, while respecting
// the error function. Additionally, the map function can return an error if
// its own.
func Map2[A, B, C any](i iter.Seq[A], errFn func() error, mapFn func(A) (B, C, error)) (iter.Seq2[B, C], func() error) {
var retErr error
return func(yield func(B, C) bool) {
for v := range i {
ma, mb, err := mapFn(v)
if err != nil {
retErr = err
return
}
if !yield(ma, mb) {
return
}
}
}, func() error {
if prevErr := errFn(); prevErr != nil {
return prevErr
}
return retErr
}
}

View File

@@ -1,3 +1,9 @@
// Copyright (C) 2025 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 protoutil
import (

View File

@@ -0,0 +1,27 @@
// Copyright (C) 2025 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 timeutil
import (
"sync/atomic"
"time"
)
var prevNanos atomic.Int64
// StrictlyMonotonicNanos returns the current time in Unix nanoseconds.
// Guaranteed to strictly increase for each call, regardless of the
// underlying OS timer resolution or clock jumps.
func StrictlyMonotonicNanos() int64 {
for {
old := prevNanos.Load()
now := max(time.Now().UnixNano(), old+1)
if prevNanos.CompareAndSwap(old, now) {
return now
}
}
}

View File

@@ -40,10 +40,10 @@ import (
"golang.org/x/text/transform"
"golang.org/x/text/unicode/norm"
"github.com/syncthing/syncthing/internal/db"
"github.com/syncthing/syncthing/lib/build"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/connections"
"github.com/syncthing/syncthing/lib/db"
"github.com/syncthing/syncthing/lib/discover"
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/fs"
@@ -91,7 +91,8 @@ type service struct {
startupErr error
listenerAddr net.Addr
exitChan chan *svcutil.FatalErr
miscDB *db.NamespacedKV
miscDB *db.Typed
shutdownTimeout time.Duration
guiErrors logger.Recorder
systemLog logger.Recorder
@@ -105,7 +106,7 @@ type Service interface {
WaitForStart() error
}
func New(id protocol.DeviceID, cfg config.Wrapper, assetDir, tlsDefaultCommonName string, m model.Model, defaultSub, diskSub events.BufferedSubscription, evLogger events.Logger, discoverer discover.Manager, connectionsService connections.Service, urService *ur.Service, fss model.FolderSummaryService, errors, systemLog logger.Recorder, noUpgrade bool, miscDB *db.NamespacedKV) Service {
func New(id protocol.DeviceID, cfg config.Wrapper, assetDir, tlsDefaultCommonName string, m model.Model, defaultSub, diskSub events.BufferedSubscription, evLogger events.Logger, discoverer discover.Manager, connectionsService connections.Service, urService *ur.Service, fss model.FolderSummaryService, errors, systemLog logger.Recorder, noUpgrade bool, miscDB *db.Typed) Service {
return &service{
id: id,
cfg: cfg,
@@ -129,6 +130,7 @@ func New(id protocol.DeviceID, cfg config.Wrapper, assetDir, tlsDefaultCommonNam
startedOnce: make(chan struct{}),
exitChan: make(chan *svcutil.FatalErr, 1),
miscDB: miscDB,
shutdownTimeout: 100 * time.Millisecond,
}
}
@@ -402,6 +404,9 @@ func (s *service) Serve(ctx context.Context) error {
// care about we log ourselves from the handlers.
ErrorLog: log.New(io.Discard, "", 0),
}
if shouldDebugHTTP() {
srv.ErrorLog = log.Default()
}
l.Infoln("GUI and API listening on", listener.Addr())
l.Infoln("Access the GUI via the following URL:", guiCfg.URL())
@@ -448,7 +453,7 @@ func (s *service) Serve(ctx context.Context) error {
}
// Give it a moment to shut down gracefully, e.g. if we are restarting
// due to a config change through the API, let that finish successfully.
timeout, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
timeout, cancel := context.WithTimeout(context.Background(), s.shutdownTimeout)
defer cancel()
if err := srv.Shutdown(timeout); err == timeout.Err() {
srv.Close()
@@ -979,16 +984,11 @@ func (s *service) getDBFile(w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
mtimeMapping, mtimeErr := s.model.GetMtimeMapping(folder, file)
sendJSON(w, map[string]interface{}{
"global": jsonFileInfo(gf),
"local": jsonFileInfo(lf),
"availability": av,
"mtime": map[string]interface{}{
"err": mtimeErr,
"value": mtimeMapping,
},
})
}
@@ -997,28 +997,14 @@ func (s *service) getDebugFile(w http.ResponseWriter, r *http.Request) {
folder := qs.Get("folder")
file := qs.Get("file")
snap, err := s.model.DBSnapshot(folder)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
mtimeMapping, mtimeErr := s.model.GetMtimeMapping(folder, file)
lf, _ := snap.Get(protocol.LocalDeviceID, file)
gf, _ := snap.GetGlobal(file)
av := snap.Availability(file)
vl := snap.DebugGlobalVersions(file)
lf, _, _ := s.model.CurrentFolderFile(folder, file)
gf, _, _ := s.model.CurrentGlobalFile(folder, file)
av, _ := s.model.Availability(folder, protocol.FileInfo{Name: file}, protocol.BlockInfo{})
sendJSON(w, map[string]interface{}{
"global": jsonFileInfo(gf),
"local": jsonFileInfo(lf),
"availability": av,
"globalVersions": vl.String(),
"mtime": map[string]interface{}{
"err": mtimeErr,
"value": mtimeMapping,
},
"global": jsonFileInfo(gf),
"local": jsonFileInfo(lf),
"availability": av,
})
}
@@ -1759,7 +1745,7 @@ func browse(fsType fs.FilesystemType, current string) []string {
return browseRoots(fsType)
}
parent, base := parentAndBase(current)
parent, base := filepath.Split(current)
ffs := fs.NewFilesystem(fsType, parent)
files := browseFiles(ffs, base)
for i := range files {
@@ -1795,27 +1781,6 @@ func browseRoots(fsType fs.FilesystemType) []string {
return nil
}
// parentAndBase returns the parent directory and the remaining base of the
// path. The base may be empty if the path ends with a path separator.
func parentAndBase(current string) (string, string) {
search, _ := fs.ExpandTilde(current)
pathSeparator := string(fs.PathSeparator)
if strings.HasSuffix(current, pathSeparator) && !strings.HasSuffix(search, pathSeparator) {
search = search + pathSeparator
}
searchDir := filepath.Dir(search)
// The searchFile should be the last component of search, or empty if it
// ends with a path separator
var searchFile string
if !strings.HasSuffix(search, pathSeparator) {
searchFile = filepath.Base(search)
}
return searchDir, searchFile
}
func browseFiles(ffs fs.Filesystem, search string) []string {
subdirectories, _ := ffs.DirNames(".")
pathSeparator := string(fs.PathSeparator)

View File

@@ -10,10 +10,9 @@ import (
"testing"
"time"
"github.com/syncthing/syncthing/internal/db"
"github.com/syncthing/syncthing/internal/db/sqlite"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/db"
"github.com/syncthing/syncthing/lib/db/backend"
"github.com/syncthing/syncthing/lib/events"
)
var guiCfg config.GUIConfiguration
@@ -131,8 +130,14 @@ func (c *mockClock) wind(t time.Duration) {
func TestTokenManager(t *testing.T) {
t.Parallel()
mdb, _ := db.NewLowlevel(backend.OpenMemory(), events.NoopLogger)
kdb := db.NewNamespacedKV(mdb, "test")
mdb, err := sqlite.OpenTemp()
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
mdb.Close()
})
kdb := db.NewMiscDB(mdb)
clock := &mockClock{now: time.Now()}
// Token manager keeps up to three tokens with a validity time of 24 hours.

View File

@@ -11,7 +11,7 @@ import (
"strings"
"time"
"github.com/syncthing/syncthing/lib/db"
"github.com/syncthing/syncthing/internal/db"
)
const (
@@ -34,7 +34,7 @@ type apiKeyValidator interface {
// Check for CSRF token on /rest/ URLs. If a correct one is not given, reject
// the request with 403. For / and /index.html, set a new CSRF cookie if none
// is currently set.
func newCsrfManager(unique string, prefix string, apiKeyValidator apiKeyValidator, next http.Handler, miscDB *db.NamespacedKV) *csrfManager {
func newCsrfManager(unique string, prefix string, apiKeyValidator apiKeyValidator, next http.Handler, miscDB *db.Typed) *csrfManager {
m := &csrfManager{
unique: unique,
prefix: prefix,

View File

@@ -27,12 +27,12 @@ import (
"github.com/d4l3k/messagediff"
"github.com/thejerf/suture/v4"
"github.com/syncthing/syncthing/internal/db"
"github.com/syncthing/syncthing/internal/db/sqlite"
"github.com/syncthing/syncthing/lib/assets"
"github.com/syncthing/syncthing/lib/build"
"github.com/syncthing/syncthing/lib/config"
connmocks "github.com/syncthing/syncthing/lib/connections/mocks"
"github.com/syncthing/syncthing/lib/db"
"github.com/syncthing/syncthing/lib/db/backend"
discovermocks "github.com/syncthing/syncthing/lib/discover/mocks"
"github.com/syncthing/syncthing/lib/events"
eventmocks "github.com/syncthing/syncthing/lib/events/mocks"
@@ -84,8 +84,14 @@ func TestStopAfterBrokenConfig(t *testing.T) {
}
w := config.Wrap("/dev/null", cfg, protocol.LocalDeviceID, events.NoopLogger)
mdb, _ := db.NewLowlevel(backend.OpenMemory(), events.NoopLogger)
kdb := db.NewMiscDataNamespace(mdb)
mdb, err := sqlite.OpenTemp()
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
mdb.Close()
})
kdb := db.NewMiscDB(mdb)
srv := New(protocol.LocalDeviceID, w, "", "syncthing", nil, nil, nil, events.NoopLogger, nil, nil, nil, nil, nil, nil, false, kdb).(*service)
srv.started = make(chan string)
@@ -217,11 +223,7 @@ type httpTestCase struct {
func TestAPIServiceRequests(t *testing.T) {
t.Parallel()
baseURL, cancel, err := startHTTP(apiCfg)
if err != nil {
t.Fatal(err)
}
t.Cleanup(cancel)
baseURL := startHTTP(t, apiCfg)
cases := []httpTestCase{
// /rest/db
@@ -492,13 +494,18 @@ func testHTTPRequest(t *testing.T, baseURL string, tc httpTestCase, apikey strin
}
}
func hasSessionCookie(cookies []*http.Cookie) bool {
func getSessionCookie(cookies []*http.Cookie) (*http.Cookie, bool) {
for _, cookie := range cookies {
if cookie.MaxAge >= 0 && strings.HasPrefix(cookie.Name, "sessionid") {
return true
return cookie, true
}
}
return false
return nil, false
}
func hasSessionCookie(cookies []*http.Cookie) bool {
_, ok := getSessionCookie(cookies)
return ok
}
func hasDeleteSessionCookie(cookies []*http.Cookie) bool {
@@ -510,11 +517,19 @@ func hasDeleteSessionCookie(cookies []*http.Cookie) bool {
return false
}
func httpGet(url string, basicAuthUsername string, basicAuthPassword string, xapikeyHeader string, authorizationBearer string, cookies []*http.Cookie, t *testing.T) *http.Response {
req, err := http.NewRequest("GET", url, nil)
for _, cookie := range cookies {
req.AddCookie(cookie)
func httpRequest(method string, url string, body any, basicAuthUsername, basicAuthPassword, xapikeyHeader, authorizationBearer, csrfTokenName, csrfTokenValue string, cookies []*http.Cookie, t *testing.T) *http.Response {
t.Helper()
var bodyReader io.Reader = nil
if body != nil {
bodyBytes, err := json.Marshal(body)
if err != nil {
t.Fatal(err)
}
bodyReader = bytes.NewReader(bodyBytes)
}
req, err := http.NewRequest(method, url, bodyReader)
if err != nil {
t.Fatal(err)
}
@@ -531,30 +546,16 @@ func httpGet(url string, basicAuthUsername string, basicAuthPassword string, xap
req.Header.Set("Authorization", "Bearer "+authorizationBearer)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
return resp
}
func httpPost(url string, body map[string]string, cookies []*http.Cookie, t *testing.T) *http.Response {
bodyBytes, err := json.Marshal(body)
if err != nil {
t.Fatal(err)
}
req, err := http.NewRequest("POST", url, bytes.NewReader(bodyBytes))
if err != nil {
t.Fatal(err)
if csrfTokenName != "" && csrfTokenValue != "" {
req.Header.Set("X-"+csrfTokenName, csrfTokenValue)
}
for _, cookie := range cookies {
req.AddCookie(cookie)
}
resp, err := http.DefaultClient.Do(req)
client := http.Client{Timeout: 15 * time.Second}
resp, err := client.Do(req)
if err != nil {
t.Fatal(err)
}
@@ -562,18 +563,31 @@ func httpPost(url string, body map[string]string, cookies []*http.Cookie, t *tes
return resp
}
func httpGet(url string, basicAuthUsername, basicAuthPassword, xapikeyHeader, authorizationBearer string, cookies []*http.Cookie, t *testing.T) *http.Response {
t.Helper()
return httpRequest(http.MethodGet, url, nil, basicAuthUsername, basicAuthPassword, xapikeyHeader, authorizationBearer, "", "", cookies, t)
}
func httpPost(url string, body map[string]string, cookies []*http.Cookie, t *testing.T) *http.Response {
t.Helper()
return httpRequest(http.MethodPost, url, body, "", "", "", "", "", "", cookies, t)
}
func TestHTTPLogin(t *testing.T) {
t.Parallel()
httpGetBasicAuth := func(url string, username string, password string) *http.Response {
t.Helper()
return httpGet(url, username, password, "", "", nil, t)
}
httpGetXapikey := func(url string, xapikeyHeader string) *http.Response {
t.Helper()
return httpGet(url, "", "", xapikeyHeader, "", nil, t)
}
httpGetAuthorizationBearer := func(url string, bearer string) *http.Response {
t.Helper()
return httpGet(url, "", "", "", bearer, nil, t)
}
@@ -586,11 +600,7 @@ func TestHTTPLogin(t *testing.T) {
APIKey: testAPIKey,
SendBasicAuthPrompt: sendBasicAuthPrompt,
})
baseURL, cancel, err := startHTTP(cfg)
if err != nil {
t.Fatal(err)
}
t.Cleanup(cancel)
baseURL := startHTTP(t, cfg)
url := baseURL + path
t.Run(fmt.Sprintf("%d path", expectedOkStatus), func(t *testing.T) {
@@ -739,6 +749,113 @@ func TestHTTPLogin(t *testing.T) {
testWith(false, http.StatusOK, http.StatusOK, "/")
testWith(false, http.StatusOK, http.StatusForbidden, "/meta.js")
testWith(false, http.StatusNotFound, http.StatusForbidden, "/any-path/that/does/nooooooot/match-any/noauth-pattern")
t.Run("Password change invalidates old and enables new password", func(t *testing.T) {
t.Parallel()
// This test needs a longer-than-default shutdown timeout to finish saving
// config changes when running on GitHub Actions
shutdownTimeout := time.Second
initConfig := func(password string, t *testing.T) config.Wrapper {
gui := config.GUIConfiguration{
RawAddress: "127.0.0.1:0",
APIKey: testAPIKey,
User: "user",
}
if err := gui.SetPassword(password); err != nil {
t.Fatal(err, "Failed to set initial password")
}
cfg := config.Configuration{
GUI: gui,
}
tmpFile, err := os.CreateTemp("", "syncthing-testConfig-Password-*")
if err != nil {
t.Fatal(err, "Failed to create tmpfile for test")
}
w := config.Wrap(tmpFile.Name(), cfg, protocol.LocalDeviceID, events.NoopLogger)
tmpFile.Close()
cfgCtx, cfgCancel := context.WithCancel(context.Background())
go w.Serve(cfgCtx)
t.Cleanup(func() {
os.Remove(tmpFile.Name())
cfgCancel()
})
return w
}
initialPassword := "Asdf123!"
newPassword := "123!asdF"
t.Run("when done via /rest/config", func(t *testing.T) {
t.Parallel()
w := initConfig(initialPassword, t)
{
baseURL := startHTTPWithShutdownTimeout(t, w, shutdownTimeout)
cfgPath := baseURL + "/rest/config"
path := baseURL + "/meta.js"
resp := httpGetBasicAuth(path, "user", initialPassword)
if resp.StatusCode != http.StatusOK {
t.Fatalf("Unexpected non-200 return code %d for auth with initial password", resp.StatusCode)
}
cfg := w.RawCopy()
cfg.GUI.Password = newPassword
httpRequest(http.MethodPut, cfgPath, cfg, "", "", testAPIKey, "", "", "", nil, t)
}
{
baseURL := startHTTP(t, w)
path := baseURL + "/meta.js"
resp := httpGetBasicAuth(path, "user", initialPassword)
if resp.StatusCode != http.StatusForbidden {
t.Errorf("Unexpected non-403 return code %d for request authed with old password", resp.StatusCode)
}
resp2 := httpGetBasicAuth(path, "user", newPassword)
if resp2.StatusCode != http.StatusOK {
t.Errorf("Unexpected non-200 return code %d for request authed with new password", resp2.StatusCode)
}
}
})
t.Run("when done via /rest/config/gui", func(t *testing.T) {
t.Parallel()
w := initConfig(initialPassword, t)
{
baseURL := startHTTPWithShutdownTimeout(t, w, shutdownTimeout)
cfgPath := baseURL + "/rest/config/gui"
path := baseURL + "/meta.js"
resp := httpGetBasicAuth(path, "user", initialPassword)
if resp.StatusCode != http.StatusOK {
t.Fatalf("Unexpected non-200 return code %d for auth with initial password", resp.StatusCode)
}
cfg := w.RawCopy()
cfg.GUI.Password = newPassword
httpRequest(http.MethodPut, cfgPath, cfg.GUI, "", "", testAPIKey, "", "", "", nil, t)
}
{
baseURL := startHTTP(t, w)
path := baseURL + "/meta.js"
resp := httpGetBasicAuth(path, "user", initialPassword)
if resp.StatusCode != http.StatusForbidden {
t.Errorf("Unexpected non-403 return code %d for request authed with old password", resp.StatusCode)
}
resp2 := httpGetBasicAuth(path, "user", newPassword)
if resp2.StatusCode != http.StatusOK {
t.Errorf("Unexpected non-200 return code %d for request authed with new password", resp2.StatusCode)
}
}
})
})
}
func TestHtmlFormLogin(t *testing.T) {
@@ -750,21 +867,19 @@ func TestHtmlFormLogin(t *testing.T) {
Password: "$2a$10$IdIZTxTg/dCNuNEGlmLynOjqg4B1FvDKuIV5e0BB3pnWVHNb8.GSq", // bcrypt of "räksmörgås" in UTF-8
SendBasicAuthPrompt: false,
})
baseURL, cancel, err := startHTTP(cfg)
if err != nil {
t.Fatal(err)
}
t.Cleanup(cancel)
baseURL := startHTTP(t, cfg)
loginUrl := baseURL + "/rest/noauth/auth/password"
resourceUrl := baseURL + "/meta.js"
resourceUrl404 := baseURL + "/any-path/that/does/nooooooot/match-any/noauth-pattern"
performLogin := func(username string, password string) *http.Response {
t.Helper()
return httpPost(loginUrl, map[string]string{"username": username, "password": password}, nil, t)
}
performResourceRequest := func(url string, cookies []*http.Cookie) *http.Response {
t.Helper()
return httpGet(url, "", "", "", "", cookies, t)
}
@@ -893,11 +1008,7 @@ func TestApiCache(t *testing.T) {
RawAddress: "127.0.0.1:0",
APIKey: testAPIKey,
})
baseURL, cancel, err := startHTTP(cfg)
if err != nil {
t.Fatal(err)
}
t.Cleanup(cancel)
baseURL := startHTTP(t, cfg)
httpGet := func(url string, bearer string) *http.Response {
return httpGet(url, "", "", "", bearer, nil, t)
@@ -922,7 +1033,11 @@ func TestApiCache(t *testing.T) {
})
}
func startHTTP(cfg config.Wrapper) (string, context.CancelFunc, error) {
func startHTTP(t *testing.T, cfg config.Wrapper) string {
return startHTTPWithShutdownTimeout(t, cfg, 0)
}
func startHTTPWithShutdownTimeout(t *testing.T, cfg config.Wrapper, shutdownTimeout time.Duration) string {
m := new(modelmocks.Model)
assetDir := "../../gui"
eventSub := new(eventmocks.BufferedSubscription)
@@ -945,25 +1060,35 @@ func startHTTP(cfg config.Wrapper) (string, context.CancelFunc, error) {
// Instantiate the API service
urService := ur.New(cfg, m, connections, false)
mdb, _ := db.NewLowlevel(backend.OpenMemory(), events.NoopLogger)
kdb := db.NewMiscDataNamespace(mdb)
mdb, err := sqlite.OpenTemp()
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
mdb.Close()
})
kdb := db.NewMiscDB(mdb)
svc := New(protocol.LocalDeviceID, cfg, assetDir, "syncthing", m, eventSub, diskEventSub, events.NoopLogger, discoverer, connections, urService, mockedSummary, errorLog, systemLog, false, kdb).(*service)
svc.started = addrChan
if shutdownTimeout > 0 {
svc.shutdownTimeout = shutdownTimeout
}
// Actually start the API service
supervisor := suture.New("API test", suture.Spec{
PassThroughPanics: true,
})
supervisor.Add(svc)
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
supervisor.ServeBackground(ctx)
// Make sure the API service is listening, and get the URL to use.
addr := <-addrChan
tcpAddr, err := net.ResolveTCPAddr("tcp", addr)
if err != nil {
cancel()
return "", cancel, fmt.Errorf("weird address from API service: %w", err)
t.Fatal(fmt.Errorf("weird address from API service: %w", err))
}
host, _, _ := net.SplitHostPort(cfg.GUI().RawAddress)
@@ -972,17 +1097,13 @@ func startHTTP(cfg config.Wrapper) (string, context.CancelFunc, error) {
}
baseURL := fmt.Sprintf("http://%s", net.JoinHostPort(host, strconv.Itoa(tcpAddr.Port)))
return baseURL, cancel, nil
return baseURL
}
func TestCSRFRequired(t *testing.T) {
t.Parallel()
baseURL, cancel, err := startHTTP(apiCfg)
if err != nil {
t.Fatal("Unexpected error from getting base URL:", err)
}
t.Cleanup(cancel)
baseURL := startHTTP(t, apiCfg)
cli := &http.Client{
Timeout: time.Minute,
@@ -1100,11 +1221,7 @@ func TestCSRFRequired(t *testing.T) {
func TestRandomString(t *testing.T) {
t.Parallel()
baseURL, cancel, err := startHTTP(apiCfg)
if err != nil {
t.Fatal(err)
}
defer cancel()
baseURL := startHTTP(t, apiCfg)
cli := &http.Client{
Timeout: time.Second,
}
@@ -1159,7 +1276,7 @@ func TestConfigPostOK(t *testing.T) {
]
}`))
resp, err := testConfigPost(cfg)
resp, err := testConfigPost(t, cfg)
if err != nil {
t.Fatal(err)
}
@@ -1180,7 +1297,7 @@ func TestConfigPostDupFolder(t *testing.T) {
]
}`))
resp, err := testConfigPost(cfg)
resp, err := testConfigPost(t, cfg)
if err != nil {
t.Fatal(err)
}
@@ -1189,12 +1306,10 @@ func TestConfigPostDupFolder(t *testing.T) {
}
}
func testConfigPost(data io.Reader) (*http.Response, error) {
baseURL, cancel, err := startHTTP(apiCfg)
if err != nil {
return nil, err
}
defer cancel()
func testConfigPost(t *testing.T, data io.Reader) (*http.Response, error) {
t.Helper()
baseURL := startHTTP(t, apiCfg)
cli := &http.Client{
Timeout: time.Second,
}
@@ -1211,11 +1326,7 @@ func TestHostCheck(t *testing.T) {
cfg := newMockedConfig()
cfg.GUIReturns(config.GUIConfiguration{RawAddress: "127.0.0.1:0"})
baseURL, cancel, err := startHTTP(cfg)
if err != nil {
t.Fatal(err)
}
defer cancel()
baseURL := startHTTP(t, cfg)
// A normal HTTP get to the localhost-bound service should succeed
@@ -1274,11 +1385,7 @@ func TestHostCheck(t *testing.T) {
RawAddress: "127.0.0.1:0",
InsecureSkipHostCheck: true,
})
baseURL, cancel, err = startHTTP(cfg)
if err != nil {
t.Fatal(err)
}
defer cancel()
baseURL = startHTTP(t, cfg)
// A request with a suspicious Host header should be allowed
@@ -1300,11 +1407,7 @@ func TestHostCheck(t *testing.T) {
cfg.GUIReturns(config.GUIConfiguration{
RawAddress: "0.0.0.0:0",
})
baseURL, cancel, err = startHTTP(cfg)
if err != nil {
t.Fatal(err)
}
defer cancel()
baseURL = startHTTP(t, cfg)
// A request with a suspicious Host header should be allowed
@@ -1331,11 +1434,7 @@ func TestHostCheck(t *testing.T) {
cfg.GUIReturns(config.GUIConfiguration{
RawAddress: "[::1]:0",
})
baseURL, cancel, err = startHTTP(cfg)
if err != nil {
t.Fatal(err)
}
defer cancel()
baseURL = startHTTP(t, cfg)
// A normal HTTP get to the localhost-bound service should succeed
@@ -1423,11 +1522,7 @@ func TestAddressIsLocalhost(t *testing.T) {
func TestAccessControlAllowOriginHeader(t *testing.T) {
t.Parallel()
baseURL, cancel, err := startHTTP(apiCfg)
if err != nil {
t.Fatal(err)
}
defer cancel()
baseURL := startHTTP(t, apiCfg)
cli := &http.Client{
Timeout: time.Second,
}
@@ -1451,11 +1546,7 @@ func TestAccessControlAllowOriginHeader(t *testing.T) {
func TestOptionsRequest(t *testing.T) {
t.Parallel()
baseURL, cancel, err := startHTTP(apiCfg)
if err != nil {
t.Fatal(err)
}
defer cancel()
baseURL := startHTTP(t, apiCfg)
cli := &http.Client{
Timeout: time.Second,
}
@@ -1487,8 +1578,14 @@ func TestEventMasks(t *testing.T) {
cfg := newMockedConfig()
defSub := new(eventmocks.BufferedSubscription)
diskSub := new(eventmocks.BufferedSubscription)
mdb, _ := db.NewLowlevel(backend.OpenMemory(), events.NoopLogger)
kdb := db.NewMiscDataNamespace(mdb)
mdb, err := sqlite.OpenTemp()
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
mdb.Close()
})
kdb := db.NewMiscDB(mdb)
svc := New(protocol.LocalDeviceID, cfg, "", "syncthing", nil, defSub, diskSub, events.NoopLogger, nil, nil, nil, nil, nil, nil, false, kdb).(*service)
if mask := svc.getEventMask(""); mask != DefaultEventMask {
@@ -1635,11 +1732,7 @@ func TestConfigChanges(t *testing.T) {
cfgCtx, cfgCancel := context.WithCancel(context.Background())
go w.Serve(cfgCtx)
defer cfgCancel()
baseURL, cancel, err := startHTTP(w)
if err != nil {
t.Fatal("Unexpected error from getting base URL:", err)
}
defer cancel()
baseURL := startHTTP(t, w)
cli := &http.Client{
Timeout: time.Minute,

View File

@@ -318,13 +318,10 @@ func (c *configMuxBuilder) adjustConfig(w http.ResponseWriter, r *http.Request)
var errMsg string
var status int
waiter, err := c.cfg.Modify(func(cfg *config.Configuration) {
if to.GUI.Password != cfg.GUI.Password {
if err := to.GUI.SetPassword(to.GUI.Password); err != nil {
l.Warnln("hashing password:", err)
errMsg = err.Error()
status = http.StatusInternalServerError
return
}
if err := c.postAdjustGui(&cfg.GUI, &to.GUI); err != nil {
errMsg = err.Error()
status = http.StatusInternalServerError
return
}
*cfg = to
})
@@ -391,7 +388,6 @@ func (c *configMuxBuilder) adjustOptions(w http.ResponseWriter, r *http.Request,
}
func (c *configMuxBuilder) adjustGUI(w http.ResponseWriter, r *http.Request, gui config.GUIConfiguration) {
oldPassword := gui.Password
err := unmarshalTo(r.Body, &gui)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
@@ -400,13 +396,10 @@ func (c *configMuxBuilder) adjustGUI(w http.ResponseWriter, r *http.Request, gui
var errMsg string
var status int
waiter, err := c.cfg.Modify(func(cfg *config.Configuration) {
if gui.Password != oldPassword {
if err := gui.SetPassword(gui.Password); err != nil {
l.Warnln("hashing password:", err)
errMsg = err.Error()
status = http.StatusInternalServerError
return
}
if err := c.postAdjustGui(&cfg.GUI, &gui); err != nil {
errMsg = err.Error()
status = http.StatusInternalServerError
return
}
cfg.GUI = gui
})
@@ -419,6 +412,16 @@ func (c *configMuxBuilder) adjustGUI(w http.ResponseWriter, r *http.Request, gui
c.finish(w, waiter)
}
func (c *configMuxBuilder) postAdjustGui(from *config.GUIConfiguration, to *config.GUIConfiguration) error {
if to.Password != from.Password {
if err := to.SetPassword(to.Password); err != nil {
l.Warnln("hashing password:", err)
return err
}
}
return nil
}
func (c *configMuxBuilder) adjustLDAP(w http.ResponseWriter, r *http.Request, ldap config.LDAPConfiguration) {
if err := unmarshalTo(r.Body, &ldap); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)

View File

@@ -18,7 +18,6 @@
<disableSparseFiles>false</disableSparseFiles>
<disableTempIndexes>false</disableTempIndexes>
<paused>false</paused>
<weakHashThresholdPct>25</weakHashThresholdPct>
<markerName>.stfolder</markerName>
<useLargeBlocks>true</useLargeBlocks>
</folder>
@@ -39,7 +38,6 @@
<disableSparseFiles>false</disableSparseFiles>
<disableTempIndexes>false</disableTempIndexes>
<paused>false</paused>
<weakHashThresholdPct>25</weakHashThresholdPct>
<markerName>.stfolder</markerName>
<useLargeBlocks>true</useLargeBlocks>
</folder>

View File

@@ -14,9 +14,9 @@ import (
"google.golang.org/protobuf/proto"
"github.com/syncthing/syncthing/internal/db"
"github.com/syncthing/syncthing/internal/gen/apiproto"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/db"
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/rand"
"github.com/syncthing/syncthing/lib/sync"
@@ -24,7 +24,7 @@ import (
type tokenManager struct {
key string
miscDB *db.NamespacedKV
miscDB *db.Typed
lifetime time.Duration
maxItems int
@@ -35,12 +35,13 @@ type tokenManager struct {
saveTimer *time.Timer
}
func newTokenManager(key string, miscDB *db.NamespacedKV, lifetime time.Duration, maxItems int) *tokenManager {
tokens := &apiproto.TokenSet{
Tokens: make(map[string]int64),
}
func newTokenManager(key string, miscDB *db.Typed, lifetime time.Duration, maxItems int) *tokenManager {
var tokens apiproto.TokenSet
if bs, ok, _ := miscDB.Bytes(key); ok {
_ = proto.Unmarshal(bs, tokens) // best effort
_ = proto.Unmarshal(bs, &tokens) // best effort
}
if tokens.Tokens == nil {
tokens.Tokens = make(map[string]int64)
}
return &tokenManager{
key: key,
@@ -49,7 +50,7 @@ func newTokenManager(key string, miscDB *db.NamespacedKV, lifetime time.Duration
maxItems: maxItems,
timeNow: time.Now,
mut: sync.NewMutex(),
tokens: tokens,
tokens: &tokens,
}
}
@@ -151,7 +152,7 @@ type tokenCookieManager struct {
tokens *tokenManager
}
func newTokenCookieManager(shortID string, guiCfg config.GUIConfiguration, evLogger events.Logger, miscDB *db.NamespacedKV) *tokenCookieManager {
func newTokenCookieManager(shortID string, guiCfg config.GUIConfiguration, evLogger events.Logger, miscDB *db.Typed) *tokenCookieManager {
return &tokenCookieManager{
cookieName: "sessionid-" + shortID,
shortID: shortID,

View File

@@ -18,7 +18,7 @@ import (
"time"
)
const Codename = "Gold Grasshopper"
const Codename = "Hafnium Hornet"
var (
// Injected by build script
@@ -28,6 +28,9 @@ var (
Stamp = "0"
Tags = ""
// Added to by other packages
extraTags []string
// Set by init()
Date time.Time
IsRelease bool
@@ -43,6 +46,11 @@ var (
"STNORESTART",
"STNOUPGRADE",
}
replaceTags = map[string]string{
"sqlite_omit_load_extension": "",
"osusergo": "",
"netgo": "",
}
)
const versionExtraAllowedChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-. "
@@ -108,8 +116,23 @@ func TagsList() []string {
if Extra != "" {
tags = append(tags, Extra)
}
tags = append(tags, extraTags...)
// Replace any tag values we want to have more user friendly versions,
// or be removed
for i, tag := range tags {
if repl, ok := replaceTags[tag]; ok {
tags[i] = repl
}
}
sort.Strings(tags)
// Remove any empty tags, which will be at the front of the list now
for len(tags) > 0 && tags[0] == "" {
tags = tags[1:]
}
return tags
}
@@ -124,3 +147,8 @@ func filterString(s, allowedChars string) string {
}
return res.String()
}
func AddTag(tag string) {
extraTags = append(extraTags, tag)
LongVersion = LongVersionFor("syncthing")
}

View File

@@ -114,10 +114,9 @@ func TestDefaultValues(t *testing.T) {
CleanupIntervalS: 3600,
Params: map[string]string{},
},
MaxConflicts: 10,
WeakHashThresholdPct: 25,
MarkerName: ".stfolder",
MaxConcurrentWrites: 2,
MaxConflicts: 10,
MarkerName: ".stfolder",
MaxConcurrentWrites: 2,
XattrFilter: XattrFilter{
Entries: []XattrFilterEntry{},
MaxSingleEntrySize: 1024,
@@ -190,10 +189,9 @@ func TestDeviceConfig(t *testing.T) {
Versioning: VersioningConfiguration{
Params: map[string]string{},
},
WeakHashThresholdPct: 25,
MarkerName: DefaultMarkerName,
JunctionsAsDirs: true,
MaxConcurrentWrites: maxConcurrentWritesDefault,
MarkerName: DefaultMarkerName,
JunctionsAsDirs: true,
MaxConcurrentWrites: maxConcurrentWritesDefault,
XattrFilter: XattrFilter{
Entries: []XattrFilterEntry{},
},
@@ -486,7 +484,7 @@ func TestIssue1262(t *testing.T) {
t.Fatal(err)
}
actual := cfg.Folders()["test"].Filesystem(nil).URI()
actual := cfg.Folders()["test"].Filesystem().URI()
expected := `e:\`
if actual != expected {
@@ -523,7 +521,7 @@ func TestFolderPath(t *testing.T) {
Path: "~/tmp",
}
realPath := folder.Filesystem(nil).URI()
realPath := folder.Filesystem().URI()
if !filepath.IsAbs(realPath) {
t.Error(realPath, "should be absolute")
}

View File

@@ -20,7 +20,6 @@ import (
"github.com/shirou/gopsutil/v4/disk"
"github.com/syncthing/syncthing/lib/build"
"github.com/syncthing/syncthing/lib/db"
"github.com/syncthing/syncthing/lib/fs"
"github.com/syncthing/syncthing/lib/protocol"
)
@@ -70,7 +69,6 @@ type FolderConfiguration struct {
DisableSparseFiles bool `json:"disableSparseFiles" xml:"disableSparseFiles"`
DisableTempIndexes bool `json:"disableTempIndexes" xml:"disableTempIndexes"`
Paused bool `json:"paused" xml:"paused"`
WeakHashThresholdPct int `json:"weakHashThresholdPct" xml:"weakHashThresholdPct"`
MarkerName string `json:"markerName" xml:"markerName"`
CopyOwnershipFromParent bool `json:"copyOwnershipFromParent" xml:"copyOwnershipFromParent"`
RawModTimeWindowS int `json:"modTimeWindowS" xml:"modTimeWindowS"`
@@ -120,26 +118,24 @@ func (f FolderConfiguration) Copy() FolderConfiguration {
// Filesystem creates a filesystem for the path and options of this folder.
// The fset parameter may be nil, in which case no mtime handling on top of
// the filesystem is provided.
func (f FolderConfiguration) Filesystem(fset *db.FileSet) fs.Filesystem {
func (f FolderConfiguration) Filesystem(extraOpts ...fs.Option) fs.Filesystem {
// This is intentionally not a pointer method, because things like
// cfg.Folders["default"].Filesystem(nil) should be valid.
opts := make([]fs.Option, 0, 3)
var opts []fs.Option
if f.FilesystemType == FilesystemTypeBasic && f.JunctionsAsDirs {
opts = append(opts, new(fs.OptionJunctionsAsDirs))
}
if !f.CaseSensitiveFS {
opts = append(opts, new(fs.OptionDetectCaseConflicts))
}
if fset != nil {
opts = append(opts, fset.MtimeOption())
}
opts = append(opts, extraOpts...)
return fs.NewFilesystem(f.FilesystemType.ToFS(), f.Path, opts...)
}
func (f FolderConfiguration) ModTimeWindow() time.Duration {
dur := time.Duration(f.RawModTimeWindowS) * time.Second
if f.RawModTimeWindowS < 1 && build.IsAndroid {
if usage, err := disk.Usage(f.Filesystem(nil).URI()); err != nil {
if usage, err := disk.Usage(f.Filesystem().URI()); err != nil {
dur = 2 * time.Second
l.Debugf(`Detecting FS at "%v" on android: Setting mtime window to 2s: err == "%v"`, f.Path, err)
} else if strings.HasPrefix(strings.ToLower(usage.Fstype), "ext2") || strings.HasPrefix(strings.ToLower(usage.Fstype), "ext3") || strings.HasPrefix(strings.ToLower(usage.Fstype), "ext4") {
@@ -163,7 +159,7 @@ func (f *FolderConfiguration) CreateMarker() error {
return nil
}
ffs := f.Filesystem(nil)
ffs := f.Filesystem()
// Create the marker as a directory
err := ffs.Mkdir(DefaultMarkerName, 0o755)
@@ -190,7 +186,7 @@ func (f *FolderConfiguration) CreateMarker() error {
}
func (f *FolderConfiguration) RemoveMarker() error {
ffs := f.Filesystem(nil)
ffs := f.Filesystem()
_ = ffs.Remove(filepath.Join(DefaultMarkerName, f.markerFilename()))
return ffs.Remove(DefaultMarkerName)
}
@@ -210,7 +206,7 @@ func (f *FolderConfiguration) markerContents() []byte {
// CheckPath returns nil if the folder root exists and contains the marker file
func (f *FolderConfiguration) CheckPath() error {
return f.checkFilesystemPath(f.Filesystem(nil), ".")
return f.checkFilesystemPath(f.Filesystem(), ".")
}
func (f *FolderConfiguration) checkFilesystemPath(ffs fs.Filesystem, path string) error {
@@ -253,7 +249,7 @@ func (f *FolderConfiguration) CreateRoot() (err error) {
permBits = 0o700
}
filesystem := f.Filesystem(nil)
filesystem := f.Filesystem()
if _, err = filesystem.Stat("."); fs.IsNotExist(err) {
err = filesystem.MkdirAll(".", permBits)
@@ -311,10 +307,6 @@ func (f *FolderConfiguration) prepare(myID protocol.DeviceID, existingDevices ma
f.Versioning.CleanupIntervalS = 0
}
if f.WeakHashThresholdPct == 0 {
f.WeakHashThresholdPct = 25
}
if f.MarkerName == "" {
f.MarkerName = DefaultMarkerName
}
@@ -368,7 +360,7 @@ func (f *FolderConfiguration) CheckAvailableSpace(req uint64) error {
if val <= 0 {
return nil
}
fs := f.Filesystem(nil)
fs := f.Filesystem()
usage, err := fs.Usage(".")
if err != nil {
return nil //nolint: nilerr

View File

@@ -208,7 +208,7 @@ func migrateToConfigV23(cfg *Configuration) {
// marker name in later versions.
for i := range cfg.Folders {
fs := cfg.Folders[i].Filesystem(nil)
fs := cfg.Folders[i].Filesystem()
// Invalid config posted, or tests.
if fs == nil {
continue
@@ -244,18 +244,18 @@ func migrateToConfigV21(cfg *Configuration) {
switch folder.Versioning.Type {
case "simple", "trashcan":
// Clean out symlinks in the known place
cleanSymlinks(folder.Filesystem(nil), ".stversions")
cleanSymlinks(folder.Filesystem(), ".stversions")
case "staggered":
versionDir := folder.Versioning.Params["versionsPath"]
if versionDir == "" {
// default place
cleanSymlinks(folder.Filesystem(nil), ".stversions")
cleanSymlinks(folder.Filesystem(), ".stversions")
} else if filepath.IsAbs(versionDir) {
// absolute
cleanSymlinks(fs.NewFilesystem(fs.FilesystemTypeBasic, versionDir), ".")
} else {
// relative to folder
cleanSymlinks(folder.Filesystem(nil), versionDir)
cleanSymlinks(folder.Filesystem(), versionDir)
}
}
}

View File

@@ -9,6 +9,7 @@ package config
import (
"fmt"
"runtime"
"slices"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/rand"
@@ -60,7 +61,6 @@ type OptionsConfiguration struct {
StunKeepaliveStartS int `json:"stunKeepaliveStartS" xml:"stunKeepaliveStartS" default:"180"`
StunKeepaliveMinS int `json:"stunKeepaliveMinS" xml:"stunKeepaliveMinS" default:"20"`
RawStunServers []string `json:"stunServers" xml:"stunServer" default:"default"`
DatabaseTuning Tuning `json:"databaseTuning" xml:"databaseTuning" restart:"true"`
RawMaxCIRequestKiB int `json:"maxConcurrentIncomingRequestKiB" xml:"maxConcurrentIncomingRequestKiB"`
AnnounceLANAddresses bool `json:"announceLANAddresses" xml:"announceLANAddresses" default:"true"`
SendFullIndexOnUpgrade bool `json:"sendFullIndexOnUpgrade" xml:"sendFullIndexOnUpgrade"`
@@ -268,13 +268,7 @@ func (opts OptionsConfiguration) AutoUpgradeEnabled() bool {
}
func (opts OptionsConfiguration) FeatureFlag(name string) bool {
for _, flag := range opts.FeatureFlags {
if flag == name {
return true
}
}
return false
return slices.Contains(opts.FeatureFlags, name)
}
// LowestConnectionLimit is the lower of ConnectionLimitEnough or

View File

@@ -70,7 +70,6 @@
<disableSparseFiles>false</disableSparseFiles>
<disableTempIndexes>false</disableTempIndexes>
<paused>false</paused>
<weakHashThresholdPct>25</weakHashThresholdPct>
<markerName>.stfolder</markerName>
<copyOwnershipFromParent>false</copyOwnershipFromParent>
<modTimeWindowS>0</modTimeWindowS>

View File

@@ -1,46 +0,0 @@
// Copyright (C) 2019 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
package config
type Tuning int32
const (
TuningAuto Tuning = 0
TuningSmall Tuning = 1
TuningLarge Tuning = 2
)
func (t Tuning) String() string {
switch t {
case TuningAuto:
return "auto"
case TuningSmall:
return "small"
case TuningLarge:
return "large"
default:
return "unknown"
}
}
func (t Tuning) MarshalText() ([]byte, error) {
return []byte(t.String()), nil
}
func (t *Tuning) UnmarshalText(bs []byte) error {
switch string(bs) {
case "auto":
*t = TuningAuto
case "small":
*t = TuningSmall
case "large":
*t = TuningLarge
default:
*t = TuningAuto
}
return nil
}

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