Compare commits
82
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
647c2fd5bd | ||
|
|
946e2b83a1 | ||
|
|
13f38e9abf | ||
|
|
bcef5c5bc6 | ||
|
|
952da4224b | ||
|
|
b2092b188f | ||
|
|
ed6b56d48f | ||
|
|
f145da2613 | ||
|
|
2f55d28b5c | ||
|
|
733f63592f | ||
|
|
b8a255cc96 | ||
|
|
703b185982 | ||
|
|
119d5e72ef | ||
|
|
3993f62beb | ||
|
|
130850097d | ||
|
|
ec35a95318 | ||
|
|
b15ff42d52 | ||
|
|
2f81bf5d34 | ||
|
|
39b0bd20d0 | ||
|
|
977f790143 | ||
|
|
5fa3d9f418 | ||
|
|
d73c00799b | ||
|
|
9f7b4d4104 | ||
|
|
d710874903 | ||
|
|
578cc5a165 | ||
|
|
a0171e3a9b | ||
|
|
1722f0a7b2 | ||
|
|
c904140b3c | ||
|
|
c1d8045d86 | ||
|
|
d4cffd848e | ||
|
|
5277033906 | ||
|
|
ef4629ea56 | ||
|
|
35576881ff | ||
|
|
466d30bd70 | ||
|
|
2775f424f2 | ||
|
|
44cbfcad56 | ||
|
|
e4d08b336e | ||
|
|
97cb72a608 | ||
|
|
c9236b1adc | ||
|
|
5313c75eba | ||
|
|
40febaa811 | ||
|
|
92ff9ac078 | ||
|
|
f7f2e7c70b | ||
|
|
3d168d6e44 | ||
|
|
8cc4c4ffb3 | ||
|
|
f1d631d66e | ||
|
|
ee275fee65 | ||
|
|
6606e6b4d3 | ||
|
|
f6428af4c8 | ||
|
|
a5cbeeafea | ||
|
|
5dbf809a4c | ||
|
|
e16b8013a6 | ||
|
|
6df85dc95c | ||
|
|
f93306c819 | ||
|
|
9cdff04877 | ||
|
|
9bb5f9b4dd | ||
|
|
8961f642c6 | ||
|
|
d91f8849a2 | ||
|
|
6be1ff8480 | ||
|
|
3ec73403c1 | ||
|
|
8ca3cca0a0 | ||
|
|
0c489f4ae2 | ||
|
|
deb1e5b38a | ||
|
|
49a2688caa | ||
|
|
05b4f6abda | ||
|
|
9152d7fb2f | ||
|
|
4404b4dfb4 | ||
|
|
b537090d91 | ||
|
|
79423edbdf | ||
|
|
33075974cb | ||
|
|
8a3a06f7ca | ||
|
|
d0b35021c6 | ||
|
|
6322091462 | ||
|
|
5464970c5d | ||
|
|
3962a23723 | ||
|
|
feaa90408e | ||
|
|
a8ed6e4855 | ||
|
|
5b1e1c0520 | ||
|
|
c17be06192 | ||
|
|
4ba01b05a1 | ||
|
|
14c4ad3af2 | ||
|
|
08036b1d87 |
@@ -1,88 +0,0 @@
|
||||
name: Build Infrastructure Images
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- infrastructure
|
||||
- infra-*
|
||||
|
||||
env:
|
||||
GO_VERSION: "~1.26.0"
|
||||
CGO_ENABLED: "0"
|
||||
BUILD_USER: docker
|
||||
BUILD_HOST: github.syncthing.net
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
docker-syncthing:
|
||||
name: Build and push Docker images
|
||||
if: github.repository_owner == 'syncthing'
|
||||
runs-on: ubuntu-latest
|
||||
environment: docker
|
||||
strategy:
|
||||
matrix:
|
||||
pkg:
|
||||
- stcrashreceiver
|
||||
- strelaypoolsrv
|
||||
- stupgrades
|
||||
- ursrv
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
check-latest: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build binaries
|
||||
run: |
|
||||
for arch in arm64 amd64; do
|
||||
go run build.go -goos linux -goarch "$arch" build ${{ matrix.pkg }}
|
||||
mv ${{ matrix.pkg }} ${{ matrix.pkg }}-linux-"$arch"
|
||||
done
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
|
||||
|
||||
- name: Set Docker tags (all branches)
|
||||
run: |
|
||||
tags=docker.io/syncthing/${{ matrix.pkg }}:${{ github.sha }},ghcr.io/syncthing/infra/${{ matrix.pkg }}:${{ github.sha }}
|
||||
echo "TAGS=$tags" >> $GITHUB_ENV
|
||||
|
||||
- name: Set Docker tags (latest)
|
||||
if: github.ref == 'refs/heads/infrastructure'
|
||||
run: |
|
||||
tags=docker.io/syncthing/${{ matrix.pkg }}:latest,ghcr.io/syncthing/infra/${{ matrix.pkg }}:latest,${{ env.TAGS }}
|
||||
echo "TAGS=$tags" >> $GITHUB_ENV
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile.${{ matrix.pkg }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ env.TAGS }}
|
||||
labels: |
|
||||
org.opencontainers.image.revision=${{ github.sha }}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,18 +0,0 @@
|
||||
name: Mirrors
|
||||
|
||||
on: [push, delete]
|
||||
|
||||
jobs:
|
||||
codeberg:
|
||||
name: Mirror to Codeberg
|
||||
if: github.repository_owner == 'syncthing'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: yesolutions/mirror-action@662fce0eced8996f64d7fa264d76cddd84827f33 # master
|
||||
with:
|
||||
REMOTE: ssh://git@codeberg.org/${{ github.repository }}.git
|
||||
GIT_SSH_PRIVATE_KEY: ${{ secrets.CODEBERG_PUSH_KEY }}
|
||||
GIT_SSH_NO_VERIFY_HOST: "true"
|
||||
@@ -1,21 +0,0 @@
|
||||
name: Org membership recommendations
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 0 1 * *'
|
||||
|
||||
jobs:
|
||||
|
||||
run-recommendation:
|
||||
name: Check for a recommendation
|
||||
if: github.repository_owner == 'syncthing'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- uses: docker://ghcr.io/calmh/github-org-members:latest
|
||||
env:
|
||||
GITHUB_ORGANISATION: syncthing
|
||||
GITHUB_TOKEN: ${{ secrets.GOM_GITHUB_TOKEN }}
|
||||
GOM_IGNORE_USERS: ${{ secrets.GOM_IGNORE_USERS }}
|
||||
GOM_ALSO_REPOS: ${{ secrets.GOM_ALSO_REPOS }}
|
||||
@@ -1,28 +0,0 @@
|
||||
name: PR metadata
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- edited
|
||||
- synchronize
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
|
||||
#
|
||||
# Set labels on PRs, which are then used to categorise release notes
|
||||
#
|
||||
|
||||
labels:
|
||||
name: Set labels
|
||||
if: github.repository_owner == 'syncthing'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: srvaroa/labeler@9c29ad1ef33d169f9ef33c52722faf47a566bcf3 # v1
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
@@ -1,61 +0,0 @@
|
||||
name: Release Syncthing
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- release
|
||||
- release-rc*
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
create-release-tag:
|
||||
name: Create release tag
|
||||
if: github.repository_owner == 'syncthing'
|
||||
runs-on: ubuntu-latest
|
||||
environment: release
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.ref }} # https://github.com/actions/checkout/issues/882
|
||||
token: ${{ secrets.ACTIONS_GITHUB_TOKEN }}
|
||||
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: stable
|
||||
|
||||
- name: Determine version to release
|
||||
run: |
|
||||
if [[ "$GITHUB_REF_NAME" == "release" ]] ; then
|
||||
next=$(go run ./script/next-version.go)
|
||||
else
|
||||
next=$(go run ./script/next-version.go --pre)
|
||||
fi
|
||||
echo "NEXT=$next" >> $GITHUB_ENV
|
||||
echo "Next version is $next"
|
||||
|
||||
prev=$(git describe --exclude "*-*" --abbrev=0)
|
||||
echo "PREV=$prev" >> $GITHUB_ENV
|
||||
echo "Previous version is $prev"
|
||||
|
||||
- name: Determine release notes
|
||||
run: |
|
||||
go run ./script/relnotes.go --new-ver "$NEXT" --branch "$GITHUB_REF_NAME" --prev-ver "$PREV" > notes.md
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ACTIONS_GITHUB_TOKEN }}
|
||||
|
||||
- name: Create and push tag
|
||||
run: |
|
||||
git config --global user.name 'Syncthing Release Automation'
|
||||
git config --global user.email 'release@syncthing.net'
|
||||
git tag -a -F notes.md --cleanup=whitespace "$NEXT"
|
||||
git push origin "$NEXT"
|
||||
|
||||
- name: Trigger the build
|
||||
uses: benc-uk/workflow-dispatch@7a027648b88c2413826b6ddd6c76114894dc5ec4 # v1
|
||||
with:
|
||||
workflow: build-syncthing.yaml
|
||||
ref: refs/tags/${{ env.NEXT }}
|
||||
token: ${{ secrets.ACTIONS_GITHUB_TOKEN }}
|
||||
@@ -1,22 +0,0 @@
|
||||
name: Trigger nightly build & release
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
# Run nightly build at 01:00 UTC
|
||||
- cron: '00 01 * * *'
|
||||
|
||||
jobs:
|
||||
|
||||
trigger-nightly:
|
||||
name: Push to release-nightly to trigger build
|
||||
if: github.repository_owner == 'syncthing'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
token: ${{ secrets.ACTIONS_GITHUB_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- run: |
|
||||
git push origin main:release-nightly
|
||||
@@ -1,28 +0,0 @@
|
||||
name: Update translations and documentation
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '42 3 * * 1'
|
||||
|
||||
jobs:
|
||||
|
||||
update_transifex_docs:
|
||||
runs-on: ubuntu-latest
|
||||
name: Update translations and documentation
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.ACTIONS_GITHUB_TOKEN }}
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: stable
|
||||
- run: |
|
||||
set -euo pipefail
|
||||
git config --global user.name 'Syncthing Release Automation'
|
||||
git config --global user.email 'release@syncthing.net'
|
||||
bash build.sh translate
|
||||
bash build.sh prerelease
|
||||
git push
|
||||
env:
|
||||
WEBLATE_TOKEN: ${{ secrets.WEBLATE_TOKEN }}
|
||||
@@ -50,6 +50,7 @@ Alexandre Alves <alexandrealvesdb.contact@gmail.com>
|
||||
Aman Gupta <aman@tmm1.net>
|
||||
Andreas Sommer <andreas.sommer87@googlemail.com>
|
||||
andresvia <andres.via@gmail.com>
|
||||
Andrew Gunnerson <accounts+github@chiller3.com>
|
||||
Andrew Rabert (nvllsvm) <ar@nullsum.net> <6550543+nvllsvm@users.noreply.github.com>
|
||||
Andrey D (scienmind) <scintertech@cryptolab.net> <scienmind@users.noreply.github.com>
|
||||
andyleap <andyleap@gmail.com>
|
||||
@@ -116,6 +117,7 @@ Dmitry Saveliev (dsaveliev) <d.e.saveliev@gmail.com>
|
||||
domain <32405309+szu17dmy@users.noreply.github.com>
|
||||
Domenic Horner <domenic@tgxn.net>
|
||||
Dominik Heidler (asdil12) <dominik@heidler.eu>
|
||||
Elias <1elias.bauer@gmail.com>
|
||||
Elias Jarlebring (jarlebring) <jarlebring@gmail.com>
|
||||
Elliot Huffman <thelich2@gmail.com>
|
||||
Emil Hessman (ceh) <emil@hessman.se>
|
||||
@@ -133,6 +135,7 @@ Felix Lampe <mail@flampe.de>
|
||||
Felix Unterpaintner (bigbear2nd) <bigbear2nd@gmail.com>
|
||||
Francois-Xavier Gsell (zukoo) <fxgsell@gmail.com>
|
||||
Frank Isemann (fti7) <frank@isemann.name>
|
||||
Frederic <1665799+Finomosec@users.noreply.github.com>
|
||||
Gahl Saraf <saraf.gahl@gmail.com> <gahl@raftt.io>
|
||||
georgespatton <georgespatton@users.noreply.github.com>
|
||||
ghjklw <malo@jaffre.info>
|
||||
@@ -148,6 +151,7 @@ HansK-p <42314815+HansK-p@users.noreply.github.com>
|
||||
Harrison Jones (harrisonhjones) <harrisonhjones@users.noreply.github.com>
|
||||
Hazem Krimi <me@hazemkrimi.tech>
|
||||
Heiko Zuerker (Smiley73) <heiko@zuerker.org>
|
||||
Henrik Bråthen <henrikbs@proton.me>
|
||||
Hireworks <129852174+hireworksltd@users.noreply.github.com>
|
||||
Hugo Locurcio <hugo.locurcio@hugo.pro>
|
||||
Iain Barnett <iainspeed@gmail.com>
|
||||
@@ -216,6 +220,7 @@ Matic Potočnik <hairyfotr@gmail.com>
|
||||
Matt Burke (burkemw3) <mburke@amplify.com> <burkemw3@gmail.com>
|
||||
Matt Robenolt <matt@ydekproductions.com>
|
||||
Matteo Ruina <matteo.ruina@gmail.com>
|
||||
mattn <mattn.jp@gmail.com>
|
||||
Maurizio Tomasi <ziotom78@gmail.com>
|
||||
Max <github@germancoding.com>
|
||||
Max Schulze (kralo) <max.schulze@online.de> <kralo@users.noreply.github.com>
|
||||
@@ -274,6 +279,7 @@ red_led <red-led@users.noreply.github.com>
|
||||
Robert Carosi (nov1n) <robert@carosi.nl>
|
||||
Roberto Santalla <roobre@users.noreply.github.com>
|
||||
Robin Schoonover <robin@cornhooves.org>
|
||||
Rohit Tanwar <31792358+rohitanwar@users.noreply.github.com>
|
||||
Roman Zaynetdinov (zaynetro) <romanznet@gmail.com>
|
||||
rubenbe <github-com-00ff86@vandamme.email>
|
||||
Ruslan Yevdokymov <38809160+ruslanye@users.noreply.github.com>
|
||||
@@ -286,6 +292,7 @@ Sergey Mishin (ralder) <ralder@yandex.ru>
|
||||
Sertonix <83883937+Sertonix@users.noreply.github.com>
|
||||
Severin von Wnuck-Lipinski <ss7@live.de>
|
||||
Shaarad Dalvi <60266155+shaaraddalvi@users.noreply.github.com> <shdalv@microsoft.com>
|
||||
Shablone <20610621+Shablone@users.noreply.github.com>
|
||||
Shivam Kumar <155747305+maishivamhoo123@users.noreply.github.com>
|
||||
Simon Mwepu <simonmwepu@gmail.com>
|
||||
Simon Pickup <simon@pickupinfinity.com>
|
||||
@@ -298,6 +305,7 @@ Sven Bachmann <dev@mcbachmann.de>
|
||||
Sébastien WENSKE <sebastien@wenske.fr>
|
||||
Tao <mail@steadytao.com>
|
||||
Taylor Khan (nelsonkhan) <nelsonkhan@gmail.com>
|
||||
tbodt <tbodt@tbodt.com>
|
||||
Terrance <git@terrance.allofti.me>
|
||||
TheCreeper <TheCreeper@users.noreply.github.com>
|
||||
Thomas <9749173+uhthomas@users.noreply.github.com>
|
||||
|
||||
+18
-33
@@ -1,25 +1,14 @@
|
||||
## Reporting Bugs
|
||||
## Reporting Bugs & Feature Requests
|
||||
|
||||
Please file bugs in the [GitHub Issue
|
||||
Tracker](https://github.com/syncthing/syncthing/issues). Include at
|
||||
least the following:
|
||||
For general discussion and troubleshooting, please use the [Support
|
||||
Forum](https://forum.syncthing.net/). For actionable bug reports and feature
|
||||
requests, please file issues in the [GitHub Issue
|
||||
Tracker](https://github.com/syncthing/syncthing/issues), using one of the
|
||||
provided templates.
|
||||
|
||||
- What happened
|
||||
|
||||
- What did you expect to happen instead of what *did* happen, if it's
|
||||
not crazy obvious
|
||||
|
||||
- What operating system, operating system version and version of
|
||||
Syncthing you are running
|
||||
|
||||
- The same for other connected devices, where relevant
|
||||
|
||||
- Screenshot if the issue concerns something visible in the GUI
|
||||
|
||||
- Console log entries, where possible and relevant
|
||||
|
||||
If you're not sure whether something is relevant, erring on the side of
|
||||
too much information will never get you yelled at. :)
|
||||
:warning: Do not submit AI generated issues or comments. Report any details
|
||||
you experienced as a human, and let anyone who wants to perform AI analysis
|
||||
do so on their own.
|
||||
|
||||
## Contributing Translations
|
||||
|
||||
@@ -42,20 +31,17 @@ repository](https://github.com/syncthing/docs).
|
||||
|
||||
## Contributing Code
|
||||
|
||||
Every contribution is welcome. If you want to contribute but are unsure
|
||||
where to start, any open issues are fair game! Here's a short rundown of
|
||||
what you need to keep in mind:
|
||||
We welcome contributions. However:
|
||||
|
||||
- Don't worry. You are not expected to get everything right on the first
|
||||
attempt, we'll guide you through it.
|
||||
:warning: We do not accept contributions that are wholly or mostly AI
|
||||
generated. You may assume that we are fully capable of prompting an AI
|
||||
ourselves when so inclined.
|
||||
|
||||
- Make sure there is an
|
||||
[issue](https://github.com/syncthing/syncthing/issues) that describes the
|
||||
change you want to do. If the thing you want to do does not have an issue
|
||||
yet, please file one before starting work on it.
|
||||
|
||||
- Fork the repository and make your changes in a new branch. Once it's ready
|
||||
for review, create a pull request.
|
||||
:warning: We do not accept unsolicited pull requests from new contributors.
|
||||
Most such contributions fall afoul of the previous rule. If yours does not,
|
||||
please post a message to the relevant issue with a link to your branch and a
|
||||
short description of the reasoning behind it. We'll take care of onboarding
|
||||
you.
|
||||
|
||||
### Authorship
|
||||
|
||||
@@ -205,4 +191,3 @@ otherwise stated this means MPLv2, but there are exceptions:
|
||||
|
||||
Regardless of the license in effect, you retain the copyright to your
|
||||
contribution.
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<application>
|
||||
<windowsSettings>
|
||||
<consoleAllocationPolicy xmlns="http://schemas.microsoft.com/SMI/2024/WindowsSettings">detached</consoleAllocationPolicy>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
</assembly>
|
||||
@@ -718,7 +718,7 @@ func shouldBuildSyso(dir string) (string, error) {
|
||||
}
|
||||
|
||||
jsonPath := filepath.Join(dir, "versioninfo.json")
|
||||
err = os.WriteFile(jsonPath, bs, 0o644)
|
||||
err = os.WriteFile(jsonPath, bs, 0o666)
|
||||
if err != nil {
|
||||
return "", errors.New("failed to create " + jsonPath + ": " + err.Error())
|
||||
}
|
||||
@@ -732,9 +732,18 @@ func shouldBuildSyso(dir string) (string, error) {
|
||||
sysoPath := filepath.Join(dir, "cmd", "syncthing", "resource.syso")
|
||||
|
||||
// See https://github.com/josephspurrier/goversioninfo#command-line-flags
|
||||
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 {
|
||||
// For manifest see https://learn.microsoft.com/en-us/windows/console/console-allocation-policy
|
||||
isARM := strings.HasPrefix(goarch, "arm")
|
||||
is64Bit := strings.Contains(goarch, "64")
|
||||
|
||||
args := []string{
|
||||
"-manifest=assets/windows/syncthing.exe.manifest", // console-allocation-policy
|
||||
"-o", sysoPath, // output path
|
||||
fmt.Sprintf("-arm=%v", isARM),
|
||||
fmt.Sprintf("-64=%v", is64Bit),
|
||||
}
|
||||
|
||||
if _, err := runError("goversioninfo", args...); err != nil {
|
||||
return "", errors.New("failed to create " + sysoPath + ": " + err.Error())
|
||||
}
|
||||
|
||||
@@ -774,7 +783,7 @@ func copyFile(src, dst string, perm os.FileMode) error {
|
||||
}
|
||||
|
||||
copy:
|
||||
os.MkdirAll(filepath.Dir(dst), 0o777)
|
||||
os.MkdirAll(filepath.Dir(dst), os.ModePerm)
|
||||
if err := os.WriteFile(dst, in, perm); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1423,7 +1432,7 @@ func writeCompatJSON() {
|
||||
continue
|
||||
}
|
||||
bs, _ := json.MarshalIndent(e, "", " ")
|
||||
if err := os.WriteFile("compat.json", bs, 0o644); err != nil {
|
||||
if err := os.WriteFile("compat.json", bs, 0o666); err != nil {
|
||||
log.Fatal("Writing compat.json:", err)
|
||||
}
|
||||
return
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
2026-05-21 10:03:01 INF syncthing v2.1.1-dev.9.gb3b7d228.dirty-morecrashrep "Hafnium Hornet" (go1.26.3 darwin-arm64) jb@jbo-m3wl72rv 2026-05-21 07:58:11 UTC [stnoupgrade] (log.pkg=main)
|
||||
2026-05-21 10:03:01 INF No automatic upgrades; STNOUPGRADE environment variable defined (log.pkg=main)
|
||||
2026-05-21 10:03:01 INF Calculated our device ID (device=I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU log.pkg=syncthing)
|
||||
2026-05-21 10:03:01 INF Overall rate limit in use (send="is unlimited" recv="is unlimited" log.pkg=connections)
|
||||
2026-05-21 10:03:01 INF Using discovery mechanism (identity="IPv4 local broadcast discovery on port 21027" log.pkg=discover)
|
||||
2026-05-21 10:03:01 INF Using discovery mechanism (identity="IPv6 local multicast discovery on address [ff12::8384]:21027" log.pkg=discover)
|
||||
2026-05-21 10:03:01 INF TCP listener starting (address=127.0.0.1:22001 log.pkg=connections)
|
||||
2026-05-21 10:03:01 INF Ready to synchronize (folder.id=default folder.type=sendreceive log.pkg=model)
|
||||
2026-05-21 10:03:01 INF QUIC listener starting (address=127.0.0.1:22001 log.pkg=connections)
|
||||
2026-05-21 10:03:01 INF GUI and API listening (address=127.0.0.1:8081 log.pkg=api)
|
||||
...
|
||||
2026-05-21 10:03:01 INF Access the GUI via the following URL: http://127.0.0.1:8081/ (log.pkg=api)
|
||||
2026-05-21 10:03:01 INF Loaded configuration (name=s1 log.pkg=syncthing)
|
||||
2026-05-21 10:03:01 INF Loaded peer device configuration (device=MRIW7OK name=s2 address="[tcp://127.0.0.1:22002 quic://127.0.0.1:22002]" log.pkg=syncthing)
|
||||
2026-05-21 10:03:01 INF Completed initial scan (folder.id=default folder.type=sendreceive log.pkg=model)
|
||||
0xee9de1fe260
|
||||
2026-05-21 10:03:02 INF Measured hashing performance (perf="2789.71 MB/s" log.pkg=syncthing)
|
||||
Panic at 2026-05-21T10:03:02+02:00
|
||||
runtime: marked free object in span 0x108b34d20, elemsize=8 freeindex=34 (bad use of unsafe.Pointer or having race conditions? try -d=checkptr or -race)
|
||||
0xee9de1fe000 alloc marked
|
||||
0xee9de1fe008 alloc marked
|
||||
...
|
||||
0xee9de1fe250 free unmarked
|
||||
0xee9de1fe258 free unmarked
|
||||
0xee9de1fe260 free marked zombie
|
||||
7 6 5 4 3 2 1 0 f e d c b a 9 8 0123456789abcdef
|
||||
00000ee9de1fe260: 00000000 00000000 ........
|
||||
0xee9de1fe268 free unmarked
|
||||
0xee9de1fe270 free unmarked
|
||||
...
|
||||
0xee9de1fff60 free unmarked
|
||||
0xee9de1fff68 free unmarked
|
||||
0xee9de1fff70 free unmarked
|
||||
0xee9de1fff78 free unmarked
|
||||
fatal error: found pointer to free object
|
||||
|
||||
runtime stack:
|
||||
runtime.throw({0x105881781?, 0x8?})
|
||||
runtime/panic.go:1229 +0x38 fp=0x16bf82bb0 sp=0x16bf82b80 pc=0x104f0ca48
|
||||
runtime.(*mspan).reportZombies(0x108b34d20)
|
||||
runtime/mgcsweep.go:893 +0x314 fp=0x16bf82c30 sp=0x16bf82bb0 pc=0x104ec10b4
|
||||
runtime.(*sweepLocked).sweep(0x16bf82d88?, 0x0)
|
||||
runtime/mgcsweep.go:673 +0xbd0 fp=0x16bf82d50 sp=0x16bf82c30 pc=0x104ec0840
|
||||
runtime.(*mcentral).uncacheSpan(0x16bf82db8?, 0x104ea4954?)
|
||||
runtime/mcentral.go:237 +0xbc fp=0x16bf82d80 sp=0x16bf82d50 pc=0x104eaac3c
|
||||
runtime.(*mcache).releaseAll(0x1089a85f0)
|
||||
runtime/mcache.go:322 +0x188 fp=0x16bf82df0 sp=0x16bf82d80 pc=0x104eaa4e8
|
||||
runtime.(*mcache).prepareForSweep(0x1089a85f0)
|
||||
runtime/mcache.go:366 +0x4c fp=0x16bf82e20 sp=0x16bf82df0 pc=0x104eaa61c
|
||||
runtime.gcMarkTermination.func4(0xee9de005808)
|
||||
runtime/mgc.go:1546 +0x24 fp=0x16bf82e50 sp=0x16bf82e20 pc=0x104f076e4
|
||||
runtime.forEachPInternal(0x10656f798)
|
||||
runtime/proc.go:2167 +0x178 fp=0x16bf82ee0 sp=0x16bf82e50 pc=0x104eda728
|
||||
runtime.gcMarkTermination.forEachP.func7()
|
||||
runtime/proc.go:2126 +0x40 fp=0x16bf82f10 sp=0x16bf82ee0 pc=0x104eb3130
|
||||
runtime.systemstack(0x7fc000)
|
||||
runtime/asm_arm64.s:399 +0x68 fp=0x16bf82f20 sp=0x16bf82f10 pc=0x104f12888
|
||||
|
||||
goroutine 84 gp=0xee9de45c1e0 m=3 mp=0xee9de019008 [flushing proc caches]:
|
||||
runtime.systemstack_switch()
|
||||
runtime/asm_arm64.s:347 +0x8 fp=0xee9de805c40 sp=0xee9de805c30 pc=0x104f12808
|
||||
runtime.forEachP(...)
|
||||
runtime/proc.go:2112
|
||||
runtime.gcMarkTermination({0xc0?, 0x1331f928480ca?, 0xc?, 0x0?})
|
||||
runtime/mgc.go:1545 +0x5f4 fp=0xee9de805e80 sp=0xee9de805c40 pc=0x104eb28c4
|
||||
runtime.gcMarkDone()
|
||||
runtime/mgc.go:1173 +0x364 fp=0xee9de805f20 sp=0xee9de805e80 pc=0x104eb1bc4
|
||||
runtime.gcBgMarkWorker(0xee9de341810)
|
||||
runtime/mgc.go:1912 +0x29c fp=0xee9de805fb0 sp=0xee9de805f20 pc=0x104eb372c
|
||||
runtime.gcBgMarkStartWorkers.gowrap1()
|
||||
runtime/mgc.go:1695 +0x20 fp=0xee9de805fd0 sp=0xee9de805fb0 pc=0x104eb3470
|
||||
runtime.goexit({})
|
||||
runtime/asm_arm64.s:1447 +0x4 fp=0xee9de805fd0 sp=0xee9de805fd0 pc=0x104f14a04
|
||||
created by runtime.gcBgMarkStartWorkers in goroutine 1
|
||||
runtime/mgc.go:1695 +0x134
|
||||
|
||||
@@ -42,7 +42,7 @@ type currentFile struct {
|
||||
}
|
||||
|
||||
func (d *diskStore) Serve(ctx context.Context) {
|
||||
if err := os.MkdirAll(d.dir, 0o700); err != nil {
|
||||
if err := os.MkdirAll(d.dir, os.ModePerm); err != nil {
|
||||
log.Println("Creating directory:", err)
|
||||
return
|
||||
}
|
||||
@@ -62,7 +62,7 @@ func (d *diskStore) Serve(ctx context.Context) {
|
||||
case entry := <-d.inbox:
|
||||
path := d.fullPath(entry.path)
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
||||
if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
|
||||
log.Println("Creating directory:", err)
|
||||
continue
|
||||
}
|
||||
@@ -77,7 +77,7 @@ func (d *diskStore) Serve(ctx context.Context) {
|
||||
log.Println("Failed to compress crash report:", err)
|
||||
continue
|
||||
}
|
||||
if err := os.WriteFile(path, buf.Bytes(), 0o600); err != nil {
|
||||
if err := os.WriteFile(path, buf.Bytes(), 0o666); err != nil {
|
||||
log.Printf("Failed to write %s: %v", entry.path, err)
|
||||
_ = os.Remove(path)
|
||||
continue
|
||||
@@ -136,15 +136,25 @@ func (d *diskStore) Exists(path string) bool {
|
||||
}
|
||||
|
||||
func (d *diskStore) clean() {
|
||||
for len(d.currentFiles) > 0 && (len(d.currentFiles) > d.maxFiles || d.currentSize > d.maxBytes) {
|
||||
f := d.currentFiles[0]
|
||||
numDeleted := 0
|
||||
for idx := range d.currentFiles {
|
||||
if len(d.currentFiles)-numDeleted < d.maxFiles && d.currentSize < d.maxBytes {
|
||||
break
|
||||
}
|
||||
|
||||
f := d.currentFiles[idx]
|
||||
log.Println("Removing", f.path)
|
||||
if err := os.Remove(f.path); err != nil {
|
||||
log.Println("Failed to remove file:", err)
|
||||
}
|
||||
d.currentFiles = d.currentFiles[1:]
|
||||
d.currentSize -= f.size
|
||||
numDeleted = idx + 1
|
||||
}
|
||||
|
||||
// Compact currentFiles
|
||||
copy(d.currentFiles, d.currentFiles[numDeleted:])
|
||||
d.currentFiles = d.currentFiles[:len(d.currentFiles)-numDeleted]
|
||||
|
||||
var oldest time.Duration
|
||||
if len(d.currentFiles) > 0 {
|
||||
oldest = time.Since(time.Unix(d.currentFiles[0].mtime, 0)).Truncate(time.Minute)
|
||||
@@ -158,7 +168,7 @@ func (d *diskStore) clean() {
|
||||
}
|
||||
|
||||
func (d *diskStore) inventory() error {
|
||||
d.currentFiles = nil
|
||||
d.currentFiles = d.currentFiles[:0]
|
||||
d.currentSize = 0
|
||||
err := filepath.Walk(d.dir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
@@ -29,7 +30,7 @@ import (
|
||||
raven "github.com/getsentry/raven-go"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/syncthing/syncthing/lib/build"
|
||||
"github.com/syncthing/syncthing/lib/ur"
|
||||
"github.com/syncthing/syncthing/lib/ur/contract"
|
||||
)
|
||||
|
||||
const maxRequestSize = 1 << 20 // 1 MiB
|
||||
@@ -89,6 +90,7 @@ func main() {
|
||||
if params.MetricsListen != "" {
|
||||
mmux := http.NewServeMux()
|
||||
mmux.Handle("/metrics", promhttp.Handler())
|
||||
mmux.HandleFunc("/debug/pprof/", pprof.Index)
|
||||
go func() {
|
||||
if err := http.ListenAndServe(params.MetricsListen, mmux); err != nil {
|
||||
log.Fatalln("HTTP serve metrics:", err)
|
||||
@@ -123,12 +125,13 @@ func handleFailureFn(dsn, failureDir string, ignore *ignorePatterns) func(w http
|
||||
return
|
||||
}
|
||||
|
||||
if ignore.match(bs) {
|
||||
if pat, ok := ignore.match(bs); ok {
|
||||
metricIgnoreMatchesTotal.WithLabelValues(pat).Inc()
|
||||
result = "ignored"
|
||||
return
|
||||
}
|
||||
|
||||
var reports []ur.FailureReport
|
||||
var reports []contract.FailureReport
|
||||
err = json.Unmarshal(bs, &reports)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
@@ -176,7 +179,7 @@ func handleFailureFn(dsn, failureDir string, ignore *ignorePatterns) func(w http
|
||||
}
|
||||
}
|
||||
|
||||
func saveFailureWithGoroutines(data ur.FailureData, failureDir string) (string, error) {
|
||||
func saveFailureWithGoroutines(data contract.FailureData, failureDir string) (string, error) {
|
||||
bs := make([]byte, len(data.Description)+len(data.Goroutines))
|
||||
copy(bs, data.Description)
|
||||
copy(bs[len(data.Description):], data.Goroutines)
|
||||
@@ -216,14 +219,14 @@ func loadIgnorePatterns(path string) (*ignorePatterns, error) {
|
||||
return &ignorePatterns{patterns: patterns}, nil
|
||||
}
|
||||
|
||||
func (i *ignorePatterns) match(report []byte) bool {
|
||||
func (i *ignorePatterns) match(report []byte) (string, bool) {
|
||||
if i == nil {
|
||||
return false
|
||||
return "", false
|
||||
}
|
||||
for _, re := range i.patterns {
|
||||
if re.Match(report) {
|
||||
return true
|
||||
return re.String(), true
|
||||
}
|
||||
}
|
||||
return false
|
||||
return "", false
|
||||
}
|
||||
|
||||
@@ -37,4 +37,24 @@ var (
|
||||
Subsystem: "crashreceiver",
|
||||
Name: "diskstore_oldest_age_seconds",
|
||||
})
|
||||
metricSentryReportsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: "syncthing",
|
||||
Subsystem: "crashreceiver",
|
||||
Name: "sentry_reports_total",
|
||||
}, []string{"result"})
|
||||
metricIgnoreMatchesTotal = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: "syncthing",
|
||||
Subsystem: "crashreceiver",
|
||||
Name: "ignore_matches_total",
|
||||
}, []string{"pattern"})
|
||||
metricSourceCodeLoadsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: "syncthing",
|
||||
Subsystem: "crashreceiver",
|
||||
Name: "source_code_loads_total",
|
||||
}, []string{"result"})
|
||||
metricSourceCodeCacheSize = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: "syncthing",
|
||||
Subsystem: "crashreceiver",
|
||||
Name: "source_code_cache_size",
|
||||
})
|
||||
)
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"regexp"
|
||||
@@ -52,11 +53,15 @@ func (s *sentryService) Serve(ctx context.Context) {
|
||||
pkt, err := parseCrashReport(req.reportID, req.data)
|
||||
if err != nil {
|
||||
log.Println("Failed to parse crash report:", err)
|
||||
metricSentryReportsTotal.WithLabelValues("parse_failure").Inc()
|
||||
continue
|
||||
}
|
||||
if err := sendReport(s.dsn, pkt, req.userID); err != nil {
|
||||
log.Println("Failed to send crash report:", err)
|
||||
metricSentryReportsTotal.WithLabelValues("send_failure").Inc()
|
||||
continue
|
||||
}
|
||||
metricSentryReportsTotal.WithLabelValues("success").Inc()
|
||||
|
||||
case <-ctx.Done():
|
||||
return
|
||||
@@ -69,6 +74,7 @@ func (s *sentryService) Send(reportID, userID string, data []byte) bool {
|
||||
case s.inbox <- sentryRequest{reportID, userID, data}:
|
||||
return true
|
||||
default:
|
||||
metricCrashReportsTotal.WithLabelValues("overflow").Inc()
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -108,11 +114,10 @@ func parseCrashReport(path string, report []byte) (*raven.Packet, error) {
|
||||
|
||||
version, err := build.ParseVersion(string(parts[0]))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("%w in %q", err, parts[0])
|
||||
}
|
||||
report = parts[1]
|
||||
|
||||
foundPanic := false
|
||||
var subjectLine []byte
|
||||
for {
|
||||
parts = bytes.SplitN(report, []byte("\n"), 2)
|
||||
@@ -123,14 +128,9 @@ func parseCrashReport(path string, report []byte) (*raven.Packet, error) {
|
||||
line := parts[0]
|
||||
report = parts[1]
|
||||
|
||||
if foundPanic {
|
||||
// The previous line was our "Panic at ..." header. We are now
|
||||
// at the beginning of the real panic trace and this is our
|
||||
// subject line.
|
||||
if bytes.HasPrefix(line, []byte("panic:")) || bytes.HasPrefix(line, []byte("fatal error:")) {
|
||||
subjectLine = line
|
||||
break
|
||||
} else if bytes.HasPrefix(line, []byte("Panic at")) {
|
||||
foundPanic = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,26 +9,33 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseReport(t *testing.T) {
|
||||
bs, err := os.ReadFile("_testdata/panic.log")
|
||||
files, err := filepath.Glob("_testdata/*.log")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, file := range files {
|
||||
bs, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
pkt, err := parseCrashReport("1/2/345", bs)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
pkt, err := parseCrashReport("1/2/345", bs)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
bs, err = pkt.JSON()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Printf("%s\n", bs)
|
||||
}
|
||||
|
||||
bs, err = pkt.JSON()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Printf("%s\n", bs)
|
||||
}
|
||||
|
||||
func TestCrashReportFingerprint(t *testing.T) {
|
||||
|
||||
@@ -15,23 +15,33 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
lru "github.com/hashicorp/golang-lru/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
urlPrefix = "https://raw.githubusercontent.com/syncthing/syncthing/"
|
||||
httpTimeout = 10 * time.Second
|
||||
urlPrefix = "https://raw.githubusercontent.com/syncthing/syncthing/"
|
||||
httpTimeout = 10 * time.Second
|
||||
maxCacheEntries = 1000
|
||||
)
|
||||
|
||||
type cacheKey struct {
|
||||
version string
|
||||
file string
|
||||
}
|
||||
|
||||
type githubSourceCodeLoader struct {
|
||||
mut sync.Mutex
|
||||
version string
|
||||
cache map[string]map[string][][]byte // version -> file -> lines
|
||||
client *http.Client
|
||||
|
||||
cache *lru.TwoQueueCache[cacheKey, [][]byte] // version & file -> lines
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func newGithubSourceCodeLoader() *githubSourceCodeLoader {
|
||||
cache, _ := lru.New2Q[cacheKey, [][]byte](maxCacheEntries)
|
||||
return &githubSourceCodeLoader{
|
||||
cache: make(map[string]map[string][][]byte),
|
||||
cache: cache,
|
||||
client: &http.Client{Timeout: httpTimeout},
|
||||
}
|
||||
}
|
||||
@@ -39,9 +49,6 @@ func newGithubSourceCodeLoader() *githubSourceCodeLoader {
|
||||
func (l *githubSourceCodeLoader) LockWithVersion(version string) {
|
||||
l.mut.Lock()
|
||||
l.version = version
|
||||
if _, ok := l.cache[version]; !ok {
|
||||
l.cache[version] = make(map[string][][]byte)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *githubSourceCodeLoader) Unlock() {
|
||||
@@ -50,14 +57,16 @@ func (l *githubSourceCodeLoader) Unlock() {
|
||||
|
||||
func (l *githubSourceCodeLoader) Load(filename string, line, context int) ([][]byte, int) {
|
||||
filename = filepath.ToSlash(filename)
|
||||
lines, ok := l.cache[l.version][filename]
|
||||
key := cacheKey{version: l.version, file: filename}
|
||||
lines, ok := l.cache.Get(key)
|
||||
if !ok {
|
||||
// Cache whatever we managed to find (or nil if nothing, so we don't try again)
|
||||
defer func() {
|
||||
l.cache[l.version][filename] = lines
|
||||
l.cache.Add(key, lines)
|
||||
metricSourceCodeCacheSize.Set(float64(l.cache.Len()))
|
||||
}()
|
||||
|
||||
knownPrefixes := []string{"/lib/", "/cmd/"}
|
||||
knownPrefixes := []string{"/internal/", "/lib/", "/cmd/"}
|
||||
var idx int
|
||||
for _, pref := range knownPrefixes {
|
||||
idx = strings.Index(filename, pref)
|
||||
@@ -73,19 +82,25 @@ func (l *githubSourceCodeLoader) Load(filename string, line, context int) ([][]b
|
||||
resp, err := l.client.Get(url)
|
||||
if err != nil {
|
||||
fmt.Println("Loading source:", err)
|
||||
metricSourceCodeLoadsTotal.WithLabelValues("failed").Inc()
|
||||
return nil, 0
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
fmt.Println("Loading source:", resp.Status)
|
||||
metricSourceCodeLoadsTotal.WithLabelValues("failed").Inc()
|
||||
return nil, 0
|
||||
}
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
_ = resp.Body.Close()
|
||||
if err != nil {
|
||||
fmt.Println("Loading source:", err.Error())
|
||||
metricSourceCodeLoadsTotal.WithLabelValues("failed").Inc()
|
||||
return nil, 0
|
||||
}
|
||||
lines = bytes.Split(data, []byte{'\n'})
|
||||
metricSourceCodeLoadsTotal.WithLabelValues("loaded").Inc()
|
||||
} else {
|
||||
metricSourceCodeLoadsTotal.WithLabelValues("cached").Inc()
|
||||
}
|
||||
|
||||
return getLineFromLines(lines, line, context)
|
||||
|
||||
@@ -7,21 +7,18 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type crashReceiver struct {
|
||||
store *diskStore
|
||||
sentry *sentryService
|
||||
ignore *ignorePatterns
|
||||
|
||||
ignoredMut sync.RWMutex
|
||||
ignored map[string]struct{}
|
||||
}
|
||||
|
||||
func (r *crashReceiver) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
@@ -69,12 +66,6 @@ func (r *crashReceiver) serveGet(reportID string, w http.ResponseWriter, _ *http
|
||||
// serveHead responds to HEAD requests by checking if the named report
|
||||
// already exists in the system.
|
||||
func (r *crashReceiver) serveHead(reportID string, w http.ResponseWriter, _ *http.Request) {
|
||||
r.ignoredMut.RLock()
|
||||
_, ignored := r.ignored[reportID]
|
||||
r.ignoredMut.RUnlock()
|
||||
if ignored {
|
||||
return // found
|
||||
}
|
||||
if !r.store.Exists(reportID) {
|
||||
http.Error(w, "Not found", http.StatusNotFound)
|
||||
}
|
||||
@@ -87,17 +78,7 @@ func (r *crashReceiver) servePut(reportID string, w http.ResponseWriter, req *ht
|
||||
metricCrashReportsTotal.WithLabelValues(result).Inc()
|
||||
}()
|
||||
|
||||
r.ignoredMut.RLock()
|
||||
_, ignored := r.ignored[reportID]
|
||||
r.ignoredMut.RUnlock()
|
||||
if ignored {
|
||||
result = "ignored_cached"
|
||||
io.Copy(io.Discard, req.Body)
|
||||
return // found
|
||||
}
|
||||
|
||||
// Read at most maxRequestSize of report data.
|
||||
log.Println("Receiving report", reportID)
|
||||
lr := io.LimitReader(req.Body, maxRequestSize)
|
||||
bs, err := io.ReadAll(lr)
|
||||
if err != nil {
|
||||
@@ -106,14 +87,12 @@ func (r *crashReceiver) servePut(reportID string, w http.ResponseWriter, req *ht
|
||||
return
|
||||
}
|
||||
|
||||
if r.ignore.match(bs) {
|
||||
r.ignoredMut.Lock()
|
||||
if r.ignored == nil {
|
||||
r.ignored = make(map[string]struct{})
|
||||
}
|
||||
r.ignored[reportID] = struct{}{}
|
||||
r.ignoredMut.Unlock()
|
||||
first := string(bytes.TrimSpace(bytes.Split(bs, []byte("\n"))[0]))
|
||||
|
||||
if pat, ok := r.ignore.match(bs); ok {
|
||||
metricIgnoreMatchesTotal.WithLabelValues(pat).Inc()
|
||||
result = "ignored"
|
||||
log.Printf("Ignored report %s, matched: %s (%s)", reportID[:8], pat, first)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -121,13 +100,15 @@ func (r *crashReceiver) servePut(reportID string, w http.ResponseWriter, req *ht
|
||||
|
||||
// Store the report
|
||||
if !r.store.Put(reportID, bs) {
|
||||
log.Println("Failed to store report (queue full):", reportID)
|
||||
log.Println("Failed to store report (queue full):", reportID[:8])
|
||||
result = "queue_failure"
|
||||
}
|
||||
|
||||
// Send the report to Sentry
|
||||
if !r.sentry.Send(reportID, userIDFor(req), bs) {
|
||||
log.Println("Failed to send report to sentry (queue full):", reportID)
|
||||
log.Println("Failed to send report to sentry (queue full):", reportID[:8])
|
||||
result = "sentry_failure"
|
||||
}
|
||||
|
||||
log.Printf("Received report %s (%s)", reportID[:8], first)
|
||||
}
|
||||
|
||||
@@ -52,5 +52,5 @@ func compressAndWrite(bs []byte, fullPath string) error {
|
||||
gw.Close()
|
||||
|
||||
// Create an output file with the compressed report
|
||||
return os.WriteFile(fullPath, buf.Bytes(), 0o644)
|
||||
return os.WriteFile(fullPath, buf.Bytes(), 0o666)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
@@ -29,6 +30,7 @@ import (
|
||||
"github.com/syncthing/syncthing/lib/assets"
|
||||
"github.com/syncthing/syncthing/lib/build"
|
||||
"github.com/syncthing/syncthing/lib/geoip"
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/rand"
|
||||
"github.com/syncthing/syncthing/lib/relay/client"
|
||||
@@ -318,11 +320,12 @@ func handleEndpointFull(rw http.ResponseWriter, r *http.Request) {
|
||||
relays := make([]*relay, len(permanentRelays)+len(knownRelays))
|
||||
n := copy(relays, permanentRelays)
|
||||
copy(relays[n:], knownRelays)
|
||||
mut.RUnlock()
|
||||
|
||||
_ = json.NewEncoder(rw).Encode(map[string][]*relay{
|
||||
bs, _ := json.Marshal(map[string][]*relay{
|
||||
"relays": relays,
|
||||
})
|
||||
mut.RUnlock()
|
||||
|
||||
_, _ = rw.Write(bs)
|
||||
}
|
||||
|
||||
// handleEndpointShort returns the relay list with only the URL.
|
||||
@@ -332,7 +335,10 @@ func handleEndpointShort(rw http.ResponseWriter, r *http.Request) {
|
||||
|
||||
mut.RLock()
|
||||
relays := make([]relayShort, 0, len(permanentRelays)+len(knownRelays))
|
||||
for _, r := range append(permanentRelays, knownRelays...) {
|
||||
for _, r := range permanentRelays {
|
||||
relays = append(relays, relayShort{URL: slimURL(r.URL)})
|
||||
}
|
||||
for _, r := range knownRelays {
|
||||
relays = append(relays, relayShort{URL: slimURL(r.URL)})
|
||||
}
|
||||
mut.RUnlock()
|
||||
@@ -544,7 +550,7 @@ found:
|
||||
|
||||
mut.Unlock()
|
||||
|
||||
if err := saveRelays(knownRelaysFile, knownRelays); err != nil {
|
||||
if err := saveKnownRelays(knownRelaysFile); err != nil {
|
||||
log.Println("Failed to write known relays: " + err.Error())
|
||||
}
|
||||
|
||||
@@ -607,12 +613,22 @@ func loadRelays(file string, geoip *geoip.Provider) []*relay {
|
||||
return relays
|
||||
}
|
||||
|
||||
func saveRelays(file string, relays []*relay) error {
|
||||
var content string
|
||||
for _, relay := range relays {
|
||||
content += relay.uri.String() + "\n"
|
||||
func saveKnownRelays(file string) error {
|
||||
var buf bytes.Buffer
|
||||
mut.RLock()
|
||||
for _, relay := range knownRelays {
|
||||
fmt.Fprintln(&buf, relay.uri.String())
|
||||
}
|
||||
return os.WriteFile(file, []byte(content), 0o777)
|
||||
mut.RUnlock()
|
||||
|
||||
fd, err := osutil.CreateAtomic(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := fd.Write(buf.Bytes()); err != nil {
|
||||
return err
|
||||
}
|
||||
return fd.Close()
|
||||
}
|
||||
|
||||
func createTestCertificate() tls.Certificate {
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
@@ -77,7 +77,7 @@ func newInMemoryStore(dir string, flushInterval time.Duration, blobs blob.Store)
|
||||
slog.Error("Failed to find database in blob storage", "error", cerr)
|
||||
return s
|
||||
}
|
||||
fd, cerr := os.Create(path.Join(s.dir, "records.db"))
|
||||
fd, cerr := os.Create(filepath.Join(s.dir, "records.db"))
|
||||
if cerr != nil {
|
||||
slog.Error("Failed to create database file", "error", cerr)
|
||||
return s
|
||||
@@ -257,7 +257,7 @@ func (s *inMemoryStore) write() (err error) {
|
||||
}
|
||||
}()
|
||||
|
||||
dbf := path.Join(s.dir, "records.db")
|
||||
dbf := filepath.Join(s.dir, "records.db")
|
||||
fd, err := os.Create(dbf + ".tmp")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -340,7 +340,7 @@ func (s *inMemoryStore) write() (err error) {
|
||||
}
|
||||
|
||||
func (s *inMemoryStore) read() (int, error) {
|
||||
fd, err := os.Open(path.Join(s.dir, "records.db"))
|
||||
fd, err := os.Open(filepath.Join(s.dir, "records.db"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
+27
-8
@@ -7,6 +7,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
@@ -113,14 +115,11 @@ var (
|
||||
)
|
||||
|
||||
const (
|
||||
dbOpGet = "get"
|
||||
dbOpPut = "put"
|
||||
dbOpMerge = "merge"
|
||||
dbOpDelete = "delete"
|
||||
dbResSuccess = "success"
|
||||
dbResNotFound = "not_found"
|
||||
dbResError = "error"
|
||||
dbResUnmarshalError = "unmarsh_err"
|
||||
dbOpGet = "get"
|
||||
dbOpPut = "put"
|
||||
dbOpMerge = "merge"
|
||||
dbResSuccess = "success"
|
||||
dbResNotFound = "not_found"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -132,4 +131,24 @@ func init() {
|
||||
databaseOperations, databaseOperationSeconds,
|
||||
databaseWriteSeconds, databaseLastWritten,
|
||||
retryAfterLevel)
|
||||
|
||||
// Prewarm important counters so they're available with zero values at
|
||||
// startup
|
||||
|
||||
apiRequestsTotal.WithLabelValues(http.MethodGet, "200")
|
||||
apiRequestsTotal.WithLabelValues(http.MethodGet, "404")
|
||||
apiRequestsTotal.WithLabelValues(http.MethodPost, "204")
|
||||
apiRequestsTotal.WithLabelValues(http.MethodPost, "400")
|
||||
apiRequestsTotal.WithLabelValues(http.MethodPost, "403")
|
||||
|
||||
lookupRequestsTotal.WithLabelValues("success")
|
||||
lookupRequestsTotal.WithLabelValues("not_found_ever")
|
||||
lookupRequestsTotal.WithLabelValues("not_found_recent")
|
||||
|
||||
announceRequestsTotal.WithLabelValues("success")
|
||||
announceRequestsTotal.WithLabelValues("bad_request")
|
||||
announceRequestsTotal.WithLabelValues("no_certificate")
|
||||
|
||||
replicationSendsTotal.WithLabelValues("success")
|
||||
replicationRecvsTotal.WithLabelValues("success")
|
||||
}
|
||||
|
||||
@@ -167,7 +167,7 @@ func (c *CLI) process(srcFs fs.Filesystem, dstFs fs.Filesystem, path string) err
|
||||
|
||||
var plainFd fs.File
|
||||
if dstFs != nil {
|
||||
if err := dstFs.MkdirAll(filepath.Dir(plainFi.Name), 0o700); err != nil {
|
||||
if err := dstFs.MkdirAll(filepath.Dir(plainFi.Name), fs.ModePerm); err != nil {
|
||||
return fmt.Errorf("%s: %w", plainFi.Name, err)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
package main
|
||||
|
||||
type buildSpecificOptions struct {
|
||||
HideConsole bool `name:"no-console" help:"Hide console window" env:"STHIDECONSOLE"`
|
||||
HideConsole bool `name:"no-console" help:"Hide console window (Always enabled on Windows 11 24H2 and later)" env:"STHIDECONSOLE"`
|
||||
}
|
||||
|
||||
+12
-7
@@ -424,9 +424,6 @@ func (c *serveCmd) syncthingMain() {
|
||||
if c.DebugProfileHeap {
|
||||
startHeapProfiler()
|
||||
}
|
||||
if c.DebugPerfStats {
|
||||
startPerfStats()
|
||||
}
|
||||
|
||||
// Print our version information up front, so any crash that happens
|
||||
// early etc. will have it available.
|
||||
@@ -514,6 +511,10 @@ func (c *serveCmd) syncthingMain() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if c.DebugPerfStats {
|
||||
startPerfStats(sdb)
|
||||
}
|
||||
|
||||
migratingAPICancel() // we're done with the temporary API server
|
||||
|
||||
// Check if auto-upgrades is possible, and if yes, and it's enabled do an initial
|
||||
@@ -673,7 +674,7 @@ func auditWriter(auditFile string) io.Writer {
|
||||
} else {
|
||||
auditFlags = os.O_WRONLY | os.O_CREATE | os.O_APPEND
|
||||
}
|
||||
fd, err = os.OpenFile(auditFile, auditFlags, 0o600)
|
||||
fd, err = os.OpenFile(auditFile, auditFlags, 0o666)
|
||||
if err != nil {
|
||||
slog.Error("Failed to open audit file", slogutil.Error(err))
|
||||
os.Exit(svcutil.ExitError.AsInt())
|
||||
@@ -915,10 +916,14 @@ func (u upgradeCmd) Run() error {
|
||||
case err != nil && !os.IsNotExist(err):
|
||||
slog.Error("Failed to lock for upgrade", slogutil.Error(err))
|
||||
os.Exit(1)
|
||||
case locked:
|
||||
err = upgradeViaRest()
|
||||
default:
|
||||
case locked || os.IsNotExist(err):
|
||||
// We got the lock, or the config directory didn't exist, so we
|
||||
// can do a direct upgrade
|
||||
err = upgrade.To(release)
|
||||
default:
|
||||
// We didn't get the lock, because Syncthing was running, so
|
||||
// upgrade via REST.
|
||||
err = upgradeViaRest()
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
|
||||
@@ -233,7 +233,7 @@ func copyStderr(stderr io.Reader, dst io.Writer) {
|
||||
|
||||
dst.Write([]byte(line))
|
||||
|
||||
if panicFd == nil && (strings.HasPrefix(line, "panic:") || strings.HasPrefix(line, "fatal error:")) {
|
||||
if panicFd == nil && (strings.HasPrefix(line, "panic:") || strings.HasPrefix(line, "fatal error:") || strings.HasPrefix(line, "runtime:")) {
|
||||
panicFd, err = os.Create(locations.GetTimestamped(locations.PanicLog))
|
||||
if err != nil {
|
||||
slog.Error("Failed to create panic log", slogutil.Error(err))
|
||||
@@ -479,7 +479,7 @@ func (f *autoclosedFile) ensureOpenLocked() error {
|
||||
// We open the file for write only, and create it if it doesn't exist.
|
||||
flags := os.O_WRONLY | os.O_CREATE | os.O_APPEND
|
||||
|
||||
fd, err := os.OpenFile(f.name, flags, 0o644)
|
||||
fd, err := os.OpenFile(f.name, flags, 0o666)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -9,8 +9,27 @@
|
||||
|
||||
package main
|
||||
|
||||
import "os/exec"
|
||||
import "golang.org/x/sys/windows"
|
||||
|
||||
func openURL(url string) error {
|
||||
return exec.Command("cmd.exe", "/C", "start "+url).Run()
|
||||
urlPtr, err := windows.UTF16PtrFromString(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
verbPtr, err := windows.UTF16PtrFromString("open")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = windows.ShellExecute(
|
||||
0, // hwnd
|
||||
verbPtr, // operation
|
||||
urlPtr, // file
|
||||
nil, // parameters
|
||||
nil, // directory
|
||||
windows.SW_SHOWNORMAL,
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/internal/db"
|
||||
"github.com/syncthing/syncthing/lib/build"
|
||||
"github.com/syncthing/syncthing/lib/locations"
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
@@ -23,11 +24,11 @@ import (
|
||||
"golang.org/x/exp/constraints"
|
||||
)
|
||||
|
||||
func startPerfStats() {
|
||||
go savePerfStats(fmt.Sprintf("perfstats-%d.csv", syscall.Getpid()))
|
||||
func startPerfStats(db db.DB) {
|
||||
go savePerfStats(fmt.Sprintf("perfstats-%d.csv", syscall.Getpid()), db)
|
||||
}
|
||||
|
||||
func savePerfStats(file string) {
|
||||
func savePerfStats(file string, db db.DB) {
|
||||
fd, err := os.Create(file)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -42,7 +43,7 @@ func savePerfStats(file string) {
|
||||
syscall.Getrusage(syscall.RUSAGE_SELF, &prevRus)
|
||||
runtime.ReadMemStats(&prevMem)
|
||||
|
||||
fmt.Fprintf(fd, "TIME_S\tCPU_S\tHEAP_KIB\tRSS_KIB\tNETIN_KBPS\tNETOUT_KBPS\tDBSIZE_KIB\n")
|
||||
fmt.Fprintf(fd, "TIME_S\tCPU_S\tHEAP_KIB\tRSS_KIB\tNETIN_KBPS\tNETOUT_KBPS\tDBSIZE_KIB\tDBLOCAL\n")
|
||||
|
||||
for t := range time.NewTicker(250 * time.Millisecond).C {
|
||||
syscall.Getrusage(syscall.RUSAGE_SELF, &curRus)
|
||||
@@ -55,7 +56,15 @@ func savePerfStats(file string) {
|
||||
rss /= 1024
|
||||
}
|
||||
|
||||
fmt.Fprintf(fd, "%.03f\t%f\t%d\t%d\t%.0f\t%.0f\t%d\n",
|
||||
folders, _ := db.ListFolders()
|
||||
var dbLocal int
|
||||
for _, f := range folders {
|
||||
local, _ := db.CountLocal(f, protocol.LocalDeviceID)
|
||||
dbLocal += local.Files + local.Directories + local.Symlinks
|
||||
}
|
||||
|
||||
fmt.Fprintf(
|
||||
fd, "%.03f\t%f\t%d\t%d\t%.0f\t%.0f\t%d\t%d\n",
|
||||
t.Sub(t0).Seconds(),
|
||||
rate(cpusec(&prevRus), cpusec(&curRus), timeDiff, 1),
|
||||
(curMem.Sys-curMem.HeapReleased)/1024,
|
||||
@@ -63,6 +72,7 @@ func savePerfStats(file string) {
|
||||
rate(prevIn, in, timeDiff, 1e3),
|
||||
rate(prevOut, out, timeDiff, 1e3),
|
||||
osutil.DirSize(locations.Get(locations.Database))/1024,
|
||||
dbLocal,
|
||||
)
|
||||
|
||||
prevTime = t
|
||||
|
||||
@@ -9,5 +9,7 @@
|
||||
|
||||
package main
|
||||
|
||||
func startPerfStats() {
|
||||
import "github.com/syncthing/syncthing/internal/db"
|
||||
|
||||
func startPerfStats(_ db.DB) {
|
||||
}
|
||||
|
||||
@@ -4,15 +4,15 @@ go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/AudriusButkevicius/recli v0.0.7
|
||||
github.com/alecthomas/kong v1.15.0
|
||||
github.com/alecthomas/kong v1.16.0
|
||||
github.com/aws/aws-sdk-go v1.55.8
|
||||
github.com/calmh/incontainer v1.0.0
|
||||
github.com/calmh/xdr v1.2.0
|
||||
github.com/ccding/go-stun v0.1.5
|
||||
github.com/ccding/go-stun v0.1.6
|
||||
github.com/coreos/go-semver v0.3.1
|
||||
github.com/d4l3k/messagediff v1.2.1
|
||||
github.com/getsentry/raven-go v0.2.0
|
||||
github.com/go-ldap/ldap/v3 v3.4.13
|
||||
github.com/go-ldap/ldap/v3 v3.4.14
|
||||
github.com/gobwas/glob v0.2.3
|
||||
github.com/gofrs/flock v0.13.0
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7
|
||||
@@ -22,17 +22,17 @@ require (
|
||||
github.com/julienschmidt/httprouter v1.3.0
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||
github.com/maruel/panicparse/v2 v2.5.0
|
||||
github.com/mattn/go-sqlite3 v1.14.44
|
||||
github.com/mattn/go-sqlite3 v1.14.48
|
||||
github.com/maxmind/geoipupdate/v6 v6.1.0
|
||||
github.com/miscreant/miscreant.go v0.0.0-20200214223636-26d376326b75
|
||||
github.com/oschwald/geoip2-golang v1.13.0
|
||||
github.com/pierrec/lz4/v4 v4.1.26
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/pierrec/lz4/v4 v4.1.27
|
||||
github.com/prometheus/client_golang v1.24.0
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1
|
||||
github.com/quic-go/quic-go v0.59.0
|
||||
github.com/rabbitmq/amqp091-go v1.11.0
|
||||
github.com/quic-go/quic-go v0.60.0
|
||||
github.com/rabbitmq/amqp091-go v1.13.0
|
||||
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9
|
||||
github.com/shirou/gopsutil/v4 v4.26.4
|
||||
github.com/shirou/gopsutil/v4 v4.26.6
|
||||
github.com/syncthing/notify v0.0.0-20250528144937-c7027d4f7465
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d
|
||||
github.com/thejerf/suture/v4 v4.0.6
|
||||
@@ -40,19 +40,19 @@ require (
|
||||
github.com/vitrun/qart v0.0.0-20160531060029-bf64b92db6b0
|
||||
github.com/willabides/kongplete v0.4.0
|
||||
github.com/wlynxg/anet v0.0.5
|
||||
golang.org/x/crypto v0.51.0
|
||||
golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a
|
||||
golang.org/x/net v0.54.0
|
||||
golang.org/x/sys v0.44.0
|
||||
golang.org/x/text v0.37.0
|
||||
golang.org/x/crypto v0.54.0
|
||||
golang.org/x/exp v0.0.0-20260718201538-764159d718ef
|
||||
golang.org/x/net v0.57.0
|
||||
golang.org/x/sys v0.47.0
|
||||
golang.org/x/text v0.40.0
|
||||
golang.org/x/time v0.15.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
modernc.org/sqlite v1.50.0
|
||||
modernc.org/sqlite v1.54.0
|
||||
sigs.k8s.io/yaml v1.6.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ntlmssp v0.1.0 // indirect
|
||||
github.com/Azure/go-ntlmssp v0.1.1 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d // indirect
|
||||
@@ -62,7 +62,7 @@ require (
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/ebitengine/purego v0.10.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 // indirect
|
||||
@@ -82,8 +82,8 @@ 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.2 // indirect
|
||||
github.com/prometheus/common v0.66.1 // indirect
|
||||
github.com/prometheus/procfs v0.16.1 // indirect
|
||||
github.com/prometheus/common v0.70.0 // indirect
|
||||
github.com/prometheus/procfs v0.21.1 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/riywo/loginshell v0.0.0-20200815045211-7d26008be1ab // indirect
|
||||
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
||||
@@ -93,13 +93,13 @@ require (
|
||||
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
golang.org/x/mod v0.36.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6 // indirect
|
||||
golang.org/x/tools v0.45.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||
golang.org/x/mod v0.38.0 // indirect
|
||||
golang.org/x/sync v0.22.0 // indirect
|
||||
golang.org/x/telemetry v0.0.0-20260708182218-49f421fb7959 // indirect
|
||||
golang.org/x/tools v0.48.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.72.0 // indirect
|
||||
modernc.org/libc v1.74.1 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
)
|
||||
@@ -110,9 +110,6 @@ replace github.com/gobwas/glob v0.2.3 => github.com/calmh/glob v0.0.0-2022061508
|
||||
// https://github.com/jackpal/gateway/pull/49
|
||||
replace github.com/jackpal/gateway v1.1.1 => github.com/marbens-arch/gateway v1.1.2-0.20260308173556-c567cc04e7d4
|
||||
|
||||
// https://github.com/mattn/go-sqlite3/pull/1338
|
||||
replace github.com/mattn/go-sqlite3 v1.14.44 => github.com/calmh/go-sqlite3 v1.14.35-0.20260509063420-822b4765116d
|
||||
|
||||
tool (
|
||||
github.com/calmh/xdr/cmd/genxdr
|
||||
github.com/maxbrunsfeld/counterfeiter/v6
|
||||
|
||||
@@ -2,13 +2,13 @@ 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 h1:9zjbYlTupi+W5SJXm2cR2sV2mJAIg1sIfDcsW7hrkPM=
|
||||
github.com/AudriusButkevicius/recli v0.0.7/go.mod h1:Nhfib1j/VFnLrXL9cHgA+/n2O6P5THuWelOnbfPNd78=
|
||||
github.com/Azure/go-ntlmssp v0.1.0 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A=
|
||||
github.com/Azure/go-ntlmssp v0.1.0/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
|
||||
github.com/Azure/go-ntlmssp v0.1.1 h1:l+FM/EEMb0U9QZE7mKNEDw5Mu3mFiaa2GKOoTSsNDPw=
|
||||
github.com/Azure/go-ntlmssp v0.1.1/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
|
||||
github.com/BurntSushi/toml v1.5.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.15.0 h1:BVJstKbpO73zKpmIu+m/aLRrNmWwxXPIGTNin9VmLVI=
|
||||
github.com/alecthomas/kong v1.15.0/go.mod h1:wrlbXem1CWqUV5Vbmss5ISYhsVPkBb1Yo7YKJghju2I=
|
||||
github.com/alecthomas/kong v1.16.0 h1:g92/kUxBcdcTPOM79yE63viJgtcp5dNyrB3/O2cjYT4=
|
||||
github.com/alecthomas/kong v1.16.0/go.mod h1:wrlbXem1CWqUV5Vbmss5ISYhsVPkBb1Yo7YKJghju2I=
|
||||
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
|
||||
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
|
||||
@@ -19,14 +19,12 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/calmh/glob v0.0.0-20220615080505-1d823af5017b h1:Fjm4GuJ+TGMgqfGHN42IQArJb77CfD/mAwLbDUoJe6g=
|
||||
github.com/calmh/glob v0.0.0-20220615080505-1d823af5017b/go.mod h1:91K7jfEsgJSyfSrX+gmrRfZMtntx6JsHolWubGXDopg=
|
||||
github.com/calmh/go-sqlite3 v1.14.35-0.20260509063420-822b4765116d h1:liVtRMlDBqDyR4qKkMRIFTdUK6+aoG6Oh97DKnhrCHc=
|
||||
github.com/calmh/go-sqlite3 v1.14.35-0.20260509063420-822b4765116d/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
|
||||
github.com/calmh/incontainer v1.0.0 h1:g2cTUtZuFGmMGX8GoykPkN1Judj2uw8/3/aEtq4Z/rg=
|
||||
github.com/calmh/incontainer v1.0.0/go.mod h1:eOhqnw15c9X+4RNBe0W3HlUZFfX16O0EDsCOInTndHY=
|
||||
github.com/calmh/xdr v1.2.0 h1:GaGSNH4ZDw9kNdYqle6+RcAENiaQ8/611Ok+jQbBEeU=
|
||||
github.com/calmh/xdr v1.2.0/go.mod h1:vO5+lCx/8xz7Ekd/ZLf+xuy7c1x6FMO1pBJyjDebwyg=
|
||||
github.com/ccding/go-stun v0.1.5 h1:qEM367nnezmj7dv+SdT52prv5x6HUTG3nlrjX5aitlo=
|
||||
github.com/ccding/go-stun v0.1.5/go.mod h1:cCZjJ1J3WFSJV6Wj8Y9Di8JMTsEXh6uv2eNmLzKaUeM=
|
||||
github.com/ccding/go-stun v0.1.6 h1:gWPd1V50Uv2RAnIlmWgoUxjdWvvXp2w1W99oGDtYvzQ=
|
||||
github.com/ccding/go-stun v0.1.6/go.mod h1:cCZjJ1J3WFSJV6Wj8Y9Di8JMTsEXh6uv2eNmLzKaUeM=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d h1:S2NE3iHSwP0XV47EEXL8mWmRdEfGscSJ+7EgePNgt0s=
|
||||
@@ -57,10 +55,10 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs=
|
||||
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-ldap/ldap/v3 v3.4.13 h1:+x1nG9h+MZN7h/lUi5Q3UZ0fJ1GyDQYbPvbuH38baDQ=
|
||||
github.com/go-ldap/ldap/v3 v3.4.13/go.mod h1:LxsGZV6vbaK0sIvYfsv47rfh4ca0JXokCoKjZxsszv0=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8 h1:H9AZkK22UOmfX8J84ubyaZxKJZ3FMHVwn8swoMML7iQ=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-ldap/ldap/v3 v3.4.14 h1:D6PYdEgsaVzsXyr6w/yDC06Ria4uUhWm+Rb+er8lfAs=
|
||||
github.com/go-ldap/ldap/v3 v3.4.14/go.mod h1:S4eJUMUNjDkE0ZJtIZdybwyb03sGGLW6gxXT1Hs8VKA=
|
||||
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=
|
||||
@@ -130,8 +128,8 @@ github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4d
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/compress v1.19.0 h1:sXLILfc9jV2QYWkzFOPWStmcUVH2RHEB1JCdY2oVvCQ=
|
||||
github.com/klauspost/compress v1.19.0/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
@@ -149,6 +147,8 @@ github.com/maruel/panicparse/v2 v2.5.0/go.mod h1:DA2fDiBk63bKfBf4CVZP9gb4fuvzdPb
|
||||
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.48 h1:7XHIgl0a8HwOaiK4E47ozLkST78rR9+OtNGx27D/TFs=
|
||||
github.com/mattn/go-sqlite3 v1.14.48/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
|
||||
github.com/maxbrunsfeld/counterfeiter/v6 v6.12.0 h1:aOeI7xAOVdK+R6xbVsZuU9HmCZYmQVmZgPf9xJUd2Sg=
|
||||
github.com/maxbrunsfeld/counterfeiter/v6 v6.12.0/go.mod h1:0hZWbtfeCYUQeAQdPLUzETiBhUSns7O6LDj9vH88xKA=
|
||||
github.com/maxmind/geoipupdate/v6 v6.1.0 h1:sdtTHzzQNJlXF5+fd/EoPTucRHyMonYt/Cok8xzzfqA=
|
||||
@@ -179,8 +179,8 @@ github.com/oschwald/geoip2-golang v1.13.0 h1:Q44/Ldc703pasJeP5V9+aFSZFmBN7DKHbNs
|
||||
github.com/oschwald/geoip2-golang v1.13.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.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY=
|
||||
github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
||||
github.com/pierrec/lz4/v4 v4.1.27 h1:+PhzhWDrjRj89TH2sw43nE3+4+W8lSxIuQadEHZyjUk=
|
||||
github.com/pierrec/lz4/v4 v4.1.27/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
||||
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=
|
||||
@@ -190,20 +190,22 @@ github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXq
|
||||
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_golang v1.24.0 h1:5XStIklKuAtJSNpdD3s8XJj/Yv78IQmE1kbNk87JrAI=
|
||||
github.com/prometheus/client_golang v1.24.0/go.mod h1:QcsNdotprC2nS4BTM2ucbcqxd2CeXTEa9jW7zHO9iDE=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
|
||||
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
||||
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
||||
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||
github.com/prometheus/common v0.70.0 h1:bcpru3tWPVnxGnETLgOV5jbp/JRXgYEyv65CuBLAMMI=
|
||||
github.com/prometheus/common v0.70.0/go.mod h1:S/SFasQmgGiYH6C81LKCtYa8QACgthGg5zxL2udV7SY=
|
||||
github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI=
|
||||
github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY=
|
||||
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.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
github.com/rabbitmq/amqp091-go v1.11.0 h1:HxIctVm9Gid/Vtn706necmZ7Wj6pgGI2eqplRbEY8O8=
|
||||
github.com/rabbitmq/amqp091-go v1.11.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
|
||||
github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0=
|
||||
github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk=
|
||||
github.com/quic-go/quic-go v0.60.0 h1:xcQioE8OM66UQLeUMHltK1CCcOu3JbVB4JAQdDQSB+0=
|
||||
github.com/quic-go/quic-go v0.60.0/go.mod h1:wpKpjmPpftl30sL6pFh7REVpjbcCVy4zt2vDyK1TuJk=
|
||||
github.com/rabbitmq/amqp091-go v1.13.0 h1:L8NA1WtF76C6KA3LAoufjfLgbist/If1UQYcsOjtxXA=
|
||||
github.com/rabbitmq/amqp091-go v1.13.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
@@ -216,8 +218,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.26.4 h1:B4SXVbcwTyrocPHEmWBC4uCYr4Xcu3MK1TXqbprAOWY=
|
||||
github.com/shirou/gopsutil/v4 v4.26.4/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
|
||||
github.com/shirou/gopsutil/v4 v4.26.6 h1:Mzr/npDtQC/xpeEuQKHZt8Zo9CmPvhTj8nkR8w5TLDs=
|
||||
github.com/shirou/gopsutil/v4 v4.26.6/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
|
||||
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=
|
||||
@@ -258,20 +260,20 @@ 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.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
|
||||
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
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.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
||||
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
||||
golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a h1:+3jdDGGB8NGb1Zktc737jlt3/A5f6UlwSzmvqUuufxw=
|
||||
golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw=
|
||||
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
||||
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
||||
golang.org/x/exp v0.0.0-20260718201538-764159d718ef h1:LkZ48HFgy/TvhTI0bcWkjgFkgLyKUwcTbDjS0DUjw+A=
|
||||
golang.org/x/exp v0.0.0-20260718201538-764159d718ef/go.mod h1:EdfpwwqSu+0Li0mzskwHU6FWDV3t9Q+RZDo3QMUtL3Q=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
||||
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
||||
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
|
||||
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
|
||||
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=
|
||||
@@ -280,13 +282,13 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
|
||||
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
|
||||
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
||||
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
||||
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.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
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=
|
||||
@@ -309,25 +311,25 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
||||
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6 h1:HjU6IWBiAgRIdAJ9/y1rwCn+UELEmwV+VsTLzj/W4sE=
|
||||
golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6/go.mod h1:Eqhaxk/wZsWEH8CRxLwj6xzEJbz7k1EFGqx7nyCoabE=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/telemetry v0.0.0-20260708182218-49f421fb7959 h1:RJhm5l6Fo4rmEIcndxDllNhhf/fAx8qIm4t6A7vpm2A=
|
||||
golang.org/x/telemetry v0.0.0-20260708182218-49f421fb7959/go.mod h1:LV7u5Oco+Z/g6XI7PqN+EUUUGGkEcmB1uj2ceI0fOVg=
|
||||
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/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.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
||||
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
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.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
||||
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
|
||||
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
|
||||
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
|
||||
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=
|
||||
@@ -358,30 +360,30 @@ 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.27.3 h1:uNCgn37E5U09mTv1XgskEVUJ8ADKpmFMPxzGJ0TSo+U=
|
||||
modernc.org/cc/v4 v4.27.3/go.mod h1:3YjcbCqhoTTHPycJDRl2WZKKFj0nwcOIPBfEZK0Hdk8=
|
||||
modernc.org/ccgo/v4 v4.32.4 h1:L5OB8rpEX4ZsXEQwGozRfJyJSFHbbNVOoQ59DU9/KuU=
|
||||
modernc.org/ccgo/v4 v4.32.4/go.mod h1:lY7f+fiTDHfcv6YlRgSkxYfhs+UvOEEzj49jAn2TOx0=
|
||||
modernc.org/cc/v4 v4.29.0 h1:CXgwL8cvxmyzBQZzbSl/6xFtMCryb6u8IOqDci39cgc=
|
||||
modernc.org/cc/v4 v4.29.0/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||
modernc.org/ccgo/v4 v4.34.6 h1:sBgfIwyN0TQ9C5hwIeuqyeAKyMWnbvj2fvpF4L11uzU=
|
||||
modernc.org/ccgo/v4 v4.34.6/go.mod h1:SZ8YcN9NG7XVsQYdm6jYBvi8PQP1qi+kqB6OhjqI3Fk=
|
||||
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
|
||||
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/gc/v3 v3.1.4 h1:2g65LGVSmFQrXeITAw97x7hCRvZFcyE1uDP+7Vng7JI=
|
||||
modernc.org/gc/v3 v3.1.4/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.72.0 h1:IEu559v9a0XWjw0DPoVKtXpO2qt5NVLAnFaBbjq+n8c=
|
||||
modernc.org/libc v1.72.0/go.mod h1:tTU8DL8A+XLVkEY3x5E/tO7s2Q/q42EtnNWda/L5QhQ=
|
||||
modernc.org/libc v1.74.1 h1:bdR4VTKFMC4966QSNZ05XLGI/VwzVa2kTUX51Dm0riQ=
|
||||
modernc.org/libc v1.74.1/go.mod h1:uH4t5bOx3G3g9Xcmj10YKlTcVISlRDwv8VoQJG9n8Os=
|
||||
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.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
|
||||
modernc.org/opt v0.2.0/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.50.0 h1:eMowQSWLK0MeiQTdmz3lqoF5dqclujdlIKeJA11+7oM=
|
||||
modernc.org/sqlite v1.50.0/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew=
|
||||
modernc.org/sqlite v1.54.0 h1:JCxR4qwkJvOaqAoYcgDoO25Nc+ROg6EJ2LfBVzdrgog=
|
||||
modernc.org/sqlite v1.54.0/go.mod h1:4ntCLuNmnH8+GNqjka1wNg7KJd5/Hi5FYp8K+XQ7GZw=
|
||||
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=
|
||||
|
||||
@@ -445,6 +445,20 @@ ul.three-columns li, ul.two-columns li {
|
||||
-moz-column-count: 1;
|
||||
column-count: 1;
|
||||
}
|
||||
/* All buttons, except panel headings, get bottom margin, as they
|
||||
won't fit beside each other anymore. Reduce footer padding to
|
||||
compensate for the margin. */
|
||||
.btn:not(.panel-heading),
|
||||
.btn:not(.panel-heading) + .btn:not(.panel-heading) {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.panel-footer {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.modal-footer {
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width:479px) {
|
||||
@@ -477,20 +491,6 @@ ul.three-columns li, ul.two-columns li {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* All buttons, except panel headings, get bottom margin, as they
|
||||
won't fit beside each other anymore. Reduce footer padding to
|
||||
compensate for the margin. */
|
||||
.btn:not(.panel-heading),
|
||||
.btn:not(.panel-heading) + .btn:not(.panel-heading) {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.panel-footer {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.modal-footer {
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
table.table-auto td,
|
||||
table.table-auto th,
|
||||
table.table-condensed td,
|
||||
@@ -552,4 +552,4 @@ html[lang|="ko"] i {
|
||||
.select-on-click {
|
||||
-webkit-user-select: all;
|
||||
user-select: all;
|
||||
}
|
||||
}
|
||||
@@ -78,7 +78,7 @@
|
||||
"Copied from original": "Vom Original kopiert",
|
||||
"Copied!": "Kopiert!",
|
||||
"Copy": "Kopieren",
|
||||
"Copy failed! Try to select and copy manually.": "Kopieren fehlgeschlagen! Versuchen Sie, den Text manuell zu markieren und kopieren.",
|
||||
"Copy failed! Try to select and copy manually.": "Kopieren fehlgeschlagen! Versuchen Sie, den Text manuell auszuwählen und zu kopieren.",
|
||||
"Currently Shared With Devices": "Derzeit mit Geräten geteilt",
|
||||
"Custom Range": "Eigener Zeitraum",
|
||||
"Danger!": "Achtung!",
|
||||
@@ -125,6 +125,8 @@
|
||||
"Discovery Status": "Status der Gerätesuche",
|
||||
"Dismiss": "Ausblenden",
|
||||
"Do not add it to the ignore list, so this notification may recur.": "Nicht zur Ignorierliste hinzufügen, diese Benachrichtigung kann erneut auftauchen.",
|
||||
"Do not add it to the ignore list, so this notification will reappear if the device connects again.": "Nicht zur Ignorierliste hinzufügen, damit diese Benachrichtigung erneut angezeigt wird, wenn das Gerät wieder eine Verbindung herstellt.",
|
||||
"Do not add it to the ignore list, so this notification will reappear if the device offering this folder connects again.": "Nicht zur Ignorierliste hinzufügen, damit diese Benachrichtigung erneut angezeigt wird, falls das Gerät, das diesen Ordner bereitstellt, wieder eine Verbindung herstellt.",
|
||||
"Do not restore": "Nicht wiederherstellen",
|
||||
"Do not restore all": "Nicht alle wiederherstellen",
|
||||
"Do you want to enable watching for changes for all your folders?": "Möchten Sie die Überwachung von Änderungen für all Ihre Ordner aktivieren?",
|
||||
|
||||
@@ -125,6 +125,8 @@
|
||||
"Discovery Status": "Discovery Status",
|
||||
"Dismiss": "Dismiss",
|
||||
"Do not add it to the ignore list, so this notification may recur.": "Do not add it to the ignore list, so this notification may recur.",
|
||||
"Do not add it to the ignore list, so this notification will reappear if the device connects again.": "Do not add it to the ignore list, so this notification will reappear if the device connects again.",
|
||||
"Do not add it to the ignore list, so this notification will reappear if the device offering this folder connects again.": "Do not add it to the ignore list, so this notification will reappear if the device offering this folder connects again.",
|
||||
"Do not restore": "Do not restore",
|
||||
"Do not restore all": "Do not restore all",
|
||||
"Do you want to enable watching for changes for all your folders?": "Do you want to enable watching for changes for all your folders?",
|
||||
|
||||
@@ -125,6 +125,8 @@
|
||||
"Discovery Status": "Stato de malkovrado",
|
||||
"Dismiss": "Forsendi",
|
||||
"Do not add it to the ignore list, so this notification may recur.": "Ne aldonu ĝin al la listo de ignoraĵoj, por ke tiu ĉi sciigo povos reaperi poste.",
|
||||
"Do not add it to the ignore list, so this notification will reappear if the device connects again.": "Ne aldonu ĝin al la listo de ignoraĵoj, por ke tiu ĉi sciigo reaperos, se la aparato konektos denove.",
|
||||
"Do not add it to the ignore list, so this notification will reappear if the device offering this folder connects again.": "Ne aldonu ĝin al la listo de ignoraĵoj, por ke tiu ĉi sciigo reaperos, se la aparato, oferanta tiun ĉi dosierujon, konektos denove.",
|
||||
"Do not restore": "Ne restarigu",
|
||||
"Do not restore all": "Ne restarigu ĉion",
|
||||
"Do you want to enable watching for changes for all your folders?": "Ĉu vi volas ebligi atendadon por ŝanĝoj por ĉiuj viaj dosierujoj?",
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
"Allowed Networks": "Redes permitidas",
|
||||
"Alphabetic": "Alfabético",
|
||||
"Altered by ignoring deletes.": "Alterado, ignorando eliminaciones.",
|
||||
"Always turned on when the folder type is \"{%foldertype%}\".": "Siempre habilitado cuando el tipo de carpeta es \"{{foldertype}}\".",
|
||||
"Always turned on when the folder type is \"{%foldertype%}\".": "Siempre habilitado cuando el tipo de carpeta es «{{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.": "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 de uso anónimo",
|
||||
"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?",
|
||||
@@ -54,7 +54,7 @@
|
||||
"Body:": "Contenido:",
|
||||
"Bugs": "Errores",
|
||||
"Cancel": "Cancelar",
|
||||
"Cannot be enabled when the folder type is \"{%foldertype%}\".": "No se puede habilitar cuando el tipo de carpeta es \"{{foldertype}}\".",
|
||||
"Cannot be enabled when the folder type is \"{%foldertype%}\".": "No se puede habilitar cuando el tipo de carpeta es «{{foldertype}}».",
|
||||
"Changelog": "Registro de cambios",
|
||||
"Clean out after": "Limpiar tras",
|
||||
"Cleaning Versions": "Limpiando versiones",
|
||||
@@ -72,7 +72,7 @@
|
||||
"Connection Management": "Gestión de conexiones",
|
||||
"Connection Type": "Tipo de conexión",
|
||||
"Connections": "Conexiones",
|
||||
"Connections via relays might be rate limited by the relay": "Las conexiones a través de relés pueden estar limitadas por la velocidad del relé",
|
||||
"Connections via relays might be rate limited by the relay": "Las conexiones a través de retransmisores pueden estar limitadas por la velocidad del servidor de retransmisión",
|
||||
"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.": "Ahora está disponible en Syncthing el control de cambios continuo. Se detectarán los cambios en disco y se hará un escaneado sólo en las rutas modificadas. Los beneficios son que los cambios se propagan más rápido y que se requieren menos escaneos completos.",
|
||||
"Copied from elsewhere": "Copiado de otro sitio",
|
||||
"Copied from original": "Copiado del original",
|
||||
@@ -98,7 +98,7 @@
|
||||
"Deselect devices to stop sharing this folder with.": "Deselecciona dispositivos para dejar de compartir esta carpeta.",
|
||||
"Deselect folders to stop sharing with this device.": "Deselecciona carpetas para dejar de compartir con este dispositivo.",
|
||||
"Device": "Dispositivo",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "El dispositivo \"{{name}}\" ({{device}} en la dirección {{address}}) quiere conectarse. Añadir nuevo dispositivo?",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "El dispositivo «{{name}}» ({{device}} en la dirección {{address}}) quiere conectarse. ¿Añadir nuevo dispositivo?",
|
||||
"Device Certificate": "Certificado del dispositivo",
|
||||
"Device Group": "Grupo dispositivo",
|
||||
"Device ID": "ID del dispositivo",
|
||||
@@ -125,6 +125,8 @@
|
||||
"Discovery Status": "Estado de descubrimiento",
|
||||
"Dismiss": "Descartar",
|
||||
"Do not add it to the ignore list, so this notification may recur.": "No añadirlo a la lista de ignorados, de modo que esta notificación sea recurrente.",
|
||||
"Do not add it to the ignore list, so this notification will reappear if the device connects again.": "No lo añadas a la lista de ignorados, para que la notificación reaparezca si el dispositivo se conecta de nuevo.",
|
||||
"Do not add it to the ignore list, so this notification will reappear if the device offering this folder connects again.": "No lo añadas a la lista de ignorados, para que la notificación reaparezca si el dispositivo que ofrece la carpeta se conecta de nuevo.",
|
||||
"Do not restore": "No restaurar",
|
||||
"Do not restore all": "No restaurar todos",
|
||||
"Do you want to enable watching for changes for all your folders?": "¿Quieres activar el control de cambios en todas tus carpetas?",
|
||||
@@ -143,12 +145,12 @@
|
||||
"Enable Relaying": "Habilitar retransmisión",
|
||||
"Enabled": "Activado",
|
||||
"Enables sending extended attributes to other devices, and applying incoming extended attributes. May require running with elevated privileges.": "Permite enviar atributos extendidos a otros dispositivos y aplicar atributos extendidos entrantes. Puede ser necesaria la ejecución con privilegios 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.": "Permite enviar atributos extendidos a otros dispositivos, pero no aplica los atributos extendidos entrantes. Puede tener impacto en el rendimiento. Siempre se habilita cuando \"Sincronizar atributos extendidos\" está habilitado.",
|
||||
"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.": "Permite enviar atributos extendidos a otros dispositivos, pero no aplica los atributos extendidos entrantes. Puede tener impacto en el rendimiento. Siempre se habilita cuando «Sincronizar atributos extendidos» está habilitado.",
|
||||
"Enables sending ownership information to other devices, and applying incoming ownership information. Typically requires running with elevated privileges.": "Permite enviar información de propietario a otros dispositivos y que se aplique la información de propietario recibida. Puede ser necesaria la ejecución con privilegios elevados.",
|
||||
"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.": "Permite enviar información de propietario a otros dispositivos, pero no aplicar la información de propiedad entrante. Puede tener impacto en el rendimiento. Siempre se habilita cuando \"Sincronizar propietario\" está habilitado.",
|
||||
"Enter a non-negative number (e.g., \"2.35\") and select a unit. Percentages are as part of the total disk size.": "Introduce un número no negativo (por ejemplo, \"2.35\") y selecciona una unidad. Los porcentajes son como parte del tamaño total del disco.",
|
||||
"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.": "Permite enviar información de propietario a otros dispositivos, pero no aplicar la información de propiedad entrante. Puede tener impacto en el rendimiento. Siempre se habilita cuando «Sincronizar propietario» está habilitado.",
|
||||
"Enter a non-negative number (e.g., \"2.35\") and select a unit. Percentages are as part of the total disk size.": "Introduce un número no negativo (por ejemplo, «2.35») y selecciona una unidad. Los porcentajes se calculan sobre el tamaño total del disco.",
|
||||
"Enter a non-privileged port number (1024 - 65535).": "Introduce un puerto sin privilegios (1024 - 65535).",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Introduce direcciones separadas por comas (\"tcp://ip:port\", \"tcp://host:port\") o \"dynamic\" para realizar el descubrimiento automático de la dirección.",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Introduce direcciones separadas por comas («tcp://ip:port», «tcp://host:port») o «dynamic» para realizar el descubrimiento automático de la dirección.",
|
||||
"Enter ignore patterns, one per line.": "Introduce patrones a ignorar, uno por línea.",
|
||||
"Enter up to three octal digits.": "Introduce hasta tres dígitos octales.",
|
||||
"Error": "Error",
|
||||
@@ -177,8 +179,8 @@
|
||||
"Folder Path": "Ruta de la carpeta",
|
||||
"Folder Status": "Estado de la carpeta",
|
||||
"Folder Type": "Tipo de carpeta",
|
||||
"Folder type \"{%receiveEncrypted%}\" can only be set when adding a new folder.": "El tipo de carpeta \"{{receiveEncrypted}}\" sólo puede ser seleccionado al añadir una nueva carpeta.",
|
||||
"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.": "El tipo de carpeta \"{{receiveEncrypted}}\" no se puede cambiar después de añadir la carpeta. Es necesario eliminar la carpeta, eliminar o descifrar los datos en el disco y volver a añadir la carpeta.",
|
||||
"Folder type \"{%receiveEncrypted%}\" can only be set when adding a new folder.": "El tipo de carpeta «{{receiveEncrypted}}» solo puede ser seleccionado al añadir una nueva carpeta.",
|
||||
"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.": "El tipo de carpeta «{{receiveEncrypted}}» no se puede cambiar después de añadir la carpeta. Es necesario eliminar la carpeta, eliminar o descifrar los datos en el disco y volver a añadir la carpeta.",
|
||||
"Folders": "Carpetas",
|
||||
"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.": "Se ha producido un error al comenzar a buscar cambios para las siguientes carpetas. Se hará un reintento cada minuto, por lo que los errores podrían desaparecer pronto. Si persisten trata de arreglar el problema subyacente o pide ayuda.",
|
||||
"Forever": "Para siempre",
|
||||
@@ -197,7 +199,7 @@
|
||||
"Global Discovery Servers": "Servidores globales de descubrimiento",
|
||||
"Global State": "Estado global",
|
||||
"Help": "Ayuda",
|
||||
"Hint: only deny-rules detected while the default is deny. Consider adding \"permit any\" as last rule.": "Sugerencia: sólo se detectan reglas para denegar y el valor predeterminado es denegar. Considera añadir \"permitir cualquiera\" como última regla.",
|
||||
"Hint: only deny-rules detected while the default is deny. Consider adding \"permit any\" as last rule.": "Sugerencia: sólo se detectan reglas para denegar y el valor predeterminado es denegar. Considera añadir «permitir cualquiera» como última regla.",
|
||||
"Home page": "Página de inicio",
|
||||
"However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.": "Sin embargo tu configuración actual indica que puedes no querer habilitarlo. Hemos desactivado los informes automáticos de fallos por tí.",
|
||||
"Identification": "Identificador",
|
||||
@@ -219,7 +221,7 @@
|
||||
"Introduced By": "Presentado por",
|
||||
"Introducer": "Presentador",
|
||||
"Introduction": "Introducción",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Inversión de la condición dada (por ejemplo, \"no excluir\")",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Invertir la condición dada (por ejemplo «no excluir»)",
|
||||
"Keep Versions": "Versiones a conservar",
|
||||
"LDAP": "LDAP",
|
||||
"Largest First": "Mayor tamaño primero",
|
||||
@@ -324,8 +326,8 @@
|
||||
"Received data is already encrypted": "Los datos recibidos ya están cifrados",
|
||||
"Recent Changes": "Cambios recientes",
|
||||
"Reduced by ignore patterns": "Reducido por patrones a ignorar",
|
||||
"Relay LAN": "Relé LAN",
|
||||
"Relay WAN": "Relé WAN",
|
||||
"Relay LAN": "Retransmisión LAN",
|
||||
"Relay WAN": "Retransmisión WAN",
|
||||
"Release Notes": "Notas de la versión",
|
||||
"Release candidates contain the latest features and fixes. They are similar to the traditional bi-weekly Syncthing releases.": "Las versiones candidatas contienen las últimas funcionalidades y correcciones. Son similares a las tradicionales versiones bisemanales de Syncthing.",
|
||||
"Remote Devices": "Dispositivos Remotos",
|
||||
@@ -400,7 +402,7 @@
|
||||
"Statistics": "Estadísticas",
|
||||
"Stay logged in": "Permanecer conectado",
|
||||
"Stopped": "Detenido",
|
||||
"Stores and syncs only encrypted data. Folders on all connected devices need to be set up with the same password or be of type \"{%receiveEncrypted%}\" too.": "Sólo almacena y sincroniza datos cifrados. Las carpetas de todos los dispositivos conectados deben estar configuradas con la misma contraseña o ser del tipo \"{{receiveEncrypted}}\".",
|
||||
"Stores and syncs only encrypted data. Folders on all connected devices need to be set up with the same password or be of type \"{%receiveEncrypted%}\" too.": "Solo almacena y sincroniza datos cifrados. Las carpetas de todos los dispositivos conectados deben estar configuradas con la misma contraseña o ser del tipo «{{receiveEncrypted}}».",
|
||||
"Subject:": "Asunto:",
|
||||
"Support": "Soporte",
|
||||
"Support Bundle": "Paquete de soporte",
|
||||
@@ -409,7 +411,7 @@
|
||||
"Sync Protocol Listen Addresses": "Direcciones de escucha del protocolo de sincronización",
|
||||
"Sync Status": "Estado de la sincronización",
|
||||
"Syncing": "Sincronizando",
|
||||
"Syncthing device ID for \"{%devicename%}\"": "ID del dispositivo de sincronización para \"{{devicename}}\"",
|
||||
"Syncthing device ID for \"{%devicename%}\"": "ID del dispositivo de sincronización para «{{devicename}}»",
|
||||
"Syncthing has been shut down.": "Syncthing se ha detenido.",
|
||||
"Syncthing includes the following software or portions thereof:": "Syncthing incluye el siguiente software o partes de él:",
|
||||
"Syncthing is Free and Open Source Software licensed as MPL v2.0.": "Syncthing es software libre de código abierto con licencia MPL v2.0.",
|
||||
@@ -432,7 +434,7 @@
|
||||
"The cleanup interval cannot be blank.": "El intervalo de limpieza no puede estar vacío.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configuración ha sido guardada pero no activada. Syncthing debe reiniciarse para activar la nueva configuración.",
|
||||
"The device ID cannot be blank.": "El ID del dispositivo no puede estar vacío.",
|
||||
"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).": "El ID del dispositivo a introducir aquí se puede encontrar en el diálogo \"Acciones > Mostrar ID\" del otro dispositivo. Los espacios y barras son opcionales (ignorados).",
|
||||
"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).": "El ID del dispositivo a introducir aquí se puede encontrar en el diálogo «Acciones > Mostrar ID» del otro dispositivo. Los espacios y barras son opcionales (ignorados).",
|
||||
"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.": "El informe encriptado de uso se envía diariamente. Se usa para rastrear plataformas comunes, tamaños de carpetas y versiones de la aplicación. Si el conjunto de datos enviados en el informes se cambia, se le pedirá a usted autorización de nuevo.",
|
||||
"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.": "El ID del dispositivo introducido no parece válido. Debe ser una cadena de 52 ó 56 caracteres formada por letras y números, con espacios y guiones opcionales.",
|
||||
"The folder ID cannot be blank.": "El ID de la carpeta no puede estar vacío.",
|
||||
@@ -473,7 +475,7 @@
|
||||
"This setting controls the free space required on the home (i.e., index database) disk.": "Determina el espacio libre necesario en el disco principal (con la. base de datos de índices).",
|
||||
"Time": "Hora",
|
||||
"Time the item was last modified": "Hora de última modificación del elemento",
|
||||
"To connect with the Syncthing device named \"{%devicename%}\", add a new remote device on your end with this ID:": "Para conectarse con el dispositivo Syncthing llamado \"{{devicename}}\", añada un nuevo dispositivo remoto en su extremo con este ID:",
|
||||
"To connect with the Syncthing device named \"{%devicename%}\", add a new remote device on your end with this ID:": "Para conectarse con el dispositivo Syncthing llamado «{{devicename}}», añada un nuevo dispositivo remoto en su extremo con este ID:",
|
||||
"To permit a rule, have the checkbox checked. To deny a rule, leave it unchecked.": "Para permitir una regla, marca la casilla. Para denegar una regla, déjala sin marcar.",
|
||||
"Today": "Hoy",
|
||||
"Trash Can": "Papelera",
|
||||
@@ -516,10 +518,10 @@
|
||||
"Waiting to Scan": "Esperando para escanear",
|
||||
"Waiting to Sync": "Esperando para sincronizar",
|
||||
"Warning": "Atención",
|
||||
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Atención, esta ruta es un directorio principal de la carpeta ya existente \"{{otherFolder}}\".",
|
||||
"Warning, this path is a parent directory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Atención, esta ruta es una carpeta principal de una carpeta existente \"{{otherFolderLabel}}\" ({{otherFolder}}).",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Atención, esta ruta es un subdirectorio de una carpeta ya existente llamada \"{{otherFolder}}\".",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Atención, esta ruta es un subdirectorio de una carpeta ya existente llamada \"{{otherFolderLabel}}\" ({{otherFolder}}).",
|
||||
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Atención, esta ruta es un directorio principal de la carpeta ya existente «{{otherFolder}}».",
|
||||
"Warning, this path is a parent directory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Atención, esta ruta es una carpeta principal de una carpeta existente «{{otherFolderLabel}}» ({{otherFolder}}).",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Atención, esta ruta es un subdirectorio de una carpeta ya existente llamada «{{otherFolder}}».",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Atención, esta ruta es un subdirectorio de una carpeta ya existente llamada «{{otherFolderLabel}}» ({{otherFolder}}).",
|
||||
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "Atención: si estás utilizando un observador externo como {{syncthingInotify}}, debes asegurarte de que está desactivado.",
|
||||
"Watch for Changes": "Detectar cambios",
|
||||
"Watching for Changes": "Detectando cambios",
|
||||
@@ -537,7 +539,7 @@
|
||||
"You have no ignored folders.": "No hay carpetas ignoradas.",
|
||||
"You have unsaved changes. Do you really want to discard them?": "Hay cambios sin guardar. ¿Estás seguro de que quieres descartarlos?",
|
||||
"You must keep at least one version.": "Debes conservar al menos una versión.",
|
||||
"You should never add or change anything locally in a \"{%receiveEncrypted%}\" folder.": "Nunca debes añadir o cambiar nada localmente en una carpeta \"{{receiveEncrypted}}\".",
|
||||
"You should never add or change anything locally in a \"{%receiveEncrypted%}\" folder.": "Nunca debes añadir o cambiar nada localmente en una carpeta «{{receiveEncrypted}}».",
|
||||
"Your SMS app should open to let you choose the recipient and send it from your own number.": "Tu aplicación de SMS debería abrirse para permitirte elegir el destinatario y enviarlo desde tu propio número.",
|
||||
"Your email app should open to let you choose the recipient and send it from your own address.": "Tu aplicación de email debería abrirse para permitirte elegir el destinatario y enviarlo desde tu propia dirección.",
|
||||
"days": "días",
|
||||
@@ -561,7 +563,7 @@
|
||||
}
|
||||
},
|
||||
"unknown device": "dispositivo desconocido",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} quiere compartir la carpeta \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} quiere compartir la carpeta \"{{folderlabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} quiere compartir la carpeta «{{folder}}».",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} quiere compartir la carpeta «{{folderlabel}}» ({{folder}}).",
|
||||
"{%reintroducer%} might reintroduce this device.": "{{reintroducer}} puede presentar de nuevo este dispositivo."
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
"Enter a non-negative number (e.g., \"2.35\") and select a unit. Percentages are as part of the total disk size.": "Negatiboa ez den zenbaki bat hauta ezazu (\"2.35\" adib.) bai eta unitate bat. Disko osoaren ehuneko espazioa",
|
||||
"Enter a non-privileged port number (1024 - 65535).": "Abantailatua ez den portu zenbalki bat sar ezazu (1024 - 65535)",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Sartu komaz bereizitako helbideak (\"tcp://ip:port\", \"tcp://host:port\"), edo \"dynamic\" helbidea automatikoki bilatzeko.",
|
||||
"Enter ignore patterns, one per line.": "Ezkluzio filtroak sar, lerro bakoitzean bat bakarrik.",
|
||||
"Enter ignore patterns, one per line.": "Sartu baztertze-patroiak, bakarra lerro bakoitzeko.",
|
||||
"Enter up to three octal digits.": "Sartu 3 digitu bitarte",
|
||||
"Error": "Hutsa",
|
||||
"External File Versioning": "Fitxategi bertsioen kanpoko kudeaketa",
|
||||
@@ -229,7 +229,7 @@
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Partekatzearen izen hautuzkoa eta atsegina, zure gisa. Tresna bakotxean desberdina izaiten ahal da.",
|
||||
"Options": "Hautuzkoak",
|
||||
"Out of Sync": "Ez sinkronizatua",
|
||||
"Out of Sync Items": "Ez sinkronizatu elementuak",
|
||||
"Out of Sync Items": "Sinkronizatu gabeko elementuak",
|
||||
"Outgoing Rate Limit (KiB/s)": "Bidaltze emari gehienekoa (KiB/s)",
|
||||
"Override": "Gainidatzi",
|
||||
"Override Changes": "Aldaketak desegin",
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
"Automatically create or share folders that this device advertises at the default path.": "Automatiquement créer dans le chemin par défaut les partages auxquels cet appareil vous propose de participer, ou accepter leur partage s'ils pré-existent.",
|
||||
"Available debug logging facilities:": "Outils de débogage disponibles :",
|
||||
"Be careful!": "Faites attention !",
|
||||
"Block Indexing": "Indexation de blocs",
|
||||
"Block Indexing": "Indexation des blocs",
|
||||
"Body:": "Corps du message :",
|
||||
"Bugs": "Bogues",
|
||||
"Cancel": "Annuler",
|
||||
@@ -125,6 +125,8 @@
|
||||
"Discovery Status": "État de la découverte",
|
||||
"Dismiss": "Écarter",
|
||||
"Do not add it to the ignore list, so this notification may recur.": "Attendre l'expiration de cette demande : évite l'ajout immédiat à la liste noire persistante.",
|
||||
"Do not add it to the ignore list, so this notification will reappear if the device connects again.": "Ne pas ajouter cet appareil à la liste des bloqués pour que cette notification revienne s'il tente de se reconnecter.",
|
||||
"Do not add it to the ignore list, so this notification will reappear if the device offering this folder connects again.": "Ne pas ajouter ce partage à la liste des bloqués pour que cette notification revienne quand l'appareil qui le propose se reconnecte.",
|
||||
"Do not restore": "Ne pas restaurer",
|
||||
"Do not restore all": "Ne pas tout restaurer",
|
||||
"Do you want to enable watching for changes for all your folders?": "Voulez-vous activer la surveillance des changements sur tous vos partages ?",
|
||||
@@ -252,7 +254,7 @@
|
||||
"Log tailing paused. Scroll to the bottom to continue.": "Le défilement du journal est en pause. Faites défiler jusqu'en bas pour continuer.",
|
||||
"Login failed, see Syncthing logs for details.": "Échec de connexion, consultez les journaux de Syncthing pour les détails.",
|
||||
"Logs": "Journaux",
|
||||
"Maintain an index of all blocks in the folder, enabling reuse of blocks from other files when syncing changes. Disable to reduce database size at the cost of not being able to reuse blocks across files.": "Entretient un index de tous les blocs du répertoire pour permettre leur réutilisation depuis d'autres fichiers lors de changements dans la synchronisation. Désactivez pour diminuer la taille de la base de donnée, au prix de téléchargements potentiellement plus long.",
|
||||
"Maintain an index of all blocks in the folder, enabling reuse of blocks from other files when syncing changes. Disable to reduce database size at the cost of not being able to reuse blocks across files.": "Entretient un index de tous les blocs du répertoire pour permettre leur réutilisation depuis d'autres fichiers lors de changements dans la synchronisation. Désactivez pour diminuer la taille de la base de donnée, au prix de téléchargements potentiellement plus longs.",
|
||||
"Major Upgrade": "Mise à jour majeure",
|
||||
"Mass actions": "Actions multiples",
|
||||
"Maximum Age": "Ancienneté maximum",
|
||||
|
||||
@@ -125,6 +125,8 @@
|
||||
"Discovery Status": "Stádas Fionnachtana",
|
||||
"Dismiss": "Ruaig",
|
||||
"Do not add it to the ignore list, so this notification may recur.": "Ná cuir leis an liosta neamhairde é, mar sin d'fhéadfadh an fógra seo tarlú arís.",
|
||||
"Do not add it to the ignore list, so this notification will reappear if the device connects again.": "Ná cuir leis an liosta neamhaird é, mar sin feicfear an fógra seo arís má nascann an gléas arís.",
|
||||
"Do not add it to the ignore list, so this notification will reappear if the device offering this folder connects again.": "Ná cuir leis an liosta neamhaird é, mar sin feicfear an fógra seo arís má cheanglaíonn an gléas a thairgeann an fillteán seo arís.",
|
||||
"Do not restore": "Ná hathchóirigh",
|
||||
"Do not restore all": "Ná cuir gach rud ar ais",
|
||||
"Do you want to enable watching for changes for all your folders?": "An bhfuil fonn ort féachaint ar athruithe do d'fhillteáin go léir?",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"Add Device": "Dodaj uređaj",
|
||||
"Add Folder": "Dodaj mapu",
|
||||
"Add Remote Device": "Dodaj udaljeni uređaj",
|
||||
"Add devices from the introducer to our device list, for mutually shared folders.": "Dodaj uređaje od posrednika u našu listu uređaja, za međusobno dijeljene mape.",
|
||||
"Add filter entry": "Dodaj unos filtra",
|
||||
"Add ignore patterns": "Dodaj uzorke zanemarivanja",
|
||||
"Add new folder?": "Dodati novu mapu?",
|
||||
@@ -187,6 +188,7 @@
|
||||
"GUI Authentication Password": "Lozinka za GUI autentifikacju",
|
||||
"GUI Authentication User": "Korisnik za GUI autentifikacju",
|
||||
"GUI Authentication: Set User and Password": "GUI autentifikacija: Postavite korisnika i lozinku",
|
||||
"GUI Listen Address": "GUI adresa za slušanje",
|
||||
"GUI Override Directory": "Direktorij za nadjačavanje GUI-ja",
|
||||
"GUI Theme": "Tema GUI-ja",
|
||||
"General": "Općenito",
|
||||
@@ -214,6 +216,8 @@
|
||||
"Incorrect user name or password.": "Neispravno korisničko ime ili lozinka.",
|
||||
"Info": "Informacije",
|
||||
"Internally used paths:": "Interno korištene staze:",
|
||||
"Introduced By": "Uveden od",
|
||||
"Introducer": "Posrednik",
|
||||
"Introduction": "Uvod",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Inverzija zadanog uvjeta (tj. ne isključuj)",
|
||||
"Keep Versions": "Zadrži verzije",
|
||||
@@ -248,6 +252,7 @@
|
||||
"Log tailing paused. Scroll to the bottom to continue.": "Praćenje zapisa je pauzirano. Pomaknite se na dno kako biste nastavili.",
|
||||
"Login failed, see Syncthing logs for details.": "Prijava nije uspjela. Pogledaj detalje u odjeljku zapisima sinkronizacije.",
|
||||
"Logs": "Zapisi",
|
||||
"Maintain an index of all blocks in the folder, enabling reuse of blocks from other files when syncing changes. Disable to reduce database size at the cost of not being able to reuse blocks across files.": "Održavaj indeks svih blokova u mapi, što omogućava ponovnu upotrebu blokova iz drugih datoteka pri sinkronizaciji promjena. Onemogući da smanjiš veličinu baze podataka, uz cijenu nemogućnosti ponovne upotrebe blokova između datoteka.",
|
||||
"Major Upgrade": "Velika nadogradnja",
|
||||
"Mass actions": "Masovne radnje",
|
||||
"Maximum Age": "Maksimalna starost",
|
||||
@@ -401,6 +406,7 @@
|
||||
"Support Bundle": "Paket podrške",
|
||||
"Sync Extended Attributes": "Sinkroniziraj proširena svojstva",
|
||||
"Sync Ownership": "Sinkroniziraj vlasništvo",
|
||||
"Sync Protocol Listen Addresses": "Adrese za slušanje sinkronizacijskog protokola",
|
||||
"Sync Status": "Stanje sinkronizacije",
|
||||
"Syncing": "Sinkroniziranje",
|
||||
"Syncthing device ID for \"{%devicename%}\"": "Syncthing ID uređaja za „{{devicename}}“",
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
"Automatically create or share folders that this device advertises at the default path.": "Crea o condividi automaticamente le cartelle che questo dispositivo presenta sul percorso predefinito.",
|
||||
"Available debug logging facilities:": "Servizi di debug disponibili:",
|
||||
"Be careful!": "Fai attenzione!",
|
||||
"Block Indexing": "Indicizzazione a blocchi",
|
||||
"Body:": "Corpo:",
|
||||
"Bugs": "Bug",
|
||||
"Cancel": "Annulla",
|
||||
@@ -99,6 +100,7 @@
|
||||
"Device": "Dispositivo",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Il dispositivo \"{{name}}\" ({{device}} - {{address}}) chiede di connettersi. Aggiungere il nuovo dispositivo?",
|
||||
"Device Certificate": "Certificato del dispositivo",
|
||||
"Device Group": "Gruppo del Dispositivo",
|
||||
"Device ID": "ID Dispositivo",
|
||||
"Device Identification": "Identificazione Dispositivo",
|
||||
"Device Name": "Nome Dispositivo",
|
||||
@@ -123,6 +125,8 @@
|
||||
"Discovery Status": "Stato Individuazione",
|
||||
"Dismiss": "Scartato",
|
||||
"Do not add it to the ignore list, so this notification may recur.": "Non aggiungerlo all'elenco da ignorare, quindi questa notifica potrebbe ripresentarsi.",
|
||||
"Do not add it to the ignore list, so this notification will reappear if the device connects again.": "Non aggiungerlo all'elenco dei dispositivi da ignorare, cosicché questa notifica venga visualizzata nuovamente se si connetterà nuovamente.",
|
||||
"Do not add it to the ignore list, so this notification will reappear if the device offering this folder connects again.": "Non aggiungerlo all'elenco dei dispositivi da ignorare, cosicché questa notifica venga visualizzata nuovamente se il dispositivo che offre questa cartella si connetterà di nuovamente.",
|
||||
"Do not restore": "Non ripristinare",
|
||||
"Do not restore all": "Non ripristinare tutto",
|
||||
"Do you want to enable watching for changes for all your folders?": "Vuoi abilitare il monitoraggio delle modifiche per tutte le tue cartelle?",
|
||||
@@ -169,6 +173,7 @@
|
||||
"Filter by date": "Filtra per data",
|
||||
"Filter by name": "Filtra per nome",
|
||||
"Folder": "Cartella",
|
||||
"Folder Group": "Gruppo della Cartella",
|
||||
"Folder ID": "ID Cartella",
|
||||
"Folder Label": "Etichetta per la Cartella",
|
||||
"Folder Path": "Percorso Cartella",
|
||||
@@ -186,7 +191,7 @@
|
||||
"GUI Authentication User": "Utente dell'Interfaccia Grafica",
|
||||
"GUI Authentication: Set User and Password": "Autenticazione GUI: usa utente e password",
|
||||
"GUI Listen Address": "Indirizzo dell'Interfaccia Grafica",
|
||||
"GUI Override Directory": "GUI Sostituisci Directory",
|
||||
"GUI Override Directory": "Cartella di override GUI",
|
||||
"GUI Theme": "Tema GUI",
|
||||
"General": "Generale",
|
||||
"Generate": "Genera",
|
||||
@@ -249,6 +254,7 @@
|
||||
"Log tailing paused. Scroll to the bottom to continue.": "Visualizzazione log in pausa. Scorri fino in fondo per continuare.",
|
||||
"Login failed, see Syncthing logs for details.": "Accesso non riuscito, vedi i log di Syncthing per i dettagli.",
|
||||
"Logs": "Log",
|
||||
"Maintain an index of all blocks in the folder, enabling reuse of blocks from other files when syncing changes. Disable to reduce database size at the cost of not being able to reuse blocks across files.": "Mantiene un indice di tutti i blocchi nella cartella, consentendo il riutilizzo dei blocchi da altri file durante la sincronizzazione delle modifiche. Disabilitare questa opzione per ridurre le dimensioni del database, a costo di non poter riutilizzare i blocchi tra i file.",
|
||||
"Major Upgrade": "Aggiornamento Principale",
|
||||
"Mass actions": "Azioni di massa",
|
||||
"Maximum Age": "Durata Massima",
|
||||
@@ -279,6 +285,8 @@
|
||||
"Off": "Disattiva",
|
||||
"Oldest First": "Prima il Meno Recente",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Etichetta descrittiva facoltativa della cartella. Può essere diversa su ogni dispositivo.",
|
||||
"Optional group for the device. Can be different on each device.": "Gruppo opzionale per il dispositivo. Può essere diverso per ogni dispositivo.",
|
||||
"Optional group for the folder. Can be different on each device.": "Gruppo opzionale per la cartella. Può essere diverso su ciascun dispositivo.",
|
||||
"Options": "Opzioni",
|
||||
"Out of Sync": "Non sincronizzato",
|
||||
"Out of Sync Items": "Elementi Non Sincronizzati",
|
||||
@@ -390,6 +398,7 @@
|
||||
"Staggered": "Sfalsato",
|
||||
"Staggered File Versioning": "Controllo Versione Cadenzato",
|
||||
"Start Browser": "Avvia Browser",
|
||||
"Starting": "In avvio",
|
||||
"Statistics": "Statistiche",
|
||||
"Stay logged in": "Resta connesso",
|
||||
"Stopped": "Fermato",
|
||||
@@ -440,7 +449,7 @@
|
||||
"The following text will automatically be inserted into a new message.": "Il seguente testo verrà automaticamente inserito in un nuovo messaggio.",
|
||||
"The following unexpected items were found.": "Sono stati trovati i seguenti elementi imprevisti.",
|
||||
"The interval must be a positive number of seconds.": "L'intervallo deve essere un numero positivo di secondi.",
|
||||
"The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.": "L'intervallo, in secondi, per l'esecuzione della pulizia nella directory delle versioni. Zero per disabilitare la pulizia periodica.",
|
||||
"The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.": "L'intervallo, in secondi, per l'esecuzione della pulizia nella cartella delle versioni. Zero per disabilitare la pulizia periodica.",
|
||||
"The maximum age must be a number and cannot be blank.": "La durata massima dev'essere un numero e non può essere vuoto.",
|
||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "La durata massima di una versione (in giorni, imposta a 0 per mantenere le versioni per sempre).",
|
||||
"The number of connections must be a non-negative number.": "Il numero di connessioni deve essere un numero non negativo.",
|
||||
@@ -536,7 +545,7 @@
|
||||
"days": "giorni",
|
||||
"deleted": "cancellato",
|
||||
"deny": "negare",
|
||||
"directories": "directory",
|
||||
"directories": "cartelle",
|
||||
"file": "file",
|
||||
"files": "file",
|
||||
"folder": "cartella",
|
||||
|
||||
@@ -125,6 +125,8 @@
|
||||
"Discovery Status": "Stan odnajdywania",
|
||||
"Dismiss": "Odrzuć",
|
||||
"Do not add it to the ignore list, so this notification may recur.": "Nie dodaje do listy ignorowanych, więc powiadomienie to może się powtórzyć.",
|
||||
"Do not add it to the ignore list, so this notification will reappear if the device connects again.": "Nie dodaje do listy ignorowanych, więc powiadomienie pojawi się ponownie, gdy urządzenie połączy się ponownie.",
|
||||
"Do not add it to the ignore list, so this notification will reappear if the device offering this folder connects again.": "Nie dodaje do listy ignorowanych, więc powiadomienie pojawi się ponownie, gdy urządzenie oferujące ten folder ponownie się połączy.",
|
||||
"Do not restore": "Nie przywracaj",
|
||||
"Do not restore all": "Nie przywracaj wszystkich",
|
||||
"Do you want to enable watching for changes for all your folders?": "Czy chcesz włączyć obserwowanie zmian we wszystkich folderach?",
|
||||
|
||||
@@ -125,6 +125,8 @@
|
||||
"Discovery Status": "Status da Descoberta",
|
||||
"Dismiss": "Descartar",
|
||||
"Do not add it to the ignore list, so this notification may recur.": "Não o adicione à lista de ignorados, portanto, esta notificação pode ocorrer novamente.",
|
||||
"Do not add it to the ignore list, so this notification will reappear if the device connects again.": "Não o adicione à lista de ignorados, pois esta notificação reaparecerá se o dispositivo se conectar novamente.",
|
||||
"Do not add it to the ignore list, so this notification will reappear if the device offering this folder connects again.": "Não o adicione à lista de ignorados, pois esta notificação reaparecerá se o dispositivo que oferece esta pasta se conectar novamente.",
|
||||
"Do not restore": "Não restaurar",
|
||||
"Do not restore all": "Não restaurar nenhum",
|
||||
"Do you want to enable watching for changes for all your folders?": "Você deseja ativar a observação de alterações em todas as suas pastas?",
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
"Automatically create or share folders that this device advertises at the default path.": "Автоматически создавать, используя путь по умолчанию, или делиться папками, о которых сообщает это устройство.",
|
||||
"Available debug logging facilities:": "Доступные средства отладочного журнала:",
|
||||
"Be careful!": "Будьте осторожны!",
|
||||
"Block Indexing": "Блочная индексация",
|
||||
"Body:": "Тело:",
|
||||
"Bugs": "Ошибки",
|
||||
"Cancel": "Отмена",
|
||||
@@ -99,6 +100,7 @@
|
||||
"Device": "Устройство",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Устройство «{{name}}» ({{device}} на {{address}}) хочет подключиться. Добавить новое устройство?",
|
||||
"Device Certificate": "Сертификат устройства",
|
||||
"Device Group": "Группа устройства",
|
||||
"Device ID": "ID устройства",
|
||||
"Device Identification": "Идентификация устройства",
|
||||
"Device Name": "Имя устройства",
|
||||
@@ -132,9 +134,9 @@
|
||||
"Downloading": "Загрузка",
|
||||
"Edit": "Редактировать",
|
||||
"Edit Device": "Редактирование устройства",
|
||||
"Edit Device Defaults": "Изменить умолчания устройства",
|
||||
"Edit Device Defaults": "Изменить настройки устройств",
|
||||
"Edit Folder": "Редактирование папки",
|
||||
"Edit Folder Defaults": "Изменить умолчания папки",
|
||||
"Edit Folder Defaults": "Изменить настройки папок",
|
||||
"Editing {%path%}.": "Правка {{path}}.",
|
||||
"Enable Crash Reporting": "Включить отчёты о сбоях",
|
||||
"Enable NAT traversal": "Использовать обход NAT",
|
||||
@@ -169,8 +171,9 @@
|
||||
"Filter by date": "Отфильтровать по дате",
|
||||
"Filter by name": "Отфильтровать по имени",
|
||||
"Folder": "Папка",
|
||||
"Folder Group": "Группа папки",
|
||||
"Folder ID": "ID папки",
|
||||
"Folder Label": "Ярлык папки",
|
||||
"Folder Label": "Название папки",
|
||||
"Folder Path": "Путь к папке",
|
||||
"Folder Status": "Статус папки",
|
||||
"Folder Type": "Тип папки",
|
||||
@@ -182,8 +185,8 @@
|
||||
"Full Rescan Interval (s)": "Интервал полного сканирования (в секундах)",
|
||||
"GUI": "Интерфейс",
|
||||
"GUI / API HTTPS Certificate": "HTTPS-сертификат GUI/API",
|
||||
"GUI Authentication Password": "Пароль для доступа к панели управления",
|
||||
"GUI Authentication User": "Имя пользователя для доступа к панели управления",
|
||||
"GUI Authentication Password": "Пароль",
|
||||
"GUI Authentication User": "Имя пользователя",
|
||||
"GUI Authentication: Set User and Password": "GUI аутентификация: Установите имя пользователя и пароль",
|
||||
"GUI Listen Address": "Адрес GUI",
|
||||
"GUI Override Directory": "Каталог переопределения графического интерфейса",
|
||||
@@ -249,6 +252,7 @@
|
||||
"Log tailing paused. Scroll to the bottom to continue.": "Вывод журнала приостановлен. Прокрутите вниз, чтобы продолжить.",
|
||||
"Login failed, see Syncthing logs for details.": "Не удалось войти в систему. Посмотреть подробности можно в журнале Syncthing.",
|
||||
"Logs": "Журналы",
|
||||
"Maintain an index of all blocks in the folder, enabling reuse of blocks from other files when syncing changes. Disable to reduce database size at the cost of not being able to reuse blocks across files.": "Использовать индекс всех блоков в папке. Это позволит повторно использовать блоки из других файлов при синхронизации изменений. Отключите эту функцию, чтобы уменьшить размер базы данных. При этом вы не сможете повторно использовать блоки из разных файлов.",
|
||||
"Major Upgrade": "Обновление основной версии",
|
||||
"Mass actions": "Массовые действия",
|
||||
"Maximum Age": "Максимальный срок",
|
||||
@@ -271,14 +275,16 @@
|
||||
"No File Versioning": "Без управления версиями файлов",
|
||||
"No files will be deleted as a result of this operation.": "В результате этой операции никакие файлы не будут удалены.",
|
||||
"No rules set": "Правила не заданы",
|
||||
"No upgrades": "Нет обновлений",
|
||||
"No upgrades": "Выключено",
|
||||
"Not shared": "Нет общего доступа",
|
||||
"Notice": "Внимание",
|
||||
"Number of Connections": "Количество подключений",
|
||||
"OK": "ОК",
|
||||
"Off": "Отключить",
|
||||
"Oldest First": "Сначала старые",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Необязательное описательное название папки. Может различаться на разных устройствах.",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Необязательное название папки. Может различаться на разных устройствах.",
|
||||
"Optional group for the device. Can be different on each device.": "Необязательная группа для устройства. Может отличаться для каждого устройства.",
|
||||
"Optional group for the folder. Can be different on each device.": "Необязательная группа для папки. Может отличаться на каждом устройстве.",
|
||||
"Options": "Настройки",
|
||||
"Out of Sync": "Нет синхронизации",
|
||||
"Out of Sync Items": "Несинхронизированные элементы",
|
||||
@@ -398,7 +404,7 @@
|
||||
"Support": "Поддержка",
|
||||
"Support Bundle": "Данные для поддержки",
|
||||
"Sync Extended Attributes": "Синхронизировать расширенные атрибуты",
|
||||
"Sync Ownership": "Синхронизация владений",
|
||||
"Sync Ownership": "Синхронизировать владельца",
|
||||
"Sync Protocol Listen Addresses": "Адрес протокола синхронизации",
|
||||
"Sync Status": "Состояние синхронизации",
|
||||
"Syncing": "Синхронизация",
|
||||
@@ -410,7 +416,7 @@
|
||||
"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 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 не запущен или есть проблемы с подключением к Интернету. Переподключаюсь…",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"A device with that ID is already added.": "En enhet med detta ID har redan lagts till.",
|
||||
"A negative number of days doesn't make sense.": "Ett negativt antal dagar är inte rimligt.",
|
||||
"A new major version may not be compatible with previous versions.": "En ny huvudversion kan eventuellt vara inkompatibel med tidigare versioner.",
|
||||
"A new major version may not be compatible with previous versions.": "En ny huvudversion kanske inte är kompatibel med tidigare versioner.",
|
||||
"API Key": "API-nyckel",
|
||||
"About": "Om",
|
||||
"Action": "Åtgärd",
|
||||
@@ -15,22 +15,22 @@
|
||||
"Add filter entry": "Lägg till filterregel",
|
||||
"Add ignore patterns": "Lägg till ignoreringsmönster",
|
||||
"Add new folder?": "Lägg till ny mapp?",
|
||||
"Additionally the full rescan interval will be increased (times 60, i.e. new default of 1h). You can also configure it manually for every folder later after choosing No.": "Dessutom kommer det fullständiga återkommande skanningsintervallet att höjas (60 gånger, d.v.s. ny standard på 1h). Du kan också konfigurera den manuellt för varje mapp senare efter att du har valt Nej.",
|
||||
"Additionally the full rescan interval will be increased (times 60, i.e. new default of 1h). You can also configure it manually for every folder later after choosing No.": "Dessutom förlängs intervallet för fullständig omskanning 60 gånger, till det nya standardvärdet 1 timme. Du kan också konfigurera det manuellt för varje mapp senare efter att du har valt Nej.",
|
||||
"Address": "Adress",
|
||||
"Addresses": "Adresser",
|
||||
"Advanced": "Avancerat",
|
||||
"Advanced Configuration": "Avancerad konfiguration",
|
||||
"All Data": "Alla data",
|
||||
"All Time": "All tid",
|
||||
"All folders shared with this device must be protected by a password, such that all sent data is unreadable without the given password.": "Alla mappar som delas med denna enhet måste skyddas av ett lösenord, så att alla skickade data är oläsliga utan det angivna lösenordet.",
|
||||
"Allow Anonymous Usage Reporting?": "Tillåt anonym användarstatistiksrapportering?",
|
||||
"All Time": "Hela perioden",
|
||||
"All folders shared with this device must be protected by a password, such that all sent data is unreadable without the given password.": "Alla mappar som delas med den här enheten måste skyddas av ett lösenord, så att alla skickade data är oläsliga utan det angivna lösenordet.",
|
||||
"Allow Anonymous Usage Reporting?": "Tillåt anonym användningsrapportering?",
|
||||
"Allowed Networks": "Tillåtna nätverk",
|
||||
"Alphabetic": "Alfabetisk",
|
||||
"Altered by ignoring deletes.": "Ändrad genom att ignorera borttagningar.",
|
||||
"Alphabetic": "Alfabetiskt",
|
||||
"Altered by ignoring deletes.": "Ändrad eftersom borttagningar ignoreras.",
|
||||
"Always turned on when the folder type is \"{%foldertype%}\".": "Alltid på när mapptypen är \"{{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.": "Ett externt kommando hanterar versionen. Det måste ta bort filen från den delade mappen. Om sökvägen till applikationen innehåller mellanslag bör den citeras.",
|
||||
"Anonymous Usage Reporting": "Anonym användarstatistiksrapportering",
|
||||
"Anonymous usage report format has changed. Would you like to move to the new format?": "Anonymt användningsrapportformat har ändrats. Vill du flytta till det nya formatet?",
|
||||
"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.": "Ett externt kommando hanterar versionshanteringen. Det måste ta bort filen från den delade mappen. Om sökvägen till programmet innehåller mellanslag ska den omges av citattecken.",
|
||||
"Anonymous Usage Reporting": "Anonym användningsrapportering",
|
||||
"Anonymous usage report format has changed. Would you like to move to the new format?": "Formatet för anonym användningsrapportering har ändrats. Vill du byta till det nya formatet?",
|
||||
"Applied to LAN": "Tillämpas på LAN",
|
||||
"Apply": "Tillämpa",
|
||||
"Are you sure you want to override all remote changes?": "Är du säker på att du vill åsidosätta alla fjärrändringar?",
|
||||
@@ -41,39 +41,39 @@
|
||||
"Are you sure you want to revert all local changes?": "Är du säker på att du vill återställa alla lokala ändringar?",
|
||||
"Are you sure you want to upgrade?": "Är du säker på att du vill uppgradera?",
|
||||
"Authentication Required": "Autentisering krävs",
|
||||
"Authors": "Upphovsmän",
|
||||
"Authors": "Upphovspersoner",
|
||||
"Auto Accept": "Acceptera automatiskt",
|
||||
"Automatic Crash Reporting": "Automatisk kraschrapportering",
|
||||
"Automatic upgrade now offers the choice between stable releases and release candidates.": "Automatisk uppgradering erbjuder nu valet mellan stabila utgåvor och utgåvskandidater.",
|
||||
"Automatic upgrades": "Automatiska uppgraderingar",
|
||||
"Automatic upgrades are always enabled for candidate releases.": "Automatiska uppgraderingar är alltid aktiverade för kandidatutgåvor.",
|
||||
"Automatically create or share folders that this device advertises at the default path.": "Skapa eller dela automatiskt mappar som denna enhet annonserar på standardsökvägen.",
|
||||
"Available debug logging facilities:": "Tillgängliga felsökningsfunktioner:",
|
||||
"Be careful!": "Var aktsam!",
|
||||
"Automatic upgrades are always enabled for candidate releases.": "Automatiska uppgraderingar är alltid aktiverade för utgåvskandidater.",
|
||||
"Automatically create or share folders that this device advertises at the default path.": "Skapa eller dela automatiskt mappar som enheten erbjuder, med standardsökvägen.",
|
||||
"Available debug logging facilities:": "Tillgängliga funktioner för felsökningsloggning:",
|
||||
"Be careful!": "Var försiktig!",
|
||||
"Block Indexing": "Blockindexering",
|
||||
"Body:": "Meddelande:",
|
||||
"Body:": "Meddelandetext:",
|
||||
"Bugs": "Felrapporter",
|
||||
"Cancel": "Avbryt",
|
||||
"Cannot be enabled when the folder type is \"{%foldertype%}\".": "Kan inte aktiveras när mapptypen är \"{{foldertype}}\".",
|
||||
"Changelog": "Ändringslogg",
|
||||
"Clean out after": "Rensa efteråt",
|
||||
"Cleaning Versions": "Rensningsversioner",
|
||||
"Clean out after": "Rensa efter",
|
||||
"Cleaning Versions": "Rensar versioner",
|
||||
"Cleanup Interval": "Rensningsintervall",
|
||||
"Click to see full identification string and QR code.": "Klicka för att se fullständig identifieringssträng och QR-kod.",
|
||||
"Close": "Stäng",
|
||||
"Command": "Kommando",
|
||||
"Comment, when used at the start of a line": "Kommentar, vid användning i början av en rad",
|
||||
"Comment, when used at the start of a line": "Kommentar när det används i början av en rad",
|
||||
"Compression": "Komprimering",
|
||||
"Configuration Directory": "Konfigurationsmapp",
|
||||
"Configuration File": "Konfigurationsfil",
|
||||
"Configured": "Konfigurerad",
|
||||
"Connected (Unused)": "Ansluten (oanvänd)",
|
||||
"Connection Error": "Anslutningsproblem",
|
||||
"Connection Error": "Anslutningsfel",
|
||||
"Connection Management": "Anslutningshantering",
|
||||
"Connection Type": "Anslutningstyp",
|
||||
"Connections": "Anslutningar",
|
||||
"Connections via relays might be rate limited by the relay": "Anslutningar via reläer kan begränsas av reläen",
|
||||
"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.": "Kontinuerlig bevakning av ändringar är nu tillgängligt i Syncthing. Detta kommer att upptäcka ändringar på disken och utfärda en skanning på endast de ändrade sökvägarna. Fördelarna är att ändringar delas snabbare och att mindre fullständiga skanningar krävs.",
|
||||
"Connections via relays might be rate limited by the relay": "Anslutningar via reläer kan hastighetsbegränsas av reläet",
|
||||
"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.": "Kontinuerlig bevakning av ändringar är nu tillgänglig i Syncthing. Ändringar på disken upptäcks och endast de ändrade sökvägarna skannas. Det gör att ändringar sprids snabbare och att färre fullständiga skanningar behövs.",
|
||||
"Copied from elsewhere": "Kopierat från annanstans",
|
||||
"Copied from original": "Kopierat från original",
|
||||
"Copied!": "Kopierat!",
|
||||
@@ -89,85 +89,87 @@
|
||||
"Default Configuration": "Standardkonfiguration",
|
||||
"Default Device": "Standardenhet",
|
||||
"Default Folder": "Standardmapp",
|
||||
"Default Ignore Patterns": "Standard ignoreringsmönster",
|
||||
"Defaults": "Standard",
|
||||
"Default Ignore Patterns": "Standardignoreringsmönster",
|
||||
"Defaults": "Standardvärden",
|
||||
"Delete": "Ta bort",
|
||||
"Delete Unexpected Items": "Ta bort oväntade objekt",
|
||||
"Deleted {%file%}": "Tog bort {{file}}",
|
||||
"Deselect All": "Avmarkera alla",
|
||||
"Deselect devices to stop sharing this folder with.": "Avmarkera enheter som du vill sluta dela denna mapp med.",
|
||||
"Deselect folders to stop sharing with this device.": "Avmarkera mappar för att sluta dela med denna enhet.",
|
||||
"Deselect devices to stop sharing this folder with.": "Avmarkera de enheter som mappen inte längre ska delas med.",
|
||||
"Deselect folders to stop sharing with this device.": "Avmarkera de mappar som inte längre ska delas med den här enheten.",
|
||||
"Device": "Enhet",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Enhet \"{{name}}\" ({{device}} på {{address}}) vill ansluta. Lägg till ny enhet?",
|
||||
"Device Certificate": "Enhetscertifikat",
|
||||
"Device Group": "Enhetsgrupp",
|
||||
"Device ID": "Enhets-ID",
|
||||
"Device Identification": "Enhetens identifikation",
|
||||
"Device Identification": "Enhetsidentifiering",
|
||||
"Device Name": "Enhetsnamn",
|
||||
"Device Status": "Enhetsstatus",
|
||||
"Device is untrusted, enter encryption password": "Enheten är otillförlitlig, ange krypteringslösenord",
|
||||
"Device rate limits": "Enhetshastighetsgränser",
|
||||
"Device that last modified the item": "Enhet som senast ändrade objektet",
|
||||
"Device is untrusted, enter encryption password": "Enheten är inte betrodd. Ange krypteringslösenordet.",
|
||||
"Device rate limits": "Enhetens hastighetsgränser",
|
||||
"Device that last modified the item": "Enheten som senast ändrade objektet",
|
||||
"Devices": "Enheter",
|
||||
"Disable Crash Reporting": "Inaktivera kraschrapportering",
|
||||
"Disabled": "Inaktiverad",
|
||||
"Disabled periodic scanning and disabled watching for changes": "Inaktiverad periodisk skanning och inaktiverad bevakning av ändringar",
|
||||
"Disabled periodic scanning and enabled watching for changes": "Inaktiverad periodisk skanning och aktiverad bevakning av ändringar",
|
||||
"Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:": "Inaktiverad periodisk skanning och misslyckades med att ställa in bevakning av ändringar, försöker igen var 1:e minut:",
|
||||
"Disables comparing and syncing file permissions. Useful on systems with nonexistent or custom permissions (e.g. FAT, exFAT, Synology, Android).": "Inaktiverar att jämföra och synkronisera filbehörigheter. Användbart för system med obefintliga eller anpassade behörigheter (t.ex. FAT, exFAT, Synology, Android).",
|
||||
"Disabled periodic scanning and disabled watching for changes": "Periodisk skanning och bevakning av ändringar är inaktiverade",
|
||||
"Disabled periodic scanning and enabled watching for changes": "Periodisk skanning är inaktiverad och bevakning av ändringar är aktiverad",
|
||||
"Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:": "Periodisk skanning är inaktiverad och bevakning av ändringar kunde inte konfigureras. Ett nytt försök görs varje minut:",
|
||||
"Disables comparing and syncing file permissions. Useful on systems with nonexistent or custom permissions (e.g. FAT, exFAT, Synology, Android).": "Inaktiverar jämförelse och synkronisering av filbehörigheter. Användbart på system utan behörigheter eller med anpassade behörigheter, exempelvis FAT, exFAT, Synology och Android.",
|
||||
"Discard": "Kassera",
|
||||
"Disconnected": "Frånkopplad",
|
||||
"Disconnected (Inactive)": "Frånkopplad (inaktiv)",
|
||||
"Disconnected (Unused)": "Frånkopplad (oanvänd)",
|
||||
"Discovered": "Upptäckt",
|
||||
"Discovery": "Annonsering",
|
||||
"Discovery Failures": "Annonseringsmisslyckanden",
|
||||
"Discovery Failures": "Fel vid annonsering",
|
||||
"Discovery Status": "Annonseringsstatus",
|
||||
"Dismiss": "Avfärda",
|
||||
"Do not add it to the ignore list, so this notification may recur.": "Lägg inte till den i ignoreringslistan, så denna avisering kan återkomma.",
|
||||
"Do not add it to the ignore list, so this notification will reappear if the device connects again.": "Lägg inte till den i ignoreringslistan. Då visas aviseringen igen om enheten ansluter på nytt.",
|
||||
"Do not add it to the ignore list, so this notification will reappear if the device offering this folder connects again.": "Lägg inte till den i ignoreringslistan. Då visas aviseringen igen om enheten som erbjuder den här mappen ansluter på nytt.",
|
||||
"Do not restore": "Återställ inte",
|
||||
"Do not restore all": "Återställ inte allt",
|
||||
"Do you want to enable watching for changes for all your folders?": "Vill du aktivera bevakning av ändringar på alla dina mappar?",
|
||||
"Documentation": "Dokumentation",
|
||||
"Download Rate": "Hämtningshastighet",
|
||||
"Downloaded": "Hämtat",
|
||||
"Downloading": "Tar emot",
|
||||
"Downloading": "Hämtar",
|
||||
"Edit": "Redigera",
|
||||
"Edit Device": "Redigera enhet",
|
||||
"Edit Device Defaults": "Redigera enhetsstandard",
|
||||
"Edit Device Defaults": "Redigera standardvärden för enheter",
|
||||
"Edit Folder": "Redigera mapp",
|
||||
"Edit Folder Defaults": "Redigera mappstandard",
|
||||
"Edit Folder Defaults": "Redigera standardvärden för mappar",
|
||||
"Editing {%path%}.": "Redigerar {{path}}.",
|
||||
"Enable Crash Reporting": "Aktivera kraschrapportering",
|
||||
"Enable NAT traversal": "Aktivera NAT-genompassering",
|
||||
"Enable Relaying": "Aktivera vidarebefordring",
|
||||
"Enable NAT traversal": "Aktivera NAT-traversering",
|
||||
"Enable Relaying": "Aktivera reläförmedling",
|
||||
"Enabled": "Aktiverad",
|
||||
"Enables sending extended attributes to other devices, and applying incoming extended attributes. May require running with elevated privileges.": "Möjliggör sändning av utökade attribut till andra enheter, och tillämpning av inkommande utökade attribut. Kan kräva körning med förhöjda behörigheter.",
|
||||
"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.": "Möjliggör sändning av utökade attribut till andra enheter, men inte att tillämpa inkommande utökade attribut. Detta kan ha en betydande inverkan på prestanda. Alltid aktiverad när \"Synkronisera utökade attribut\" är aktiverat.",
|
||||
"Enables sending ownership information to other devices, and applying incoming ownership information. Typically requires running with elevated privileges.": "Möjliggör att skicka ägarinformation till andra enheter och tillämpa inkommande ägarinformation. Kräver vanligtvis körning med förhöjda behörigheter.",
|
||||
"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.": "Möjliggör att skicka ägarinformation till andra enheter, men inte tillämpa inkommande ägarinformation. Detta kan ha en betydande inverkan på prestanda. Alltid aktiverat när \"Synkronisera ägande\" är aktiverat.",
|
||||
"Enter a non-negative number (e.g., \"2.35\") and select a unit. Percentages are as part of the total disk size.": "Ange ett icke-negativt antal (t.ex., \"2.35\") och välj en enhet. Procenttalen är som en del av den totala diskstorleken.",
|
||||
"Enter a non-privileged port number (1024 - 65535).": "Ange ett icke-privilegierat portnummer (1024 - 65535).",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Ange kommaseparerade (\"tcp://ip:port\", \"tcp://host:port\") adresser eller \"dynamic\" för att utföra automatisk upptäckt av adressen.",
|
||||
"Enables sending extended attributes to other devices, and applying incoming extended attributes. May require running with elevated privileges.": "Gör det möjligt att skicka utökade attribut till andra enheter och tillämpa inkommande utökade attribut. Kan kräva körning med utökade behörigheter.",
|
||||
"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.": "Gör det möjligt att skicka utökade attribut till andra enheter utan att tillämpa inkommande utökade attribut. Det kan påverka prestandan avsevärt. Funktionen är alltid aktiverad när ”Synkronisera utökade attribut” är aktiverat.",
|
||||
"Enables sending ownership information to other devices, and applying incoming ownership information. Typically requires running with elevated privileges.": "Gör det möjligt att skicka ägarinformation till andra enheter och tillämpa inkommande ägarinformation. Kräver vanligtvis körning med utökade behörigheter.",
|
||||
"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.": "Gör det möjligt att skicka ägarinformation till andra enheter utan att tillämpa inkommande ägarinformation. Det kan påverka prestandan avsevärt. Funktionen är alltid aktiverad när ”Synkronisera ägarskap” är aktiverat.",
|
||||
"Enter a non-negative number (e.g., \"2.35\") and select a unit. Percentages are as part of the total disk size.": "Ange ett icke-negativt tal, t.ex. \"2.35\", och välj en enhet. Procenttal räknas som en andel av den totala diskstorleken.",
|
||||
"Enter a non-privileged port number (1024 - 65535).": "Ange ett oprivilegierat portnummer (1024–65535).",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Ange kommaseparerade adresser (\"tcp://ip:port\", \"tcp://host:port\") eller \"dynamic\" för automatisk identifiering av adressen.",
|
||||
"Enter ignore patterns, one per line.": "Ange ignoreringsmönster, en per rad.",
|
||||
"Enter up to three octal digits.": "Ange upp till tre oktala siffror.",
|
||||
"Error": "Fel",
|
||||
"Extended Attributes": "Utökade attribut",
|
||||
"Extended Attributes Filter": "Utökat attributfilter",
|
||||
"Extended Attributes Filter": "Filter för utökade attribut",
|
||||
"External": "Extern",
|
||||
"External File Versioning": "Extern filversionshantering",
|
||||
"Failed Items": "Misslyckade objekt",
|
||||
"Failed to load file versions.": "Det gick inte att läsa in filversioner.",
|
||||
"Failed to load ignore patterns.": "Det gick inte att läsa in ignoreringsmönster.",
|
||||
"Failed to set up, retrying": "Det gick inte att ställa in, försöker igen",
|
||||
"Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": "Det går inte att ansluta till IPv6-servrar om det inte finns någon IPv6-anslutning.",
|
||||
"File Pull Order": "Filhämtningsprioritering",
|
||||
"Failed to set up, retrying": "Det gick inte att konfigurera, försöker igen",
|
||||
"Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": "Det är normalt att anslutningar till IPv6-servrar misslyckas om IPv6-anslutning saknas.",
|
||||
"File Pull Order": "Ordning för filhämtning",
|
||||
"File Versioning": "Filversionshantering",
|
||||
"Files are moved to .stversions directory when replaced or deleted by Syncthing.": "Filer flyttas till .stversions-mappen när de ersätts eller tas bort av Syncthing.",
|
||||
"Files are moved to date stamped versions in a .stversions directory when replaced or deleted by Syncthing.": "Filer flyttas till datumstämplade versioner i en .stversions-mapp när de ersätts eller tas bort av Syncthing.",
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Filer är skyddade från ändringar som görs på andra enheter, men ändringar som görs på den här enheten skickas till resten av klustret.",
|
||||
"Files are synchronized from the cluster, but any changes made locally will not be sent to other devices.": "Filer synkroniseras från klustret, men alla ändringar som görs lokalt skickas inte till andra enheter.",
|
||||
"Filesystem Watcher Errors": "Fel i filsystembevakaren",
|
||||
"Files are synchronized from the cluster, but any changes made locally will not be sent to other devices.": "Filer synkroniseras från klustret, men inga lokala ändringar skickas till andra enheter.",
|
||||
"Filesystem Watcher Errors": "Fel vid bevakning av filsystemet",
|
||||
"Filter by date": "Filtrera efter datum",
|
||||
"Filter by name": "Filtrera efter namn",
|
||||
"Folder": "Mapp",
|
||||
@@ -177,36 +179,36 @@
|
||||
"Folder Path": "Mappsökväg",
|
||||
"Folder Status": "Mappstatus",
|
||||
"Folder Type": "Mapptyp",
|
||||
"Folder type \"{%receiveEncrypted%}\" can only be set when adding a new folder.": "Mapptypen \"{{receiveEncrypted}}\" kan bara ställas in vid tilläggning av en ny mapp.",
|
||||
"Folder type \"{%receiveEncrypted%}\" can only be set when adding a new folder.": "Mapptypen \"{{receiveEncrypted}}\" kan bara ställas in när en ny mapp läggs till.",
|
||||
"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.": "Mapptypen \"{{receiveEncrypted}}\" kan inte ändras efter att mappen har lagts till. Du måste ta bort mappen, ta bort eller dekryptera data på disken och lägga till mappen igen.",
|
||||
"Folders": "Mappar",
|
||||
"For the following folders an error occurred while starting to watch for changes. It will be retried every minute, so the errors might go away soon. If they persist, try to fix the underlying issue and ask for help if you can't.": "För följande mappar uppstod ett fel när du började bevaka ändringar. Det kommer att omförsökas varje minut, så felen kan försvinna snart. Om de fortsätter, försök att åtgärda det underliggande problemet och fråga om hjälp om du inte kan.",
|
||||
"For the following folders an error occurred while starting to watch for changes. It will be retried every minute, so the errors might go away soon. If they persist, try to fix the underlying issue and ask for help if you can't.": "För följande mappar uppstod ett fel när bevakning av ändringar skulle startas. Försöket upprepas varje minut, så felen kan snart försvinna. Om de kvarstår bör du försöka åtgärda det underliggande problemet och be om hjälp om du inte lyckas.",
|
||||
"Forever": "För alltid",
|
||||
"Full Rescan Interval (s)": "Intervall för fullständig omskanning (s)",
|
||||
"GUI": "Grafiskt användargränssnitt",
|
||||
"GUI / API HTTPS Certificate": "HTTPS-certifikat för gränssnitt / API",
|
||||
"GUI Authentication Password": "Autentiseringslösenord för gränssnitt",
|
||||
"GUI Authentication User": "Autentiseringsanvändare för gränssnitt",
|
||||
"GUI Authentication: Set User and Password": "Autentisering för det grafiska gränssnittet: Ställ in användare och lösenord",
|
||||
"GUI Listen Address": "Lyssnaradress för gränssnitt",
|
||||
"GUI Override Directory": "Åsidosätt mapp för gränssnitt",
|
||||
"GUI Theme": "Tema för gränssnitt",
|
||||
"GUI / API HTTPS Certificate": "HTTPS-certifikat för gränssnittet och API:t",
|
||||
"GUI Authentication Password": "Lösenord för gränssnittsautentisering",
|
||||
"GUI Authentication User": "Användarnamn för gränssnittsautentisering",
|
||||
"GUI Authentication: Set User and Password": "Gränssnittsautentisering: ange användarnamn och lösenord",
|
||||
"GUI Listen Address": "Gränssnittets lyssnaradress",
|
||||
"GUI Override Directory": "Åsidosättningsmapp för gränssnittet",
|
||||
"GUI Theme": "Gränssnittstema",
|
||||
"General": "Allmänt",
|
||||
"Generate": "Generera",
|
||||
"Global Discovery": "Global annonsering",
|
||||
"Global Discovery Servers": "Globala annonseringsservrar",
|
||||
"Global State": "Globalt tillstånd",
|
||||
"Help": "Hjälp",
|
||||
"Hint: only deny-rules detected while the default is deny. Consider adding \"permit any\" as last rule.": "Tips: endast neka-regler upptäcktes medan standard är neka. Överväg att lägga till \"tillåt alla\" som sista regel.",
|
||||
"Hint: only deny-rules detected while the default is deny. Consider adding \"permit any\" as last rule.": "Tips: Endast nekanderegler hittades när standardåtgärden är att neka. Överväg att lägga till ”permit any” som sista regel.",
|
||||
"Home page": "Webbplats",
|
||||
"However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.": "Dina aktuella inställningar indikerar dock att du kanske inte vill att ha det aktiverat. Vi har inaktiverat automatisk kraschrapportering för dig.",
|
||||
"However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.": "Dina aktuella inställningar tyder dock på att du kanske inte vill ha det aktiverat. Vi har inaktiverat automatisk kraschrapportering för dig.",
|
||||
"Identification": "Identifiering",
|
||||
"If untrusted, enter encryption password": "Om opålitlig, ange krypteringslösenord",
|
||||
"If you want to prevent other users on this computer from accessing Syncthing and through it your files, consider setting up authentication.": "Om du vill förhindra att andra användare på denna dator får åtkomst till Syncthing och dina filer via det, överväg att konfigurera autentisering.",
|
||||
"If untrusted, enter encryption password": "Om enheten inte är betrodd anger du krypteringslösenordet",
|
||||
"If you want to prevent other users on this computer from accessing Syncthing and through it your files, consider setting up authentication.": "Om du vill förhindra att andra användare på den här datorn får åtkomst till Syncthing och därigenom dina filer, bör du konfigurera autentisering.",
|
||||
"Ignore": "Ignorera",
|
||||
"Ignore Patterns": "Ignoreringsmönster",
|
||||
"Ignore Permissions": "Ignorera rättigheter",
|
||||
"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.": "Ignoreringsmönster kan bara läggas till efter att mappen har skapats. Om markerad kommer ett inmatningsfält för att ange ignoreringsmönster att visas efter att du har sparat.",
|
||||
"Ignore Permissions": "Ignorera behörigheter",
|
||||
"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.": "Ignoreringsmönster kan endast läggas till efter att mappen har skapats. Om alternativet markeras visas ett inmatningsfält för ignoreringsmönster efter att du har sparat.",
|
||||
"Ignored Devices": "Ignorerade enheter",
|
||||
"Ignored Folders": "Ignorerade mappar",
|
||||
"Ignored at": "Ignorerad vid",
|
||||
@@ -214,12 +216,12 @@
|
||||
"Incoming Rate Limit (KiB/s)": "Ingående hastighetsgräns (KiB/s)",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Felaktig konfiguration kan skada innehållet i mappen och få Syncthing att sluta fungera.",
|
||||
"Incorrect user name or password.": "Felaktigt användarnamn eller lösenord.",
|
||||
"Info": "Info",
|
||||
"Info": "Information",
|
||||
"Internally used paths:": "Internt använda sökvägar:",
|
||||
"Introduced By": "Introducerad av",
|
||||
"Introducer": "Introduktör",
|
||||
"Introduction": "Introduktion",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Inversion av det givna tillståndet (d.v.s. exkluderar inte)",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Omvändning av det angivna villkoret, det vill säga exkludera inte",
|
||||
"Keep Versions": "Behåll versioner",
|
||||
"LDAP": "LDAP",
|
||||
"Largest First": "Största först",
|
||||
@@ -232,13 +234,13 @@
|
||||
"Learn more": "Läs mer",
|
||||
"Learn more at {%url%}": "Läs mer på {{url}}",
|
||||
"Limit": "Gräns",
|
||||
"Limit Bandwidth in LAN": "Begränsa bandbredd i LAN",
|
||||
"Limit Bandwidth in LAN": "Begränsa bandbredden i LAN",
|
||||
"Listener Failures": "Lyssnarfel",
|
||||
"Listener Status": "Lyssnarstatus",
|
||||
"Listeners": "Lyssnare",
|
||||
"Loading data...": "Läser in data...",
|
||||
"Loading...": "Läser in...",
|
||||
"Local Additions": "Lokalt tillägg",
|
||||
"Loading data...": "Läser in data…",
|
||||
"Loading...": "Läser in…",
|
||||
"Local Additions": "Lokala tillägg",
|
||||
"Local Discovery": "Lokal annonsering",
|
||||
"Local State": "Lokalt tillstånd",
|
||||
"Local State (Total)": "Lokalt tillstånd (totalt)",
|
||||
@@ -249,32 +251,32 @@
|
||||
"Log Out": "Logga ut",
|
||||
"Log in to see paths information.": "Logga in för att se sökvägsinformation.",
|
||||
"Log in to see version information.": "Logga in för att se versionsinformation.",
|
||||
"Log tailing paused. Scroll to the bottom to continue.": "Loggning pausad. Bläddra till botten för att fortsätta.",
|
||||
"Login failed, see Syncthing logs for details.": "Inloggningen misslyckades, se Syncthing-loggarna för detaljer.",
|
||||
"Log tailing paused. Scroll to the bottom to continue.": "Den automatiska loggrullningen har pausats. Rulla längst ned för att fortsätta.",
|
||||
"Login failed, see Syncthing logs for details.": "Inloggningen misslyckades. Mer information finns i Syncthings loggar.",
|
||||
"Logs": "Loggar",
|
||||
"Maintain an index of all blocks in the folder, enabling reuse of blocks from other files when syncing changes. Disable to reduce database size at the cost of not being able to reuse blocks across files.": "Upprätthåll ett index över alla block i mappen, vilket möjliggör återanvändning av block från andra filer vid synkronisering av ändringar. Inaktivera för att minska databasens storlek på bekostnad av att inte kunna återanvända block mellan filer.",
|
||||
"Major Upgrade": "Större uppgradering",
|
||||
"Major Upgrade": "Uppgradering av huvudversion",
|
||||
"Mass actions": "Massåtgärder",
|
||||
"Maximum Age": "Högsta ålder",
|
||||
"Maximum Age": "Maximal ålder",
|
||||
"Maximum single entry size": "Största tillåtna poststorlek",
|
||||
"Maximum total size": "Största tillåtna totala storlek",
|
||||
"Metadata Only": "Endast metadata",
|
||||
"Minimum Free Disk Space": "Minsta lediga diskutrymme",
|
||||
"Mod. Device": "Enhet som utförde ändring",
|
||||
"Mod. Time": "Tid för ändring",
|
||||
"More than a month ago": "Mer än en månad sedan",
|
||||
"More than a month ago": "För mer än en månad sedan",
|
||||
"More than a week ago": "För mer än en vecka sedan",
|
||||
"More than a year ago": "Mer än ett år sedan",
|
||||
"Move to top of queue": "Flytta till överst i kön",
|
||||
"Multi level wildcard (matches multiple directory levels)": "Flernivå jokertecken (matchar flera mappnivåer)",
|
||||
"More than a year ago": "För mer än ett år sedan",
|
||||
"Move to top of queue": "Flytta längst upp i kön",
|
||||
"Multi level wildcard (matches multiple directory levels)": "Jokertecken på flera nivåer (matchar flera mappnivåer)",
|
||||
"Never": "Aldrig",
|
||||
"New Device": "Ny enhet",
|
||||
"New Folder": "Ny mapp",
|
||||
"Newest First": "Nyaste först",
|
||||
"No": "Nej",
|
||||
"No File Versioning": "Ingen filversionshantering",
|
||||
"No files will be deleted as a result of this operation.": "Inga filer kommer att tas bort till följd av denna operation.",
|
||||
"No rules set": "Inga regler fastställda",
|
||||
"No files will be deleted as a result of this operation.": "Inga filer tas bort till följd av den här åtgärden.",
|
||||
"No rules set": "Inga regler angivna",
|
||||
"No upgrades": "Inga uppgraderingar",
|
||||
"Not shared": "Inte delad",
|
||||
"Notice": "Observera",
|
||||
@@ -282,61 +284,61 @@
|
||||
"OK": "OK",
|
||||
"Off": "Av",
|
||||
"Oldest First": "Äldsta först",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Valfri beskrivande etikett för mappen. Kan vara olika på varje enhet.",
|
||||
"Optional group for the device. Can be different on each device.": "Valfri grupp för enheten. Kan vara olika på varje enhet.",
|
||||
"Optional group for the folder. Can be different on each device.": "Valfri grupp för mappen. Kan vara olika på varje enhet.",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Valfri beskrivande etikett för mappen. Den kan skilja sig mellan enheterna.",
|
||||
"Optional group for the device. Can be different on each device.": "Valfri grupp för enheten. Den kan skilja sig mellan enheterna.",
|
||||
"Optional group for the folder. Can be different on each device.": "Valfri grupp för mappen. Den kan skilja sig mellan enheterna.",
|
||||
"Options": "Alternativ",
|
||||
"Out of Sync": "Osynkroniserad",
|
||||
"Out of Sync Items": "Osynkroniserade objekt",
|
||||
"Outgoing Rate Limit (KiB/s)": "Utgående hastighetsgräns (KiB/s)",
|
||||
"Override": "Åsidosätt",
|
||||
"Override Changes": "Åsidosätt förändringar",
|
||||
"Override Changes": "Åsidosätt ändringar",
|
||||
"Ownership": "Ägarskap",
|
||||
"Password": "Lösenord",
|
||||
"Path": "Sökväg",
|
||||
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Sökväg till mappen på din dator. Kommer att skapas om det inte finns. Tecknet tilde (~) kan användas som en genväg för",
|
||||
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Sökväg till mappen på din dator. Den skapas om den inte finns. Tecknet tilde (~) kan användas som en genväg för",
|
||||
"Path where versions should be stored (leave empty for the default .stversions directory in the shared folder).": "Sökväg där versioner ska lagras (lämna tomt för standardmappen .stversions i den delade mappen).",
|
||||
"Paths": "Sökvägar",
|
||||
"Pause": "Pausa",
|
||||
"Pause All": "Pausa alla",
|
||||
"Paused": "Pausad",
|
||||
"Paused (Unused)": "Pausad (oanvänd)",
|
||||
"Pending changes": "Väntar på ändringar",
|
||||
"Periodic scanning at given interval and disabled watching for changes": "Periodisk skanning vid givet intervall och inaktiverad bevakning av ändringar",
|
||||
"Periodic scanning at given interval and enabled watching for changes": "Periodisk skanning vid givet intervall och aktiverad bevakning av ändringar",
|
||||
"Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:": "Periodisk skanning vid givet intervall och misslyckades med att ställa in bevakning av ändringar, försöker igen var 1:e minut:",
|
||||
"Permanently add it to the ignore list, suppressing further notifications.": "Lägg till det permanent i ignoreringslistan och undertryck ytterligare aviseringar.",
|
||||
"Please consult the release notes before performing a major upgrade.": "Läs igenom versionsnyheterna innan du utför en större uppgradering.",
|
||||
"Please set a GUI Authentication User and Password in the Settings dialog.": "Ställ in en autentiseringsanvändare och ett lösenord för det grafiska användargränssnittet i inställningsdialogrutan.",
|
||||
"Pending changes": "Väntande ändringar",
|
||||
"Periodic scanning at given interval and disabled watching for changes": "Periodisk skanning körs med angivet intervall och bevakning av ändringar är inaktiverad",
|
||||
"Periodic scanning at given interval and enabled watching for changes": "Periodisk skanning körs med angivet intervall och bevakning av ändringar är aktiverad",
|
||||
"Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:": "Periodisk skanning körs med angivet intervall, men bevakning av ändringar kunde inte konfigureras. Ett nytt försök görs varje minut:",
|
||||
"Permanently add it to the ignore list, suppressing further notifications.": "Lägg till den permanent i ignoreringslistan och dölj framtida aviseringar.",
|
||||
"Please consult the release notes before performing a major upgrade.": "Läs versionsinformationen innan du uppgraderar till en ny huvudversion.",
|
||||
"Please set a GUI Authentication User and Password in the Settings dialog.": "Ange ett användarnamn och lösenord för gränssnittsautentisering i dialogrutan Inställningar.",
|
||||
"Please wait": "Vänta",
|
||||
"Prefix indicating that the file can be deleted if preventing directory removal": "Prefix som indikerar att filen kan tas bort om den förhindrar mappborttagning",
|
||||
"Prefix indicating that the pattern should be matched without case sensitivity": "Prefix som indikerar att mönstret ska matchas utan skiftlägeskänslighet",
|
||||
"Preparing to Sync": "Förberedelser för synkronisering",
|
||||
"Prefix indicating that the file can be deleted if preventing directory removal": "Prefix som anger att filen kan tas bort om den förhindrar att mappen tas bort",
|
||||
"Prefix indicating that the pattern should be matched without case sensitivity": "Prefix som anger att mönstret ska matchas utan hänsyn till skiftläge",
|
||||
"Preparing to Sync": "Förbereder synkronisering",
|
||||
"Preview": "Förhandsgranska",
|
||||
"Preview Usage Report": "Förhandsgranska användningsrapport",
|
||||
"QR code": "QR-kod",
|
||||
"QUIC LAN": "QUIC LAN",
|
||||
"QUIC WAN": "QUIC WAN",
|
||||
"Quick guide to supported patterns": "Snabb handledning till mönster som stöds",
|
||||
"Quick guide to supported patterns": "Snabbguide till mönster som stöds",
|
||||
"Random": "Slumpmässig",
|
||||
"Receive Encrypted": "Ta emot krypterade",
|
||||
"Receive Encrypted": "Ta emot krypterat",
|
||||
"Receive Only": "Ta endast emot",
|
||||
"Received data is already encrypted": "Mottagna data är redan krypterade",
|
||||
"Recent Changes": "Senaste ändringar",
|
||||
"Reduced by ignore patterns": "Minskas med ignoreringsmönster",
|
||||
"Reduced by ignore patterns": "Minskad genom ignoreringsmönster",
|
||||
"Relay LAN": "LAN-relä",
|
||||
"Relay WAN": "WAN-relä",
|
||||
"Release Notes": "Versionsanteckningar",
|
||||
"Release Notes": "Versionsinformation",
|
||||
"Release candidates contain the latest features and fixes. They are similar to the traditional bi-weekly Syncthing releases.": "Utgåvskandidater innehåller de senaste funktionerna och korrigeringarna. De liknar de traditionella Syncthing-utgåvorna som kommer ut varannan vecka.",
|
||||
"Remote Devices": "Fjärrenheter",
|
||||
"Remote GUI": "Fjärranslutet grafiskt användargränssnitt",
|
||||
"Remote GUI": "Fjärrgränssnitt",
|
||||
"Remove": "Ta bort",
|
||||
"Remove Device": "Ta bort enhet",
|
||||
"Remove Folder": "Ta bort mapp",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "Krävs identifierare för mappen. Måste vara densamma på alla kluster enheter.",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "Obligatorisk identifierare för mappen. Den måste vara identisk på alla enheter i klustret.",
|
||||
"Rescan": "Skanna igen",
|
||||
"Rescan All": "Skanna alla igen",
|
||||
"Rescans": "Återkommande skanningar",
|
||||
"Rescans": "Omskanningar",
|
||||
"Restart": "Starta om",
|
||||
"Restart Needed": "Omstart behövs",
|
||||
"Restarting": "Startar om",
|
||||
@@ -344,25 +346,25 @@
|
||||
"Restore Versions": "Återställ versioner",
|
||||
"Resume": "Återuppta",
|
||||
"Resume All": "Återuppta alla",
|
||||
"Reused": "Återanvänds",
|
||||
"Revert": "Återgå",
|
||||
"Reused": "Återanvänt",
|
||||
"Revert": "Återställ",
|
||||
"Revert Local Changes": "Återställ lokala ändringar",
|
||||
"Save": "Spara",
|
||||
"Saving changes": "Sparar ändringar",
|
||||
"Scan Time Remaining": "Återstående skanningstid",
|
||||
"Scanning": "Skannar",
|
||||
"See external versioning help for supported templated command line parameters.": "Se hjälp för extern version för stödda mallade kommandoradsparametrar.",
|
||||
"See external versioning help for supported templated command line parameters.": "Se hjälpen för extern versionshantering för information om vilka mallbaserade kommandoradsparametrar som stöds.",
|
||||
"Select All": "Markera alla",
|
||||
"Select a version": "Välj en version",
|
||||
"Select additional devices to share this folder with.": "Välj ytterligare enheter för att dela denna mapp med.",
|
||||
"Select additional folders to share with this device.": "Välj ytterligare mappar att dela med denna enhet.",
|
||||
"Select additional devices to share this folder with.": "Välj ytterligare enheter som den här mappen ska delas med.",
|
||||
"Select additional folders to share with this device.": "Välj ytterligare mappar som ska delas med den här enheten.",
|
||||
"Select latest version": "Välj senaste versionen",
|
||||
"Select oldest version": "Välj äldsta versionen",
|
||||
"Send & Receive": "Skicka & ta emot",
|
||||
"Send & Receive": "Skicka och ta emot",
|
||||
"Send Extended Attributes": "Skicka utökade attribut",
|
||||
"Send Only": "Skicka endast",
|
||||
"Send Ownership": "Skicka ägarskap",
|
||||
"Set Ignores on Added Folder": "Ställ in ignoreringar för tillagd mapp",
|
||||
"Set Ignores on Added Folder": "Ange ignoreringsmönster för den nya mappen",
|
||||
"Settings": "Inställningar",
|
||||
"Share": "Dela",
|
||||
"Share Folder": "Dela mapp",
|
||||
@@ -373,12 +375,12 @@
|
||||
"Shared With": "Delas med",
|
||||
"Sharing": "Delning",
|
||||
"Show ID": "Visa ID",
|
||||
"Show QR": "Visa QR",
|
||||
"Show QR": "Visa QR-kod",
|
||||
"Show detailed discovery status": "Visa detaljerad annonseringsstatus",
|
||||
"Show detailed listener status": "Visa detaljerad lyssnarstatus",
|
||||
"Show diff with previous version": "Visa skillnad med tidigare version",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Visas istället för enhets-ID i klusterstatus. Kommer att annonseras på andra enheter som ett valfritt standardnamn.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Visas istället för enhets-ID i klusterstatusen. Kommer att uppdateras till det namn som enheten annonserar om det lämnas tomt.",
|
||||
"Show diff with previous version": "Visa skillnader mot föregående version",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Visas i stället för enhets-ID i klusterstatusen. Namnet annonseras till andra enheter som ett valfritt standardnamn.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Visas i stället för enhets-ID i klusterstatusen. Om fältet lämnas tomt uppdateras det till det namn som enheten annonserar.",
|
||||
"Shut Down": "Stäng av",
|
||||
"Shutdown Complete": "Avstängning slutförd",
|
||||
"Simple": "Enkel",
|
||||
@@ -386,124 +388,124 @@
|
||||
"Single level wildcard (matches within a directory only)": "Jokertecken på en nivå (matchar endast i en mapp)",
|
||||
"Size": "Storlek",
|
||||
"Smallest First": "Minsta först",
|
||||
"Some discovery methods could not be established for finding other devices or announcing this device:": "Vissa annonseringsmetoder kunde inte fastställas för att hitta andra enheter eller tillkännage denna enhet:",
|
||||
"Some discovery methods could not be established for finding other devices or announcing this device:": "Vissa metoder för att hitta andra enheter eller annonsera den här enheten kunde inte upprättas:",
|
||||
"Some items could not be restored:": "Vissa objekt kunde inte återställas:",
|
||||
"Some listening addresses could not be enabled to accept connections:": "Vissa lyssningsadresser kunde inte aktiveras för att acceptera anslutningar:",
|
||||
"Source Code": "Källkod",
|
||||
"Stable releases and release candidates": "Stabila utgåvor och utgåvskandidater",
|
||||
"Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "Stabila utgåvor är försenade med cirka två veckor. Under denna tid går de igenom testning som utgåvskandidater.",
|
||||
"Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "Stabila utgåvor publiceras cirka två veckor senare. Under tiden testas de som utgåvskandidater.",
|
||||
"Stable releases only": "Endast stabila utgåvor",
|
||||
"Staggered": "Förskjuten",
|
||||
"Staggered File Versioning": "Filversionshantering i intervall",
|
||||
"Staggered": "Intervallbaserad",
|
||||
"Staggered File Versioning": "Intervallbaserad filversionshantering",
|
||||
"Start Browser": "Starta webbläsaren",
|
||||
"Starting": "Börjar",
|
||||
"Starting": "Startar",
|
||||
"Statistics": "Statistik",
|
||||
"Stay logged in": "Förbli inloggad",
|
||||
"Stay logged in": "Håll mig inloggad",
|
||||
"Stopped": "Stoppad",
|
||||
"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.": "Lagrar och synkroniserar endast krypterade data. Mappar på alla anslutna enheter måste ställas in med samma lösenord eller också vara av typen \"{{receiveEncrypted}}\".",
|
||||
"Subject:": "Ämne:",
|
||||
"Support": "Support",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Support Bundle": "Supportpaket",
|
||||
"Sync Extended Attributes": "Synkronisera utökade attribut",
|
||||
"Sync Ownership": "Synkronisera ägarskap",
|
||||
"Sync Protocol Listen Addresses": "Lyssnaradresser för synkroniseringsprotokoll",
|
||||
"Sync Status": "Synkroniseringsstatus",
|
||||
"Syncing": "Synkroniserar",
|
||||
"Syncthing device ID for \"{%devicename%}\"": "Syncthing enhets-ID för \"{{devicename}}\"",
|
||||
"Syncthing device ID for \"{%devicename%}\"": "Syncthings enhets-ID för \"{{devicename}}\"",
|
||||
"Syncthing has been shut down.": "Syncthing har stängts av.",
|
||||
"Syncthing includes the following software or portions thereof:": "Syncthing innehåller följande mjukvarupaket eller delar av dem:",
|
||||
"Syncthing is Free and Open Source Software licensed as MPL v2.0.": "Syncthing har fri och öppen källkod licensierad som 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 är ett program för kontinuerlig filsynkronisering. Den synkroniserar filer mellan två eller flera datorer i realtid, säkert skyddade från nyfikna ögon. Dina data är dina data och du har rätt att välja var de lagras, om de delas med någon tredje part och hur de överförs över internet.",
|
||||
"Syncthing includes the following software or portions thereof:": "Syncthing innehåller följande programvara eller delar av den:",
|
||||
"Syncthing is Free and Open Source Software licensed as MPL v2.0.": "Syncthing är fri programvara med öppen källkod, licensierad enligt 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 är ett program för kontinuerlig filsynkronisering. Det synkroniserar filer mellan två eller flera datorer i realtid och skyddar dem från obehörig insyn. Dina data tillhör dig, och du ska kunna välja var de lagras, om de delas med en tredje part och hur de överförs via internet.",
|
||||
"Syncthing is listening on the following network addresses for connection attempts from other devices:": "Syncthing lyssnar på följande nätverksadresser för anslutningsförsök från andra enheter:",
|
||||
"Syncthing is not listening for connection attempts from other devices on any address. Only outgoing connections from this device may work.": "Syncthing lyssnar inte efter anslutningsförsök från andra enheter på någon adress. Endast utgående anslutningar från denna enhet kanske fungerar.",
|
||||
"Syncthing is not listening for connection attempts from other devices on any address. Only outgoing connections from this device may work.": "Syncthing lyssnar inte efter anslutningsförsök från andra enheter på någon adress. Endast utgående anslutningar från den här enheten kan fungera.",
|
||||
"Syncthing is restarting.": "Syncthing startar om.",
|
||||
"Syncthing is saving changes.": "Syncthing sparar ändringar.",
|
||||
"Syncthing is upgrading.": "Syncthing uppgraderas.",
|
||||
"Syncthing now supports automatically reporting crashes to the developers. This feature is enabled by default.": "Syncthing stöder nu automatiskt kraschrapportering till utvecklarna. Denna funktion är aktiverad som standard.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing verkar vara avstängd, eller så finns det problem med din internetanslutning. Försöker igen…",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing verkar ha drabbats av ett problem med behandlingen av din förfrågan. Uppdatera sidan eller starta om Syncthing om problemet kvarstår.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing verkar inte vara igång, eller så finns det problem med din internetanslutning. Försöker igen…",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing verkar ha problem med att behandla din begäran. Uppdatera sidan eller starta om Syncthing om problemet kvarstår.",
|
||||
"TCP LAN": "TCP LAN",
|
||||
"TCP WAN": "TCP WAN",
|
||||
"Take me back": "Ta mig tillbaka",
|
||||
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "Den grafiska gränssnittsadressen åsidosätts av startalternativ. Ändringar här träder inte i kraft så länge åsidosättningen är på plats.",
|
||||
"The Syncthing Authors": "Syncthing-upphovsmän",
|
||||
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "Gränssnittets adress åsidosätts av startalternativ. Ändringar här får ingen effekt så länge åsidosättningen gäller.",
|
||||
"The Syncthing Authors": "Syncthings upphovspersoner",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "Administratörsgränssnittet för Syncthing är konfigurerat för att möjliggöra fjärråtkomst utan lösenord.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "Den aggregerade statistiken är offentligt tillgänglig på webbadressen nedan.",
|
||||
"The cleanup interval cannot be blank.": "Rensningsintervallet kan inte vara tomt.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "Den sammanställda statistiken är offentligt tillgänglig på URL:en nedan.",
|
||||
"The cleanup interval cannot be blank.": "Rensningsintervallet får inte lämnas tomt.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Konfigurationen har sparats men inte aktiverats. Syncthing måste startas om för att aktivera den nya konfigurationen.",
|
||||
"The device ID cannot be blank.": "Enhets-ID kan inte vara tomt.",
|
||||
"The device ID cannot be blank.": "Enhets-ID får inte lämnas tomt.",
|
||||
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Enhets-ID som du anger här finns i dialogrutan \"Åtgärder > Visa ID\" på den andra enheten. Mellanslag och bindestreck är valfria (ignoreras).",
|
||||
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes, and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Den krypterade användningsrapporten skickas dagligen. Det används för att spåra vanliga plattformar, mappstorlekar och appversioner. Om den rapporterade datauppsättningen ändras kommer du att uppmanas med denna dialogruta igen.",
|
||||
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Det inmatade enhets-ID:t verkar inte vara korrekt. Det ska vara en 52 eller 56 teckensträng bestående av siffror och bokstäver, eventuellt med mellanrum och bindestreck.",
|
||||
"The folder ID cannot be blank.": "Mapp-ID får inte vara tomt.",
|
||||
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes, and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Den krypterade användningsrapporten skickas dagligen. Den används för att kartlägga vanliga plattformar, mappstorlekar och programversioner. Om den rapporterade datauppsättningen ändras visas den här dialogrutan igen.",
|
||||
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Det angivna enhets-ID:t verkar inte vara giltigt. Det ska vara en sträng med 52 eller 56 tecken som består av bokstäver och siffror. Mellanslag och bindestreck är valfria.",
|
||||
"The folder ID cannot be blank.": "Mapp-ID får inte lämnas tomt.",
|
||||
"The folder ID must be unique.": "Mapp-ID måste vara unikt.",
|
||||
"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.": "Mappinnehållet på andra enheter skrivs över så att det blir identiskt med denna enhet. Filer som inte finns här kommer att tas bort på andra enheter.",
|
||||
"The folder content on this device will be overwritten to become identical with other devices. Files newly added here will be deleted.": "Mappinnehållet på denna enhet skrivs över för att bli identiskt med andra enheter. Filer som nyligen har lagts till här kommer att tas bort.",
|
||||
"The folder path cannot be blank.": "Mappsökvägen kan inte vara tom.",
|
||||
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "De följande intervallen används: varje 30 sekunder under den första timmen; varje timme under den första dagen; varje dag för de första 30 dagarna; varje vecka tills den maximala åldersgränsen uppnås.",
|
||||
"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.": "Mappinnehållet på andra enheter skrivs över så att det blir identiskt med innehållet på den här enheten. Filer som inte finns här tas bort från de andra enheterna.",
|
||||
"The folder content on this device will be overwritten to become identical with other devices. Files newly added here will be deleted.": "Mappinnehållet på den här enheten skrivs över så att det blir identiskt med innehållet på de andra enheterna. Filer som nyligen har lagts till här tas bort.",
|
||||
"The folder path cannot be blank.": "Mappsökvägen får inte lämnas tom.",
|
||||
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "Följande intervall används: under den första timmen sparas en version var 30:e sekund, under den första dagen en version varje timme, under de första 30 dagarna en version varje dag och därefter en version varje vecka tills den maximala åldern nås.",
|
||||
"The following items could not be synchronized.": "Följande objekt kunde inte synkroniseras.",
|
||||
"The following items were changed locally.": "Följande objekt ändrades lokalt.",
|
||||
"The following methods are used to discover other devices on the network and announce this device to be found by others:": "Följande metoder används för att upptäcka andra enheter i nätverket och meddela att denna enhet ska hittas av andra:",
|
||||
"The following text will automatically be inserted into a new message.": "Följande text kommer automatiskt att infogas i ett nytt meddelande.",
|
||||
"The following methods are used to discover other devices on the network and announce this device to be found by others:": "Följande metoder används för att upptäcka andra enheter i nätverket och annonsera den här enheten så att andra kan hitta den:",
|
||||
"The following text will automatically be inserted into a new message.": "Följande text infogas automatiskt i ett nytt meddelande.",
|
||||
"The following unexpected items were found.": "Följande oväntade objekt hittades.",
|
||||
"The interval must be a positive number of seconds.": "Intervallet måste vara ett positivt antal sekunder.",
|
||||
"The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.": "Intervallet, i sekunder, för att rensa i versionsmappen. Noll för att inaktivera periodisk rensning.",
|
||||
"The maximum age must be a number and cannot be blank.": "Åldersgränsen måste vara ett tal och kan inte lämnas tomt.",
|
||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Den längsta tiden att behålla en version (i dagar, ställ in på 0 för att behålla versioner för alltid).",
|
||||
"The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.": "Intervallet i sekunder mellan rensningar av versionsmappen. Ange noll för att inaktivera periodisk rensning.",
|
||||
"The maximum age must be a number and cannot be blank.": "Den maximala åldern måste vara ett tal och får inte lämnas tom.",
|
||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Den längsta tiden som en version ska behållas, i dagar. Ange 0 för att behålla versioner för alltid.",
|
||||
"The number of connections must be a non-negative number.": "Antalet anslutningar måste vara ett icke-negativt tal.",
|
||||
"The number of days must be a number and cannot be blank.": "Antalet dagar måste vara en siffra och får inte vara tomt.",
|
||||
"The number of days must be a number and cannot be blank.": "Antalet dagar måste vara ett tal och får inte lämnas tomt.",
|
||||
"The number of days to keep files in the trash can. Zero means forever.": "Antalet dagar som filer ligger kvar i papperskorgen. Noll betyder för alltid.",
|
||||
"The number of old versions to keep, per file.": "Antalet gamla versioner som ska behållas, per fil.",
|
||||
"The number of versions must be a number and cannot be blank.": "Antalet versioner måste vara ett nummer och kan inte lämnas tomt.",
|
||||
"The path cannot be blank.": "Sökvägen kan inte vara tom.",
|
||||
"The rate limit is applied to the accumulated traffic of all connections to this device.": "Hastighetsgränsen tillämpas på den ackumulerade trafiken för alla anslutningar till denna enhet.",
|
||||
"The number of old versions to keep, per file.": "Antalet gamla versioner som ska behållas per fil.",
|
||||
"The number of versions must be a number and cannot be blank.": "Antalet versioner måste vara ett tal och får inte lämnas tomt.",
|
||||
"The path cannot be blank.": "Sökvägen får inte lämnas tom.",
|
||||
"The rate limit is applied to the accumulated traffic of all connections to this device.": "Hastighetsgränsen tillämpas på den sammanlagda trafiken för alla anslutningar till den här enheten.",
|
||||
"The rate limit must be a non-negative number (0: no limit)": "Hastighetsgränsen måste vara ett icke-negativt tal (0: ingen gräns)",
|
||||
"The remote device has not accepted sharing this folder.": "Fjärrenheten har inte accepterat delning av denna mapp.",
|
||||
"The remote device has paused this folder.": "Fjärrenheten har pausat denna mapp.",
|
||||
"The rescan interval must be a non-negative number of seconds.": "Omskanningsintervallet måste vara ett positivt antal sekunder.",
|
||||
"There are no devices to share this folder with.": "Det finns inga enheter att dela denna mapp med.",
|
||||
"The remote device has not accepted sharing this folder.": "Fjärrenheten har inte godkänt att den här mappen delas.",
|
||||
"The remote device has paused this folder.": "Fjärrenheten har pausat den här mappen.",
|
||||
"The rescan interval must be a non-negative number of seconds.": "Omskanningsintervallet måste vara ett icke-negativt antal sekunder.",
|
||||
"There are no devices to share this folder with.": "Det finns inga enheter som den här mappen kan delas med.",
|
||||
"There are no file versions to restore.": "Det finns inga filversioner att återställa.",
|
||||
"There are no folders to share with this device.": "Det finns inga mappar att dela med denna enhet.",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "De omprövas automatiskt och kommer att synkroniseras när felet är löst.",
|
||||
"This Device": "Denna enhet",
|
||||
"This Month": "Denna månad",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "Detta kan lätt ge hackare tillgång till att läsa och ändra några filer på datorn.",
|
||||
"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.": "Denna enhet kan inte automatiskt upptäcka andra enheter eller meddela sin egen adress som andra kan hitta. Endast enheter med statiskt konfigurerade adresser kan ansluta.",
|
||||
"This is a major version upgrade.": "Det här är en stor uppgradering.",
|
||||
"This setting controls the free space required on the home (i.e., index database) disk.": "Denna inställning styr hur mycket ledigt utrymme som krävs på hemdisken (dvs. indexdatabasen).",
|
||||
"There are no folders to share with this device.": "Det finns inga mappar som kan delas med den här enheten.",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "Försöken upprepas automatiskt och objekten synkroniseras när felet är löst.",
|
||||
"This Device": "Den här enheten",
|
||||
"This Month": "Den här månaden",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "Det kan enkelt ge hackare möjlighet att läsa och ändra alla filer på datorn.",
|
||||
"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.": "Den här enheten kan inte automatiskt hitta andra enheter eller annonsera sin egen adress så att andra kan hitta den. Endast enheter med statiskt konfigurerade adresser kan ansluta.",
|
||||
"This is a major version upgrade.": "Det här är en uppgradering till en ny huvudversion.",
|
||||
"This setting controls the free space required on the home (i.e., index database) disk.": "Den här inställningen styr hur mycket ledigt utrymme som krävs på hemmadisken, det vill säga disken där indexdatabasen finns.",
|
||||
"Time": "Tid",
|
||||
"Time the item was last modified": "Tidpunkten objektet var senast ändrad",
|
||||
"To connect with the Syncthing device named \"{%devicename%}\", add a new remote device on your end with this ID:": "För att ansluta till Syncthing-enheten med namnet \"{{devicename}}\", lägg till en ny fjärrenhet med detta ID:",
|
||||
"To permit a rule, have the checkbox checked. To deny a rule, leave it unchecked.": "För att tillåta en regel, markera kryssrutan. För att neka en regel, lämna den omarkerad.",
|
||||
"Time the item was last modified": "Tidpunkt då objektet senast ändrades",
|
||||
"To connect with the Syncthing device named \"{%devicename%}\", add a new remote device on your end with this ID:": "Lägg till en ny fjärrenhet med följande ID för att ansluta till Syncthing-enheten ”{{devicename}}”:",
|
||||
"To permit a rule, have the checkbox checked. To deny a rule, leave it unchecked.": "Markera kryssrutan för att tillåta en regel. Lämna den omarkerad för att neka regeln.",
|
||||
"Today": "Idag",
|
||||
"Trash Can": "Papperskorgen",
|
||||
"Trash Can File Versioning": "Papperskorgs filversionshantering",
|
||||
"Trash Can File Versioning": "Filversionshantering med papperskorg",
|
||||
"Type": "Typ",
|
||||
"UNIX Permissions": "UNIX-behörigheter",
|
||||
"Unavailable": "Otillgänglig",
|
||||
"Unavailable/Disabled by administrator or maintainer": "Otillgängligt/inaktiverat av administratör eller underhållare",
|
||||
"Undecided (will prompt)": "Obeslutad (kommer att fråga)",
|
||||
"Undecided (will prompt)": "Inte bestämt (en fråga visas)",
|
||||
"Unexpected Items": "Oväntade objekt",
|
||||
"Unexpected items have been found in this folder.": "Oväntade objekt har hittats i denna mapp.",
|
||||
"Unexpected items have been found in this folder.": "Oväntade objekt har hittats i den här mappen.",
|
||||
"Unignore": "Sluta ignorera",
|
||||
"Unknown": "Okänd",
|
||||
"Unshared": "Inte delad",
|
||||
"Unshared Devices": "Odelade enheter",
|
||||
"Unshared Folders": "Odelade mappar",
|
||||
"Untrusted": "Opålitlig",
|
||||
"Unshared Devices": "Enheter utan delade mappar",
|
||||
"Unshared Folders": "Mappar som inte delas",
|
||||
"Untrusted": "Inte betrodd",
|
||||
"Up to Date": "Uppdaterad",
|
||||
"Updated {%file%}": "Uppdaterade {{file}}",
|
||||
"Upgrade": "Uppgradering",
|
||||
"Upgrade": "Uppgradera",
|
||||
"Upgrade To {%version%}": "Uppgradera till {{version}}",
|
||||
"Upgrading": "Uppgraderar",
|
||||
"Upload Rate": "Sändningshastighet",
|
||||
"Uptime": "Driftstid",
|
||||
"Usage reporting is always enabled for candidate releases.": "Användningsrapportering är alltid aktiverad för kandidatutgåvor.",
|
||||
"Use HTTPS for GUI": "Använd HTTPS för gränssnitt",
|
||||
"Usage reporting is always enabled for candidate releases.": "Användningsrapportering är alltid aktiverad för utgåvskandidater.",
|
||||
"Use HTTPS for GUI": "Använd HTTPS för gränssnittet",
|
||||
"Use notifications from the filesystem to detect changed items.": "Använd aviseringar från filsystemet för att upptäcka ändrade objekt.",
|
||||
"User": "Användare",
|
||||
"User Home": "Användarhem",
|
||||
"Username/Password has not been set for the GUI authentication. Please consider setting it up.": "Användarnamn/lösenord har inte ställts in för autentisering av det grafiska gränssnittet. Överväg att ställa in det.",
|
||||
"User Home": "Användarens hemmapp",
|
||||
"Username/Password has not been set for the GUI authentication. Please consider setting it up.": "Användarnamn och lösenord har inte angetts för gränssnittsautentisering. Överväg att konfigurera dem.",
|
||||
"Using a QUIC connection over LAN": "Använder en QUIC-anslutning över LAN",
|
||||
"Using a QUIC connection over WAN": "Använder en QUIC-anslutning över WAN",
|
||||
"Using a direct TCP connection over LAN": "Använder en direkt TCP-anslutning över LAN",
|
||||
@@ -511,35 +513,35 @@
|
||||
"Version": "Version",
|
||||
"Versions": "Versioner",
|
||||
"Versions Path": "Sökväg för versioner",
|
||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versioner tas automatiskt bort om de är äldre än den maximala åldern eller överskrider antalet filer som tillåts i ett intervall.",
|
||||
"Waiting to Clean": "Väntar på att rensa",
|
||||
"Waiting to Scan": "Väntar på att skanna",
|
||||
"Waiting to Sync": "Väntar på att synkronisera",
|
||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versioner tas bort automatiskt om de är äldre än den högsta åldern eller om fler versioner lagras inom ett intervall än vad som tillåts.",
|
||||
"Waiting to Clean": "Väntar på rensning",
|
||||
"Waiting to Scan": "Väntar på skanning",
|
||||
"Waiting to Sync": "Väntar på synkronisering",
|
||||
"Warning": "Varning",
|
||||
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Varning, denna sökväg är en överordnad mapp för en befintlig mapp \"{{otherFolder}}\".",
|
||||
"Warning, this path is a parent directory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Varning, denna sökväg är en överordnad mapp för en befintlig mapp \"{{otherFolderLabel}}\" ({{otherFolder}}).",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Varning, denna sökväg är en undermapp för en befintlig mapp \"{{otherFolder}}\".",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Varning, denna sökväg är en undermapp för en befintlig mapp \"{{otherFolderLabel}}\" ({{otherFolder}}).",
|
||||
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Varning: Den här sökvägen är en överordnad mapp till den befintliga mappen ”{{otherFolder}}”.",
|
||||
"Warning, this path is a parent directory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Varning: Den här sökvägen är en överordnad mapp till den befintliga mappen ”{{otherFolderLabel}}” ({{otherFolder}}).",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Varning: Den här sökvägen är en undermapp till den befintliga mappen ”{{otherFolder}}”.",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Varning: Den här sökvägen är en undermapp till den befintliga mappen ”{{otherFolderLabel}}” ({{otherFolder}}).",
|
||||
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "Varning: Om du använder en extern bevakare som {{syncthingInotify}}, bör du se till att den är inaktiverad.",
|
||||
"Watch for Changes": "Bevakning av ändringar",
|
||||
"Watch for Changes": "Bevaka ändringar",
|
||||
"Watching for Changes": "Bevakar ändringar",
|
||||
"Watching for changes discovers most changes without periodic scanning.": "Bevakning av ändringar upptäcker de flesta ändringar utan periodisk skanning.",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "När du lägger till en ny enhet, kom ihåg att denna enhet måste läggas till på den andra enheten också.",
|
||||
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "När du lägger till ny mapp, tänk på att mapp-ID knyter ihop mappar mellan olika enheter. De är skiftlägeskänsliga och måste matcha precis mellan alla enheter.",
|
||||
"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.": "När den är inställd på mer än en på båda enheterna kommer Syncthing att försöka upprätta flera samtidiga anslutningar. Om värdena skiljer sig kommer det högsta att användas. Ställ in på noll för att låta Syncthing bestämma.",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "När du lägger till en ny enhet måste den här enheten även läggas till på den andra enheten.",
|
||||
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "När du lägger till en ny mapp bör du tänka på att mapp-ID:t används för att koppla ihop mappar mellan enheter. Mapp-ID:n är skiftlägeskänsliga och måste stämma exakt överens på alla enheter.",
|
||||
"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.": "När värdet är större än ett på båda enheterna försöker Syncthing upprätta flera samtidiga anslutningar. Om värdena skiljer sig används det högsta. Ange noll för att låta Syncthing bestämma.",
|
||||
"Yes": "Ja",
|
||||
"Yesterday": "Igår",
|
||||
"You can also copy and paste the text into a new message manually.": "Du kan också kopiera och klistra in texten i ett nytt meddelande manuellt.",
|
||||
"You can also select one of these nearby devices:": "Du kan också välja en av dessa närliggande enheter:",
|
||||
"You can change your choice at any time in the Settings dialog.": "Du kan ändra ditt val när som helst i inställningsdialogrutan.",
|
||||
"You can read more about the two release channels at the link below.": "Du kan läsa mer om de två publiceringsskanalerna på länken nedan.",
|
||||
"You can read more about the two release channels at the link below.": "Du kan läsa mer om de två utgåvekanalerna via länken nedan.",
|
||||
"You have no ignored devices.": "Du har inga ignorerade enheter.",
|
||||
"You have no ignored folders.": "Du har inga ignorerade mappar.",
|
||||
"You have unsaved changes. Do you really want to discard them?": "Du har osparade ändringar. Vill du verkligen kassera dem?",
|
||||
"You must keep at least one version.": "Du måste behålla åtminstone en version.",
|
||||
"You should never add or change anything locally in a \"{%receiveEncrypted%}\" folder.": "Du ska aldrig lägga till eller ändra något lokalt i en \"{{receiveEncrypted}}\"-mapp.",
|
||||
"Your SMS app should open to let you choose the recipient and send it from your own number.": "Din SMS-app bör öppnas så att du kan välja mottagare och skicka den från ditt eget nummer.",
|
||||
"Your email app should open to let you choose the recipient and send it from your own address.": "Din e-postapp bör öppnas så att du kan välja mottagare och skicka den från din egen adress.",
|
||||
"Your SMS app should open to let you choose the recipient and send it from your own number.": "Din sms-app bör öppnas så att du kan välja mottagare och skicka meddelandet från ditt eget nummer.",
|
||||
"Your email app should open to let you choose the recipient and send it from your own address.": "Din e-postapp bör öppnas så att du kan välja mottagare och skicka meddelandet från din egen adress.",
|
||||
"days": "dagar",
|
||||
"deleted": "borttagen",
|
||||
"deny": "neka",
|
||||
@@ -555,13 +557,13 @@
|
||||
"theme": {
|
||||
"name": {
|
||||
"black": "Svart",
|
||||
"dark": "Mörkt",
|
||||
"dark": "Mörk",
|
||||
"default": "Standard",
|
||||
"light": "Ljust"
|
||||
"light": "Ljus"
|
||||
}
|
||||
},
|
||||
"unknown device": "okänd enhet",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vill dela mapp \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} vill dela mapp \"{{folderlabel}}\" ({{folder}}).",
|
||||
"{%reintroducer%} might reintroduce this device.": "{{reintroducer}} kan återinföra denna enhet."
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vill dela mappen \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} vill dela mappen \"{{folderlabel}}\" ({{folder}}).",
|
||||
"{%reintroducer%} might reintroduce this device.": "{{reintroducer}} kan återinföra den här enheten."
|
||||
}
|
||||
|
||||
@@ -125,6 +125,8 @@
|
||||
"Discovery Status": "Keşif Durumu",
|
||||
"Dismiss": "Yoksay",
|
||||
"Do not add it to the ignore list, so this notification may recur.": "Yoksayma listesine eklemeyin, böylece bu bildirim tekrarlayabilir.",
|
||||
"Do not add it to the ignore list, so this notification will reappear if the device connects again.": "Bunu yoksayma listesine eklemeyin, böylece bu cihaz tekrar bağlanırsa bu bildirim yeniden görünecektir.",
|
||||
"Do not add it to the ignore list, so this notification will reappear if the device offering this folder connects again.": "Bunu yoksayma listesine eklemeyin, böylece bu klasörü sunan cihaz tekrar bağlanırsa bu bildirim yeniden görünecektir.",
|
||||
"Do not restore": "Geri yükleme yapma",
|
||||
"Do not restore all": "Hiçbirini geri yükleme",
|
||||
"Do you want to enable watching for changes for all your folders?": "Tüm klasörleriniz için değişiklikleri izlemeyi etkinleştirmek istiyor musunuz?",
|
||||
|
||||
@@ -124,7 +124,9 @@
|
||||
"Discovery Failures": "设备发现失败",
|
||||
"Discovery Status": "设备发现状态",
|
||||
"Dismiss": "忽略",
|
||||
"Do not add it to the ignore list, so this notification may recur.": "不要将其添加到忽略列表中,因此此通知可能会再次出现。",
|
||||
"Do not add it to the ignore list, so this notification may recur.": "不要将其添加到忽略列表中,否则此通知可能会再次出现。",
|
||||
"Do not add it to the ignore list, so this notification will reappear if the device connects again.": "不要将其添加到忽略列表中,否则如果设备再次连接,此通知将重新出现。",
|
||||
"Do not add it to the ignore list, so this notification will reappear if the device offering this folder connects again.": "不要将其添加到忽略列表中,否则如果提供此文件夹的设备再次连接,此通知将重新出现。",
|
||||
"Do not restore": "不要恢复",
|
||||
"Do not restore all": "不要全部恢复",
|
||||
"Do you want to enable watching for changes for all your folders?": "是否要启用监视所有文件夹的更改?",
|
||||
@@ -222,7 +224,7 @@
|
||||
"Inversion of the given condition (i.e. do not exclude)": "给定条件的反转(即不排除)",
|
||||
"Keep Versions": "保留版本数量",
|
||||
"LDAP": "LDAP",
|
||||
"Largest First": "最大优先",
|
||||
"Largest First": "从大到小",
|
||||
"Last 30 Days": "最近 30 天",
|
||||
"Last 7 Days": "最近 7 天",
|
||||
"Last Month": "上个月",
|
||||
@@ -270,7 +272,7 @@
|
||||
"Never": "从未",
|
||||
"New Device": "新设备",
|
||||
"New Folder": "新文件夹",
|
||||
"Newest First": "最新优先",
|
||||
"Newest First": "从新到旧",
|
||||
"No": "否",
|
||||
"No File Versioning": "不启用文件版本控制",
|
||||
"No files will be deleted as a result of this operation.": "此操作结果不会删除任何文件。",
|
||||
@@ -281,7 +283,7 @@
|
||||
"Number of Connections": "连接数",
|
||||
"OK": "确定",
|
||||
"Off": "关闭",
|
||||
"Oldest First": "最旧优先",
|
||||
"Oldest First": "从旧到新",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "文件夹的可选描述性标签。每个设备上可能不同。",
|
||||
"Optional group for the device. Can be different on each device.": "设备的可选分组。各设备可设置不同分组。",
|
||||
"Optional group for the folder. Can be different on each device.": "文件夹的可选分组。各设备可设置不同分组。",
|
||||
@@ -385,7 +387,7 @@
|
||||
"Simple File Versioning": "简单文件版本控制",
|
||||
"Single level wildcard (matches within a directory only)": "单级通配符(仅匹配单层文件夹)",
|
||||
"Size": "大小",
|
||||
"Smallest First": "最小优先",
|
||||
"Smallest First": "从小到大",
|
||||
"Some discovery methods could not be established for finding other devices or announcing this device:": "无法建立某些发现方法来查找其他设备或宣布此设备:",
|
||||
"Some items could not be restored:": "某些项目无法恢复:",
|
||||
"Some listening addresses could not be enabled to accept connections:": "无法启用某些监听地址以接受连接:",
|
||||
|
||||
+13
-9
@@ -222,7 +222,7 @@
|
||||
<button type="button" class="btn btn-sm btn-danger" ng-click="ignoreDevice(deviceID, pendingDevice)" tooltip data-original-title="{{'Permanently add it to the ignore list, suppressing further notifications.' | translate}}">
|
||||
<span class="fas fa-times"></span> <span translate>Ignore</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-default" ng-click="dismissPendingDevice(deviceID)" tooltip data-original-title="{{'Do not add it to the ignore list, so this notification may recur.' | translate}}">
|
||||
<button type="button" class="btn btn-sm btn-default" ng-click="dismissPendingDevice(deviceID)" tooltip data-original-title="{{'Do not add it to the ignore list, so this notification will reappear if the device connects again.' | translate}}">
|
||||
<span class="far fa-clock"></span> <span translate>Dismiss</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -269,7 +269,7 @@
|
||||
<button type="button" class="btn btn-sm btn-danger" ng-click="ignoreFolder(deviceID, folderID, offeringDevice)" tooltip data-original-title="{{'Permanently add it to the ignore list, suppressing further notifications.' | translate}}">
|
||||
<span class="fas fa-times"></span> <span translate>Ignore</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-default" ng-click="dismissPendingFolder(folderID, deviceID)" tooltip data-original-title="{{'Do not add it to the ignore list, so this notification may recur.' | translate}}">
|
||||
<button type="button" class="btn btn-sm btn-default" ng-click="dismissPendingFolder(folderID, deviceID)" tooltip data-original-title="{{'Do not add it to the ignore list, so this notification will reappear if the device offering this folder connects again.' | translate}}">
|
||||
<span class="far fa-clock"></span> <span translate>Dismiss</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -387,7 +387,7 @@
|
||||
</h4>
|
||||
<div class="panel-group" id="folders-{{ $index }}">
|
||||
<div class="panel panel-default" ng-repeat="folder in groupedFolders">
|
||||
<button class="btn panel-heading" data-toggle="collapse" data-parent="#folders-{{$index}}" data-target="#folder-{{$parent.$index}}-{{$index}}" aria-expanded="false">
|
||||
<button class="btn panel-heading" data-toggle="collapse" data-parent="#folders-{{$parent.$index}}" data-target="#folder-{{$parent.$index}}-{{$index}}" aria-expanded="false">
|
||||
<div class="panel-progress" ng-show="folderStatus(folder) == 'syncing'" ng-attr-style="width: {{syncPercentage(folder.id) | percent}}"></div>
|
||||
<div class="panel-progress" ng-show="folderStatus(folder) == 'scanning' && scanProgress[folder.id] != undefined" ng-attr-style="width: {{scanPercentage(folder.id) | percent}}"></div>
|
||||
<h4 class="panel-title">
|
||||
@@ -410,8 +410,8 @@
|
||||
</div>
|
||||
</h4>
|
||||
</button>
|
||||
<div id="folder-{{$parent.$index}}-{{$index}}" class="panel-collapse collapse">
|
||||
<div class="panel-body less-padding">
|
||||
<div id="folder-{{$parent.$index}}-{{$index}}" class="panel-collapse collapse" lazy-collapse>
|
||||
<div class="panel-body less-padding" ng-if="lazyReady">
|
||||
<table class="table table-condensed table-striped table-auto">
|
||||
<tbody>
|
||||
<tr class="visible-xs">
|
||||
@@ -686,8 +686,8 @@
|
||||
<div class="panel-title-text">{{deviceName(deviceCfg)}}</div>
|
||||
</h4>
|
||||
</button>
|
||||
<div id="device-this" class="panel-collapse collapse in">
|
||||
<div class="panel-body less-padding">
|
||||
<div id="device-this" class="panel-collapse collapse in" lazy-collapse>
|
||||
<div class="panel-body less-padding" ng-if="lazyReady">
|
||||
<table class="table table-condensed table-striped table-auto">
|
||||
<tbody>
|
||||
<tr>
|
||||
@@ -805,8 +805,8 @@
|
||||
<div class="panel-title-text">{{deviceName(deviceCfg)}}</div>
|
||||
</h4>
|
||||
</button>
|
||||
<div id="device-{{$parent.$index}}-{{$index}}" class="panel-collapse collapse">
|
||||
<div class="panel-body less-padding">
|
||||
<div id="device-{{$parent.$index}}-{{$index}}" class="panel-collapse collapse" lazy-collapse>
|
||||
<div class="panel-body less-padding" ng-if="lazyReady">
|
||||
<table class="table table-condensed table-striped table-auto">
|
||||
<tbody>
|
||||
<tr class="visible-xs">
|
||||
@@ -1017,6 +1017,9 @@
|
||||
</div> <!-- /row -->
|
||||
|
||||
</div> <!-- /container -->
|
||||
<footer class="container text-center text-muted small" aria-label="Custom build marker">
|
||||
It syncs .stignore now!
|
||||
</footer>
|
||||
</div> <!-- /ng-cloak -->
|
||||
|
||||
<ng-include src="'syncthing/core/networkErrorDialogView.html'"></ng-include>
|
||||
@@ -1083,6 +1086,7 @@
|
||||
<script type="text/javascript" src="syncthing/core/popoverDirective.js"></script>
|
||||
<script type="text/javascript" src="syncthing/core/syncthingController.js"></script>
|
||||
<script type="text/javascript" src="syncthing/core/tooltipDirective.js"></script>
|
||||
<script type="text/javascript" src="syncthing/core/lazyCollapseDirective.js"></script>
|
||||
<script type="text/javascript" src="syncthing/core/uncamelFilter.js"></script>
|
||||
<script type="text/javascript" src="syncthing/core/uniqueFolderDirective.js"></script>
|
||||
<script type="text/javascript" src="syncthing/core/validDeviceidDirective.js"></script>
|
||||
|
||||
@@ -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, Simon Frei, Tomasz Wilczyński, Alexander Graf, Alexandre Viau, Anderson Mesquita, André Colomb, Antony Male, Ben Schulz, bt90, Caleb Callaway, Daniel Harte, Emil Lundberg, Eric P, Evgeny Kuznetsov, greatroar, Lars K.W. Gohlke, Lode Hoste, Marcus B Spencer, Michael Ploujnikov, Ross Smith II, Stefan Tatschner, Tommy van der Vorst, Wulf Weich, Adam Piggott, Adel Qalieh, Aleksey Vasenev, Alessandro G., Alex Ionescu, Alex Lindeman, Alex Xu, Alexander Seiler, Alexandre Alves, Aman Gupta, Andreas Sommer, andresvia, Andrew Rabert, Andrey D, andyleap, Anjan Momi, Anthony Goeckner, Antoine Lamielle, Anur, Aranjedeath, ardevd, Arkadiusz Tymiński, Aroun, Arthur Axel fREW Schmidt, Artur Zubilewicz, Ashish Bhate, Aurélien Rainone, BAHADIR YILMAZ, Bart De Vries, Beat Reichenbach, Ben Norcombe, Ben Shepherd, Ben Sidhom, Benedikt Heine, Benno Fünfstück, Benny Ng, boomsquared, Boqin Qin, Boris Rybalkin, Brendan Long, Catfriend1, Cathryne Linenweaver, Cedric Staniewski, Chih-Hsuan Yen, Choongkyu, Chris Howie, Chris Joel, Christian Kujau, Christian Prescott, chucic, cjc7373, Colin Kennedy, Cromefire_, cui, Cyprien Devillez, d-volution, Dan, Daniel Barczyk, Daniel Bergmann, Daniel Martí, Daniel Padrta, Daniil Gentili, Darshil Chanpura, dashangcun, David Rimmer, DeflateAwning, Denis A., Dennis Wilson, derekriemer, DerRockWolf, desbma, Devon G. Redekopp, digital, Dimitri Papadopoulos Orfanos, Dmitry Saveliev, domain, Domenic Horner, Dominik Heidler, Elias Jarlebring, Elliot Huffman, Emil Hessman, Eng Zer Jun, entity0xfe, Epifeny, epifeny, Eric Lesiuta, Erik Meitner, Evan Spensley, Federico Castagnini, Felix, Felix Ableitner, Felix Lampe, Felix Unterpaintner, Francois-Xavier Gsell, Frank Isemann, Gahl Saraf, georgespatton, ghjklw, Gilli Sigurdsson, Gleb Sinyavskiy, Graham Miln, Greg, guangwu, gudvinr, Gusted, Han Boetes, HansK-p, Harrison Jones, Hazem Krimi, Heiko Zuerker, Hireworks, Hugo Locurcio, Iain Barnett, Ian Johnson, ignacy123, Iskander Sharipov, Jaakko Hannikainen, Jack Croft, Jacob, Jake Peterson, James O'Beirne, James Patterson, Jaroslav Lichtblau, Jaroslav Malec, Jaspitta, Jaya Chithra, Jaya Kumar, Jeffery To, jelle van der Waa, Jens Diemer, Jochen Voss, Johan Vromans, John Rinehart, Jonas Thelemann, Jonathan, Jose Manuel Delicado, JRNitre, jtagcat, Julian Lehrhuber, Jörg Thalheim, Jędrzej Kula, Kapil Sareen, Karol Różycki, Kebin Liu, Keith Harrison, Kelong Cong, Ken'ichi Kamada, Kevin Allen, Kevin Bushiri, Kevin White, Jr., klemens, Kurt Fitzner, kylosus, Lars Lehtonen, Laurent Etiemble, Leo Arias, Liu Siyuan, Lord Landon Agahnim, LSmithx2, Luiz Angelo Daros de Luca, Lukas Lihotzki, Luke Hamburg, luzpaz, Majed Abdulaziz, Marc Laporte, Marcel Meyer, Marcin Dziadus, Marcus Legendre, Mario Majila, Mark Pulford, Martchus, Mateusz Naściszewski, Mateusz Ż, mathias4833, Matic Potočnik, Matt Burke, Matt Robenolt, Matteo Ruina, Maurizio Tomasi, Max, Max Schulze, MaximAL, Maximilian, Maxwell G, Michael Jephcote, Michael Rienstra, Michael Wang 汪東陽, MichaIng, Migelo, Mike Boone, MikeLund, MikolajTwarog, Mingxuan Lin, mv1005, Nate Morrison, nf, Nicholas Rishel, Nick Busey, Nico Stapelbroek, Nicolas Braud-Santoni, Nicolas Perraut, Niels Peter Roest, Nils Jakobi, NinoM4ster, Nitroretro, NoLooseEnds, Oliver Freyermuth, orangekame3, otbutz, overkill, Oyebanji Jacob Mayowa, Pablo, Pascal Jungblut, Paul Brit, Paul Donald, Pawel Palenica, perewa, Peter Badida, Peter Dave Hello, Peter Hoeg, Peter Marquardt, Phani Rithvij, Phil Davis, Philippe Schommers, Phill Luby, Piotr Bejda, polyfloyd, Prathik P Kulkarni, pullmerge, Quentin Hibon, Rahmi Pruitt, RealCharlesChia, red_led, Robert Carosi, Roberto Santalla, Robin Schoonover, Roman Zaynetdinov, rubenbe, Ruslan Yevdokymov, Ryan Qian, Ryan Sullivan, Sacheendra Talluri, Scott Klupfel, sec65, Sergey Mishin, Sertonix, Severin von Wnuck-Lipinski, Shaarad Dalvi, Shivam Kumar, Simon Mwepu, Simon Pickup, Sly_tom_cat, Sonu Kumar Saw, Stefan Kuntz, Steven Eckhoff, Suhas Gundimeda, Sven Bachmann, Sébastien WENSKE, Tao, Taylor Khan, Terrance, TheCreeper, Thomas, Thomas Hipp, Tim Abell, Tim Howes, Tobias Frölich, Tobias Klauser, Tobias Nygren, Tobias Tom, Tom Jakubowski, Tully Robinson, Tyler Brazier, Tyler Kropp, Umer-Azaz, Unrud, Val Markovic, vapatel2, Veeti Paananen, Victor Buinsky, Vik, Vil Brekin, villekalliomaki, Vladimir Rusinov, vvaswani, wangguoliang, WangXi, Will Rouesnel, William A. Kennington III, wouter bolsterlee, xarx00, Xavier O., xjtdy888, Yannic A., yparitcher, 佛跳墙, 落心
|
||||
Jakob Borg, Audrius Butkevicius, Simon Frei, Tomasz Wilczyński, Alexander Graf, Alexandre Viau, Anderson Mesquita, André Colomb, Antony Male, Ben Schulz, bt90, Caleb Callaway, Daniel Harte, Emil Lundberg, Eric P, Evgeny Kuznetsov, greatroar, Lars K.W. Gohlke, Lode Hoste, Marcus B Spencer, Michael Ploujnikov, Ross Smith II, Stefan Tatschner, Tommy van der Vorst, Wulf Weich, Adam Piggott, Adel Qalieh, Aleksey Vasenev, Alessandro G., Alex Ionescu, Alex Lindeman, Alex Xu, Alexander Seiler, Alexandre Alves, Aman Gupta, Andreas Sommer, andresvia, Andrew Gunnerson, Andrew Rabert, Andrey D, andyleap, Anjan Momi, Anthony Goeckner, Antoine Lamielle, Anur, Aranjedeath, ardevd, Arkadiusz Tymiński, Aroun, Arthur Axel fREW Schmidt, Artur Zubilewicz, Ashish Bhate, Aurélien Rainone, BAHADIR YILMAZ, Bart De Vries, Beat Reichenbach, Ben Norcombe, Ben Shepherd, Ben Sidhom, Benedikt Heine, Benno Fünfstück, Benny Ng, boomsquared, Boqin Qin, Boris Rybalkin, Brendan Long, Catfriend1, Cathryne Linenweaver, Cedric Staniewski, Chih-Hsuan Yen, Choongkyu, Chris Howie, Chris Joel, Christian Kujau, Christian Prescott, chucic, cjc7373, Colin Kennedy, Cromefire_, cui, Cyprien Devillez, d-volution, Dan, Daniel Barczyk, Daniel Bergmann, Daniel Martí, Daniel Padrta, Daniil Gentili, Darshil Chanpura, dashangcun, David Rimmer, DeflateAwning, Denis A., Dennis Wilson, derekriemer, DerRockWolf, desbma, Devon G. Redekopp, digital, Dimitri Papadopoulos Orfanos, Dmitry Saveliev, domain, Domenic Horner, Dominik Heidler, Elias, Elias Jarlebring, Elliot Huffman, Emil Hessman, Eng Zer Jun, entity0xfe, Epifeny, epifeny, Eric Lesiuta, Erik Meitner, Evan Spensley, Federico Castagnini, Felix, Felix Ableitner, Felix Lampe, Felix Unterpaintner, Francois-Xavier Gsell, Frank Isemann, Frederic, Gahl Saraf, georgespatton, ghjklw, Gilli Sigurdsson, Gleb Sinyavskiy, Graham Miln, Greg, guangwu, gudvinr, Gusted, Han Boetes, HansK-p, Harrison Jones, Hazem Krimi, Heiko Zuerker, Henrik Bråthen, Hireworks, Hugo Locurcio, Iain Barnett, Ian Johnson, ignacy123, Iskander Sharipov, Jaakko Hannikainen, Jack Croft, Jacob, Jake Peterson, James O'Beirne, James Patterson, Jaroslav Lichtblau, Jaroslav Malec, Jaspitta, Jaya Chithra, Jaya Kumar, Jeffery To, jelle van der Waa, Jens Diemer, Jochen Voss, Johan Vromans, John Rinehart, Jonas Thelemann, Jonathan, Jose Manuel Delicado, JRNitre, jtagcat, Julian Lehrhuber, Jörg Thalheim, Jędrzej Kula, Kapil Sareen, Karol Różycki, Kebin Liu, Keith Harrison, Kelong Cong, Ken'ichi Kamada, Kevin Allen, Kevin Bushiri, Kevin White, Jr., klemens, Kurt Fitzner, kylosus, Lars Lehtonen, Laurent Etiemble, Leo Arias, Liu Siyuan, Lord Landon Agahnim, LSmithx2, Luiz Angelo Daros de Luca, Lukas Lihotzki, Luke Hamburg, luzpaz, Majed Abdulaziz, Marc Laporte, Marcel Meyer, Marcin Dziadus, Marcus Legendre, Mario Majila, Mark Pulford, Martchus, Mateusz Naściszewski, Mateusz Ż, mathias4833, Matic Potočnik, Matt Burke, Matt Robenolt, Matteo Ruina, mattn, Maurizio Tomasi, Max, Max Schulze, MaximAL, Maximilian, Maxwell G, Michael Jephcote, Michael Rienstra, Michael Wang 汪東陽, MichaIng, Migelo, Mike Boone, MikeLund, MikolajTwarog, Mingxuan Lin, mv1005, Nate Morrison, nf, Nicholas Rishel, Nick Busey, Nico Stapelbroek, Nicolas Braud-Santoni, Nicolas Perraut, Niels Peter Roest, Nils Jakobi, NinoM4ster, Nitroretro, NoLooseEnds, Oliver Freyermuth, orangekame3, otbutz, overkill, Oyebanji Jacob Mayowa, Pablo, Pascal Jungblut, Paul Brit, Paul Donald, Pawel Palenica, perewa, Peter Badida, Peter Dave Hello, Peter Hoeg, Peter Marquardt, Phani Rithvij, Phil Davis, Philippe Schommers, Phill Luby, Piotr Bejda, polyfloyd, Prathik P Kulkarni, pullmerge, Quentin Hibon, Rahmi Pruitt, RealCharlesChia, red_led, Robert Carosi, Roberto Santalla, Robin Schoonover, Rohit Tanwar, Roman Zaynetdinov, rubenbe, Ruslan Yevdokymov, Ryan Qian, Ryan Sullivan, Sacheendra Talluri, Scott Klupfel, sec65, Sergey Mishin, Sertonix, Severin von Wnuck-Lipinski, Shaarad Dalvi, Shablone, Shivam Kumar, Simon Mwepu, Simon Pickup, Sly_tom_cat, Sonu Kumar Saw, Stefan Kuntz, Steven Eckhoff, Suhas Gundimeda, Sven Bachmann, Sébastien WENSKE, Tao, Taylor Khan, tbodt, Terrance, TheCreeper, Thomas, Thomas Hipp, Tim Abell, Tim Howes, Tobias Frölich, Tobias Klauser, Tobias Nygren, Tobias Tom, Tom Jakubowski, Tully Robinson, Tyler Brazier, Tyler Kropp, Umer-Azaz, Unrud, Val Markovic, vapatel2, Veeti Paananen, Victor Buinsky, Vik, Vil Brekin, villekalliomaki, Vladimir Rusinov, vvaswani, wangguoliang, WangXi, Will Rouesnel, William A. Kennington III, wouter bolsterlee, xarx00, Xavier O., xjtdy888, Yannic A., yparitcher, 佛跳墙, 落心
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -62,7 +62,6 @@ Jakob Borg, Audrius Butkevicius, Simon Frei, Tomasz Wilczyński, Alexander Graf,
|
||||
<li><a href="https://github.com/davecgh/go-spew">davecgh/go-spew</a>, Copyright © 2012-2016 Dave Collins.</li>
|
||||
<li><a href="https://github.com/go-asn1-ber/asn1-ber">go-asn1-ber/asn1-ber</a>, Copyright © 2011-2015 Michael Mitton (mmitton@gmail.com).</li>
|
||||
<li><a href="https://github.com/go-ldap/ldap">go-ldap/ldap</a>, Copyright © 2011-2015 Michael Mitton (mmitton@gmail.com).</li>
|
||||
<li><a href="https://go.yaml.in/yaml/v2">go.yaml.in/yaml/v2</a>, Copyright © 2026, the go.yaml.in/yaml/v2 authors.</li>
|
||||
<li><a href="https://github.com/gobwas/glob">gobwas/glob</a>, Copyright © 2016 Sergey Kamardin.</li>
|
||||
<li><a href="https://github.com/gofrs/flock">gofrs/flock</a>, Copyright © 2018-2025, The Gofrs.</li>
|
||||
<li><a href="https://github.com/golang/snappy">golang/snappy</a>, Copyright © 2011 The Snappy-Go Authors.</li>
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
angular.module('syncthing.core')
|
||||
.directive('lazyCollapse', function () {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function (scope, element) {
|
||||
// Render panel content only while expanded. Collapsed panels
|
||||
// contribute zero watchers. Bootstrap's show/hidden events
|
||||
// fire before/after the animation, so the DOM is present
|
||||
// during the transition.
|
||||
scope.lazyReady = element.hasClass('in');
|
||||
|
||||
$(element).on('show.bs.collapse', function () {
|
||||
scope.$apply(function () {
|
||||
scope.lazyReady = true;
|
||||
});
|
||||
});
|
||||
|
||||
$(element).on('hidden.bs.collapse', function () {
|
||||
scope.$apply(function () {
|
||||
scope.lazyReady = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -1139,20 +1139,20 @@ angular.module('syncthing.core')
|
||||
};
|
||||
|
||||
$scope.syncPercentage = function (folder) {
|
||||
if (typeof $scope.model[folder] === 'undefined') {
|
||||
var model = $scope.model[folder];
|
||||
if (typeof model === 'undefined') {
|
||||
return 100;
|
||||
}
|
||||
if ($scope.model[folder].needTotalItems === 0) {
|
||||
if (model.needTotalItems === 0) {
|
||||
return 100;
|
||||
}
|
||||
if (($scope.model[folder].needBytes == 0 && $scope.model[folder].needDeletes > 0) || $scope.model[folder].globalBytes == 0) {
|
||||
// We don't need any data, but we have deletes that we need
|
||||
// to do. Drop down the completion percentage to indicate
|
||||
// that we have stuff to do.
|
||||
// Do the same thing in case we only have zero byte files to sync.
|
||||
if (model.needBytes == 0 && model.needTotalItems > 0) {
|
||||
// We don't need any data, but we have deletes, directories,
|
||||
// symlinks or zero byte files that we need to do. Drop down the
|
||||
// completion percentage to indicate that we have stuff to do.
|
||||
return 95;
|
||||
}
|
||||
return progressIntegerPercentage($scope.model[folder].inSyncBytes, $scope.model[folder].globalBytes);
|
||||
return progressIntegerPercentage(model.inSyncBytes, model.globalBytes);
|
||||
};
|
||||
|
||||
$scope.scanPercentage = function (folder) {
|
||||
@@ -2997,6 +2997,9 @@ angular.module('syncthing.core')
|
||||
$scope.restoreVersions.tree = $("#restoreTree").fancytree({
|
||||
extensions: ["table", "filter", "glyph"],
|
||||
quicksearch: true,
|
||||
// Node titles are remote-controlled file/path
|
||||
// components; render them as text, not HTML.
|
||||
escapeTitles: true,
|
||||
filter: {
|
||||
hideExpanders: true,
|
||||
mode: "hide"
|
||||
|
||||
@@ -79,6 +79,7 @@ type DB interface {
|
||||
|
||||
// Cleanup
|
||||
DropAllFiles(folder string, device protocol.DeviceID) error
|
||||
DropFolderDevice(folder string, device protocol.DeviceID) error
|
||||
DropDevice(device protocol.DeviceID) error
|
||||
DropFilesNamed(folder string, device protocol.DeviceID, names []string) error
|
||||
DropFolder(folder string) error
|
||||
|
||||
@@ -128,6 +128,11 @@ func (m metricsDB) DropAllFiles(folder string, device protocol.DeviceID) error {
|
||||
return m.DB.DropAllFiles(folder, device)
|
||||
}
|
||||
|
||||
func (m metricsDB) DropFolderDevice(folder string, device protocol.DeviceID) error {
|
||||
defer m.account(folder, "DropFolderDevice")()
|
||||
return m.DB.DropFolderDevice(folder, device)
|
||||
}
|
||||
|
||||
func (m metricsDB) DropDevice(device protocol.DeviceID) error {
|
||||
defer m.account("-", "DropDevice")()
|
||||
return m.DB.DropDevice(device)
|
||||
|
||||
@@ -27,7 +27,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
currentSchemaVersion = 5
|
||||
currentSchemaVersion = 6
|
||||
applicationIDMain = 0x53546d6e // "STmn", Syncthing main database
|
||||
applicationIDFolder = 0x53546664 // "STfd", Syncthing folder database
|
||||
)
|
||||
@@ -49,7 +49,7 @@ type baseDB struct {
|
||||
tplInput map[string]any
|
||||
}
|
||||
|
||||
func openBase(path string, maxConns int, pragmas, schemaScripts, migrationScripts []string) (*baseDB, error) {
|
||||
func openBase(path string, maxOpenConns, maxIdleConns int, pragmas, schemaScripts, migrationScripts []string) (*baseDB, error) {
|
||||
// Open the database with options to enable foreign keys and recursive
|
||||
// triggers (needed for the delete+insert triggers on row replace).
|
||||
pathURL := url.URL{
|
||||
@@ -62,8 +62,8 @@ func openBase(path string, maxConns int, pragmas, schemaScripts, migrationScript
|
||||
return nil, wrap(err)
|
||||
}
|
||||
|
||||
sqlDB.SetMaxOpenConns(maxConns)
|
||||
sqlDB.SetMaxIdleConns(maxConns)
|
||||
sqlDB.SetMaxOpenConns(maxOpenConns)
|
||||
sqlDB.SetMaxIdleConns(maxIdleConns)
|
||||
|
||||
for _, pragma := range pragmas {
|
||||
if _, err := sqlDB.Exec("PRAGMA " + pragma); err != nil {
|
||||
|
||||
@@ -255,6 +255,17 @@ func (s *DB) DropAllFiles(folder string, device protocol.DeviceID) error {
|
||||
return fdb.DropAllFiles(device)
|
||||
}
|
||||
|
||||
func (s *DB) DropFolderDevice(folder string, device protocol.DeviceID) error {
|
||||
fdb, err := s.getFolderDB(folder, false)
|
||||
if errors.Is(err, errNoSuchFolder) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fdb.DropDevice(device)
|
||||
}
|
||||
|
||||
func (s *DB) DropFilesNamed(folder string, device protocol.DeviceID, names []string) error {
|
||||
fdb, err := s.getFolderDB(folder, false)
|
||||
if errors.Is(err, errNoSuchFolder) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -20,10 +21,23 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
maxDBConns = 6
|
||||
// maxIdleConns is set so that most db operations will usually fit
|
||||
// within one of those connections and hence use their persistent page
|
||||
// cache. Additional connections on top of these will allocate their own
|
||||
// page cache (same as any other), but it will be deallocated on
|
||||
// connection close.
|
||||
maxIdleConns = 4
|
||||
|
||||
minDeleteRetention = 24 * time.Hour
|
||||
)
|
||||
|
||||
// maxOpenConns is sized for handling spikes. The primary driver is the
|
||||
// Copiers folder option which may result in up to 2*NumCPU concurrent
|
||||
// iterations. We reserve additional space on top of this to serve
|
||||
// additional operations, some of which may be reentrant (queries within
|
||||
// iterators) without deadlock.
|
||||
var maxOpenConns = max(16, 4*runtime.NumCPU())
|
||||
|
||||
type DB struct {
|
||||
*baseDB
|
||||
|
||||
@@ -69,7 +83,7 @@ func Open(path string, opts ...Option) (*DB, error) {
|
||||
initTmpDir(path)
|
||||
|
||||
mainPath := filepath.Join(path, "main.db")
|
||||
mainBase, err := openBase(mainPath, maxDBConns, pragmas, schemas, migrations)
|
||||
mainBase, err := openBase(mainPath, maxOpenConns, maxIdleConns, pragmas, schemas, migrations)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -120,7 +134,7 @@ func OpenForMigration(path string) (*DB, error) {
|
||||
initTmpDir(path)
|
||||
|
||||
mainPath := filepath.Join(path, "main.db")
|
||||
mainBase, err := openBase(mainPath, 1, pragmas, schemas, migrations)
|
||||
mainBase, err := openBase(mainPath, 1, 1, pragmas, schemas, migrations)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"errors"
|
||||
"iter"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
@@ -30,7 +29,6 @@ import (
|
||||
const (
|
||||
folderID = "test"
|
||||
blockSize = 128 << 10
|
||||
dirSize = 128
|
||||
)
|
||||
|
||||
func TestBasics(t *testing.T) {
|
||||
@@ -73,11 +71,11 @@ func TestBasics(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
const (
|
||||
localSize = (1+2+3)*blockSize + dirSize
|
||||
localSize = (1+2+3)*blockSize
|
||||
remoteSize = (3 + 4 + 5) * blockSize
|
||||
globalSize = (2+3+3+4+5)*blockSize + dirSize
|
||||
globalSize = (2+3+3+4+5)*blockSize
|
||||
needSizeLocal = remoteSize
|
||||
needSizeRemote = (2+3)*blockSize + dirSize
|
||||
needSizeRemote = (2+3)*blockSize
|
||||
)
|
||||
|
||||
t.Run("SchemaVersion", func(t *testing.T) {
|
||||
@@ -799,6 +797,13 @@ func TestDropAllFiles(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// The sequence is non-zero before the drop
|
||||
if seq, err := db.GetDeviceSequence("a", protocol.DeviceID{1}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if seq == 0 {
|
||||
t.Error("expected non-zero sequence before drop")
|
||||
}
|
||||
|
||||
// Drop folder A
|
||||
if err := db.DropAllFiles("a", protocol.DeviceID{1}); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -826,6 +831,20 @@ func TestDropAllFiles(t *testing.T) {
|
||||
t.Error("expected count to be two")
|
||||
}
|
||||
|
||||
// The device sequence for the dropped folder is reset to zero.
|
||||
if seq, err := db.GetDeviceSequence("a", protocol.DeviceID{1}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if seq != 0 {
|
||||
t.Log(seq)
|
||||
t.Error("expected sequence to be reset to zero after DropAllFiles")
|
||||
}
|
||||
// Sequence for the untouched folder is unaffected.
|
||||
if seq, err := db.GetDeviceSequence("b", protocol.DeviceID{1}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if seq == 0 {
|
||||
t.Error("expected non-zero sequence for untouched folder")
|
||||
}
|
||||
|
||||
// Drop things that don't exist
|
||||
if err := db.DropAllFiles("a", protocol.DeviceID{99}); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -838,6 +857,67 @@ func TestDropAllFiles(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDropFolderDevice(t *testing.T) {
|
||||
db, err := Open(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := db.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
|
||||
// Files from device 1 in folder a
|
||||
err = db.Update("a", protocol.DeviceID{1}, []protocol.FileInfo{
|
||||
genFile("test1", 1, 101),
|
||||
genFile("test2", 2, 102),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Device 1 has an index ID.
|
||||
if err := db.SetIndexID("a", protocol.DeviceID{1}, protocol.IndexID(0xdeadbeef)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if id, err := db.GetIndexID("a", protocol.DeviceID{1}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if id != protocol.IndexID(0xdeadbeef) {
|
||||
t.Errorf("expected index ID to be set, got %v", id)
|
||||
}
|
||||
|
||||
// Drop device 1 from folder a
|
||||
if err := db.DropFolderDevice("a", protocol.DeviceID{1}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Files for device 1 in folder a are gone
|
||||
if _, ok, err := db.GetDeviceFile("a", protocol.DeviceID{1}, "test1"); err != nil || ok {
|
||||
t.Log(err, ok)
|
||||
t.Error("expected device 1 file in folder A to not exist")
|
||||
}
|
||||
if c, err := db.CountLocal("a", protocol.DeviceID{1}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if c.Files != 0 {
|
||||
t.Log(c)
|
||||
t.Error("expected device 1 count in folder A to be zero")
|
||||
}
|
||||
|
||||
// The index ID for device 1 in folder A is gone.
|
||||
if id, err := db.GetIndexID("a", protocol.DeviceID{1}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if id != 0 {
|
||||
t.Errorf("expected index ID to be cleared, got %v", id)
|
||||
}
|
||||
if seq, err := db.GetDeviceSequence("a", protocol.DeviceID{1}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if seq != 0 {
|
||||
t.Log(seq)
|
||||
t.Error("expected sequence to be zero after DropFolderDevice")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConcurrentUpdate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -1168,7 +1248,7 @@ func TestOpenSpecialName(t *testing.T) {
|
||||
|
||||
// Create a "base" dir that is in the way if the path becomes
|
||||
// incorrectly truncated in the next steps.
|
||||
base := path.Join(dir, "test")
|
||||
base := filepath.Join(dir, "test")
|
||||
if err := os.Mkdir(base, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ func openFolderDB(folder, path string, deleteRetention time.Duration) (*folderDB
|
||||
"sql/migrations/folder/*",
|
||||
}
|
||||
|
||||
base, err := openBase(path, maxDBConns, pragmas, schemas, migrations)
|
||||
base, err := openBase(path, maxOpenConns, maxIdleConns, pragmas, schemas, migrations)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -75,7 +75,7 @@ func openFolderDBForMigration(folder, path string, deleteRetention time.Duration
|
||||
"sql/schema/folder/*",
|
||||
}
|
||||
|
||||
base, err := openBase(path, 1, pragmas, schemas, nil)
|
||||
base, err := openBase(path, 1, 1, pragmas, schemas, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -111,10 +111,6 @@ func (s *folderDB) Update(device protocol.DeviceID, fs []protocol.FileInfo, opti
|
||||
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
|
||||
@@ -235,6 +231,13 @@ func (s *folderDB) DropAllFiles(device protocol.DeviceID) error {
|
||||
defer tx.Rollback() //nolint:errcheck
|
||||
txp := &txPreparedStmts{Tx: tx}
|
||||
|
||||
if _, err := tx.Exec(`
|
||||
UPDATE indexids SET sequence = 0
|
||||
WHERE device_idx = ?
|
||||
`, deviceIdx); err != nil {
|
||||
return wrap(err)
|
||||
}
|
||||
|
||||
// Drop all the file entries
|
||||
|
||||
result, err := tx.Exec(`
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
-- Copyright (C) 2026 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/.
|
||||
|
||||
-- All non-file entries should have size zero.
|
||||
-- Directories were previously stored with a "synthetic" size of 128.
|
||||
UPDATE files
|
||||
SET size = 0
|
||||
WHERE type != 0
|
||||
;
|
||||
UPDATE counts
|
||||
SET size = 0
|
||||
WHERE type != 0
|
||||
;
|
||||
@@ -106,6 +106,10 @@ func (i indirectFI) FileInfo() (protocol.FileInfo, error) {
|
||||
fi.Blocks = bl.Blocks
|
||||
}
|
||||
fi.Name = osutil.NativeFilename(fi.Name)
|
||||
// Undo earlier behaviour of setting a synthetic size on dirs.
|
||||
if fi.Type != protocol.FileInfoTypeFile && fi.Size > 0 {
|
||||
fi.Size = 0
|
||||
}
|
||||
return protocol.FileInfoFromDB(&fi), nil
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -1248,7 +1248,7 @@ func (s *service) getSupportBundle(w http.ResponseWriter, r *http.Request) {
|
||||
zipFilePath := filepath.Join(locations.GetBaseDir(locations.ConfigBaseDir), zipFileName)
|
||||
|
||||
// Write buffer zip to local zip file (back up)
|
||||
if err := os.WriteFile(zipFilePath, zipFilesBuffer.Bytes(), 0o600); err != nil {
|
||||
if err := os.WriteFile(zipFilePath, zipFilesBuffer.Bytes(), 0o666); err != nil {
|
||||
slog.Warn("Failed to create support bundle zip (file)", slogutil.FilePath(zipFilePath), slogutil.Error(err))
|
||||
}
|
||||
|
||||
@@ -1774,7 +1774,7 @@ func fileIntfJSONMap(f protocol.FileInfo) map[string]interface{} {
|
||||
out := map[string]interface{}{
|
||||
"name": f.FileName(),
|
||||
"type": f.FileType().String(),
|
||||
"size": f.FileSize(),
|
||||
"size": f.Size,
|
||||
"deleted": f.IsDeleted(),
|
||||
"invalid": f.IsInvalid(),
|
||||
"ignored": f.IsIgnored(),
|
||||
|
||||
@@ -74,11 +74,7 @@ func (s *staticsServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *staticsServer) serveAsset(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Cache-Control", "no-cache, must-revalidate")
|
||||
|
||||
file := r.URL.Path
|
||||
|
||||
if file[0] == '/' {
|
||||
file = file[1:]
|
||||
}
|
||||
file := strings.TrimPrefix(r.URL.Path, "/")
|
||||
|
||||
if file == "" {
|
||||
file = "index.html"
|
||||
|
||||
+9
-3
@@ -1762,9 +1762,10 @@ func TestConfigChanges(t *testing.T) {
|
||||
|
||||
folder2Path := "/rest/config/folders/folder2"
|
||||
|
||||
// Create a folder and add another
|
||||
// Create a folder and add another. Give folder2 a non-default
|
||||
// RescanIntervalS so we can verify it survives a later partial PATCH.
|
||||
mod(http.MethodPut, "/rest/config/folders", []config.FolderConfiguration{{ID: "folder1", Path: "folder1"}})
|
||||
mod(http.MethodPut, folder2Path, config.FolderConfiguration{ID: "folder2", Path: "folder2"})
|
||||
mod(http.MethodPut, folder2Path, config.FolderConfiguration{ID: "folder2", Path: "folder2", RescanIntervalS: 1234})
|
||||
|
||||
// Check they are there
|
||||
get("/rest/config/folders/folder1").Body.Close()
|
||||
@@ -1779,9 +1780,14 @@ func TestConfigChanges(t *testing.T) {
|
||||
if err := unmarshalTo(resp.Body, &folder); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !dev.Paused {
|
||||
if !folder.Paused {
|
||||
t.Error("Expected folder to be paused")
|
||||
}
|
||||
// A partial PATCH must not reset other (default-tagged) attributes to
|
||||
// their default values.
|
||||
if folder.RescanIntervalS != 1234 {
|
||||
t.Error("Expected RescanIntervalS to be preserved as 1234, got", folder.RescanIntervalS)
|
||||
}
|
||||
|
||||
// Delete folder2
|
||||
req, _ := http.NewRequest(http.MethodDelete, baseURL+folder2Path, nil)
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright (C) 2026 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 auto
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const customBuildMarker = "It syncs .stignore now!"
|
||||
|
||||
func TestCustomBuildMarkerIsEmbedded(t *testing.T) {
|
||||
asset, ok := Assets()["default/index.html"]
|
||||
if !ok {
|
||||
t.Fatal("default/index.html is missing from embedded GUI assets")
|
||||
}
|
||||
|
||||
content := asset.Content
|
||||
if asset.Gzipped {
|
||||
reader, err := gzip.NewReader(strings.NewReader(asset.Content))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
data, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
content = string(data)
|
||||
}
|
||||
|
||||
if !strings.Contains(content, customBuildMarker) {
|
||||
t.Fatalf("embedded GUI assets do not contain custom build marker %q", customBuildMarker)
|
||||
}
|
||||
}
|
||||
Vendored
-1
@@ -116,7 +116,6 @@
|
||||
<autoUpgradeIntervalH>12</autoUpgradeIntervalH>
|
||||
<upgradeToPreReleases>false</upgradeToPreReleases>
|
||||
<keepTemporariesH>24</keepTemporariesH>
|
||||
<cacheIgnoredFiles>false</cacheIgnoredFiles>
|
||||
<progressUpdateIntervalS>5</progressUpdateIntervalS>
|
||||
<limitBandwidthInLan>false</limitBandwidthInLan>
|
||||
<minHomeDiskFree unit="%">1</minHomeDiskFree>
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ import (
|
||||
// or, somewhere along the way the "+" in the version tag disappeared:
|
||||
// syncthing v1.23.7-dev.26.gdf7b56ae.dirty-stversionextra "Fermium Flea" (go1.20.5 darwin-arm64) jb@ok.kastelo.net 2023-07-12 06:55:26 UTC [Some Wrapper, purego, stnoupgrade]
|
||||
var (
|
||||
longVersionRE = regexp.MustCompile(`syncthing\s+(v[^\s]+)\s+"([^"]+)"\s\(([^\s]+)\s+([^-]+)-([^)]+)\)\s+([^\s]+)[^\[]*(?:\[(.+)\])?$`)
|
||||
longVersionRE = regexp.MustCompile(`syncthing\s+(v[^\s]+)\s+"([^"]+)"\s\(([^\s]+)\s+([^-]+)-([^)]+)\)\s+([^\s]+)[^\[]*(?:\[(.+)\])?`)
|
||||
gitExtraRE = regexp.MustCompile(`\.\d+\.g[0-9a-f]+`) // ".1.g6aaae618"
|
||||
gitExtraSepRE = regexp.MustCompile(`[.-]`) // dot or dash
|
||||
)
|
||||
|
||||
@@ -57,6 +57,20 @@ func TestParseVersion(t *testing.T) {
|
||||
Extra: []string{"Some Wrapper", "purego", "stnoupgrade"},
|
||||
},
|
||||
},
|
||||
{
|
||||
longVersion: `2026-05-18 14:53:32 INF syncthing v2.0.3 "Hafnium Hornet" (go1.25.0 darwin-amd64) builder@github.syncthing.net 2025-08-22 07:00:05 UTC [stnoupgrade] (log.pkg=main)`,
|
||||
parsed: VersionParts{
|
||||
Version: "v2.0.3",
|
||||
Tag: "v2.0.3",
|
||||
Commit: "",
|
||||
Codename: "Hafnium Hornet",
|
||||
Runtime: "go1.25.0",
|
||||
GOOS: "darwin",
|
||||
GOARCH: "amd64",
|
||||
Builder: "builder@github.syncthing.net",
|
||||
Extra: []string{"stnoupgrade"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
|
||||
@@ -72,7 +72,6 @@ func TestDefaultValues(t *testing.T) {
|
||||
NATTimeoutS: 10,
|
||||
AutoUpgradeIntervalH: 12,
|
||||
KeepTemporariesH: 24,
|
||||
CacheIgnoredFiles: false,
|
||||
ProgressUpdateIntervalS: 5,
|
||||
LimitBandwidthInLan: false,
|
||||
MinHomeDiskFree: Size{1, "%"},
|
||||
@@ -278,7 +277,6 @@ func TestOverriddenValues(t *testing.T) {
|
||||
NATTimeoutS: 15,
|
||||
AutoUpgradeIntervalH: 24,
|
||||
KeepTemporariesH: 48,
|
||||
CacheIgnoredFiles: true,
|
||||
ProgressUpdateIntervalS: 10,
|
||||
LimitBandwidthInLan: true,
|
||||
MinHomeDiskFree: Size{5.2, "%"},
|
||||
|
||||
@@ -9,7 +9,6 @@ package config
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -168,7 +167,7 @@ func (f *FolderConfiguration) CreateMarker() error {
|
||||
ffs := f.Filesystem()
|
||||
|
||||
// Create the marker as a directory
|
||||
err := ffs.Mkdir(DefaultMarkerName, 0o755)
|
||||
err := ffs.Mkdir(DefaultMarkerName, fs.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -176,7 +175,7 @@ func (f *FolderConfiguration) CreateMarker() error {
|
||||
// Create a file inside it, reducing the risk of the marker directory
|
||||
// being removed by automated cleanup tools.
|
||||
markerFile := filepath.Join(DefaultMarkerName, f.markerFilename())
|
||||
if err := fs.WriteFile(ffs, markerFile, f.markerContents(), 0o644); err != nil {
|
||||
if err := fs.WriteFile(ffs, markerFile, f.markerContents(), 0o666); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -246,19 +245,10 @@ func (f *FolderConfiguration) checkFilesystemPath(ffs fs.Filesystem, path string
|
||||
}
|
||||
|
||||
func (f *FolderConfiguration) CreateRoot() (err error) {
|
||||
// Directory permission bits. Will be filtered down to something
|
||||
// sane by umask on Unixes.
|
||||
permBits := fs.FileMode(0o777)
|
||||
if build.IsWindows {
|
||||
// Windows has no umask so we must chose a safer set of bits to
|
||||
// begin with.
|
||||
permBits = 0o700
|
||||
}
|
||||
|
||||
filesystem := f.Filesystem()
|
||||
|
||||
if _, err = filesystem.Stat("."); fs.IsNotExist(err) {
|
||||
err = filesystem.MkdirAll(".", permBits)
|
||||
err = filesystem.MkdirAll(".", fs.ModePerm)
|
||||
}
|
||||
|
||||
return err
|
||||
@@ -404,16 +394,6 @@ func (f XattrFilter) GetMaxTotalSize() int {
|
||||
return f.MaxTotalSize
|
||||
}
|
||||
|
||||
func (f *FolderConfiguration) UnmarshalJSON(data []byte) error {
|
||||
structutil.SetDefaults(f)
|
||||
|
||||
// avoid recursing into this method
|
||||
type noCustomUnmarshal FolderConfiguration
|
||||
ptr := (*noCustomUnmarshal)(f)
|
||||
|
||||
return json.Unmarshal(data, ptr)
|
||||
}
|
||||
|
||||
func (f *FolderConfiguration) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
structutil.SetDefaults(f)
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/syncthing/syncthing/internal/slogutil"
|
||||
"github.com/syncthing/syncthing/lib/build"
|
||||
"github.com/syncthing/syncthing/lib/fs"
|
||||
"github.com/syncthing/syncthing/lib/netutil"
|
||||
"github.com/syncthing/syncthing/lib/upgrade"
|
||||
@@ -224,27 +223,20 @@ func migrateToConfigV24(cfg *Configuration) {
|
||||
}
|
||||
|
||||
func migrateToConfigV23(cfg *Configuration) {
|
||||
permBits := fs.FileMode(0o777)
|
||||
if build.IsWindows {
|
||||
// Windows has no umask so we must chose a safer set of bits to
|
||||
// begin with.
|
||||
permBits = 0o700
|
||||
}
|
||||
|
||||
// Upgrade code remains hardcoded for .stfolder despite configurable
|
||||
// marker name in later versions.
|
||||
|
||||
for i := range cfg.Folders {
|
||||
fs := cfg.Folders[i].Filesystem()
|
||||
ffs := cfg.Folders[i].Filesystem()
|
||||
// Invalid config posted, or tests.
|
||||
if fs == nil {
|
||||
if ffs == nil {
|
||||
continue
|
||||
}
|
||||
if stat, err := fs.Stat(DefaultMarkerName); err == nil && !stat.IsDir() {
|
||||
err = fs.Remove(DefaultMarkerName)
|
||||
if stat, err := ffs.Stat(DefaultMarkerName); err == nil && !stat.IsDir() {
|
||||
err = ffs.Remove(DefaultMarkerName)
|
||||
if err == nil {
|
||||
err = fs.Mkdir(DefaultMarkerName, permBits)
|
||||
fs.Hide(DefaultMarkerName) // ignore error
|
||||
err = ffs.Mkdir(DefaultMarkerName, fs.ModePerm)
|
||||
ffs.Hide(DefaultMarkerName) // ignore error
|
||||
}
|
||||
if err != nil {
|
||||
slog.Warn("Failed to upgrade folder marker", slogutil.Error(err))
|
||||
@@ -328,10 +320,6 @@ func migrateToConfigV15(cfg *Configuration) {
|
||||
}
|
||||
|
||||
func migrateToConfigV14(cfg *Configuration) {
|
||||
// Not using the ignore cache is the new default. Disable it on existing
|
||||
// configurations.
|
||||
cfg.Options.CacheIgnoredFiles = false
|
||||
|
||||
// Migrate UPnP -> NAT options
|
||||
cfg.Options.NATEnabled = cfg.Options.DeprecatedUPnPEnabled
|
||||
cfg.Options.DeprecatedUPnPEnabled = false
|
||||
|
||||
@@ -45,7 +45,6 @@ type OptionsConfiguration struct {
|
||||
AutoUpgradeIntervalH int `json:"autoUpgradeIntervalH" xml:"autoUpgradeIntervalH" default:"12"`
|
||||
UpgradeToPreReleases bool `json:"upgradeToPreReleases" xml:"upgradeToPreReleases"`
|
||||
KeepTemporariesH int `json:"keepTemporariesH" xml:"keepTemporariesH" default:"24"`
|
||||
CacheIgnoredFiles bool `json:"cacheIgnoredFiles" xml:"cacheIgnoredFiles" default:"false"`
|
||||
ProgressUpdateIntervalS int `json:"progressUpdateIntervalS" xml:"progressUpdateIntervalS" default:"5"`
|
||||
LimitBandwidthInLan bool `json:"limitBandwidthInLan" xml:"limitBandwidthInLan" default:"false"`
|
||||
MinHomeDiskFree Size `json:"minHomeDiskFree" xml:"minHomeDiskFree" default:"1 %"`
|
||||
|
||||
Vendored
-1
@@ -42,7 +42,6 @@
|
||||
<restartOnWakeup>true</restartOnWakeup>
|
||||
<autoUpgradeIntervalH>0</autoUpgradeIntervalH>
|
||||
<keepTemporariesH>24</keepTemporariesH>
|
||||
<cacheIgnoredFiles>true</cacheIgnoredFiles>
|
||||
<progressUpdateIntervalS>5</progressUpdateIntervalS>
|
||||
<symlinksEnabled>true</symlinksEnabled>
|
||||
<limitBandwidthInLan>false</limitBandwidthInLan>
|
||||
|
||||
-1
@@ -22,7 +22,6 @@
|
||||
<restartOnWakeup>false</restartOnWakeup>
|
||||
<autoUpgradeIntervalH>24</autoUpgradeIntervalH>
|
||||
<keepTemporariesH>48</keepTemporariesH>
|
||||
<cacheIgnoredFiles>true</cacheIgnoredFiles>
|
||||
<progressUpdateIntervalS>10</progressUpdateIntervalS>
|
||||
<symlinksEnabled>false</symlinksEnabled>
|
||||
<limitBandwidthInLan>true</limitBandwidthInLan>
|
||||
|
||||
@@ -827,6 +827,11 @@ func (s *service) logListenAddressesChangedEvent(l ListenerAddresses) {
|
||||
func (s *service) CommitConfiguration(from, to config.Configuration) bool {
|
||||
newDevices := make(map[protocol.DeviceID]bool, len(to.Devices))
|
||||
for _, dev := range to.Devices {
|
||||
if dev.DeviceID == s.myID {
|
||||
// Do not report connection metrics for ourselves
|
||||
continue
|
||||
}
|
||||
|
||||
newDevices[dev.DeviceID] = true
|
||||
registerDeviceMetrics(dev.DeviceID.String())
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/url"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -185,9 +186,10 @@ func (t *tcpListener) WANAddresses() []*url.URL {
|
||||
|
||||
t.mut.RUnlock()
|
||||
|
||||
// If we support ReusePort, add an unspecified zero port address, which will be resolved by the discovery server
|
||||
// in hopes that TCP punch through works.
|
||||
if dialer.SupportsReusePort {
|
||||
// If we support ReusePort, and we are already announcing an unspecified
|
||||
// address, add an unspecified zero port address, which will be resolved
|
||||
// by the discovery server in hopes that TCP punch through works.
|
||||
if dialer.SupportsReusePort && slices.ContainsFunc(uris, func(u *url.URL) bool { return u.Hostname() == "0.0.0.0" }) {
|
||||
uri := *t.uri
|
||||
uri.Host = "0.0.0.0:0"
|
||||
uris = append([]*url.URL{&uri}, uris...)
|
||||
|
||||
@@ -289,7 +289,6 @@ func (c *globalClient) sendAnnouncement(ctx context.Context, timer *time.Timer)
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode > 299 {
|
||||
slog.DebugContext(ctx, "announce POST", "server", c.server, "status", resp.Status)
|
||||
c.setError(errors.New(resp.Status))
|
||||
|
||||
if h := resp.Header.Get("Retry-After"); h != "" {
|
||||
@@ -408,6 +407,7 @@ func (c *idCheckingHTTPClient) Get(ctx context.Context, url string) (*http.Respo
|
||||
return nil, err
|
||||
}
|
||||
if err := c.check(resp); err != nil {
|
||||
resp.Body.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -420,6 +420,7 @@ func (c *idCheckingHTTPClient) Post(ctx context.Context, url, ctype string, data
|
||||
return nil, err
|
||||
}
|
||||
if err := c.check(resp); err != nil {
|
||||
resp.Body.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
+14
-19
@@ -174,7 +174,7 @@ func (f *BasicFilesystem) MkdirAll(path string, perm FileMode) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return f.mkdirAll(path, os.FileMode(perm))
|
||||
return os.MkdirAll(path, os.FileMode(perm))
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) Lstat(name string) (FileInfo, error) {
|
||||
@@ -240,23 +240,24 @@ func (f *BasicFilesystem) DirNames(name string) ([]string, error) {
|
||||
return names, nil
|
||||
}
|
||||
|
||||
// Open opens the file for reading, while not following symlinks
|
||||
func (f *BasicFilesystem) Open(name string) (File, error) {
|
||||
rootedName, err := f.rooted(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fd, err := os.Open(rootedName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return basicFile{fd, name}, err
|
||||
return f.OpenFile(name, os.O_RDONLY, 0)
|
||||
}
|
||||
|
||||
// OpenFile is the generalised file open call, using the flags to determine
|
||||
// the open semantics. We always add O_NOFOLLOW, disallowing opening files
|
||||
// by following symlinks, unless OptFollow is set.
|
||||
func (f *BasicFilesystem) OpenFile(name string, flags int, mode FileMode) (File, error) {
|
||||
rootedName, err := f.rooted(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if flags&OptFollow != 0 {
|
||||
flags &^= OptFollow // unset the synthetic OptFollow bit
|
||||
} else {
|
||||
flags |= optNoFollow
|
||||
}
|
||||
fd, err := os.OpenFile(rootedName, flags, os.FileMode(mode))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -264,16 +265,10 @@ func (f *BasicFilesystem) OpenFile(name string, flags int, mode FileMode) (File,
|
||||
return basicFile{fd, name}, err
|
||||
}
|
||||
|
||||
// Create opens a file for writing, creating it as required. The Create will
|
||||
// fail if the destination is a symlink.
|
||||
func (f *BasicFilesystem) Create(name string) (File, error) {
|
||||
rootedName, err := f.rooted(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fd, err := os.Create(rootedName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return basicFile{fd, name}, err
|
||||
return f.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o666)
|
||||
}
|
||||
|
||||
func (*BasicFilesystem) Walk(_ string, _ WalkFunc) error {
|
||||
|
||||
@@ -14,8 +14,11 @@ import (
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
const optNoFollow = syscall.O_NOFOLLOW
|
||||
|
||||
func (f *BasicFilesystem) CreateSymlink(target, name string) error {
|
||||
name, err := f.rooted(name)
|
||||
if err != nil {
|
||||
@@ -32,10 +35,6 @@ func (f *BasicFilesystem) ReadSymlink(name string) (string, error) {
|
||||
return os.Readlink(name)
|
||||
}
|
||||
|
||||
func (*BasicFilesystem) mkdirAll(path string, perm os.FileMode) error {
|
||||
return os.MkdirAll(path, perm)
|
||||
}
|
||||
|
||||
// Unhide is a noop on unix, as unhiding files requires renaming them.
|
||||
// We still check that the relative path does not try to escape the root
|
||||
func (f *BasicFilesystem) Unhide(name string) error {
|
||||
|
||||
@@ -4,11 +4,10 @@
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
//go:build !(solaris && !cgo) && !(darwin && !cgo) && !(darwin && kqueue) && !(android && amd64)
|
||||
//go:build !(solaris && !cgo) && !(darwin && !cgo) && !(darwin && kqueue)
|
||||
// +build !solaris cgo
|
||||
// +build !darwin cgo
|
||||
// +build !darwin !kqueue
|
||||
// +build !android !amd64
|
||||
|
||||
package fs
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
//go:build (solaris && !cgo) || (darwin && !cgo) || (android && amd64) || (darwin && kqueue)
|
||||
// +build solaris,!cgo darwin,!cgo android,amd64 darwin,kqueue
|
||||
//go:build (solaris && !cgo) || (darwin && !cgo) || (darwin && kqueue)
|
||||
// +build solaris,!cgo darwin,!cgo darwin,kqueue
|
||||
|
||||
package fs
|
||||
|
||||
|
||||
+28
-104
@@ -5,22 +5,22 @@
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package fs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"math/bits"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
const optNoFollow = 0 // not defined for Windows
|
||||
|
||||
var errNotSupported = errors.New("symlinks not supported")
|
||||
|
||||
func (BasicFilesystem) ReadSymlink(path string) (string, error) {
|
||||
@@ -31,57 +31,6 @@ func (BasicFilesystem) CreateSymlink(target, name string) error {
|
||||
return errNotSupported
|
||||
}
|
||||
|
||||
// Required due to https://github.com/golang/go/issues/10900
|
||||
func (f *BasicFilesystem) mkdirAll(path string, perm os.FileMode) error {
|
||||
// Fast path: if we can tell whether path is a directory or file, stop with success or error.
|
||||
dir, err := os.Stat(path)
|
||||
if err == nil {
|
||||
if dir.IsDir() {
|
||||
return nil
|
||||
}
|
||||
return &os.PathError{
|
||||
Op: "mkdir",
|
||||
Path: path,
|
||||
Err: syscall.ENOTDIR,
|
||||
}
|
||||
}
|
||||
|
||||
// Slow path: make sure parent exists and then call Mkdir for path.
|
||||
i := len(path)
|
||||
for i > 0 && IsPathSeparator(path[i-1]) { // Skip trailing path separator.
|
||||
i--
|
||||
}
|
||||
|
||||
j := i
|
||||
for j > 0 && !IsPathSeparator(path[j-1]) { // Scan backward over element.
|
||||
j--
|
||||
}
|
||||
|
||||
if j > 1 {
|
||||
// Create parent
|
||||
parent := path[0 : j-1]
|
||||
if parent != filepath.VolumeName(parent) {
|
||||
err = f.mkdirAll(parent, perm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parent now exists; invoke Mkdir and use its result.
|
||||
err = os.Mkdir(path, perm)
|
||||
if err != nil {
|
||||
// Handle arguments like "foo/." by
|
||||
// double-checking that directory doesn't exist.
|
||||
dir, err1 := os.Lstat(path)
|
||||
if err1 == nil && dir.IsDir() {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) Unhide(name string) error {
|
||||
name, err := f.rooted(name)
|
||||
if err != nil {
|
||||
@@ -121,30 +70,17 @@ func (f *BasicFilesystem) Hide(name string) error {
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) Roots() ([]string, error) {
|
||||
kernel32, err := syscall.LoadDLL("kernel32.dll")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
getLogicalDriveStringsHandle, err := kernel32.FindProc("GetLogicalDriveStringsA")
|
||||
mask, err := windows.GetLogicalDrives()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
buffer := [1024]byte{}
|
||||
bufferSize := uint32(len(buffer))
|
||||
|
||||
hr, _, _ := getLogicalDriveStringsHandle.Call(uintptr(unsafe.Pointer(&bufferSize)), uintptr(unsafe.Pointer(&buffer)))
|
||||
if hr == 0 {
|
||||
return nil, errors.New("syscall failed")
|
||||
}
|
||||
|
||||
var drives []string
|
||||
parts := bytes.Split(buffer[:], []byte{0})
|
||||
for _, part := range parts {
|
||||
if len(part) == 0 {
|
||||
break
|
||||
drives := make([]string, 0, bits.OnesCount32(mask))
|
||||
for letter := byte('A'); mask != 0; letter++ {
|
||||
if mask&1 == 1 {
|
||||
drives = append(drives, string([]byte{letter, ':', '\\'}))
|
||||
}
|
||||
drives = append(drives, string(part))
|
||||
mask >>= 1
|
||||
}
|
||||
|
||||
return drives, nil
|
||||
@@ -168,14 +104,14 @@ func (f *BasicFilesystem) Lchown(name, uid, gid string) error {
|
||||
// SetSecurityInfo.
|
||||
|
||||
var si windows.SECURITY_INFORMATION
|
||||
var ownerSID, groupSID *syscall.SID
|
||||
var ownerSID, groupSID *windows.SID
|
||||
if uid != "" {
|
||||
ownerSID, err = syscall.StringToSid(uid)
|
||||
ownerSID, err = windows.StringToSid(uid)
|
||||
if err == nil {
|
||||
si |= windows.OWNER_SECURITY_INFORMATION
|
||||
}
|
||||
} else if gid != "" {
|
||||
groupSID, err = syscall.StringToSid(uid)
|
||||
groupSID, err = windows.StringToSid(uid)
|
||||
if err == nil {
|
||||
si |= windows.GROUP_SECURITY_INFORMATION
|
||||
}
|
||||
@@ -183,7 +119,7 @@ func (f *BasicFilesystem) Lchown(name, uid, gid string) error {
|
||||
return errors.New("neither uid nor gid specified")
|
||||
}
|
||||
|
||||
return windows.SetSecurityInfo(hdl, windows.SE_FILE_OBJECT, si, (*windows.SID)(ownerSID), (*windows.SID)(groupSID), nil, nil)
|
||||
return windows.SetSecurityInfo(hdl, windows.SE_FILE_OBJECT, si, ownerSID, groupSID, nil, nil)
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) Remove(name string) error {
|
||||
@@ -267,47 +203,35 @@ func getFinalPathName(in string) (string, error) {
|
||||
// Wrap the call to GetFinalPathNameByHandleW
|
||||
// The string returned by this function uses the \?\ syntax
|
||||
// Implies GetFullPathName + GetLongPathName
|
||||
kernel32, err := syscall.LoadDLL("kernel32.dll")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
GetFinalPathNameByHandleW, err := kernel32.FindProc("GetFinalPathNameByHandleW")
|
||||
// https://github.com/golang/go/blob/ff048033e4304898245d843e79ed1a0897006c6d/src/internal/syscall/windows/syscall_windows.go#L303
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
inPath, err := syscall.UTF16PtrFromString(in)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// Get a file handler
|
||||
h, err := syscall.CreateFile(inPath,
|
||||
syscall.GENERIC_READ,
|
||||
syscall.FILE_SHARE_READ,
|
||||
// Get a file handle
|
||||
h, err := windows.CreateFile(inPath,
|
||||
windows.GENERIC_READ,
|
||||
windows.FILE_SHARE_READ,
|
||||
nil,
|
||||
syscall.OPEN_EXISTING,
|
||||
uint32(syscall.FILE_FLAG_BACKUP_SEMANTICS),
|
||||
windows.OPEN_EXISTING,
|
||||
windows.FILE_FLAG_BACKUP_SEMANTICS,
|
||||
0)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer syscall.CloseHandle(h)
|
||||
// Call GetFinalPathNameByHandleW
|
||||
var VOLUME_NAME_DOS uint32 = 0x0 // not yet defined in syscall
|
||||
var bufSize uint32 = syscall.MAX_PATH // 260
|
||||
defer windows.CloseHandle(h)
|
||||
|
||||
const VOLUME_NAME_DOS = 0x0 // not yet defined in x/sys/windows
|
||||
var bufSize uint32 = windows.MAX_PATH
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
buf := make([]uint16, bufSize)
|
||||
var ret uintptr
|
||||
ret, _, err = GetFinalPathNameByHandleW.Call(
|
||||
uintptr(h), // HANDLE hFile
|
||||
uintptr(unsafe.Pointer(&buf[0])), // LPWSTR lpszFilePath
|
||||
uintptr(bufSize), // DWORD cchFilePath
|
||||
uintptr(VOLUME_NAME_DOS), // DWORD dwFlags
|
||||
)
|
||||
var ret uint32
|
||||
ret, err = windows.GetFinalPathNameByHandle(
|
||||
h, &buf[0], bufSize, VOLUME_NAME_DOS)
|
||||
// The returned value is the actual length of the norm path
|
||||
// After Win 10 build 1607, MAX_PATH limitations have been removed
|
||||
// so it is necessary to check newBufSize
|
||||
newBufSize := uint32(ret) + 1
|
||||
newBufSize := ret + 1
|
||||
if ret == 0 || newBufSize > bufSize*100 {
|
||||
break
|
||||
}
|
||||
|
||||
+111
-39
@@ -40,6 +40,8 @@ func IsErrCaseConflict(err error) bool {
|
||||
type realCaser interface {
|
||||
realCase(name string) (string, error)
|
||||
dropCache()
|
||||
noteAdded(name string)
|
||||
noteRemoved(name string)
|
||||
}
|
||||
|
||||
type fskey struct {
|
||||
@@ -154,39 +156,40 @@ type caseFilesystem struct {
|
||||
}
|
||||
|
||||
func (f *caseFilesystem) Chmod(name string, mode FileMode) error {
|
||||
if err := f.checkCase(name); err != nil {
|
||||
if _, err := f.checkCase(name); err != nil {
|
||||
return err
|
||||
}
|
||||
return f.Filesystem.Chmod(name, mode)
|
||||
}
|
||||
|
||||
func (f *caseFilesystem) Lchown(name, uid, gid string) error {
|
||||
if err := f.checkCase(name); err != nil {
|
||||
if _, err := f.checkCase(name); err != nil {
|
||||
return err
|
||||
}
|
||||
return f.Filesystem.Lchown(name, uid, gid)
|
||||
}
|
||||
|
||||
func (f *caseFilesystem) Chtimes(name string, atime time.Time, mtime time.Time) error {
|
||||
if err := f.checkCase(name); err != nil {
|
||||
if _, err := f.checkCase(name); err != nil {
|
||||
return err
|
||||
}
|
||||
return f.Filesystem.Chtimes(name, atime, mtime)
|
||||
}
|
||||
|
||||
func (f *caseFilesystem) Mkdir(name string, perm FileMode) error {
|
||||
if err := f.checkCase(name); err != nil {
|
||||
can, err := f.checkCase(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := f.Filesystem.Mkdir(name, perm); err != nil {
|
||||
return err
|
||||
}
|
||||
f.dropCache()
|
||||
f.noteAdded(can)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *caseFilesystem) MkdirAll(path string, perm FileMode) error {
|
||||
if err := f.checkCase(path); err != nil {
|
||||
if _, err := f.checkCase(path); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := f.Filesystem.MkdirAll(path, perm); err != nil {
|
||||
@@ -205,25 +208,26 @@ func (f *caseFilesystem) Lstat(name string) (FileInfo, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = f.checkCaseExisting(name); err != nil {
|
||||
if _, err = f.checkCaseExisting(name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return stat, nil
|
||||
}
|
||||
|
||||
func (f *caseFilesystem) Remove(name string) error {
|
||||
if err := f.checkCase(name); err != nil {
|
||||
can, err := f.checkCase(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := f.Filesystem.Remove(name); err != nil {
|
||||
return err
|
||||
}
|
||||
f.dropCache()
|
||||
f.noteRemoved(can)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *caseFilesystem) RemoveAll(name string) error {
|
||||
if err := f.checkCase(name); err != nil {
|
||||
if _, err := f.checkCase(name); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := f.Filesystem.RemoveAll(name); err != nil {
|
||||
@@ -234,10 +238,12 @@ func (f *caseFilesystem) RemoveAll(name string) error {
|
||||
}
|
||||
|
||||
func (f *caseFilesystem) Rename(oldpath, newpath string) error {
|
||||
if err := f.checkCase(oldpath); err != nil {
|
||||
oldcan, err := f.checkCase(oldpath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := f.checkCase(newpath); err != nil {
|
||||
newcan, err := f.checkCase(newpath)
|
||||
if err != nil {
|
||||
// Case-only rename is ok
|
||||
e := &CaseConflictError{}
|
||||
if !errors.As(err, &e) || e.Real != oldpath {
|
||||
@@ -247,7 +253,8 @@ func (f *caseFilesystem) Rename(oldpath, newpath string) error {
|
||||
if err := f.Filesystem.Rename(oldpath, newpath); err != nil {
|
||||
return err
|
||||
}
|
||||
f.dropCache()
|
||||
f.noteRemoved(oldcan)
|
||||
f.noteAdded(newcan)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -260,65 +267,68 @@ func (f *caseFilesystem) Stat(name string) (FileInfo, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = f.checkCaseExisting(name); err != nil {
|
||||
if _, err = f.checkCaseExisting(name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return stat, nil
|
||||
}
|
||||
|
||||
func (f *caseFilesystem) DirNames(name string) ([]string, error) {
|
||||
if err := f.checkCase(name); err != nil {
|
||||
if _, err := f.checkCase(name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f.Filesystem.DirNames(name)
|
||||
}
|
||||
|
||||
func (f *caseFilesystem) Open(name string) (File, error) {
|
||||
if err := f.checkCase(name); err != nil {
|
||||
if _, err := f.checkCase(name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f.Filesystem.Open(name)
|
||||
}
|
||||
|
||||
func (f *caseFilesystem) OpenFile(name string, flags int, mode FileMode) (File, error) {
|
||||
if err := f.checkCase(name); err != nil {
|
||||
can, err := f.checkCase(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
file, err := f.Filesystem.OpenFile(name, flags, mode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f.dropCache()
|
||||
f.noteAdded(can)
|
||||
return file, nil
|
||||
}
|
||||
|
||||
func (f *caseFilesystem) ReadSymlink(name string) (string, error) {
|
||||
if err := f.checkCase(name); err != nil {
|
||||
if _, err := f.checkCase(name); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return f.Filesystem.ReadSymlink(name)
|
||||
}
|
||||
|
||||
func (f *caseFilesystem) Create(name string) (File, error) {
|
||||
if err := f.checkCase(name); err != nil {
|
||||
can, err := f.checkCase(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
file, err := f.Filesystem.Create(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f.dropCache()
|
||||
f.noteAdded(can)
|
||||
return file, nil
|
||||
}
|
||||
|
||||
func (f *caseFilesystem) CreateSymlink(target, name string) error {
|
||||
if err := f.checkCase(name); err != nil {
|
||||
can, err := f.checkCase(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := f.Filesystem.CreateSymlink(target, name); err != nil {
|
||||
return err
|
||||
}
|
||||
f.dropCache()
|
||||
f.noteAdded(can)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -326,28 +336,28 @@ func (f *caseFilesystem) Walk(root string, walkFn WalkFunc) error {
|
||||
// Walking the filesystem is likely (in Syncthing's case certainly) done
|
||||
// to pick up external changes, for which caching is undesirable.
|
||||
f.dropCache()
|
||||
if err := f.checkCase(root); err != nil {
|
||||
if _, err := f.checkCase(root); err != nil {
|
||||
return err
|
||||
}
|
||||
return f.Filesystem.Walk(root, walkFn)
|
||||
}
|
||||
|
||||
func (f *caseFilesystem) Watch(path string, ignore Matcher, ctx context.Context, ignorePerms bool) (<-chan Event, <-chan error, error) {
|
||||
if err := f.checkCase(path); err != nil {
|
||||
if _, err := f.checkCase(path); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return f.Filesystem.Watch(path, ignore, ctx, ignorePerms)
|
||||
}
|
||||
|
||||
func (f *caseFilesystem) Hide(name string) error {
|
||||
if err := f.checkCase(name); err != nil {
|
||||
if _, err := f.checkCase(name); err != nil {
|
||||
return err
|
||||
}
|
||||
return f.Filesystem.Hide(name)
|
||||
}
|
||||
|
||||
func (f *caseFilesystem) Unhide(name string) error {
|
||||
if err := f.checkCase(name); err != nil {
|
||||
if _, err := f.checkCase(name); err != nil {
|
||||
return err
|
||||
}
|
||||
return f.Filesystem.Unhide(name)
|
||||
@@ -357,25 +367,25 @@ func (f *caseFilesystem) underlying() (Filesystem, bool) {
|
||||
return f.Filesystem, true
|
||||
}
|
||||
|
||||
func (f *caseFilesystem) checkCase(name string) error {
|
||||
func (f *caseFilesystem) checkCase(name string) (string, error) {
|
||||
var err error
|
||||
if name, err = Canonicalize(name); err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
// Stat is necessary for case sensitive FS, as it's then not a conflict
|
||||
// if name is e.g. "foo" and on dir there is "Foo".
|
||||
if _, err := f.Filesystem.Lstat(name); err != nil {
|
||||
if IsNotExist(err) {
|
||||
return nil
|
||||
return name, nil
|
||||
}
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
return f.checkCaseExisting(name)
|
||||
}
|
||||
|
||||
// checkCaseExisting must only be called after successfully canonicalizing and
|
||||
// stating the file.
|
||||
func (f *caseFilesystem) checkCaseExisting(name string) error {
|
||||
func (f *caseFilesystem) checkCaseExisting(name string) (string, error) {
|
||||
realName, err := f.realCase(name)
|
||||
if IsNotExist(err) {
|
||||
// It did exist just before -> cache is outdated, try again
|
||||
@@ -383,15 +393,15 @@ func (f *caseFilesystem) checkCaseExisting(name string) error {
|
||||
realName, err = f.realCase(name)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
// We normalize the normalization (hah!) of the strings before
|
||||
// comparing, as we don't want to treat a normalization difference as a
|
||||
// case conflict.
|
||||
if norm.NFC.String(realName) != norm.NFC.String(name) {
|
||||
return &CaseConflictError{name, realName}
|
||||
return "", &CaseConflictError{name, realName}
|
||||
}
|
||||
return nil
|
||||
return realName, nil
|
||||
}
|
||||
|
||||
type defaultRealCaser struct {
|
||||
@@ -422,6 +432,9 @@ func (r *defaultRealCaser) realCase(name string) (string, error) {
|
||||
return realName, nil
|
||||
}
|
||||
|
||||
r.cache.mut.Lock()
|
||||
defer r.cache.mut.Unlock()
|
||||
|
||||
for _, comp := range PathComponents(name) {
|
||||
node := r.cache.getExpireAdd(realName, r.fs)
|
||||
|
||||
@@ -447,11 +460,70 @@ func (r *defaultRealCaser) dropCache() {
|
||||
r.cache.Purge()
|
||||
}
|
||||
|
||||
// getExpireAdd gets an entry for the given key. If no entry exists, or it is
|
||||
// expired a new one is created and added to the cache.
|
||||
func (c *caseCache) getExpireAdd(key string, fs Filesystem) *caseNode {
|
||||
func (r *defaultRealCaser) noteAdded(canonicalName string) {
|
||||
r.cache.added(canonicalName)
|
||||
}
|
||||
|
||||
func (r *defaultRealCaser) noteRemoved(canonicalName string) {
|
||||
r.cache.removed(canonicalName)
|
||||
}
|
||||
|
||||
// added updates the cached listing of name's parent directory to include
|
||||
// name, dropping any cached listing for name itself.
|
||||
func (c *caseCache) added(name string) {
|
||||
c.mut.Lock()
|
||||
defer c.mut.Unlock()
|
||||
|
||||
// Remove any stale entry for the name itself, though it should already
|
||||
// have been removed by a paired noteRemoved if it existed before.
|
||||
c.Remove(name)
|
||||
|
||||
// Get and update parent
|
||||
|
||||
node, ok := c.Get(filepath.Dir(name))
|
||||
if !ok || node.err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
base := filepath.Base(name)
|
||||
node.children[base] = struct{}{}
|
||||
node.lowerToReal[UnicodeLowercaseNormalized(base)] = base
|
||||
}
|
||||
|
||||
// removed updates the cached listing of name's parent directory to no longer
|
||||
// include name, dropping any cached listing for name itself.
|
||||
func (c *caseCache) removed(name string) {
|
||||
c.mut.Lock()
|
||||
defer c.mut.Unlock()
|
||||
|
||||
// Remove the cache entry for the name itself, if it exists
|
||||
c.Remove(name)
|
||||
|
||||
// Get the parent and remove it from its set of children
|
||||
|
||||
node, ok := c.Get(filepath.Dir(name))
|
||||
if !ok || node.err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(node.children) != len(node.lowerToReal) {
|
||||
// Some entries share a lowercase form, only possible on a
|
||||
// case-sensitive filesystem. That makes precise incremental removal
|
||||
// awkward (we'd need to rebuild lowerToReal in sorted order etc),
|
||||
// so drop the listing and let it be re-read.
|
||||
c.Remove(filepath.Dir(name))
|
||||
return
|
||||
}
|
||||
|
||||
base := filepath.Base(name)
|
||||
delete(node.children, base)
|
||||
delete(node.lowerToReal, UnicodeLowercaseNormalized(base))
|
||||
}
|
||||
|
||||
// getExpireAdd gets an entry for the given key. If no entry exists, or it is
|
||||
// expired a new one is created and added to the cache. The caller must hold
|
||||
// c.mut.
|
||||
func (c *caseCache) getExpireAdd(key string, fs Filesystem) *caseNode {
|
||||
node, ok := c.Get(key)
|
||||
if !ok {
|
||||
node := newCaseNode(key, fs)
|
||||
@@ -499,7 +571,7 @@ func newCaseNode(name string, filesystem Filesystem) *caseNode {
|
||||
lower := UnicodeLowercaseNormalized(n)
|
||||
if lower != lastLower {
|
||||
node.lowerToReal[lower] = n
|
||||
lastLower = n
|
||||
lastLower = lower
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -153,7 +153,7 @@ func newFakeFilesystem(rootURI string, _ ...Option) *fakeFS {
|
||||
for (files == 0 || createdFiles < files) && (maxsize == 0 || writtenData>>20 < int64(maxsize)) {
|
||||
dir := filepath.Join(fmt.Sprintf("%02x", rng.Intn(255)), fmt.Sprintf("%02x", rng.Intn(255)))
|
||||
file := fmt.Sprintf("%016x", rng.Int63())
|
||||
_ = fs.MkdirAll(dir, 0o755)
|
||||
_ = fs.MkdirAll(dir, ModePerm)
|
||||
|
||||
fd, _ := fs.Create(filepath.Join(dir, file))
|
||||
createdFiles++
|
||||
@@ -169,7 +169,7 @@ func newFakeFilesystem(rootURI string, _ ...Option) *fakeFS {
|
||||
|
||||
if !nostfolder {
|
||||
// Also create a default folder marker for good measure
|
||||
_ = fs.Mkdir(".stfolder", 0o700)
|
||||
_ = fs.Mkdir(".stfolder", ModePerm)
|
||||
}
|
||||
|
||||
// We only set the latency after doing the operations required to create
|
||||
|
||||
+13
-2
@@ -12,6 +12,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"math/bits"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -173,8 +174,18 @@ const (
|
||||
OptSync = os.O_SYNC
|
||||
OptTruncate = os.O_TRUNC
|
||||
OptWriteOnly = os.O_WRONLY
|
||||
OptFollow = 1 << (bits.UintSize - 2)
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Ensure OptFollow doesn't clash with any other flag.
|
||||
flags := OptAppend | OptCreate | OptExclusive | OptReadOnly | OptReadWrite | OptSync | OptTruncate | OptWriteOnly
|
||||
flags &= OptFollow
|
||||
if flags != 0 {
|
||||
panic("bug: flag clash")
|
||||
}
|
||||
}
|
||||
|
||||
// SkipDir is used as a return value from WalkFuncs to indicate that
|
||||
// the directory named in the call is to be skipped. It is not returned
|
||||
// as an error by any function.
|
||||
@@ -284,8 +295,8 @@ func NewFilesystem(fsType FilesystemType, uri string, opts ...Option) Filesystem
|
||||
}
|
||||
|
||||
// fs cannot import config or versioner, so we hard code .stfolder
|
||||
// (config.DefaultMarkerName) and .stversions (versioner.DefaultPath)
|
||||
var internals = []string{".stfolder", ".stignore", ".stversions"}
|
||||
// (config.DefaultMarkerName) and .stversions (versioner.DefaultPath).
|
||||
var internals = []string{".stfolder", ".stversions"}
|
||||
|
||||
// IsInternal returns true if the file, as a path relative to the folder
|
||||
// root, represents an internal file that should always be ignored. The file
|
||||
|
||||
@@ -21,10 +21,8 @@ func TestIsInternal(t *testing.T) {
|
||||
internal bool
|
||||
}{
|
||||
{".stfolder", true},
|
||||
{".stignore", true},
|
||||
{".stversions", true},
|
||||
{".stfolder/foo", true},
|
||||
{".stignore/foo", true},
|
||||
{".stversions/foo", true},
|
||||
|
||||
{".stfolderfoo", false},
|
||||
@@ -34,6 +32,8 @@ func TestIsInternal(t *testing.T) {
|
||||
{"foo.stignore", false},
|
||||
{"foo.stversions", false},
|
||||
{"foo/.stfolder", false},
|
||||
{".stignore", false},
|
||||
{".stignore/foo", false},
|
||||
{"foo/.stignore", false},
|
||||
{"foo/.stversions", false},
|
||||
}
|
||||
@@ -234,3 +234,29 @@ func TestRepro9677MissingMtimeFS(t *testing.T) {
|
||||
newFS := NewFilesystem(FilesystemTypeFake, fmt.Sprintf("%v?insens=true&timeprecisionsecond=true", t.Name()), &OptionDetectCaseConflicts{}, NewMtimeOption(mtimeDB, ""))
|
||||
checkMtime(newFS)
|
||||
}
|
||||
|
||||
func TestBasicFilesystemRoots(t *testing.T) {
|
||||
var bfs BasicFilesystem
|
||||
roots, err := bfs.Roots()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// The expected results depends on the system and we can't say too much,
|
||||
// but do some sanity checking.
|
||||
t.Logf("%d roots: %+v", len(roots), roots)
|
||||
if len(roots) > 25 {
|
||||
t.Errorf("suspiciously large number of filesystem roots: %d", len(roots))
|
||||
}
|
||||
rm := make(map[string]struct{})
|
||||
for _, root := range roots {
|
||||
if root == "" {
|
||||
t.Error("empty root")
|
||||
continue
|
||||
}
|
||||
if _, ok := rm[root]; ok {
|
||||
t.Errorf("duplicate root %q", root)
|
||||
continue
|
||||
}
|
||||
rm[root] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,66 +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 ignore
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/ignore/ignoreresult"
|
||||
)
|
||||
|
||||
type nower interface {
|
||||
Now() time.Time
|
||||
}
|
||||
|
||||
var clock = nower(defaultClock{})
|
||||
|
||||
type cache struct {
|
||||
entries map[string]cacheEntry
|
||||
}
|
||||
|
||||
type cacheEntry struct {
|
||||
result ignoreresult.R
|
||||
access int64 // Unix nanosecond count. Sufficient until the year 2262.
|
||||
}
|
||||
|
||||
func newCache() *cache {
|
||||
return &cache{
|
||||
entries: make(map[string]cacheEntry),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cache) clean(d time.Duration) {
|
||||
for k, v := range c.entries {
|
||||
if clock.Now().Sub(time.Unix(0, v.access)) > d {
|
||||
delete(c.entries, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cache) get(key string) (ignoreresult.R, bool) {
|
||||
entry, ok := c.entries[key]
|
||||
if ok {
|
||||
entry.access = clock.Now().UnixNano()
|
||||
c.entries[key] = entry
|
||||
}
|
||||
return entry.result, ok
|
||||
}
|
||||
|
||||
func (c *cache) set(key string, result ignoreresult.R) {
|
||||
c.entries[key] = cacheEntry{result, time.Now().UnixNano()}
|
||||
}
|
||||
|
||||
func (c *cache) len() int {
|
||||
l := len(c.entries)
|
||||
return l
|
||||
}
|
||||
|
||||
type defaultClock struct{}
|
||||
|
||||
func (defaultClock) Now() time.Time {
|
||||
return time.Now()
|
||||
}
|
||||
@@ -1,94 +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 ignore
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/ignore/ignoreresult"
|
||||
)
|
||||
|
||||
func TestCache(t *testing.T) {
|
||||
fc := new(fakeClock)
|
||||
oldClock := clock
|
||||
clock = fc
|
||||
defer func() {
|
||||
clock = oldClock
|
||||
}()
|
||||
|
||||
c := newCache()
|
||||
|
||||
res, ok := c.get("nonexistent")
|
||||
if res.IsIgnored() || res.IsDeletable() || ok {
|
||||
t.Errorf("res %v, ok %v for nonexistent item", res, ok)
|
||||
}
|
||||
|
||||
// Set and check some items
|
||||
|
||||
c.set("true", ignoreresult.IgnoredDeletable)
|
||||
c.set("false", 0)
|
||||
|
||||
res, ok = c.get("true")
|
||||
if !res.IsIgnored() || !res.IsDeletable() || !ok {
|
||||
t.Errorf("res %v, ok %v for true item", res, ok)
|
||||
}
|
||||
|
||||
res, ok = c.get("false")
|
||||
if res.IsIgnored() || res.IsDeletable() || !ok {
|
||||
t.Errorf("res %v, ok %v for false item", res, ok)
|
||||
}
|
||||
|
||||
// Don't clean anything
|
||||
|
||||
c.clean(time.Second)
|
||||
|
||||
// Same values should exist
|
||||
|
||||
res, ok = c.get("true")
|
||||
if !res.IsIgnored() || !res.IsDeletable() || !ok {
|
||||
t.Errorf("res %v, ok %v for true item", res, ok)
|
||||
}
|
||||
|
||||
res, ok = c.get("false")
|
||||
if res.IsIgnored() || res.IsDeletable() || !ok {
|
||||
t.Errorf("res %v, ok %v for false item", res, ok)
|
||||
}
|
||||
|
||||
// Sleep and access, to get some data for clean
|
||||
|
||||
*fc += 500 // milliseconds
|
||||
|
||||
c.get("true")
|
||||
|
||||
*fc += 100 // milliseconds
|
||||
|
||||
// "false" was accessed ~600 ms ago, "true" was accessed ~100 ms ago.
|
||||
// This should clean out "false" but not "true"
|
||||
|
||||
c.clean(300 * time.Millisecond)
|
||||
|
||||
// Same values should exist
|
||||
|
||||
_, ok = c.get("true")
|
||||
if !ok {
|
||||
t.Error("item should still exist")
|
||||
}
|
||||
|
||||
_, ok = c.get("false")
|
||||
if ok {
|
||||
t.Errorf("item should have been cleaned")
|
||||
}
|
||||
}
|
||||
|
||||
type fakeClock int64 // milliseconds
|
||||
|
||||
func (f *fakeClock) Now() time.Time {
|
||||
t := time.Unix(int64(*f)/1000, (int64(*f)%1000)*int64(time.Millisecond))
|
||||
*f++
|
||||
return t
|
||||
}
|
||||
+9
-56
@@ -92,7 +92,7 @@ func (p Pattern) allowsSkippingIgnoredDirs() bool {
|
||||
if p.result.IsIgnored() {
|
||||
return true
|
||||
}
|
||||
if p.pattern[0] != '/' {
|
||||
if p.pattern == "" || p.pattern[0] != '/' {
|
||||
return false
|
||||
}
|
||||
// A "/**" at the end is allowed and doesn't have any bearing on the
|
||||
@@ -124,10 +124,7 @@ type Matcher struct {
|
||||
fs fs.Filesystem
|
||||
lines []string // exact lines read from .stignore
|
||||
patterns []Pattern // patterns including those from included files
|
||||
withCache bool
|
||||
matches *cache
|
||||
curHash string
|
||||
stop chan struct{}
|
||||
changeDetector ChangeDetector
|
||||
mut sync.Mutex
|
||||
}
|
||||
@@ -135,13 +132,6 @@ type Matcher struct {
|
||||
// An Option can be passed to New()
|
||||
type Option func(*Matcher)
|
||||
|
||||
// WithCache enables or disables lookup caching. The default is disabled.
|
||||
func WithCache(v bool) Option {
|
||||
return func(m *Matcher) {
|
||||
m.withCache = v
|
||||
}
|
||||
}
|
||||
|
||||
// WithChangeDetector sets a custom ChangeDetector. The default is to simply
|
||||
// use the on disk modtime for comparison.
|
||||
func WithChangeDetector(cd ChangeDetector) Option {
|
||||
@@ -152,8 +142,7 @@ func WithChangeDetector(cd ChangeDetector) Option {
|
||||
|
||||
func New(fs fs.Filesystem, opts ...Option) *Matcher {
|
||||
m := &Matcher{
|
||||
fs: fs,
|
||||
stop: make(chan struct{}),
|
||||
fs: fs,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(m)
|
||||
@@ -161,9 +150,6 @@ func New(fs fs.Filesystem, opts ...Option) *Matcher {
|
||||
if m.changeDetector == nil {
|
||||
m.changeDetector = newModtimeChecker()
|
||||
}
|
||||
if m.withCache {
|
||||
go m.clean(2 * time.Hour)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
@@ -219,9 +205,6 @@ func (m *Matcher) parseLocked(r io.Reader, file string) error {
|
||||
|
||||
m.curHash = newHash
|
||||
m.patterns = patterns
|
||||
if m.withCache {
|
||||
m.matches = newCache()
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -232,7 +215,7 @@ func (m *Matcher) parseLocked(r io.Reader, file string) error {
|
||||
// NFC everywhere else). This is always the case in real usage in syncthing, as
|
||||
// we ensure native unicode normalisation on all entry points (scanning and from
|
||||
// protocol) - so no need to normalize when calling this, except e.g. in tests.
|
||||
func (m *Matcher) Match(file string) (result ignoreresult.R) {
|
||||
func (m *Matcher) Match(file string) ignoreresult.R {
|
||||
switch {
|
||||
case fs.IsTemporary(file):
|
||||
return ignoreresult.IgnoreAndSkip
|
||||
@@ -254,19 +237,6 @@ func (m *Matcher) Match(file string) (result ignoreresult.R) {
|
||||
// Change backslashes to slashes (on Windows only)
|
||||
file = filepath.ToSlash(file)
|
||||
|
||||
if m.matches != nil {
|
||||
// Check the cache for a known result.
|
||||
res, ok := m.matches.get(file)
|
||||
if ok {
|
||||
return res
|
||||
}
|
||||
|
||||
// Update the cache with the result at return time
|
||||
defer func() {
|
||||
m.matches.set(file, result)
|
||||
}()
|
||||
}
|
||||
|
||||
// Check all the patterns for a match. Track whether the patterns so far
|
||||
// allow skipping matched directories or not. As soon as we hit an
|
||||
// exclude pattern (with some exceptions), we can't skip directories
|
||||
@@ -327,27 +297,6 @@ func (m *Matcher) Hash() string {
|
||||
return m.curHash
|
||||
}
|
||||
|
||||
func (m *Matcher) Stop() {
|
||||
close(m.stop)
|
||||
}
|
||||
|
||||
func (m *Matcher) clean(d time.Duration) {
|
||||
t := time.NewTimer(d / 2)
|
||||
for {
|
||||
select {
|
||||
case <-m.stop:
|
||||
return
|
||||
case <-t.C:
|
||||
m.mut.Lock()
|
||||
if m.matches != nil {
|
||||
m.matches.clean(d)
|
||||
}
|
||||
t.Reset(d / 2)
|
||||
m.mut.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func hashPatterns(patterns []Pattern) string {
|
||||
h := sha256.New()
|
||||
for _, pat := range patterns {
|
||||
@@ -357,8 +306,8 @@ func hashPatterns(patterns []Pattern) string {
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
func loadIgnoreFile(fs fs.Filesystem, file string) (fs.File, fs.FileInfo, error) {
|
||||
fd, err := fs.Open(file)
|
||||
func loadIgnoreFile(ffs fs.Filesystem, file string) (fs.File, fs.FileInfo, error) {
|
||||
fd, err := ffs.OpenFile(file, fs.OptReadOnly|fs.OptFollow, 0o666)
|
||||
if err != nil {
|
||||
return fd, nil, err
|
||||
}
|
||||
@@ -463,6 +412,10 @@ func parseLine(line string) ([]Pattern, error) {
|
||||
patterns[0] = pattern
|
||||
|
||||
line = line[3:]
|
||||
if line == "" {
|
||||
// Pattern was exactly "**/", already covered by patterns[0].
|
||||
return patterns[:1], nil
|
||||
}
|
||||
pattern.pattern = line
|
||||
pattern.match, err = glob.Compile(line, '/')
|
||||
if err != nil {
|
||||
|
||||
+77
-205
@@ -52,7 +52,7 @@ func newTestFS() fs.Filesystem {
|
||||
func TestIgnore(t *testing.T) {
|
||||
testFs := newTestFS()
|
||||
|
||||
pats := New(testFs, WithCache(true))
|
||||
pats := New(testFs)
|
||||
err := pats.Load(".stignore")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -104,7 +104,7 @@ func TestExcludes(t *testing.T) {
|
||||
i*2
|
||||
!ign2
|
||||
`
|
||||
pats := New(testFs, WithCache(true))
|
||||
pats := New(testFs)
|
||||
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -151,7 +151,7 @@ func TestFlagOrder(t *testing.T) {
|
||||
(?i)(?d)(?d)!ign9
|
||||
(?d)(?d)!ign10
|
||||
`
|
||||
pats := New(testFs, WithCache(true))
|
||||
pats := New(testFs)
|
||||
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -188,7 +188,7 @@ func TestDeletables(t *testing.T) {
|
||||
ign7
|
||||
(?i)ign8
|
||||
`
|
||||
pats := New(testFs, WithCache(true))
|
||||
pats := New(testFs)
|
||||
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -229,7 +229,7 @@ func TestBadPatterns(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, pat := range badPatterns {
|
||||
err := New(testFs, WithCache(true)).Parse(bytes.NewBufferString(pat), ".stignore")
|
||||
err := New(testFs).Parse(bytes.NewBufferString(pat), ".stignore")
|
||||
if err == nil {
|
||||
t.Errorf("No error for pattern %q", pat)
|
||||
}
|
||||
@@ -247,7 +247,7 @@ func TestBadPatterns(t *testing.T) {
|
||||
func TestCaseSensitivity(t *testing.T) {
|
||||
testFs := newTestFS()
|
||||
|
||||
ign := New(testFs, WithCache(true))
|
||||
ign := New(testFs)
|
||||
err := ign.Parse(bytes.NewBufferString("test"), ".stignore")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
@@ -275,126 +275,6 @@ func TestCaseSensitivity(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCaching(t *testing.T) {
|
||||
fs := fs.NewFilesystem(fs.FilesystemTypeFake, rand.String(32)+"?content=true")
|
||||
|
||||
fd1, err := osutil.TempFile(fs, "", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fd2, err := osutil.TempFile(fs, "", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
defer fd1.Close()
|
||||
defer fd2.Close()
|
||||
defer fs.Remove(fd1.Name())
|
||||
defer fs.Remove(fd2.Name())
|
||||
|
||||
_, err = fd1.Write([]byte("/x/\n#include " + filepath.Base(fd2.Name()) + "\n"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fd2.Write([]byte("/y/\n"))
|
||||
|
||||
pats := New(fs, WithCache(true))
|
||||
err = pats.Load(fd1.Name())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if pats.matches.len() != 0 {
|
||||
t.Fatal("Expected empty cache")
|
||||
}
|
||||
|
||||
// Cache some outcomes
|
||||
|
||||
for _, letter := range []string{"a", "b", "x", "y"} {
|
||||
pats.Match(letter)
|
||||
}
|
||||
|
||||
if pats.matches.len() != 4 {
|
||||
t.Fatal("Expected 4 cached results")
|
||||
}
|
||||
|
||||
// Reload file, expect old outcomes to be preserved
|
||||
|
||||
err = pats.Load(fd1.Name())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if pats.matches.len() != 4 {
|
||||
t.Fatal("Expected 4 cached results")
|
||||
}
|
||||
|
||||
// Modify the include file, expect empty cache. Ensure the timestamp on
|
||||
// the file changes.
|
||||
|
||||
fd2.Write([]byte("/z/\n"))
|
||||
fd2.Sync()
|
||||
fakeTime := time.Now().Add(5 * time.Second)
|
||||
fs.Chtimes(fd2.Name(), fakeTime, fakeTime)
|
||||
|
||||
err = pats.Load(fd1.Name())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if pats.matches.len() != 0 {
|
||||
t.Fatal("Expected 0 cached results")
|
||||
}
|
||||
|
||||
// Cache some outcomes again
|
||||
|
||||
for _, letter := range []string{"b", "x", "y"} {
|
||||
pats.Match(letter)
|
||||
}
|
||||
|
||||
// Verify that outcomes preserved on next load
|
||||
|
||||
err = pats.Load(fd1.Name())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if pats.matches.len() != 3 {
|
||||
t.Fatal("Expected 3 cached results")
|
||||
}
|
||||
|
||||
// Modify the root file, expect cache to be invalidated
|
||||
|
||||
fd1.Write([]byte("/a/\n"))
|
||||
fd1.Sync()
|
||||
fakeTime = time.Now().Add(5 * time.Second)
|
||||
fs.Chtimes(fd1.Name(), fakeTime, fakeTime)
|
||||
|
||||
err = pats.Load(fd1.Name())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if pats.matches.len() != 0 {
|
||||
t.Fatal("Expected cache invalidation")
|
||||
}
|
||||
|
||||
// Cache some outcomes again
|
||||
|
||||
for _, letter := range []string{"b", "x", "y"} {
|
||||
pats.Match(letter)
|
||||
}
|
||||
|
||||
// Verify that outcomes provided on next load
|
||||
|
||||
err = pats.Load(fd1.Name())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if pats.matches.len() != 3 {
|
||||
t.Fatal("Expected 3 cached results")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommentsAndBlankLines(t *testing.T) {
|
||||
testFs := newTestFS()
|
||||
|
||||
@@ -409,7 +289,7 @@ func TestCommentsAndBlankLines(t *testing.T) {
|
||||
|
||||
|
||||
`
|
||||
pats := New(testFs, WithCache(true))
|
||||
pats := New(testFs)
|
||||
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
@@ -419,6 +299,17 @@ func TestCommentsAndBlankLines(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNegatedGlobstarOnly(t *testing.T) {
|
||||
testFs := newTestFS()
|
||||
|
||||
pats := New(testFs)
|
||||
if err := pats.Parse(bytes.NewBufferString("!**/\n"), ".stignore"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Must not panic on an empty pattern component.
|
||||
pats.Match("foo")
|
||||
}
|
||||
|
||||
var result ignoreresult.R
|
||||
|
||||
func BenchmarkMatch(b *testing.B) {
|
||||
@@ -451,59 +342,6 @@ flamingo
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMatchCached(b *testing.B) {
|
||||
stignore := `
|
||||
.frog
|
||||
.frog*
|
||||
.frogfox
|
||||
.whale
|
||||
.whale/*
|
||||
.dolphin
|
||||
.dolphin/*
|
||||
~ferret~.*
|
||||
.ferret.*
|
||||
flamingo.*
|
||||
flamingo
|
||||
*.crow
|
||||
*.crow
|
||||
`
|
||||
// Caches per file, hence write the patterns to a file.
|
||||
|
||||
fs := fs.NewFilesystem(fs.FilesystemTypeFake, rand.String(32)+"?content=true")
|
||||
|
||||
fd, err := osutil.TempFile(fs, "", "")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = fd.Write([]byte(stignore))
|
||||
defer fd.Close()
|
||||
defer fs.Remove(fd.Name())
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
// Load the patterns
|
||||
pats := New(fs, WithCache(true))
|
||||
err = pats.Load(fd.Name())
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
// Cache the outcome for "filename"
|
||||
pats.Match("filename")
|
||||
|
||||
// This load should now load the cached outcomes as the set of patterns
|
||||
// has not changed.
|
||||
err = pats.Load(fd.Name())
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result = pats.Match("filename")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCacheReload(t *testing.T) {
|
||||
fs := fs.NewFilesystem(fs.FilesystemTypeFake, rand.String(32)+"?content=true")
|
||||
|
||||
@@ -522,7 +360,7 @@ func TestCacheReload(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
pats := New(fs, WithCache(true))
|
||||
pats := New(fs)
|
||||
err = pats.Load(fd.Name())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -579,7 +417,7 @@ func TestCacheReload(t *testing.T) {
|
||||
func TestHash(t *testing.T) {
|
||||
testFs := newTestFS()
|
||||
|
||||
p1 := New(testFs, WithCache(true))
|
||||
p1 := New(testFs)
|
||||
err := p1.Load(".stignore")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -595,7 +433,7 @@ func TestHash(t *testing.T) {
|
||||
/ffile
|
||||
lost+found
|
||||
`
|
||||
p2 := New(testFs, WithCache(true))
|
||||
p2 := New(testFs)
|
||||
err = p2.Parse(bytes.NewBufferString(stignore), ".stignore")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -610,7 +448,7 @@ func TestHash(t *testing.T) {
|
||||
/ffile
|
||||
lost+found
|
||||
`
|
||||
p3 := New(testFs, WithCache(true))
|
||||
p3 := New(testFs)
|
||||
err = p3.Parse(bytes.NewBufferString(stignore), ".stignore")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -636,7 +474,7 @@ func TestHash(t *testing.T) {
|
||||
func TestHashOfEmpty(t *testing.T) {
|
||||
testFs := newTestFS()
|
||||
|
||||
p1 := New(testFs, WithCache(true))
|
||||
p1 := New(testFs)
|
||||
|
||||
err := p1.Load(".stignore")
|
||||
if err != nil {
|
||||
@@ -678,7 +516,7 @@ func TestWindowsPatterns(t *testing.T) {
|
||||
a/b
|
||||
c\d
|
||||
`
|
||||
pats := New(testFs, WithCache(true))
|
||||
pats := New(testFs)
|
||||
|
||||
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
|
||||
if err != nil {
|
||||
@@ -706,7 +544,7 @@ func TestAutomaticCaseInsensitivity(t *testing.T) {
|
||||
A/B
|
||||
c/d
|
||||
`
|
||||
pats := New(testFs, WithCache(true))
|
||||
pats := New(testFs)
|
||||
|
||||
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
|
||||
if err != nil {
|
||||
@@ -728,7 +566,7 @@ func TestCommas(t *testing.T) {
|
||||
foo,bar.txt
|
||||
{baz,quux}.txt
|
||||
`
|
||||
pats := New(testFs, WithCache(true))
|
||||
pats := New(testFs)
|
||||
|
||||
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
|
||||
if err != nil {
|
||||
@@ -762,7 +600,7 @@ func TestIssue3164(t *testing.T) {
|
||||
(?d)(?i)/foo
|
||||
(?d)(?i)**/bar
|
||||
`
|
||||
pats := New(testFs, WithCache(true))
|
||||
pats := New(testFs)
|
||||
|
||||
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
|
||||
if err != nil {
|
||||
@@ -801,7 +639,7 @@ func TestIssue3174(t *testing.T) {
|
||||
stignore := `
|
||||
*ä*
|
||||
`
|
||||
pats := New(testFs, WithCache(true))
|
||||
pats := New(testFs)
|
||||
|
||||
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
|
||||
if err != nil {
|
||||
@@ -822,7 +660,7 @@ func TestIssue3639(t *testing.T) {
|
||||
stignore := `
|
||||
foo/
|
||||
`
|
||||
pats := New(testFs, WithCache(true))
|
||||
pats := New(testFs)
|
||||
|
||||
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
|
||||
if err != nil {
|
||||
@@ -858,7 +696,7 @@ func TestIssue3674(t *testing.T) {
|
||||
{"as/dc", true},
|
||||
}
|
||||
|
||||
pats := New(testFs, WithCache(true))
|
||||
pats := New(testFs)
|
||||
|
||||
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
|
||||
if err != nil {
|
||||
@@ -893,7 +731,7 @@ func TestGobwasGlobIssue18(t *testing.T) {
|
||||
{"bbaa", false},
|
||||
}
|
||||
|
||||
pats := New(testFs, WithCache(true))
|
||||
pats := New(testFs)
|
||||
|
||||
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
|
||||
if err != nil {
|
||||
@@ -925,7 +763,7 @@ func TestRoot(t *testing.T) {
|
||||
{"b", true},
|
||||
}
|
||||
|
||||
pats := New(testFs, WithCache(true))
|
||||
pats := New(testFs)
|
||||
|
||||
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
|
||||
if err != nil {
|
||||
@@ -951,7 +789,7 @@ func TestLines(t *testing.T) {
|
||||
!/a
|
||||
`
|
||||
|
||||
pats := New(testFs, WithCache(true))
|
||||
pats := New(testFs)
|
||||
|
||||
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
|
||||
if err != nil {
|
||||
@@ -992,7 +830,7 @@ func TestDuplicateLines(t *testing.T) {
|
||||
/*
|
||||
`
|
||||
|
||||
pats := New(testFs, WithCache(true))
|
||||
pats := New(testFs)
|
||||
|
||||
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
|
||||
if err != nil {
|
||||
@@ -1025,7 +863,7 @@ func TestIssue4680(t *testing.T) {
|
||||
{"#snapshot/foo", true},
|
||||
}
|
||||
|
||||
pats := New(testFs, WithCache(true))
|
||||
pats := New(testFs)
|
||||
|
||||
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
|
||||
if err != nil {
|
||||
@@ -1045,7 +883,7 @@ func TestIssue4689(t *testing.T) {
|
||||
|
||||
stignore := `// orig`
|
||||
|
||||
pats := New(testFs, WithCache(true))
|
||||
pats := New(testFs)
|
||||
|
||||
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
|
||||
if err != nil {
|
||||
@@ -1076,7 +914,7 @@ func TestIssue4901(t *testing.T) {
|
||||
puppy
|
||||
`
|
||||
|
||||
pats := New(testFs, WithCache(true))
|
||||
pats := New(testFs)
|
||||
|
||||
fd, err := pats.fs.Create(".stignore")
|
||||
if err != nil {
|
||||
@@ -1119,7 +957,7 @@ func TestIssue4901(t *testing.T) {
|
||||
func TestIssue5009(t *testing.T) {
|
||||
testFs := newTestFS()
|
||||
|
||||
pats := New(testFs, WithCache(true))
|
||||
pats := New(testFs)
|
||||
|
||||
stignore := `
|
||||
ign1
|
||||
@@ -1152,7 +990,7 @@ func TestIssue5009(t *testing.T) {
|
||||
func TestSpecialChars(t *testing.T) {
|
||||
testFs := newTestFS()
|
||||
|
||||
pats := New(testFs, WithCache(true))
|
||||
pats := New(testFs)
|
||||
|
||||
stignore := `(?i)/#recycle
|
||||
(?i)/#nosync
|
||||
@@ -1179,7 +1017,7 @@ func TestSpecialChars(t *testing.T) {
|
||||
func TestIntlWildcards(t *testing.T) {
|
||||
testFs := newTestFS()
|
||||
|
||||
pats := New(testFs, WithCache(true))
|
||||
pats := New(testFs)
|
||||
|
||||
stignore := `1000春
|
||||
200?春
|
||||
@@ -1208,7 +1046,7 @@ func TestPartialIncludeLine(t *testing.T) {
|
||||
|
||||
// Loading a partial #include line (no file mentioned) should error but not crash.
|
||||
|
||||
pats := New(testFs, WithCache(true))
|
||||
pats := New(testFs)
|
||||
|
||||
cases := []string{
|
||||
"#include",
|
||||
@@ -1267,7 +1105,7 @@ func TestSkipIgnoredDirs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
pats := New(testFs, WithCache(true))
|
||||
pats := New(testFs)
|
||||
|
||||
stignore := `
|
||||
/foo/ign*
|
||||
@@ -1711,7 +1549,7 @@ func testEscape(t *testing.T, tests []escapeTest, noErrors bool) {
|
||||
for name, content := range testEscapeFiles {
|
||||
fs.WriteFile(testFS, name, []byte(content), 0o666)
|
||||
}
|
||||
pats := New(testFS, WithCache(true))
|
||||
pats := New(testFS)
|
||||
|
||||
err := pats.Parse(bytes.NewBufferString(test.pattern), ".stignore")
|
||||
if noErrors {
|
||||
@@ -1731,3 +1569,37 @@ func testEscape(t *testing.T, tests []escapeTest, noErrors bool) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestIgnoreThroughSymlink verifies that an ignore file can be loaded when
|
||||
// it is itself a symlink pointing at the real file. This exercises the
|
||||
// SkipDefaultOpenFlags path, as our normal Open enforces O_NOFOLLOW. We use
|
||||
// a real filesystem here because the fake one does not implement the
|
||||
// O_NOFOLLOW semantics that make this distinction meaningful.
|
||||
func TestIgnoreThroughSymlink(t *testing.T) {
|
||||
if build.IsWindows {
|
||||
t.Skip("symlinks not supported on Windows")
|
||||
}
|
||||
|
||||
testFS := fs.NewFilesystem(fs.FilesystemTypeBasic, t.TempDir())
|
||||
|
||||
// The real ignore file lives under a different name, and .stignore is a
|
||||
// symlink pointing at it.
|
||||
if err := fs.WriteFile(testFS, "real-ignores", []byte("bfile\n"), 0o666); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := testFS.CreateSymlink("real-ignores", ".stignore"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
pats := New(testFS)
|
||||
if err := pats.Load(".stignore"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !pats.Match("bfile").IsIgnored() {
|
||||
t.Error("bfile should be ignored")
|
||||
}
|
||||
if pats.Match("afile").IsIgnored() {
|
||||
t.Error("afile should not be ignored")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,20 +8,19 @@ package model
|
||||
|
||||
import (
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// How many files to send in each Index/IndexUpdate message.
|
||||
// How many files & blocks to send in each Index/IndexUpdate message.
|
||||
const (
|
||||
MaxBatchSizeBytes = 250 * 1024 // Aim for making index messages no larger than 250 KiB (uncompressed)
|
||||
MaxBatchSizeFiles = 1000 // Either way, don't include more files than this
|
||||
MaxBatchSizeFiles = 1000
|
||||
MaxBatchSizeBlocks = 5000
|
||||
)
|
||||
|
||||
// FileInfoBatch is a utility to do file operations on the database in suitably
|
||||
// sized batches.
|
||||
type FileInfoBatch struct {
|
||||
infos []protocol.FileInfo
|
||||
size int
|
||||
nblocks int
|
||||
flushFn func([]protocol.FileInfo) error
|
||||
error error
|
||||
}
|
||||
@@ -47,11 +46,11 @@ func (b *FileInfoBatch) Append(f protocol.FileInfo) {
|
||||
b.infos = make([]protocol.FileInfo, 0, MaxBatchSizeFiles)
|
||||
}
|
||||
b.infos = append(b.infos, f)
|
||||
b.size += proto.Size(f.ToWire(true))
|
||||
b.nblocks += len(f.Blocks)
|
||||
}
|
||||
|
||||
func (b *FileInfoBatch) Full() bool {
|
||||
return len(b.infos) >= MaxBatchSizeFiles || b.size >= MaxBatchSizeBytes
|
||||
return len(b.infos) >= MaxBatchSizeFiles || b.nblocks >= MaxBatchSizeBlocks
|
||||
}
|
||||
|
||||
func (b *FileInfoBatch) FlushIfFull() error {
|
||||
@@ -82,9 +81,5 @@ func (b *FileInfoBatch) Flush() error {
|
||||
func (b *FileInfoBatch) Reset() {
|
||||
b.infos = nil
|
||||
b.error = nil
|
||||
b.size = 0
|
||||
}
|
||||
|
||||
func (b *FileInfoBatch) Size() int {
|
||||
return b.size
|
||||
b.nblocks = 0
|
||||
}
|
||||
|
||||
+74
-48
@@ -195,19 +195,23 @@ func (f *folder) Serve(ctx context.Context) error {
|
||||
case <-f.pullScheduled:
|
||||
if f.PullerDelayS > 0 {
|
||||
// Wait for incoming updates to settle before doing the
|
||||
// actual pull. Only set the state to SyncWaiting if we have
|
||||
// reason to believe there is something to sync, to avoid
|
||||
// unnecessary flashing in the GUI.
|
||||
if needCount, err := f.db.CountNeed(f.folderID, protocol.LocalDeviceID); err == nil && needCount.TotalItems() > 0 {
|
||||
f.setState(FolderSyncWaiting)
|
||||
// actual pull.
|
||||
//
|
||||
// Only set the state to SyncWaiting if we are Idle (and
|
||||
// not, e.g., errored) and have reason to believe there is
|
||||
// something to sync, to avoid unnecessary flashing in the
|
||||
// GUI.
|
||||
if cur, _, _ := f.getState(); cur == FolderIdle {
|
||||
if needCount, err := f.db.CountNeed(f.folderID, protocol.LocalDeviceID); err == nil && needCount.TotalItems() > 0 {
|
||||
f.setState(FolderSyncWaiting)
|
||||
}
|
||||
}
|
||||
pullTimer.Reset(time.Duration(float64(time.Second) * f.PullerDelayS))
|
||||
} else {
|
||||
_, err = f.pull(ctx)
|
||||
continue
|
||||
}
|
||||
_, err = f.pull(ctx)
|
||||
|
||||
case <-pullTimer.C:
|
||||
f.setState(FolderIdle)
|
||||
_, err = f.pull(ctx)
|
||||
|
||||
case <-f.pullFailTimer.C:
|
||||
@@ -238,10 +242,12 @@ func (f *folder) Serve(ctx context.Context) error {
|
||||
case next := <-f.scanDelay:
|
||||
f.sl.DebugContext(ctx, "Delaying scan")
|
||||
f.scanTimer.Reset(next)
|
||||
continue
|
||||
|
||||
case <-f.scanScheduled:
|
||||
f.sl.DebugContext(ctx, "Scan was scheduled")
|
||||
f.scanTimer.Reset(0)
|
||||
continue
|
||||
|
||||
case fsEvents := <-f.watchChan:
|
||||
f.sl.DebugContext(ctx, "Scan due to watcher")
|
||||
@@ -252,16 +258,20 @@ func (f *folder) Serve(ctx context.Context) error {
|
||||
err = f.restartWatch(ctx)
|
||||
|
||||
case <-f.versionCleanupTimer.C:
|
||||
if _, _, healthErr := f.getState(); healthErr != nil {
|
||||
continue
|
||||
}
|
||||
f.sl.DebugContext(ctx, "Doing version cleanup")
|
||||
f.versionCleanupTimerFired(ctx)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if svcutil.IsFatal(err) {
|
||||
return err
|
||||
}
|
||||
f.setError(ctx, err)
|
||||
if svcutil.IsFatal(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set the state to FolderError when err is non-nil, otherwise reset
|
||||
// it back to FolderIdle.
|
||||
f.setError(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -403,6 +413,15 @@ func (f *folder) pull(ctx context.Context) (success bool, err error) {
|
||||
}
|
||||
}()
|
||||
|
||||
// Abort early (before acquiring a limiter token) if there's a folder
|
||||
// error. This must happen before the "nothing to do" check below so
|
||||
// that up-to-date folders are also periodically health-checked.
|
||||
err = f.getHealthErrorWithoutIgnores()
|
||||
if err != nil {
|
||||
f.sl.DebugContext(ctx, "Skipping pull due to folder error", slogutil.Error(err))
|
||||
return false, err
|
||||
}
|
||||
|
||||
// If there is nothing to do, don't even enter sync-waiting state.
|
||||
needCount, err := f.db.CountNeed(f.folderID, protocol.LocalDeviceID)
|
||||
if err != nil {
|
||||
@@ -416,13 +435,6 @@ func (f *folder) pull(ctx context.Context) (success bool, err error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Abort early (before acquiring a token) if there's a folder error
|
||||
err = f.getHealthErrorWithoutIgnores()
|
||||
if err != nil {
|
||||
f.sl.DebugContext(ctx, "Skipping pull due to folder error", slogutil.Error(err))
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Send only folder doesn't do any io, it only checks for out-of-sync
|
||||
// items that differ in metadata and updates those.
|
||||
if f.Type != config.FolderTypeSendOnly {
|
||||
@@ -487,7 +499,6 @@ func (f *folder) scanSubdirs(ctx context.Context, subDirs []string) error {
|
||||
}()
|
||||
|
||||
f.setState(FolderScanWaiting)
|
||||
defer f.setState(FolderIdle)
|
||||
|
||||
if err := f.ioLimiter.TakeWithContext(ctx, 1); err != nil {
|
||||
return err
|
||||
@@ -571,17 +582,17 @@ func (f *folder) scanSubdirs(ctx context.Context, subDirs []string) error {
|
||||
const maxToRemove = 1000
|
||||
|
||||
type scanBatch struct {
|
||||
f *folder
|
||||
updateBatch *FileInfoBatch
|
||||
toRemove []string
|
||||
deleted map[string]struct{}
|
||||
f *folder
|
||||
updateBatch *FileInfoBatch
|
||||
toRemove []string
|
||||
skipInFindRename map[string]struct{}
|
||||
}
|
||||
|
||||
func (f *folder) newScanBatch() *scanBatch {
|
||||
b := &scanBatch{
|
||||
f: f,
|
||||
toRemove: make([]string, 0, maxToRemove),
|
||||
deleted: make(map[string]struct{}),
|
||||
f: f,
|
||||
toRemove: make([]string, 0, maxToRemove),
|
||||
skipInFindRename: make(map[string]struct{}),
|
||||
}
|
||||
b.updateBatch = NewFileInfoBatch(func(fs []protocol.FileInfo) error {
|
||||
if err := b.f.getHealthErrorWithoutIgnores(); err != nil {
|
||||
@@ -589,7 +600,7 @@ func (f *folder) newScanBatch() *scanBatch {
|
||||
return err
|
||||
}
|
||||
b.f.updateLocalsFromScanning(fs)
|
||||
clear(b.deleted)
|
||||
clear(b.skipInFindRename)
|
||||
return nil
|
||||
})
|
||||
return b
|
||||
@@ -625,12 +636,12 @@ func (b *scanBatch) FlushIfFull() error {
|
||||
return b.updateBatch.FlushIfFull()
|
||||
}
|
||||
|
||||
func (b *scanBatch) markDeleted(name string) {
|
||||
b.deleted[name] = struct{}{}
|
||||
func (b *scanBatch) markSkipInFindRename(name string) {
|
||||
b.skipInFindRename[name] = struct{}{}
|
||||
}
|
||||
|
||||
func (b *scanBatch) hasDeleted(name string) bool {
|
||||
_, ok := b.deleted[name]
|
||||
func (b *scanBatch) shouldSkipInFindRenames(name string) bool {
|
||||
_, ok := b.skipInFindRename[name]
|
||||
return ok
|
||||
}
|
||||
|
||||
@@ -734,12 +745,18 @@ func (f *folder) scanSubdirsChangedAndNew(ctx context.Context, subDirs []string,
|
||||
switch f.Type {
|
||||
case config.FolderTypeReceiveOnly, config.FolderTypeReceiveEncrypted:
|
||||
default:
|
||||
if nf, ok := f.findRename(ctx, res.File, batch); ok {
|
||||
if ok, err := batch.Update(nf); err != nil {
|
||||
return 0, err
|
||||
} else if ok {
|
||||
changes++
|
||||
batch.markDeleted(nf.Name)
|
||||
// Rename detection is comparatively expensive, so only attempt
|
||||
// it for files that appeared as new on disk during this scan. A
|
||||
// rename that overwrites an existing file (the destination path
|
||||
// already had an entry, so it scans as an update rather than a
|
||||
// new file) is not optimised as a rename.
|
||||
if res.File.New && res.File.Size > 0 {
|
||||
if nf, ok := f.findRename(ctx, res.File, batch); ok {
|
||||
if ok, err := batch.Update(nf); err != nil {
|
||||
return 0, err
|
||||
} else if ok {
|
||||
changes++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -932,7 +949,7 @@ loop:
|
||||
continue
|
||||
}
|
||||
|
||||
if batch.hasDeleted(fi.Name) {
|
||||
if batch.shouldSkipInFindRenames(fi.Name) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -952,6 +969,7 @@ loop:
|
||||
}
|
||||
|
||||
if !osutil.IsDeleted(f.mtimefs, fi.Name) {
|
||||
batch.markSkipInFindRename(fi.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -963,6 +981,7 @@ loop:
|
||||
nf.SetDeleted(f.shortID)
|
||||
nf.LocalFlags = f.localFlags
|
||||
found = true
|
||||
batch.markSkipInFindRename(fi.Name)
|
||||
break
|
||||
}
|
||||
|
||||
@@ -975,10 +994,13 @@ func (f *folder) scanTimerFired(ctx context.Context) error {
|
||||
select {
|
||||
case <-f.initialScanFinished:
|
||||
default:
|
||||
if err != nil {
|
||||
f.sl.ErrorContext(ctx, "Failed initial scan", slogutil.Error(err))
|
||||
} else {
|
||||
switch {
|
||||
case err == nil:
|
||||
f.sl.InfoContext(ctx, "Completed initial scan")
|
||||
case errors.Is(err, context.Canceled):
|
||||
// Suppressed: context was canceled (e.g. by user)
|
||||
default:
|
||||
f.sl.ErrorContext(ctx, "Failed initial scan", slogutil.Error(err))
|
||||
}
|
||||
close(f.initialScanFinished)
|
||||
}
|
||||
@@ -990,7 +1012,6 @@ func (f *folder) scanTimerFired(ctx context.Context) error {
|
||||
|
||||
func (f *folder) versionCleanupTimerFired(ctx context.Context) {
|
||||
f.setState(FolderCleanWaiting)
|
||||
defer f.setState(FolderIdle)
|
||||
|
||||
if err := f.ioLimiter.TakeWithContext(ctx, 1); err != nil {
|
||||
return
|
||||
@@ -1181,8 +1202,13 @@ func (f *folder) setError(ctx context.Context, err error) {
|
||||
default:
|
||||
}
|
||||
|
||||
_, _, oldErr := f.getState()
|
||||
if (err != nil && oldErr != nil && oldErr.Error() == err.Error()) || (err == nil && oldErr == nil) {
|
||||
state, _, oldErr := f.getState()
|
||||
switch {
|
||||
case err != nil && oldErr != nil && oldErr.Error() == err.Error():
|
||||
// The error is the same as the old error, no change is required
|
||||
return
|
||||
case err == nil && oldErr == nil && state == FolderIdle:
|
||||
// No error before or now, and we are already Idle
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1192,7 +1218,7 @@ func (f *folder) setError(ctx context.Context, err error) {
|
||||
} else {
|
||||
f.sl.InfoContext(ctx, "Folder error changed", slogutil.Error(err), slog.Any("previously", oldErr))
|
||||
}
|
||||
} else {
|
||||
} else if oldErr != nil {
|
||||
f.sl.InfoContext(ctx, "Folder error cleared")
|
||||
f.SchedulePull()
|
||||
}
|
||||
@@ -1200,7 +1226,7 @@ func (f *folder) setError(ctx context.Context, err error) {
|
||||
if f.FSWatcherEnabled {
|
||||
if err != nil {
|
||||
f.stopWatch()
|
||||
} else {
|
||||
} else if oldErr != nil {
|
||||
f.scheduleWatchRestart()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,6 @@ func (f *receiveEncryptedFolder) revert(ctx context.Context) error {
|
||||
f.sl.InfoContext(ctx, "Reverting unexpected items")
|
||||
|
||||
f.setState(FolderScanning)
|
||||
defer f.setState(FolderIdle)
|
||||
|
||||
batch := NewFileInfoBatch(func(fs []protocol.FileInfo) error {
|
||||
f.updateLocalsFromScanning(fs)
|
||||
|
||||
@@ -74,7 +74,6 @@ func (f *receiveOnlyFolder) revert(ctx context.Context) error {
|
||||
f.sl.InfoContext(ctx, "Reverting folder")
|
||||
|
||||
f.setState(FolderScanning)
|
||||
defer f.setState(FolderIdle)
|
||||
|
||||
scanChan := make(chan string)
|
||||
go f.pullScannerRoutine(ctx, scanChan)
|
||||
|
||||
@@ -60,15 +60,15 @@ func TestRecvOnlyRevertDeletes(t *testing.T) {
|
||||
|
||||
must(t, m.ScanFolder("ro"))
|
||||
|
||||
// We should now have two files and two directories, with global state unchanged.
|
||||
// We should now have three files and two directories, with global state unchanged.
|
||||
|
||||
size = mustV(m.GlobalSize("ro"))
|
||||
if size.Files != 1 || size.Directories != 1 {
|
||||
t.Fatalf("Global: expected 1 file and 1 directory: %+v", size)
|
||||
}
|
||||
size = mustV(m.LocalSize("ro", protocol.LocalDeviceID))
|
||||
if size.Files != 2 || size.Directories != 2 {
|
||||
t.Fatalf("Local: expected 2 files and 2 directories: %+v", size)
|
||||
if size.Files != 3 || size.Directories != 2 {
|
||||
t.Fatalf("Local: expected 3 files and 2 directories: %+v", size)
|
||||
}
|
||||
size = mustV(m.ReceiveOnlySize("ro"))
|
||||
if size.Files+size.Directories == 0 {
|
||||
@@ -123,10 +123,12 @@ func TestRecvOnlyRevertNeeds(t *testing.T) {
|
||||
oldData := []byte("hello\n")
|
||||
knownFiles := setupKnownFiles(t, ffs, oldData)
|
||||
|
||||
// Send and index update for the known stuff
|
||||
// Send an index update for the known stuff. Update the local index
|
||||
// prior to getting index data from the remote device, so we avoid a
|
||||
// pull starting and causing race flakiness for the rest of the test.
|
||||
|
||||
must(t, m.Index(conn, &protocol.Index{Folder: "ro", Files: knownFiles}))
|
||||
f.updateLocalsFromScanning(knownFiles)
|
||||
must(t, m.Index(conn, &protocol.Index{Folder: "ro", Files: knownFiles}))
|
||||
|
||||
// Scan the folder.
|
||||
|
||||
@@ -163,12 +165,11 @@ func TestRecvOnlyRevertNeeds(t *testing.T) {
|
||||
// We now have a newer file than the rest of the cluster. Global state should reflect this.
|
||||
|
||||
size = mustV(m.GlobalSize("ro"))
|
||||
const sizeOfDir = 128
|
||||
if size.Files != 1 || size.Bytes != sizeOfDir+int64(len(oldData)) {
|
||||
if size.Files != 1 || size.Bytes != int64(len(oldData)) {
|
||||
t.Fatalf("Global: expected no change due to the new file: %+v", size)
|
||||
}
|
||||
size = mustV(m.LocalSize("ro", protocol.LocalDeviceID))
|
||||
if size.Files != 1 || size.Bytes != sizeOfDir+int64(len(newData)) {
|
||||
if size.Files != 1 || size.Bytes != int64(len(newData)) {
|
||||
t.Fatalf("Local: expected the new file to be reflected: %+v", size)
|
||||
}
|
||||
size = mustV(m.NeedSize("ro", protocol.LocalDeviceID))
|
||||
@@ -185,11 +186,11 @@ func TestRecvOnlyRevertNeeds(t *testing.T) {
|
||||
m.Revert("ro")
|
||||
|
||||
size = mustV(m.GlobalSize("ro"))
|
||||
if size.Files != 1 || size.Bytes != sizeOfDir+int64(len(oldData)) {
|
||||
if size.Files != 1 || size.Bytes != int64(len(oldData)) {
|
||||
t.Fatalf("Global: expected the global size to revert: %+v", size)
|
||||
}
|
||||
size = mustV(m.LocalSize("ro", protocol.LocalDeviceID))
|
||||
if size.Files != 1 || size.Bytes != sizeOfDir+int64(len(newData)) {
|
||||
if size.Files != 1 || size.Bytes != int64(len(newData)) {
|
||||
t.Fatalf("Local: expected the local size to remain: %+v", size)
|
||||
}
|
||||
size = mustV(m.NeedSize("ro", protocol.LocalDeviceID))
|
||||
|
||||
@@ -98,7 +98,6 @@ func (f *sendOnlyFolder) override(ctx context.Context) error {
|
||||
f.sl.InfoContext(ctx, "Overriding global state ")
|
||||
|
||||
f.setState(FolderScanning)
|
||||
defer f.setState(FolderIdle)
|
||||
|
||||
batch := NewFileInfoBatch(func(files []protocol.FileInfo) error {
|
||||
f.updateLocalsFromScanning(files)
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"io"
|
||||
"log/slog"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -143,8 +144,13 @@ func newSendReceiveFolder(model *model, ignores *ignore.Matcher, cfg config.Fold
|
||||
f.puller = f
|
||||
|
||||
if f.Copiers == 0 {
|
||||
// "Copiers" is effectively the concurrency level for a number of
|
||||
// different processes in the folder runner, not only the specific
|
||||
// copy step. TODO: Rename this config option at some point.
|
||||
f.Copiers = defaultCopiers
|
||||
}
|
||||
// Cap the copiers at 2*NumCPU, so that we have a known upper bound.
|
||||
f.Copiers = min(f.Copiers, 2*runtime.NumCPU())
|
||||
|
||||
// If the configured max amount of pending data is zero, we use the
|
||||
// default. If it's configured to something non-zero but less than the
|
||||
@@ -168,7 +174,6 @@ func (f *sendReceiveFolder) pull(ctx context.Context) (bool, error) {
|
||||
go f.pullScannerRoutine(ctx, scanChan)
|
||||
defer func() {
|
||||
close(scanChan)
|
||||
f.setState(FolderIdle)
|
||||
}()
|
||||
|
||||
metricFolderPulls.WithLabelValues(f.ID).Inc()
|
||||
@@ -244,10 +249,10 @@ func (f *sendReceiveFolder) pullerIteration(ctx context.Context, scanChan chan<-
|
||||
f.tempPullErrors = make(map[string]string)
|
||||
f.errorsMut.Unlock()
|
||||
|
||||
pullChan := make(chan pullBlockState)
|
||||
copyChan := make(chan copyBlocksState)
|
||||
finisherChan := make(chan *sharedPullerState)
|
||||
dbUpdateChan := make(chan dbUpdateJob)
|
||||
pullChan := make(chan pullBlockState, f.Copiers)
|
||||
copyChan := make(chan copyBlocksState, f.Copiers)
|
||||
finisherChan := make(chan *sharedPullerState, f.Copiers)
|
||||
dbUpdateChan := make(chan dbUpdateJob, f.Copiers)
|
||||
|
||||
var pullWg sync.WaitGroup
|
||||
var copyWg sync.WaitGroup
|
||||
@@ -275,9 +280,11 @@ func (f *sendReceiveFolder) pullerIteration(ctx context.Context, scanChan chan<-
|
||||
})
|
||||
|
||||
// finisherRoutine finishes when finisherChan is closed
|
||||
doneWg.Go(func() {
|
||||
f.finisherRoutine(ctx, finisherChan, dbUpdateChan, scanChan)
|
||||
})
|
||||
for range f.Copiers {
|
||||
doneWg.Go(func() {
|
||||
f.finisherRoutine(ctx, finisherChan, dbUpdateChan, scanChan)
|
||||
})
|
||||
}
|
||||
|
||||
fileDeletions, dirDeletions, err := f.processNeeded(ctx, dbUpdateChan, copyChan, scanChan)
|
||||
|
||||
@@ -382,7 +389,7 @@ loop:
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if hasCurFile && file.BlocksEqual(curFile) {
|
||||
if hasCurFile && curFile.Type == file.Type && file.BlocksEqual(curFile) {
|
||||
// We are supposed to copy the entire file, and then fetch nothing. We
|
||||
// are only updating metadata, so we don't actually *need* to make the
|
||||
// copy.
|
||||
@@ -701,7 +708,7 @@ func (f *sendReceiveFolder) checkParent(file string, scanChan chan<- string) boo
|
||||
return true
|
||||
}
|
||||
f.sl.Debug("Creating parent directory", slogutil.FilePath(file))
|
||||
if err := f.mtimefs.MkdirAll(parent, 0o755); err != nil {
|
||||
if err := f.mtimefs.MkdirAll(parent, fs.ModePerm); err != nil {
|
||||
f.newPullError(file, fmt.Errorf("creating parent dir: %w", err))
|
||||
return false
|
||||
}
|
||||
@@ -1345,6 +1352,12 @@ func (f *sendReceiveFolder) copierRoutine(ctx context.Context, in <-chan copyBlo
|
||||
default:
|
||||
}
|
||||
|
||||
if block.Size == 0 {
|
||||
// Copying zero bytes is a no-op.
|
||||
state.copyDone(block)
|
||||
continue
|
||||
}
|
||||
|
||||
if !f.DisableSparseFiles && state.reused == 0 && block.IsEmpty() {
|
||||
// The block is a block of all zeroes, and we are not reusing
|
||||
// a temp file, so there is no need to do anything with it.
|
||||
@@ -1428,15 +1441,15 @@ func (f *sendReceiveFolder) copyBlock(ctx context.Context, block protocol.BlockI
|
||||
// Returns true when the block was successfully copied.
|
||||
// The passed buffer must be large enough to accommodate the block.
|
||||
func (f *sendReceiveFolder) copyBlockFromFolder(ctx context.Context, folderID string, block protocol.BlockInfo, state copyBlocksState, ffs fs.Filesystem, buf []byte) bool {
|
||||
for e, err := range itererr.Zip(f.model.sdb.AllLocalBlocksWithHash(folderID, block.Hash)) {
|
||||
if err != nil {
|
||||
// We just ignore this and continue pulling instead (though
|
||||
// there's a good chance that will fail too, if the DB is
|
||||
// unhealthy).
|
||||
f.sl.DebugContext(ctx, "Failed to get block information from database", "blockHash", block.Hash, slogutil.FilePath(state.file.Name), slogutil.Error(err))
|
||||
return false
|
||||
}
|
||||
candidates, err := itererr.Collect(f.model.sdb.AllLocalBlocksWithHash(folderID, block.Hash))
|
||||
if err != nil {
|
||||
// We just ignore this and continue pulling instead (though there's
|
||||
// a good chance that will fail too, if the DB is unhealthy).
|
||||
f.sl.DebugContext(ctx, "Failed to get block information from database", "blockHash", block.Hash, slogutil.FilePath(state.file.Name), slogutil.Error(err))
|
||||
return false
|
||||
}
|
||||
|
||||
for _, e := range candidates {
|
||||
if !f.copyBlockFromFile(ctx, e.FileName, e.Offset, state, ffs, block, buf) {
|
||||
if state.failed() != nil {
|
||||
return false
|
||||
@@ -1534,6 +1547,13 @@ func (f *sendReceiveFolder) pullerRoutine(ctx context.Context, in <-chan pullBlo
|
||||
|
||||
bytes := state.block.Size
|
||||
|
||||
if bytes == 0 {
|
||||
// Pulling zero bytes is a no-op.
|
||||
state.pullDone(state.block)
|
||||
out <- state.sharedPullerState
|
||||
continue
|
||||
}
|
||||
|
||||
if err := requestLimiter.TakeWithContext(ctx, bytes); err != nil {
|
||||
state.fail(err)
|
||||
out <- state.sharedPullerState
|
||||
@@ -1760,21 +1780,10 @@ func (f *sendReceiveFolder) dbUpdaterRoutine(dbUpdateChan <-chan dbUpdateJob) in
|
||||
tick := time.NewTicker(maxBatchTime)
|
||||
defer tick.Stop()
|
||||
batch := NewFileInfoBatch(func(files []protocol.FileInfo) error {
|
||||
// sync directories
|
||||
for dir := range changedDirs {
|
||||
delete(changedDirs, dir)
|
||||
if !f.DisableFsync {
|
||||
fd, err := f.mtimefs.Open(dir)
|
||||
if err != nil {
|
||||
f.sl.Debug("Fsync failed", slogutil.FilePath(dir), slogutil.Error(err))
|
||||
continue
|
||||
}
|
||||
if err := fd.Sync(); err != nil {
|
||||
f.sl.Debug("Fsync failed", slogutil.FilePath(dir), slogutil.Error(err))
|
||||
}
|
||||
fd.Close()
|
||||
}
|
||||
if !f.DisableFsync {
|
||||
f.fsyncDirs(changedDirs)
|
||||
}
|
||||
clear(changedDirs)
|
||||
|
||||
// All updates to file/folder objects that originated remotely
|
||||
// (across the network) use this call to updateLocals
|
||||
@@ -1828,6 +1837,27 @@ loop:
|
||||
return changed
|
||||
}
|
||||
|
||||
func (f *sendReceiveFolder) fsyncDirs(changedDirs map[string]struct{}) {
|
||||
var wg sync.WaitGroup
|
||||
sem := make(chan struct{}, f.Copiers)
|
||||
for dir := range changedDirs {
|
||||
sem <- struct{}{}
|
||||
wg.Go(func() {
|
||||
defer func() { <-sem }()
|
||||
fd, err := f.mtimefs.Open(dir)
|
||||
if err != nil {
|
||||
f.sl.Debug("Fsync failed", slogutil.FilePath(dir), slogutil.Error(err))
|
||||
return
|
||||
}
|
||||
if err := fd.Sync(); err != nil {
|
||||
f.sl.Debug("Fsync failed", slogutil.FilePath(dir), slogutil.Error(err))
|
||||
}
|
||||
fd.Close()
|
||||
})
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// pullScannerRoutine aggregates paths to be scanned after pulling. The scan is
|
||||
// scheduled once when scanChan is closed (scanning can not happen during pulling).
|
||||
func (f *sendReceiveFolder) pullScannerRoutine(ctx context.Context, scanChan <-chan string) {
|
||||
|
||||
@@ -891,7 +891,7 @@ func TestPullCtxCancel(t *testing.T) {
|
||||
emptyState := func() pullBlockState {
|
||||
return pullBlockState{
|
||||
sharedPullerState: newSharedPullerState(protocol.FileInfo{}, nil, f.folderID, "", nil, nil, false, false, protocol.FileInfo{}, false, false),
|
||||
block: protocol.BlockInfo{},
|
||||
block: protocol.BlockInfo{Size: 42},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user