Compare commits

..
Author SHA1 Message Date
felixfoertsch 43c0c88e18 build releases on their native hosts 2026-08-12 11:02:56 +02:00
felixfoertsch 30c2b4fd17 make gatekeeper assessment optional
custom release / build-custom-release (push) Successful in 1m36s
2026-05-24 20:24:17 +02:00
felixfoertsch 623d697e66 avoid daemon default keychain write
custom release / build-custom-release (push) Failing after 9s
2026-05-24 20:04:35 +02:00
felixfoertsch 0dba7d9d9e set codesign default keychain search list
custom release / build-custom-release (push) Failing after 11s
2026-05-24 20:03:30 +02:00
felixfoertsch ffbbd99a79 set codesign dynamic keychain domain
custom release / build-custom-release (push) Failing after 11s
2026-05-24 19:59:31 +02:00
felixfoertsch 6ec6ce1d4e place signing keychain in runner library
custom release / build-custom-release (push) Failing after 10s
2026-05-24 19:56:47 +02:00
felixfoertsch fca9102314 resolve codesign identity from default keychain
custom release / build-custom-release (push) Failing after 9s
2026-05-24 19:55:09 +02:00
felixfoertsch b8ea748973 order codesign signing arguments
custom release / build-custom-release (push) Failing after 10s
2026-05-24 19:52:13 +02:00
felixfoertsch 4b71e4f324 sign releases with developer id common name
custom release / build-custom-release (push) Failing after 10s
2026-05-24 19:50:43 +02:00
felixfoertsch fcaef0869f allow codesign access to imported p12 key
custom release / build-custom-release (push) Successful in 10s
2026-05-24 19:40:13 +02:00
felixfoertsch 4f2829ed48 place codesign keychain before signing options
custom release / build-custom-release (push) Successful in 12s
2026-05-24 19:35:46 +02:00
felixfoertsch cda7f54a48 unlock imported signing keychain during release
custom release / build-custom-release (push) Successful in 10s
2026-05-24 19:12:59 +02:00
felixfoertsch 55f5a70ff5 sign releases with imported keychain identity
custom release / build-custom-release (push) Successful in 11s
2026-05-24 19:10:02 +02:00
felixfoertsch 083f8e7068 allow passwordless developer id p12
custom release / build-custom-release (push) Successful in 13s
2026-05-24 18:58:11 +02:00
felixfoertsch 018d62af14 make release tea login repeatable
custom release / build-custom-release (push) Failing after 9s
2026-05-24 15:51:26 +02:00
felixfoertsch d45edca330 fail clearly when signing secrets are missing
custom release / build-custom-release (push) Failing after 9s
2026-05-24 15:50:01 +02:00
felixfoertsch 4cac15184c build syncthing macos release on ffmini, validate signing
custom release / build-custom-release (push) Failing after 1m12s
2026-05-24 15:18:13 +02:00
felixfoertsch eb67464ca7 add reusable stignore sync release automation
Keep main as an upstream mirror while storing the local .stignore behavior, GUI marker, reusable patch files, and Gitea release workflow in one replayable branch commit.
2026-05-24 13:13:09 +02:00
87 changed files with 2924 additions and 702 deletions
+3
View File
@@ -6,3 +6,6 @@ vendor/** -text=auto
# Diffs on these files are meaningless # Diffs on these files are meaningless
*.svg -diff *.svg -diff
# Patch files intentionally contain diff context whitespace
patches/*.patch -whitespace
+135
View File
@@ -0,0 +1,135 @@
name: custom release
permissions:
contents: write
releases: write
on:
push:
branches:
- main
paths:
- ".gitea/workflows/custom-release.yml"
- "patches/**"
- "scripts/update-custom-release.sh"
workflow_dispatch:
inputs:
upstream_tag:
description: "Optional upstream Syncthing tag, for example v2.1.0"
required: false
suffix:
description: "Optional custom release suffix, for example stignore.7"
required: false
schedule:
- cron: "17 04 * * *"
jobs:
build-custom-release:
runs-on: ffmini_macos_arm64
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: false
- name: Configure Git author
run: |
git config user.name "Gitea Actions"
git config user.email "actions@git.felixfoertsch.de"
- name: Set up tea
run: |
go install code.gitea.io/tea@v0.14.1
echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH"
"$(go env GOPATH)/bin/tea" logins delete actions >/dev/null 2>&1 || true
"$(go env GOPATH)/bin/tea" logins add --name actions --url https://git.felixfoertsch.de --token "$GITEA_TOKEN" --no-version-check
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
- name: Import Developer ID certificate
run: |
set -euo pipefail
keychain_dir="$HOME/Library/Keychains"
mkdir -p "$keychain_dir"
keychain_path="$keychain_dir/syncthing-release-signing-${GITHUB_RUN_ID:-$$}.keychain-db"
keychain_password="$(openssl rand -hex 24)"
certificate_path="$RUNNER_TEMP/developer-id-application.p12"
previous_default_keychain="$(security default-keychain -d user 2>/dev/null | sed 's/[ "]//g' || true)"
echo "CUSTOM_RELEASE_KEYCHAIN_PATH=$keychain_path" >> "$GITHUB_ENV"
echo "CUSTOM_RELEASE_KEYCHAIN_PASSWORD=$keychain_password" >> "$GITHUB_ENV"
echo "CUSTOM_RELEASE_PREVIOUS_DEFAULT_KEYCHAIN=$previous_default_keychain" >> "$GITHUB_ENV"
if [ -z "$DEVELOPER_ID_APPLICATION_P12_BASE64" ]; then
echo "DEVELOPER_ID_APPLICATION_P12_BASE64 secret is required" >&2
exit 1
fi
printf '%s' "$DEVELOPER_ID_APPLICATION_P12_BASE64" | base64 -D > "$certificate_path"
rm -f "$keychain_path"
security create-keychain -p "$keychain_password" "$keychain_path"
security set-keychain-settings -lut 21600 "$keychain_path"
security unlock-keychain -p "$keychain_password" "$keychain_path"
security import "$certificate_path" -k "$keychain_path" -P "$DEVELOPER_ID_APPLICATION_P12_PASSWORD" -A -T /usr/bin/codesign -T /usr/bin/security
existing_keychains=()
while IFS= read -r existing_keychain; do
existing_keychain="$(printf '%s' "$existing_keychain" | sed 's/[ "]//g')"
if [ -n "$existing_keychain" ] && [ -e "$existing_keychain" ] && [[ "$existing_keychain" != *"/syncthing-release-signing-"*".keychain-db" ]]; then
existing_keychains+=("$existing_keychain")
fi
done < <(security list-keychains)
security list-keychains -s "$keychain_path" "${existing_keychains[@]}"
security list-keychains -d user -s "$keychain_path" "${existing_keychains[@]}" || true
security default-keychain -d user -s "$keychain_path" || true
security list-keychains
security list-keychains -d user || true
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$keychain_password" "$keychain_path"
identity_output="$(security find-identity -v -p codesigning "$keychain_path")"
printf '%s\n' "$identity_output"
security find-identity -v -p codesigning
codesign_identity_sha1="$(printf '%s\n' "$identity_output" | awk '/"Developer ID Application:/ { print $2; exit }')"
codesign_identity="$(printf '%s\n' "$identity_output" | sed -n 's/.*"\(Developer ID Application:[^"]*\)".*/\1/p' | head -n 1)"
if [ -z "$codesign_identity" ]; then
echo "Developer ID Application signing identity is required in DEVELOPER_ID_APPLICATION_P12_BASE64" >&2
exit 1
fi
probe_binary="$RUNNER_TEMP/codesign-probe"
cp /usr/bin/true "$probe_binary"
codesign --force --dryrun --sign "$codesign_identity" --keychain "$keychain_path" --options runtime --timestamp "$probe_binary"
echo "CUSTOM_RELEASE_CODESIGN_IDENTITY=$codesign_identity" >> "$GITHUB_ENV"
echo "CUSTOM_RELEASE_CODESIGN_IDENTITY_SHA1=$codesign_identity_sha1" >> "$GITHUB_ENV"
env:
DEVELOPER_ID_APPLICATION_P12_BASE64: ${{ secrets.DEVELOPER_ID_APPLICATION_P12_BASE64 }}
DEVELOPER_ID_APPLICATION_P12_PASSWORD: ${{ secrets.DEVELOPER_ID_APPLICATION_P12_PASSWORD }}
- name: Build patched Syncthing release
run: ./scripts/update-custom-release.sh
env:
CUSTOM_RELEASE_UPSTREAM_TAG: ${{ github.event.inputs.upstream_tag }}
CUSTOM_RELEASE_SUFFIX: ${{ github.event.inputs.suffix }}
CUSTOM_RELEASE_PUSH: "1"
CUSTOM_RELEASE_PUSH_BRANCH: "0"
CUSTOM_RELEASE_REMOTE: origin
CUSTOM_RELEASE_BUILDS: "darwin/arm64/zip/1 linux/amd64/tar/0 linux/arm64/tar/0"
CUSTOM_RELEASE_CODESIGN_TEAM_ID: "NG5W75WE8U"
CUSTOM_RELEASE_REQUIRE_GATEKEEPER_ASSESSMENT: "0"
CUSTOM_RELEASE_CREATE_GITEA_RELEASE: "1"
CUSTOM_RELEASE_TEA_REPO: felixfoertsch/syncthing
- name: Delete temporary keychain
if: always()
run: |
if [ -n "${CUSTOM_RELEASE_PREVIOUS_DEFAULT_KEYCHAIN:-}" ] && [ -e "$CUSTOM_RELEASE_PREVIOUS_DEFAULT_KEYCHAIN" ]; then
security default-keychain -d user -s "$CUSTOM_RELEASE_PREVIOUS_DEFAULT_KEYCHAIN" || true
fi
if [ -n "${CUSTOM_RELEASE_KEYCHAIN_PATH:-}" ]; then
security delete-keychain "$CUSTOM_RELEASE_KEYCHAIN_PATH" || true
fi
@@ -0,0 +1,88 @@
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
strategy:
fail-fast: false
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
+80
View File
@@ -0,0 +1,80 @@
name: custom release
permissions:
contents: write
on:
push:
branches:
- main
paths:
- ".github/workflows/custom-release.yml"
- "patches/**"
- "scripts/update-custom-release.sh"
workflow_dispatch:
inputs:
upstream_tag:
description: "Optional upstream Syncthing tag, for example v2.1.3"
required: false
suffix:
description: "Optional custom release suffix, for example stignore.7"
required: false
schedule:
- cron: "17 04 * * *"
jobs:
build-custom-release:
runs-on: macos-14
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: false
- name: Configure Git author
run: |
git config user.name "GitHub Actions"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Build patched Syncthing release
run: |
case "$(uname -m)" in
arm64) darwin_arch=arm64 ;;
x86_64) darwin_arch=amd64 ;;
*) echo "Unsupported macOS runner architecture: $(uname -m)" >&2; exit 1 ;;
esac
export CUSTOM_RELEASE_BUILDS="darwin/$darwin_arch/zip/1 linux/amd64/tar/0 linux/arm64/tar/0"
./scripts/update-custom-release.sh
env:
CUSTOM_RELEASE_UPSTREAM_TAG: ${{ github.event.inputs.upstream_tag }}
CUSTOM_RELEASE_SUFFIX: ${{ github.event.inputs.suffix }}
CUSTOM_RELEASE_PUSH: "1"
CUSTOM_RELEASE_PUSH_BRANCH: "0"
CUSTOM_RELEASE_REMOTE: origin
CUSTOM_RELEASE_CREATE_GITEA_RELEASE: "0"
GH_TOKEN: ${{ github.token }}
- name: Publish GitHub release
run: |
tag="${CUSTOM_RELEASE_UPSTREAM_TAG:-$(git ls-remote --refs --tags --sort='version:refname' https://github.com/syncthing/syncthing.git 'v[0-9]*' | awk '{ tag = $2; sub("refs/tags/", "", tag); if (tag ~ /^v[0-9]+\.[0-9]+\.[0-9]+$/) latest = tag } END { print latest }')}-$CUSTOM_RELEASE_SUFFIX"
if gh release view "$tag" >/dev/null 2>&1; then
echo "GitHub release $tag already exists; nothing to do."
exit 0
fi
assets=()
for asset in dist/*; do
[ -f "$asset" ] || continue
[ "$(basename "$asset")" = release-notes.md ] && continue
assets+=("$asset")
done
gh release create "$tag" "${assets[@]}" --title "$tag" --notes-file dist/release-notes.md --verify-tag
env:
CUSTOM_RELEASE_UPSTREAM_TAG: ${{ github.event.inputs.upstream_tag }}
CUSTOM_RELEASE_SUFFIX: ${{ github.event.inputs.suffix || 'stignore.7' }}
GH_TOKEN: ${{ github.token }}
+21
View File
@@ -0,0 +1,21 @@
name: Mirrors
on: [push, delete]
permissions:
contents: read
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"
+21
View File
@@ -0,0 +1,21 @@
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 }}
+28
View File
@@ -0,0 +1,28 @@
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 }}"
+60
View File
@@ -0,0 +1,60 @@
name: Release Syncthing
on:
push:
branches:
- release
- release-rc*
permissions:
contents: write
jobs:
create-release-tag:
name: Create release tag
if: github.repository_owner == 'syncthing'
runs-on: ubuntu-latest
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 }}
+25
View File
@@ -0,0 +1,25 @@
name: Trigger nightly build & release
on:
workflow_dispatch:
schedule:
# Run nightly build at 01:00 UTC
- cron: '00 01 * * *'
permissions:
contents: write
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
@@ -0,0 +1,31 @@
name: Update translations and documentation
on:
workflow_dispatch:
schedule:
- cron: '42 3 * * 1'
permissions:
contents: write
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 }}
+1
View File
@@ -18,3 +18,4 @@ deb
/repos /repos
/proto/scripts/protoc-gen-gosyncthing /proto/scripts/protoc-gen-gosyncthing
/compat.json /compat.json
/dist/
-4
View File
@@ -116,7 +116,6 @@ Dmitry Saveliev (dsaveliev) <d.e.saveliev@gmail.com>
domain <32405309+szu17dmy@users.noreply.github.com> domain <32405309+szu17dmy@users.noreply.github.com>
Domenic Horner <domenic@tgxn.net> Domenic Horner <domenic@tgxn.net>
Dominik Heidler (asdil12) <dominik@heidler.eu> Dominik Heidler (asdil12) <dominik@heidler.eu>
Elias <1elias.bauer@gmail.com>
Elias Jarlebring (jarlebring) <jarlebring@gmail.com> Elias Jarlebring (jarlebring) <jarlebring@gmail.com>
Elliot Huffman <thelich2@gmail.com> Elliot Huffman <thelich2@gmail.com>
Emil Hessman (ceh) <emil@hessman.se> Emil Hessman (ceh) <emil@hessman.se>
@@ -149,7 +148,6 @@ HansK-p <42314815+HansK-p@users.noreply.github.com>
Harrison Jones (harrisonhjones) <harrisonhjones@users.noreply.github.com> Harrison Jones (harrisonhjones) <harrisonhjones@users.noreply.github.com>
Hazem Krimi <me@hazemkrimi.tech> Hazem Krimi <me@hazemkrimi.tech>
Heiko Zuerker (Smiley73) <heiko@zuerker.org> Heiko Zuerker (Smiley73) <heiko@zuerker.org>
Henrik Bråthen <henrikbs@proton.me>
Hireworks <129852174+hireworksltd@users.noreply.github.com> Hireworks <129852174+hireworksltd@users.noreply.github.com>
Hugo Locurcio <hugo.locurcio@hugo.pro> Hugo Locurcio <hugo.locurcio@hugo.pro>
Iain Barnett <iainspeed@gmail.com> Iain Barnett <iainspeed@gmail.com>
@@ -218,7 +216,6 @@ Matic Potočnik <hairyfotr@gmail.com>
Matt Burke (burkemw3) <mburke@amplify.com> <burkemw3@gmail.com> Matt Burke (burkemw3) <mburke@amplify.com> <burkemw3@gmail.com>
Matt Robenolt <matt@ydekproductions.com> Matt Robenolt <matt@ydekproductions.com>
Matteo Ruina <matteo.ruina@gmail.com> Matteo Ruina <matteo.ruina@gmail.com>
mattn <mattn.jp@gmail.com>
Maurizio Tomasi <ziotom78@gmail.com> Maurizio Tomasi <ziotom78@gmail.com>
Max <github@germancoding.com> Max <github@germancoding.com>
Max Schulze (kralo) <max.schulze@online.de> <kralo@users.noreply.github.com> Max Schulze (kralo) <max.schulze@online.de> <kralo@users.noreply.github.com>
@@ -289,7 +286,6 @@ Sergey Mishin (ralder) <ralder@yandex.ru>
Sertonix <83883937+Sertonix@users.noreply.github.com> Sertonix <83883937+Sertonix@users.noreply.github.com>
Severin von Wnuck-Lipinski <ss7@live.de> Severin von Wnuck-Lipinski <ss7@live.de>
Shaarad Dalvi <60266155+shaaraddalvi@users.noreply.github.com> <shdalv@microsoft.com> 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> Shivam Kumar <155747305+maishivamhoo123@users.noreply.github.com>
Simon Mwepu <simonmwepu@gmail.com> Simon Mwepu <simonmwepu@gmail.com>
Simon Pickup <simon@pickupinfinity.com> Simon Pickup <simon@pickupinfinity.com>
-8
View File
@@ -1,8 +0,0 @@
<?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>
+6 -15
View File
@@ -718,7 +718,7 @@ func shouldBuildSyso(dir string) (string, error) {
} }
jsonPath := filepath.Join(dir, "versioninfo.json") jsonPath := filepath.Join(dir, "versioninfo.json")
err = os.WriteFile(jsonPath, bs, 0o666) err = os.WriteFile(jsonPath, bs, 0o644)
if err != nil { if err != nil {
return "", errors.New("failed to create " + jsonPath + ": " + err.Error()) return "", errors.New("failed to create " + jsonPath + ": " + err.Error())
} }
@@ -732,18 +732,9 @@ func shouldBuildSyso(dir string) (string, error) {
sysoPath := filepath.Join(dir, "cmd", "syncthing", "resource.syso") sysoPath := filepath.Join(dir, "cmd", "syncthing", "resource.syso")
// See https://github.com/josephspurrier/goversioninfo#command-line-flags // See https://github.com/josephspurrier/goversioninfo#command-line-flags
// For manifest see https://learn.microsoft.com/en-us/windows/console/console-allocation-policy arm := strings.HasPrefix(goarch, "arm")
isARM := strings.HasPrefix(goarch, "arm") a64 := strings.Contains(goarch, "64")
is64Bit := strings.Contains(goarch, "64") if _, err := runError("goversioninfo", "-o", sysoPath, fmt.Sprintf("-arm=%v", arm), fmt.Sprintf("-64=%v", a64)); err != nil {
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()) return "", errors.New("failed to create " + sysoPath + ": " + err.Error())
} }
@@ -783,7 +774,7 @@ func copyFile(src, dst string, perm os.FileMode) error {
} }
copy: copy:
os.MkdirAll(filepath.Dir(dst), os.ModePerm) os.MkdirAll(filepath.Dir(dst), 0o777)
if err := os.WriteFile(dst, in, perm); err != nil { if err := os.WriteFile(dst, in, perm); err != nil {
return err return err
} }
@@ -1432,7 +1423,7 @@ func writeCompatJSON() {
continue continue
} }
bs, _ := json.MarshalIndent(e, "", " ") bs, _ := json.MarshalIndent(e, "", " ")
if err := os.WriteFile("compat.json", bs, 0o666); err != nil { if err := os.WriteFile("compat.json", bs, 0o644); err != nil {
log.Fatal("Writing compat.json:", err) log.Fatal("Writing compat.json:", err)
} }
return return
+3 -3
View File
@@ -42,7 +42,7 @@ type currentFile struct {
} }
func (d *diskStore) Serve(ctx context.Context) { func (d *diskStore) Serve(ctx context.Context) {
if err := os.MkdirAll(d.dir, os.ModePerm); err != nil { if err := os.MkdirAll(d.dir, 0o700); err != nil {
log.Println("Creating directory:", err) log.Println("Creating directory:", err)
return return
} }
@@ -62,7 +62,7 @@ func (d *diskStore) Serve(ctx context.Context) {
case entry := <-d.inbox: case entry := <-d.inbox:
path := d.fullPath(entry.path) path := d.fullPath(entry.path)
if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil { if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
log.Println("Creating directory:", err) log.Println("Creating directory:", err)
continue continue
} }
@@ -77,7 +77,7 @@ func (d *diskStore) Serve(ctx context.Context) {
log.Println("Failed to compress crash report:", err) log.Println("Failed to compress crash report:", err)
continue continue
} }
if err := os.WriteFile(path, buf.Bytes(), 0o666); err != nil { if err := os.WriteFile(path, buf.Bytes(), 0o600); err != nil {
log.Printf("Failed to write %s: %v", entry.path, err) log.Printf("Failed to write %s: %v", entry.path, err)
_ = os.Remove(path) _ = os.Remove(path)
continue continue
+1 -1
View File
@@ -52,5 +52,5 @@ func compressAndWrite(bs []byte, fullPath string) error {
gw.Close() gw.Close()
// Create an output file with the compressed report // Create an output file with the compressed report
return os.WriteFile(fullPath, buf.Bytes(), 0o666) return os.WriteFile(fullPath, buf.Bytes(), 0o644)
} }
+1 -1
View File
@@ -612,7 +612,7 @@ func saveRelays(file string, relays []*relay) error {
for _, relay := range relays { for _, relay := range relays {
content += relay.uri.String() + "\n" content += relay.uri.String() + "\n"
} }
return os.WriteFile(file, []byte(content), 0o666) return os.WriteFile(file, []byte(content), 0o777)
} }
func createTestCertificate() tls.Certificate { func createTestCertificate() tls.Certificate {
+1 -1
View File
@@ -167,7 +167,7 @@ func (c *CLI) process(srcFs fs.Filesystem, dstFs fs.Filesystem, path string) err
var plainFd fs.File var plainFd fs.File
if dstFs != nil { if dstFs != nil {
if err := dstFs.MkdirAll(filepath.Dir(plainFi.Name), fs.ModePerm); err != nil { if err := dstFs.MkdirAll(filepath.Dir(plainFi.Name), 0o700); err != nil {
return fmt.Errorf("%s: %w", plainFi.Name, err) return fmt.Errorf("%s: %w", plainFi.Name, err)
} }
+1 -1
View File
@@ -7,5 +7,5 @@
package main package main
type buildSpecificOptions struct { type buildSpecificOptions struct {
HideConsole bool `name:"no-console" help:"Hide console window (Always enabled on Windows 11 24H2 and later)" env:"STHIDECONSOLE"` HideConsole bool `name:"no-console" help:"Hide console window" env:"STHIDECONSOLE"`
} }
+1 -1
View File
@@ -673,7 +673,7 @@ func auditWriter(auditFile string) io.Writer {
} else { } else {
auditFlags = os.O_WRONLY | os.O_CREATE | os.O_APPEND auditFlags = os.O_WRONLY | os.O_CREATE | os.O_APPEND
} }
fd, err = os.OpenFile(auditFile, auditFlags, 0o666) fd, err = os.OpenFile(auditFile, auditFlags, 0o600)
if err != nil { if err != nil {
slog.Error("Failed to open audit file", slogutil.Error(err)) slog.Error("Failed to open audit file", slogutil.Error(err))
os.Exit(svcutil.ExitError.AsInt()) os.Exit(svcutil.ExitError.AsInt())
+1 -1
View File
@@ -479,7 +479,7 @@ func (f *autoclosedFile) ensureOpenLocked() error {
// We open the file for write only, and create it if it doesn't exist. // 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 flags := os.O_WRONLY | os.O_CREATE | os.O_APPEND
fd, err := os.OpenFile(f.name, flags, 0o666) fd, err := os.OpenFile(f.name, flags, 0o644)
if err != nil { if err != nil {
return err return err
} }
+2 -21
View File
@@ -9,27 +9,8 @@
package main package main
import "golang.org/x/sys/windows" import "os/exec"
func openURL(url string) error { func openURL(url string) error {
urlPtr, err := windows.UTF16PtrFromString(url) return exec.Command("cmd.exe", "/C", "start "+url).Run()
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
} }
+18 -15
View File
@@ -22,17 +22,17 @@ require (
github.com/julienschmidt/httprouter v1.3.0 github.com/julienschmidt/httprouter v1.3.0
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/maruel/panicparse/v2 v2.5.0 github.com/maruel/panicparse/v2 v2.5.0
github.com/mattn/go-sqlite3 v1.14.45 github.com/mattn/go-sqlite3 v1.14.44
github.com/maxmind/geoipupdate/v6 v6.1.0 github.com/maxmind/geoipupdate/v6 v6.1.0
github.com/miscreant/miscreant.go v0.0.0-20200214223636-26d376326b75 github.com/miscreant/miscreant.go v0.0.0-20200214223636-26d376326b75
github.com/oschwald/geoip2-golang v1.13.0 github.com/oschwald/geoip2-golang v1.13.0
github.com/pierrec/lz4/v4 v4.1.27 github.com/pierrec/lz4/v4 v4.1.26
github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_golang v1.23.2
github.com/puzpuzpuz/xsync/v3 v3.5.1 github.com/puzpuzpuz/xsync/v3 v3.5.1
github.com/quic-go/quic-go v0.60.0 github.com/quic-go/quic-go v0.59.0
github.com/rabbitmq/amqp091-go v1.11.0 github.com/rabbitmq/amqp091-go v1.11.0
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9
github.com/shirou/gopsutil/v4 v4.26.5 github.com/shirou/gopsutil/v4 v4.26.4
github.com/syncthing/notify v0.0.0-20250528144937-c7027d4f7465 github.com/syncthing/notify v0.0.0-20250528144937-c7027d4f7465
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d
github.com/thejerf/suture/v4 v4.0.6 github.com/thejerf/suture/v4 v4.0.6
@@ -40,14 +40,14 @@ require (
github.com/vitrun/qart v0.0.0-20160531060029-bf64b92db6b0 github.com/vitrun/qart v0.0.0-20160531060029-bf64b92db6b0
github.com/willabides/kongplete v0.4.0 github.com/willabides/kongplete v0.4.0
github.com/wlynxg/anet v0.0.5 github.com/wlynxg/anet v0.0.5
golang.org/x/crypto v0.53.0 golang.org/x/crypto v0.51.0
golang.org/x/exp v0.0.0-20260611194520-c48552f49976 golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a
golang.org/x/net v0.56.0 golang.org/x/net v0.55.0
golang.org/x/sys v0.46.0 golang.org/x/sys v0.45.0
golang.org/x/text v0.38.0 golang.org/x/text v0.37.0
golang.org/x/time v0.15.0 golang.org/x/time v0.15.0
google.golang.org/protobuf v1.36.11 google.golang.org/protobuf v1.36.11
modernc.org/sqlite v1.52.0 modernc.org/sqlite v1.50.0
sigs.k8s.io/yaml v1.6.0 sigs.k8s.io/yaml v1.6.0
) )
@@ -94,12 +94,12 @@ require (
github.com/tklauser/numcpus v0.11.0 // indirect github.com/tklauser/numcpus v0.11.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/mod v0.37.0 // indirect golang.org/x/mod v0.36.0 // indirect
golang.org/x/sync v0.21.0 // indirect golang.org/x/sync v0.20.0 // indirect
golang.org/x/telemetry v0.0.0-20260610154732-fb80ec83bdd9 // indirect golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6 // indirect
golang.org/x/tools v0.46.0 // indirect golang.org/x/tools v0.45.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.72.3 // indirect modernc.org/libc v1.72.0 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect modernc.org/memory v1.11.0 // indirect
) )
@@ -110,6 +110,9 @@ replace github.com/gobwas/glob v0.2.3 => github.com/calmh/glob v0.0.0-2022061508
// https://github.com/jackpal/gateway/pull/49 // 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 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 ( tool (
github.com/calmh/xdr/cmd/genxdr github.com/calmh/xdr/cmd/genxdr
github.com/maxbrunsfeld/counterfeiter/v6 github.com/maxbrunsfeld/counterfeiter/v6
+36 -38
View File
@@ -19,6 +19,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/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 h1:Fjm4GuJ+TGMgqfGHN42IQArJb77CfD/mAwLbDUoJe6g=
github.com/calmh/glob v0.0.0-20220615080505-1d823af5017b/go.mod h1:91K7jfEsgJSyfSrX+gmrRfZMtntx6JsHolWubGXDopg= 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 h1:g2cTUtZuFGmMGX8GoykPkN1Judj2uw8/3/aEtq4Z/rg=
github.com/calmh/incontainer v1.0.0/go.mod h1:eOhqnw15c9X+4RNBe0W3HlUZFfX16O0EDsCOInTndHY= 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 h1:GaGSNH4ZDw9kNdYqle6+RcAENiaQ8/611Ok+jQbBEeU=
@@ -147,8 +149,6 @@ 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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.45 h1:6KA/spDguL3KV8rnybG7ezSaE4SeMR3KC9VbUoAQaIk=
github.com/mattn/go-sqlite3 v1.14.45/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
github.com/maxbrunsfeld/counterfeiter/v6 v6.12.0 h1:aOeI7xAOVdK+R6xbVsZuU9HmCZYmQVmZgPf9xJUd2Sg= 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/maxbrunsfeld/counterfeiter/v6 v6.12.0/go.mod h1:0hZWbtfeCYUQeAQdPLUzETiBhUSns7O6LDj9vH88xKA=
github.com/maxmind/geoipupdate/v6 v6.1.0 h1:sdtTHzzQNJlXF5+fd/EoPTucRHyMonYt/Cok8xzzfqA= 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/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 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8= github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
github.com/pierrec/lz4/v4 v4.1.27 h1:+PhzhWDrjRj89TH2sw43nE3+4+W8lSxIuQadEHZyjUk= github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY=
github.com/pierrec/lz4/v4 v4.1.27/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -200,10 +200,8 @@ github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzM
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg= 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/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0= github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk= github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
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.11.0 h1:HxIctVm9Gid/Vtn706necmZ7Wj6pgGI2eqplRbEY8O8= 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/rabbitmq/amqp091-go v1.11.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 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg=
@@ -218,8 +216,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/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 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8=
github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM= github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM=
github.com/shirou/gopsutil/v4 v4.26.5 h1:RPcBXkpz7kOj9PqGFQOlBPZHsyaPvPVQc098y9RmCNM= github.com/shirou/gopsutil/v4 v4.26.4 h1:B4SXVbcwTyrocPHEmWBC4uCYr4Xcu3MK1TXqbprAOWY=
github.com/shirou/gopsutil/v4 v4.26.5/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= github.com/shirou/gopsutil/v4 v4.26.4/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -267,13 +265,13 @@ 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-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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/exp v0.0.0-20260611194520-c48552f49976 h1:X8Hz2ImujgbmetVuW+w2YkyZChE3cBpZi2P158rTG9M= golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a h1:+3jdDGGB8NGb1Zktc737jlt3/A5f6UlwSzmvqUuufxw=
golang.org/x/exp v0.0.0-20260611194520-c48552f49976/go.mod h1:vnf4pv9iKZXY58sQE1L86zmNWJ4159e1RkcWiLCkeEY= golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 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-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -282,13 +280,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-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-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sync v0.20.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-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-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -311,25 +309,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.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.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20260610154732-fb80ec83bdd9 h1:FjUup8XrRy7lv+XHONi6KKUSizeF2NnVrTnz/HhbohQ= golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6 h1:HjU6IWBiAgRIdAJ9/y1rwCn+UELEmwV+VsTLzj/W4sE=
golang.org/x/telemetry v0.0.0-20260610154732-fb80ec83bdd9/go.mod h1:3AWMyWHS+caVoiEXpiq6+tzKA40J4vQT3MYr80ZtQpc= golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6/go.mod h1:Eqhaxk/wZsWEH8CRxLwj6xzEJbz7k1EFGqx7nyCoabE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.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.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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/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.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= 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-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-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.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.46.0 h1:7jTurBkPZu4moS/Uy4OQT1M+QBlsj3wejyZwsT8Z7rk= golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
golang.org/x/tools v0.46.0/go.mod h1:FrD85F8l+NWL+9XWBSyVSHO6Ne4jutsfIFba7AWQ5Ys= golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -360,10 +358,10 @@ 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.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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY= modernc.org/cc/v4 v4.27.3 h1:uNCgn37E5U09mTv1XgskEVUJ8ADKpmFMPxzGJ0TSo+U=
modernc.org/cc/v4 v4.28.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= modernc.org/cc/v4 v4.27.3/go.mod h1:3YjcbCqhoTTHPycJDRl2WZKKFj0nwcOIPBfEZK0Hdk8=
modernc.org/ccgo/v4 v4.34.0 h1:yRLPFZieg532OT4rp4JFNIVcquwalMX26G95WQDqwCQ= modernc.org/ccgo/v4 v4.32.4 h1:L5OB8rpEX4ZsXEQwGozRfJyJSFHbbNVOoQ59DU9/KuU=
modernc.org/ccgo/v4 v4.34.0/go.mod h1:AS5WYMyBakQ+fhsHhtP8mWB82KTGPkNNJDGfGQCe0/A= modernc.org/ccgo/v4 v4.32.4/go.mod h1:lY7f+fiTDHfcv6YlRgSkxYfhs+UvOEEzj49jAn2TOx0=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= 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 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
@@ -372,18 +370,18 @@ 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.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.72.3 h1:ZnDF4tXn4NBXFutMMQC4vtbTFSXhhKzR73fv0beZEAU= modernc.org/libc v1.72.0 h1:IEu559v9a0XWjw0DPoVKtXpO2qt5NVLAnFaBbjq+n8c=
modernc.org/libc v1.72.3/go.mod h1:dn0dZNnnn1clLyvRxLxYExxiKRZIRENOfqQ8XEeg4Qs= modernc.org/libc v1.72.0/go.mod h1:tTU8DL8A+XLVkEY3x5E/tO7s2Q/q42EtnNWda/L5QhQ=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= 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 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg= modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.52.0 h1:p4dhYh2tXZCiyaqHwRVJDjIGKWyXayiQpThxgDzJaxo= modernc.org/sqlite v1.50.0 h1:eMowQSWLK0MeiQTdmz3lqoF5dqclujdlIKeJA11+7oM=
modernc.org/sqlite v1.52.0/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM= modernc.org/sqlite v1.50.0/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+2 -2
View File
@@ -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-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 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 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.": "Sartu baztertze-patroiak, bakarra lerro bakoitzeko.", "Enter ignore patterns, one per line.": "Ezkluzio filtroak sar, lerro bakoitzean bat bakarrik.",
"Enter up to three octal digits.": "Sartu 3 digitu bitarte", "Enter up to three octal digits.": "Sartu 3 digitu bitarte",
"Error": "Hutsa", "Error": "Hutsa",
"External File Versioning": "Fitxategi bertsioen kanpoko kudeaketa", "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.", "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", "Options": "Hautuzkoak",
"Out of Sync": "Ez sinkronizatua", "Out of Sync": "Ez sinkronizatua",
"Out of Sync Items": "Sinkronizatu gabeko elementuak", "Out of Sync Items": "Ez sinkronizatu elementuak",
"Outgoing Rate Limit (KiB/s)": "Bidaltze emari gehienekoa (KiB/s)", "Outgoing Rate Limit (KiB/s)": "Bidaltze emari gehienekoa (KiB/s)",
"Override": "Gainidatzi", "Override": "Gainidatzi",
"Override Changes": "Aldaketak desegin", "Override Changes": "Aldaketak desegin",
-6
View File
@@ -11,7 +11,6 @@
"Add Device": "Dodaj uređaj", "Add Device": "Dodaj uređaj",
"Add Folder": "Dodaj mapu", "Add Folder": "Dodaj mapu",
"Add Remote Device": "Dodaj udaljeni uređaj", "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 filter entry": "Dodaj unos filtra",
"Add ignore patterns": "Dodaj uzorke zanemarivanja", "Add ignore patterns": "Dodaj uzorke zanemarivanja",
"Add new folder?": "Dodati novu mapu?", "Add new folder?": "Dodati novu mapu?",
@@ -188,7 +187,6 @@
"GUI Authentication Password": "Lozinka za GUI autentifikacju", "GUI Authentication Password": "Lozinka za GUI autentifikacju",
"GUI Authentication User": "Korisnik za GUI autentifikacju", "GUI Authentication User": "Korisnik za GUI autentifikacju",
"GUI Authentication: Set User and Password": "GUI autentifikacija: Postavite korisnika i lozinku", "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 Override Directory": "Direktorij za nadjačavanje GUI-ja",
"GUI Theme": "Tema GUI-ja", "GUI Theme": "Tema GUI-ja",
"General": "Općenito", "General": "Općenito",
@@ -216,8 +214,6 @@
"Incorrect user name or password.": "Neispravno korisničko ime ili lozinka.", "Incorrect user name or password.": "Neispravno korisničko ime ili lozinka.",
"Info": "Informacije", "Info": "Informacije",
"Internally used paths:": "Interno korištene staze:", "Internally used paths:": "Interno korištene staze:",
"Introduced By": "Uveden od",
"Introducer": "Posrednik",
"Introduction": "Uvod", "Introduction": "Uvod",
"Inversion of the given condition (i.e. do not exclude)": "Inverzija zadanog uvjeta (tj. ne isključuj)", "Inversion of the given condition (i.e. do not exclude)": "Inverzija zadanog uvjeta (tj. ne isključuj)",
"Keep Versions": "Zadrži verzije", "Keep Versions": "Zadrži verzije",
@@ -252,7 +248,6 @@
"Log tailing paused. Scroll to the bottom to continue.": "Praćenje zapisa je pauzirano. Pomaknite se na dno kako biste nastavili.", "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.", "Login failed, see Syncthing logs for details.": "Prijava nije uspjela. Pogledaj detalje u odjeljku zapisima sinkronizacije.",
"Logs": "Zapisi", "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", "Major Upgrade": "Velika nadogradnja",
"Mass actions": "Masovne radnje", "Mass actions": "Masovne radnje",
"Maximum Age": "Maksimalna starost", "Maximum Age": "Maksimalna starost",
@@ -406,7 +401,6 @@
"Support Bundle": "Paket podrške", "Support Bundle": "Paket podrške",
"Sync Extended Attributes": "Sinkroniziraj proširena svojstva", "Sync Extended Attributes": "Sinkroniziraj proširena svojstva",
"Sync Ownership": "Sinkroniziraj vlasništvo", "Sync Ownership": "Sinkroniziraj vlasništvo",
"Sync Protocol Listen Addresses": "Adrese za slušanje sinkronizacijskog protokola",
"Sync Status": "Stanje sinkronizacije", "Sync Status": "Stanje sinkronizacije",
"Syncing": "Sinkroniziranje", "Syncing": "Sinkroniziranje",
"Syncthing device ID for \"{%devicename%}\"": "Syncthing ID uređaja za „{{devicename}}“", "Syncthing device ID for \"{%devicename%}\"": "Syncthing ID uređaja za „{{devicename}}“",
-3
View File
@@ -1017,9 +1017,6 @@
</div> <!-- /row --> </div> <!-- /row -->
</div> <!-- /container --> </div> <!-- /container -->
<footer class="container text-center text-muted small" aria-label="Custom build marker">
It syncs .stignore now!
</footer>
</div> <!-- /ng-cloak --> </div> <!-- /ng-cloak -->
<ng-include src="'syncthing/core/networkErrorDialogView.html'"></ng-include> <ng-include src="'syncthing/core/networkErrorDialogView.html'"></ng-include>
@@ -30,7 +30,7 @@
<h4 class="text-center" translate>The Syncthing Authors</h4> <h4 class="text-center" translate>The Syncthing Authors</h4>
<div class="row"> <div class="row">
<div class="col-md-12" id="contributor-list"> <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, 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, 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, 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, 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 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, 佛跳墙, 落心
</div> </div>
</div> </div>
</div> </div>
@@ -1139,20 +1139,20 @@ angular.module('syncthing.core')
}; };
$scope.syncPercentage = function (folder) { $scope.syncPercentage = function (folder) {
var model = $scope.model[folder]; if (typeof $scope.model[folder] === 'undefined') {
if (typeof model === 'undefined') {
return 100; return 100;
} }
if (model.needTotalItems === 0) { if ($scope.model[folder].needTotalItems === 0) {
return 100; return 100;
} }
if (model.needBytes == 0 && model.needTotalItems > 0) { 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, directories, // We don't need any data, but we have deletes that we need
// symlinks or zero byte files that we need to do. Drop down the // to do. Drop down the completion percentage to indicate
// completion percentage to indicate that we have stuff to do. // that we have stuff to do.
// Do the same thing in case we only have zero byte files to sync.
return 95; return 95;
} }
return progressIntegerPercentage(model.inSyncBytes, model.globalBytes); return progressIntegerPercentage($scope.model[folder].inSyncBytes, $scope.model[folder].globalBytes);
}; };
$scope.scanPercentage = function (folder) { $scope.scanPercentage = function (folder) {
@@ -2997,9 +2997,6 @@ angular.module('syncthing.core')
$scope.restoreVersions.tree = $("#restoreTree").fancytree({ $scope.restoreVersions.tree = $("#restoreTree").fancytree({
extensions: ["table", "filter", "glyph"], extensions: ["table", "filter", "glyph"],
quicksearch: true, quicksearch: true,
// Node titles are remote-controlled file/path
// components; render them as text, not HTML.
escapeTitles: true,
filter: { filter: {
hideExpanders: true, hideExpanders: true,
mode: "hide" mode: "hide"
-1
View File
@@ -79,7 +79,6 @@ type DB interface {
// Cleanup // Cleanup
DropAllFiles(folder string, device protocol.DeviceID) error DropAllFiles(folder string, device protocol.DeviceID) error
DropFolderDevice(folder string, device protocol.DeviceID) error
DropDevice(device protocol.DeviceID) error DropDevice(device protocol.DeviceID) error
DropFilesNamed(folder string, device protocol.DeviceID, names []string) error DropFilesNamed(folder string, device protocol.DeviceID, names []string) error
DropFolder(folder string) error DropFolder(folder string) error
-5
View File
@@ -128,11 +128,6 @@ func (m metricsDB) DropAllFiles(folder string, device protocol.DeviceID) error {
return m.DB.DropAllFiles(folder, device) 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 { func (m metricsDB) DropDevice(device protocol.DeviceID) error {
defer m.account("-", "DropDevice")() defer m.account("-", "DropDevice")()
return m.DB.DropDevice(device) return m.DB.DropDevice(device)
+1 -1
View File
@@ -27,7 +27,7 @@ import (
) )
const ( const (
currentSchemaVersion = 6 currentSchemaVersion = 5
applicationIDMain = 0x53546d6e // "STmn", Syncthing main database applicationIDMain = 0x53546d6e // "STmn", Syncthing main database
applicationIDFolder = 0x53546664 // "STfd", Syncthing folder database applicationIDFolder = 0x53546664 // "STfd", Syncthing folder database
) )
-11
View File
@@ -255,17 +255,6 @@ func (s *DB) DropAllFiles(folder string, device protocol.DeviceID) error {
return fdb.DropAllFiles(device) 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 { func (s *DB) DropFilesNamed(folder string, device protocol.DeviceID, names []string) error {
fdb, err := s.getFolderDB(folder, false) fdb, err := s.getFolderDB(folder, false)
if errors.Is(err, errNoSuchFolder) { if errors.Is(err, errNoSuchFolder) {
+4 -85
View File
@@ -29,6 +29,7 @@ import (
const ( const (
folderID = "test" folderID = "test"
blockSize = 128 << 10 blockSize = 128 << 10
dirSize = 128
) )
func TestBasics(t *testing.T) { func TestBasics(t *testing.T) {
@@ -71,11 +72,11 @@ func TestBasics(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
const ( const (
localSize = (1+2+3)*blockSize localSize = (1+2+3)*blockSize + dirSize
remoteSize = (3 + 4 + 5) * blockSize remoteSize = (3 + 4 + 5) * blockSize
globalSize = (2+3+3+4+5)*blockSize globalSize = (2+3+3+4+5)*blockSize + dirSize
needSizeLocal = remoteSize needSizeLocal = remoteSize
needSizeRemote = (2+3)*blockSize needSizeRemote = (2+3)*blockSize + dirSize
) )
t.Run("SchemaVersion", func(t *testing.T) { t.Run("SchemaVersion", func(t *testing.T) {
@@ -797,13 +798,6 @@ func TestDropAllFiles(t *testing.T) {
t.Fatal(err) 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 // Drop folder A
if err := db.DropAllFiles("a", protocol.DeviceID{1}); err != nil { if err := db.DropAllFiles("a", protocol.DeviceID{1}); err != nil {
t.Fatal(err) t.Fatal(err)
@@ -831,20 +825,6 @@ func TestDropAllFiles(t *testing.T) {
t.Error("expected count to be two") 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 // Drop things that don't exist
if err := db.DropAllFiles("a", protocol.DeviceID{99}); err != nil { if err := db.DropAllFiles("a", protocol.DeviceID{99}); err != nil {
t.Fatal(err) t.Fatal(err)
@@ -857,67 +837,6 @@ 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) { func TestConcurrentUpdate(t *testing.T) {
t.Parallel() t.Parallel()
+4 -7
View File
@@ -111,6 +111,10 @@ func (s *folderDB) Update(device protocol.DeviceID, fs []protocol.FileInfo, opti
f.BlocksHash = nil f.BlocksHash = nil
} }
if f.Type == protocol.FileInfoTypeDirectory {
f.Size = 128 // synthetic directory size
}
// Insert the file. // Insert the file.
// //
// If it is a remote file, set remote_sequence otherwise leave it at // If it is a remote file, set remote_sequence otherwise leave it at
@@ -231,13 +235,6 @@ func (s *folderDB) DropAllFiles(device protocol.DeviceID) error {
defer tx.Rollback() //nolint:errcheck defer tx.Rollback() //nolint:errcheck
txp := &txPreparedStmts{Tx: tx} 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 // Drop all the file entries
result, err := tx.Exec(` result, err := tx.Exec(`
@@ -1,16 +0,0 @@
-- 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
;
-4
View File
@@ -106,10 +106,6 @@ func (i indirectFI) FileInfo() (protocol.FileInfo, error) {
fi.Blocks = bl.Blocks fi.Blocks = bl.Blocks
} }
fi.Name = osutil.NativeFilename(fi.Name) 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 return protocol.FileInfoFromDB(&fi), nil
} }
+2 -2
View File
@@ -1248,7 +1248,7 @@ func (s *service) getSupportBundle(w http.ResponseWriter, r *http.Request) {
zipFilePath := filepath.Join(locations.GetBaseDir(locations.ConfigBaseDir), zipFileName) zipFilePath := filepath.Join(locations.GetBaseDir(locations.ConfigBaseDir), zipFileName)
// Write buffer zip to local zip file (back up) // Write buffer zip to local zip file (back up)
if err := os.WriteFile(zipFilePath, zipFilesBuffer.Bytes(), 0o666); err != nil { if err := os.WriteFile(zipFilePath, zipFilesBuffer.Bytes(), 0o600); err != nil {
slog.Warn("Failed to create support bundle zip (file)", slogutil.FilePath(zipFilePath), slogutil.Error(err)) 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{}{ out := map[string]interface{}{
"name": f.FileName(), "name": f.FileName(),
"type": f.FileType().String(), "type": f.FileType().String(),
"size": f.Size, "size": f.FileSize(),
"deleted": f.IsDeleted(), "deleted": f.IsDeleted(),
"invalid": f.IsInvalid(), "invalid": f.IsInvalid(),
"ignored": f.IsIgnored(), "ignored": f.IsIgnored(),
+3 -9
View File
@@ -1762,10 +1762,9 @@ func TestConfigChanges(t *testing.T) {
folder2Path := "/rest/config/folders/folder2" folder2Path := "/rest/config/folders/folder2"
// Create a folder and add another. Give folder2 a non-default // Create a folder and add another
// 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, "/rest/config/folders", []config.FolderConfiguration{{ID: "folder1", Path: "folder1"}})
mod(http.MethodPut, folder2Path, config.FolderConfiguration{ID: "folder2", Path: "folder2", RescanIntervalS: 1234}) mod(http.MethodPut, folder2Path, config.FolderConfiguration{ID: "folder2", Path: "folder2"})
// Check they are there // Check they are there
get("/rest/config/folders/folder1").Body.Close() get("/rest/config/folders/folder1").Body.Close()
@@ -1780,14 +1779,9 @@ func TestConfigChanges(t *testing.T) {
if err := unmarshalTo(resp.Body, &folder); err != nil { if err := unmarshalTo(resp.Body, &folder); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if !folder.Paused { if !dev.Paused {
t.Error("Expected folder to be 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 // Delete folder2
req, _ := http.NewRequest(http.MethodDelete, baseURL+folder2Path, nil) req, _ := http.NewRequest(http.MethodDelete, baseURL+folder2Path, nil)
-42
View File
@@ -1,42 +0,0 @@
// 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)
}
}
+23 -3
View File
@@ -9,6 +9,7 @@ package config
import ( import (
"bytes" "bytes"
"crypto/sha256" "crypto/sha256"
"encoding/json"
"encoding/xml" "encoding/xml"
"errors" "errors"
"fmt" "fmt"
@@ -167,7 +168,7 @@ func (f *FolderConfiguration) CreateMarker() error {
ffs := f.Filesystem() ffs := f.Filesystem()
// Create the marker as a directory // Create the marker as a directory
err := ffs.Mkdir(DefaultMarkerName, fs.ModePerm) err := ffs.Mkdir(DefaultMarkerName, 0o755)
if err != nil { if err != nil {
return err return err
} }
@@ -175,7 +176,7 @@ func (f *FolderConfiguration) CreateMarker() error {
// Create a file inside it, reducing the risk of the marker directory // Create a file inside it, reducing the risk of the marker directory
// being removed by automated cleanup tools. // being removed by automated cleanup tools.
markerFile := filepath.Join(DefaultMarkerName, f.markerFilename()) markerFile := filepath.Join(DefaultMarkerName, f.markerFilename())
if err := fs.WriteFile(ffs, markerFile, f.markerContents(), 0o666); err != nil { if err := fs.WriteFile(ffs, markerFile, f.markerContents(), 0o644); err != nil {
return err return err
} }
@@ -245,10 +246,19 @@ func (f *FolderConfiguration) checkFilesystemPath(ffs fs.Filesystem, path string
} }
func (f *FolderConfiguration) CreateRoot() (err error) { 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() filesystem := f.Filesystem()
if _, err = filesystem.Stat("."); fs.IsNotExist(err) { if _, err = filesystem.Stat("."); fs.IsNotExist(err) {
err = filesystem.MkdirAll(".", fs.ModePerm) err = filesystem.MkdirAll(".", permBits)
} }
return err return err
@@ -394,6 +404,16 @@ func (f XattrFilter) GetMaxTotalSize() int {
return f.MaxTotalSize 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 { func (f *FolderConfiguration) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
structutil.SetDefaults(f) structutil.SetDefaults(f)
+14 -6
View File
@@ -18,6 +18,7 @@ import (
"sync" "sync"
"github.com/syncthing/syncthing/internal/slogutil" "github.com/syncthing/syncthing/internal/slogutil"
"github.com/syncthing/syncthing/lib/build"
"github.com/syncthing/syncthing/lib/fs" "github.com/syncthing/syncthing/lib/fs"
"github.com/syncthing/syncthing/lib/netutil" "github.com/syncthing/syncthing/lib/netutil"
"github.com/syncthing/syncthing/lib/upgrade" "github.com/syncthing/syncthing/lib/upgrade"
@@ -223,20 +224,27 @@ func migrateToConfigV24(cfg *Configuration) {
} }
func migrateToConfigV23(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 // Upgrade code remains hardcoded for .stfolder despite configurable
// marker name in later versions. // marker name in later versions.
for i := range cfg.Folders { for i := range cfg.Folders {
ffs := cfg.Folders[i].Filesystem() fs := cfg.Folders[i].Filesystem()
// Invalid config posted, or tests. // Invalid config posted, or tests.
if ffs == nil { if fs == nil {
continue continue
} }
if stat, err := ffs.Stat(DefaultMarkerName); err == nil && !stat.IsDir() { if stat, err := fs.Stat(DefaultMarkerName); err == nil && !stat.IsDir() {
err = ffs.Remove(DefaultMarkerName) err = fs.Remove(DefaultMarkerName)
if err == nil { if err == nil {
err = ffs.Mkdir(DefaultMarkerName, fs.ModePerm) err = fs.Mkdir(DefaultMarkerName, permBits)
ffs.Hide(DefaultMarkerName) // ignore error fs.Hide(DefaultMarkerName) // ignore error
} }
if err != nil { if err != nil {
slog.Warn("Failed to upgrade folder marker", slogutil.Error(err)) slog.Warn("Failed to upgrade folder marker", slogutil.Error(err))
-5
View File
@@ -827,11 +827,6 @@ func (s *service) logListenAddressesChangedEvent(l ListenerAddresses) {
func (s *service) CommitConfiguration(from, to config.Configuration) bool { func (s *service) CommitConfiguration(from, to config.Configuration) bool {
newDevices := make(map[protocol.DeviceID]bool, len(to.Devices)) newDevices := make(map[protocol.DeviceID]bool, len(to.Devices))
for _, dev := range to.Devices { for _, dev := range to.Devices {
if dev.DeviceID == s.myID {
// Do not report connection metrics for ourselves
continue
}
newDevices[dev.DeviceID] = true newDevices[dev.DeviceID] = true
registerDeviceMetrics(dev.DeviceID.String()) registerDeviceMetrics(dev.DeviceID.String())
} }
+19 -4
View File
@@ -174,7 +174,7 @@ func (f *BasicFilesystem) MkdirAll(path string, perm FileMode) error {
return err return err
} }
return os.MkdirAll(path, os.FileMode(perm)) return f.mkdirAll(path, os.FileMode(perm))
} }
func (f *BasicFilesystem) Lstat(name string) (FileInfo, error) { func (f *BasicFilesystem) Lstat(name string) (FileInfo, error) {
@@ -241,7 +241,15 @@ func (f *BasicFilesystem) DirNames(name string) ([]string, error) {
} }
func (f *BasicFilesystem) Open(name string) (File, error) { func (f *BasicFilesystem) Open(name string) (File, error) {
return f.OpenFile(name, os.O_RDONLY, 0) 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
} }
func (f *BasicFilesystem) OpenFile(name string, flags int, mode FileMode) (File, error) { func (f *BasicFilesystem) OpenFile(name string, flags int, mode FileMode) (File, error) {
@@ -249,7 +257,6 @@ func (f *BasicFilesystem) OpenFile(name string, flags int, mode FileMode) (File,
if err != nil { if err != nil {
return nil, err return nil, err
} }
flags |= alwaysOpenFlags // enforce extra bits in flags
fd, err := os.OpenFile(rootedName, flags, os.FileMode(mode)) fd, err := os.OpenFile(rootedName, flags, os.FileMode(mode))
if err != nil { if err != nil {
return nil, err return nil, err
@@ -258,7 +265,15 @@ func (f *BasicFilesystem) OpenFile(name string, flags int, mode FileMode) (File,
} }
func (f *BasicFilesystem) Create(name string) (File, error) { func (f *BasicFilesystem) Create(name string) (File, error) {
return f.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o666) 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
} }
func (*BasicFilesystem) Walk(_ string, _ WalkFunc) error { func (*BasicFilesystem) Walk(_ string, _ WalkFunc) error {
+4 -3
View File
@@ -14,11 +14,8 @@ import (
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"syscall"
) )
const alwaysOpenFlags = syscall.O_NOFOLLOW // never open symlinks as the final path component
func (f *BasicFilesystem) CreateSymlink(target, name string) error { func (f *BasicFilesystem) CreateSymlink(target, name string) error {
name, err := f.rooted(name) name, err := f.rooted(name)
if err != nil { if err != nil {
@@ -35,6 +32,10 @@ func (f *BasicFilesystem) ReadSymlink(name string) (string, error) {
return os.Readlink(name) 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. // 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 // We still check that the relative path does not try to escape the root
func (f *BasicFilesystem) Unhide(name string) error { func (f *BasicFilesystem) Unhide(name string) error {
+51 -2
View File
@@ -21,8 +21,6 @@ import (
"golang.org/x/sys/windows" "golang.org/x/sys/windows"
) )
const alwaysOpenFlags = 0 // no extra flags
var errNotSupported = errors.New("symlinks not supported") var errNotSupported = errors.New("symlinks not supported")
func (BasicFilesystem) ReadSymlink(path string) (string, error) { func (BasicFilesystem) ReadSymlink(path string) (string, error) {
@@ -33,6 +31,57 @@ func (BasicFilesystem) CreateSymlink(target, name string) error {
return errNotSupported 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 { func (f *BasicFilesystem) Unhide(name string) error {
name, err := f.rooted(name) name, err := f.rooted(name)
if err != nil { if err != nil {
+2 -2
View File
@@ -153,7 +153,7 @@ func newFakeFilesystem(rootURI string, _ ...Option) *fakeFS {
for (files == 0 || createdFiles < files) && (maxsize == 0 || writtenData>>20 < int64(maxsize)) { 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))) dir := filepath.Join(fmt.Sprintf("%02x", rng.Intn(255)), fmt.Sprintf("%02x", rng.Intn(255)))
file := fmt.Sprintf("%016x", rng.Int63()) file := fmt.Sprintf("%016x", rng.Int63())
_ = fs.MkdirAll(dir, ModePerm) _ = fs.MkdirAll(dir, 0o755)
fd, _ := fs.Create(filepath.Join(dir, file)) fd, _ := fs.Create(filepath.Join(dir, file))
createdFiles++ createdFiles++
@@ -169,7 +169,7 @@ func newFakeFilesystem(rootURI string, _ ...Option) *fakeFS {
if !nostfolder { if !nostfolder {
// Also create a default folder marker for good measure // Also create a default folder marker for good measure
_ = fs.Mkdir(".stfolder", ModePerm) _ = fs.Mkdir(".stfolder", 0o700)
} }
// We only set the latency after doing the operations required to create // We only set the latency after doing the operations required to create
+3 -6
View File
@@ -975,13 +975,10 @@ func (f *folder) scanTimerFired(ctx context.Context) error {
select { select {
case <-f.initialScanFinished: case <-f.initialScanFinished:
default: default:
switch { if err != nil {
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)) f.sl.ErrorContext(ctx, "Failed initial scan", slogutil.Error(err))
} else {
f.sl.InfoContext(ctx, "Completed initial scan")
} }
close(f.initialScanFinished) close(f.initialScanFinished)
} }
+5 -4
View File
@@ -163,11 +163,12 @@ func TestRecvOnlyRevertNeeds(t *testing.T) {
// We now have a newer file than the rest of the cluster. Global state should reflect this. // We now have a newer file than the rest of the cluster. Global state should reflect this.
size = mustV(m.GlobalSize("ro")) size = mustV(m.GlobalSize("ro"))
if size.Files != 1 || size.Bytes != int64(len(oldData)) { const sizeOfDir = 128
if size.Files != 1 || size.Bytes != sizeOfDir+int64(len(oldData)) {
t.Fatalf("Global: expected no change due to the new file: %+v", size) t.Fatalf("Global: expected no change due to the new file: %+v", size)
} }
size = mustV(m.LocalSize("ro", protocol.LocalDeviceID)) size = mustV(m.LocalSize("ro", protocol.LocalDeviceID))
if size.Files != 1 || size.Bytes != int64(len(newData)) { if size.Files != 1 || size.Bytes != sizeOfDir+int64(len(newData)) {
t.Fatalf("Local: expected the new file to be reflected: %+v", size) t.Fatalf("Local: expected the new file to be reflected: %+v", size)
} }
size = mustV(m.NeedSize("ro", protocol.LocalDeviceID)) size = mustV(m.NeedSize("ro", protocol.LocalDeviceID))
@@ -184,11 +185,11 @@ func TestRecvOnlyRevertNeeds(t *testing.T) {
m.Revert("ro") m.Revert("ro")
size = mustV(m.GlobalSize("ro")) size = mustV(m.GlobalSize("ro"))
if size.Files != 1 || size.Bytes != int64(len(oldData)) { if size.Files != 1 || size.Bytes != sizeOfDir+int64(len(oldData)) {
t.Fatalf("Global: expected the global size to revert: %+v", size) t.Fatalf("Global: expected the global size to revert: %+v", size)
} }
size = mustV(m.LocalSize("ro", protocol.LocalDeviceID)) size = mustV(m.LocalSize("ro", protocol.LocalDeviceID))
if size.Files != 1 || size.Bytes != int64(len(newData)) { if size.Files != 1 || size.Bytes != sizeOfDir+int64(len(newData)) {
t.Fatalf("Local: expected the local size to remain: %+v", size) t.Fatalf("Local: expected the local size to remain: %+v", size)
} }
size = mustV(m.NeedSize("ro", protocol.LocalDeviceID)) size = mustV(m.NeedSize("ro", protocol.LocalDeviceID))
+2 -15
View File
@@ -382,7 +382,7 @@ loop:
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
if hasCurFile && curFile.Type == file.Type && file.BlocksEqual(curFile) { if hasCurFile && file.BlocksEqual(curFile) {
// We are supposed to copy the entire file, and then fetch nothing. We // 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 // are only updating metadata, so we don't actually *need* to make the
// copy. // copy.
@@ -701,7 +701,7 @@ func (f *sendReceiveFolder) checkParent(file string, scanChan chan<- string) boo
return true return true
} }
f.sl.Debug("Creating parent directory", slogutil.FilePath(file)) f.sl.Debug("Creating parent directory", slogutil.FilePath(file))
if err := f.mtimefs.MkdirAll(parent, fs.ModePerm); err != nil { if err := f.mtimefs.MkdirAll(parent, 0o755); err != nil {
f.newPullError(file, fmt.Errorf("creating parent dir: %w", err)) f.newPullError(file, fmt.Errorf("creating parent dir: %w", err))
return false return false
} }
@@ -1345,12 +1345,6 @@ func (f *sendReceiveFolder) copierRoutine(ctx context.Context, in <-chan copyBlo
default: default:
} }
if block.Size == 0 {
// Copying zero bytes is a no-op.
state.copyDone(block)
continue
}
if !f.DisableSparseFiles && state.reused == 0 && block.IsEmpty() { if !f.DisableSparseFiles && state.reused == 0 && block.IsEmpty() {
// The block is a block of all zeroes, and we are not reusing // 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. // a temp file, so there is no need to do anything with it.
@@ -1540,13 +1534,6 @@ func (f *sendReceiveFolder) pullerRoutine(ctx context.Context, in <-chan pullBlo
bytes := state.block.Size 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 { if err := requestLimiter.TakeWithContext(ctx, bytes); err != nil {
state.fail(err) state.fail(err)
out <- state.sharedPullerState out <- state.sharedPullerState
+1 -1
View File
@@ -891,7 +891,7 @@ func TestPullCtxCancel(t *testing.T) {
emptyState := func() pullBlockState { emptyState := func() pullBlockState {
return pullBlockState{ return pullBlockState{
sharedPullerState: newSharedPullerState(protocol.FileInfo{}, nil, f.folderID, "", nil, nil, false, false, protocol.FileInfo{}, false, false), sharedPullerState: newSharedPullerState(protocol.FileInfo{}, nil, f.folderID, "", nil, nil, false, false, protocol.FileInfo{}, false, false),
block: protocol.BlockInfo{Size: 42}, block: protocol.BlockInfo{},
} }
} }
+24 -11
View File
@@ -371,7 +371,7 @@ func (m *model) addAndStartFolderLockedWithIgnores(cfg config.FolderConfiguratio
for _, available := range devs { for _, available := range devs {
if _, ok := expected[available]; !ok { if _, ok := expected[available]; !ok {
l.Debugln("dropping", folder, "state for", available) l.Debugln("dropping", folder, "state for", available)
_ = m.sdb.DropFolderDevice(folder, available) _ = m.sdb.DropAllFiles(folder, available)
} }
} }
@@ -1784,7 +1784,7 @@ func (m *model) handleAutoAccepts(deviceID protocol.DeviceID, folder protocol.Fo
// Attempt to create it to make sure it does, now. // Attempt to create it to make sure it does, now.
fullPath := filepath.Join(defaultFolderCfg.Path, path) fullPath := filepath.Join(defaultFolderCfg.Path, path)
if err := defaultPathFs.MkdirAll(path, fs.ModePerm); err != nil { if err := defaultPathFs.MkdirAll(path, 0o700); err != nil {
slog.Error("Failed to create path for auto-accepted folder", folder.LogAttr(), slogutil.FilePath(fullPath), slogutil.Error(err)) slog.Error("Failed to create path for auto-accepted folder", folder.LogAttr(), slogutil.FilePath(fullPath), slogutil.Error(err))
continue continue
} }
@@ -2079,7 +2079,7 @@ func (m *model) Request(conn protocol.Connection, req *protocol.Request) (out pr
return nil, protocol.ErrGeneric return nil, protocol.ErrGeneric
} }
if folderCfg.Type != config.FolderTypeReceiveEncrypted && !scanner.Validate(res.data[:n], req.Hash) { if folderCfg.Type != config.FolderTypeReceiveEncrypted && len(req.Hash) > 0 && !scanner.Validate(res.data[:n], req.Hash) {
m.recheckFile(deviceID, req.Folder, req.Name, req.Offset, req.Hash) m.recheckFile(deviceID, req.Folder, req.Name, req.Offset, req.Hash)
l.Debugf("%v REQ(in) failed validating data: %s: %q / %q o=%d s=%d", m, deviceID.Short(), req.Folder, req.Name, req.Offset, req.Size) l.Debugf("%v REQ(in) failed validating data: %s: %q / %q o=%d s=%d", m, deviceID.Short(), req.Folder, req.Name, req.Offset, req.Size)
return nil, protocol.ErrNoSuchFile return nil, protocol.ErrNoSuchFile
@@ -2548,6 +2548,13 @@ func (m *model) DelayScan(folder string, next time.Duration) {
func (m *model) numHashers(folder string) int { func (m *model) numHashers(folder string) int {
m.mut.RLock() m.mut.RLock()
folderCfg := m.folderCfgs[folder] folderCfg := m.folderCfgs[folder]
numFolders := max(1, len(m.folderCfgs))
// MaxFolderConcurrency already limits the number of scanned folders, so
// prefer it over the overall number of folders to avoid limiting performance
// further for no reason.
if concurrency := m.cfg.Options().MaxFolderConcurrency(); concurrency > 0 {
numFolders = min(numFolders, concurrency)
}
m.mut.RUnlock() m.mut.RUnlock()
if folderCfg.Hashers > 0 { if folderCfg.Hashers > 0 {
@@ -2555,15 +2562,21 @@ func (m *model) numHashers(folder string) int {
return folderCfg.Hashers return folderCfg.Hashers
} }
numCPUs := runtime.GOMAXPROCS(-1) numCpus := runtime.GOMAXPROCS(-1)
switch { if build.IsWindows || build.IsDarwin || build.IsIOS || build.IsAndroid {
case build.IsWindows || build.IsIOS || build.IsAndroid: // Interactive operating systems; don't load the system too heavily by
// Use a quarter of the CPU cores on interactive or constrained OSes // default.
return max(1, numCPUs/4) numCpus = max(1, numCpus/4)
default:
// Otherwise use up to half
return max(1, numCPUs/2)
} }
// For other operating systems and architectures, lets try to get some
// work done... Divide the available CPU cores among the configured
// folders.
if perFolder := numCpus / numFolders; perFolder > 0 {
return perFolder
}
return 1
} }
// generateClusterConfig returns a ClusterConfigMessage that is correct and the // generateClusterConfig returns a ClusterConfigMessage that is correct and the
+11 -24
View File
@@ -9,7 +9,6 @@ package model
import ( import (
"bytes" "bytes"
"context" "context"
"crypto/sha256"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@@ -94,10 +93,8 @@ func TestRequest(t *testing.T) {
m.ScanFolder("default") m.ScanFolder("default")
foobarHash := sha256.Sum256([]byte("foobar"))
// Existing, shared file // Existing, shared file
res, err := m.Request(device1Conn, &protocol.Request{Folder: "default", Name: "foo", Size: 6, Hash: foobarHash[:]}) res, err := m.Request(device1Conn, &protocol.Request{Folder: "default", Name: "foo", Size: 6})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -107,42 +104,35 @@ func TestRequest(t *testing.T) {
} }
// Existing, nonshared file // Existing, nonshared file
_, err = m.Request(device2Conn, &protocol.Request{Folder: "default", Name: "foo", Size: 6, Hash: foobarHash[:]}) _, err = m.Request(device2Conn, &protocol.Request{Folder: "default", Name: "foo", Size: 6})
if err == nil { if err == nil {
t.Error("Unexpected nil error on insecure file read") t.Error("Unexpected nil error on insecure file read")
} }
// Nonexistent file // Nonexistent file
_, err = m.Request(device1Conn, &protocol.Request{Folder: "default", Name: "nonexistent", Size: 6, Hash: foobarHash[:]}) _, err = m.Request(device1Conn, &protocol.Request{Folder: "default", Name: "nonexistent", Size: 6})
if err == nil { if err == nil {
t.Error("Unexpected nil error on insecure file read") t.Error("Unexpected nil error on insecure file read")
} }
// Shared folder, but disallowed file name // Shared folder, but disallowed file name
_, err = m.Request(device1Conn, &protocol.Request{Folder: "default", Name: "../walk.go", Size: 6, Hash: foobarHash[:]}) _, err = m.Request(device1Conn, &protocol.Request{Folder: "default", Name: "../walk.go", Size: 6})
if err == nil { if err == nil {
t.Error("Unexpected nil error on insecure file read") t.Error("Unexpected nil error on insecure file read")
} }
// Negative size // Negative size
_, err = m.Request(device1Conn, &protocol.Request{Folder: "default", Name: "foo", Size: -4, Hash: foobarHash[:]}) _, err = m.Request(device1Conn, &protocol.Request{Folder: "default", Name: "foo", Size: -4})
if err == nil { if err == nil {
t.Error("Unexpected nil error on insecure file read") t.Error("Unexpected nil error on insecure file read")
} }
// Missing hash // Larger block than available
_, err = m.Request(device1Conn, &protocol.Request{Folder: "default", Name: "foo", Size: 6})
if err == nil {
t.Error("Unexpected nil error on request without hash")
}
// Larger block than available, with a mismatched hash
_, err = m.Request(device1Conn, &protocol.Request{Folder: "default", Name: "foo", Size: 42, Hash: []byte("hash necessary but not checked")}) _, err = m.Request(device1Conn, &protocol.Request{Folder: "default", Name: "foo", Size: 42, Hash: []byte("hash necessary but not checked")})
if err == nil { if err == nil {
t.Error("Unexpected nil error on read past end of file") t.Error("Unexpected nil error on read past end of file")
} }
// Larger block than available, with the matching hash of the short read _, err = m.Request(device1Conn, &protocol.Request{Folder: "default", Name: "foo", Size: 42})
_, err = m.Request(device1Conn, &protocol.Request{Folder: "default", Name: "foo", Size: 42, Hash: foobarHash[:]})
if err != nil { if err != nil {
t.Error("Unexpected error when large read should be permitted") t.Error("Unexpected error when large read should be permitted")
} }
@@ -1787,12 +1777,10 @@ func TestGlobalDirectoryTree(t *testing.T) {
b := func(isfile bool, path ...string) protocol.FileInfo { b := func(isfile bool, path ...string) protocol.FileInfo {
typ := protocol.FileInfoTypeDirectory typ := protocol.FileInfoTypeDirectory
var blocks []protocol.BlockInfo var blocks []protocol.BlockInfo
var size int64
if isfile { if isfile {
typ = protocol.FileInfoTypeFile typ = protocol.FileInfoTypeFile
blocks = []protocol.BlockInfo{{Offset: 0x0, Size: 0xa, Hash: []uint8{0x2f, 0x72, 0xcc, 0x11, 0xa6, 0xfc, 0xd0, 0x27, 0x1e, 0xce, 0xf8, 0xc6, 0x10, 0x56, 0xee, 0x1e, 0xb1, 0x24, 0x3b, 0xe3, 0x80, 0x5b, 0xf9, 0xa9, 0xdf, 0x98, 0xf9, 0x2f, 0x76, 0x36, 0xb0, 0x5c}}} blocks = []protocol.BlockInfo{{Offset: 0x0, Size: 0xa, Hash: []uint8{0x2f, 0x72, 0xcc, 0x11, 0xa6, 0xfc, 0xd0, 0x27, 0x1e, 0xce, 0xf8, 0xc6, 0x10, 0x56, 0xee, 0x1e, 0xb1, 0x24, 0x3b, 0xe3, 0x80, 0x5b, 0xf9, 0xa9, 0xdf, 0x98, 0xf9, 0x2f, 0x76, 0x36, 0xb0, 0x5c}}}
size = 0xa
} }
seq++ seq++
return protocol.FileInfo{ return protocol.FileInfo{
@@ -1800,7 +1788,7 @@ func TestGlobalDirectoryTree(t *testing.T) {
Type: typ, Type: typ,
ModifiedS: 0x666, ModifiedS: 0x666,
Blocks: blocks, Blocks: blocks,
Size: size, Size: 0xa,
Sequence: seq, Sequence: seq,
} }
} }
@@ -1816,7 +1804,7 @@ func TestGlobalDirectoryTree(t *testing.T) {
return &TreeEntry{ return &TreeEntry{
Name: name, Name: name,
ModTime: time.Unix(0x666, 0), ModTime: time.Unix(0x666, 0),
Size: 0, Size: 128,
Type: protocol.FileInfoTypeDirectory.String(), Type: protocol.FileInfoTypeDirectory.String(),
Children: entries, Children: entries,
} }
@@ -2991,16 +2979,15 @@ func TestRequestLimit(t *testing.T) {
defer cleanupModel(m) defer cleanupModel(m)
m.ScanFolder("default") m.ScanFolder("default")
emptyHash := sha256.Sum256(nil)
befReq := time.Now() befReq := time.Now()
first, err := m.Request(conn, &protocol.Request{Folder: "default", Name: file, Size: 2000, Hash: emptyHash[:]}) first, err := m.Request(conn, &protocol.Request{Folder: "default", Name: file, Size: 2000})
if err != nil { if err != nil {
t.Fatalf("First request failed: %v", err) t.Fatalf("First request failed: %v", err)
} }
reqDur := time.Since(befReq) reqDur := time.Since(befReq)
returned := make(chan struct{}) returned := make(chan struct{})
go func() { go func() {
second, err := m.Request(conn, &protocol.Request{Folder: "default", Name: file, Size: 2000, Hash: emptyHash[:]}) second, err := m.Request(conn, &protocol.Request{Folder: "default", Name: file, Size: 2000})
if err != nil { if err != nil {
t.Errorf("Second request failed: %v", err) t.Errorf("Second request failed: %v", err)
} }
+5 -23
View File
@@ -8,10 +8,8 @@ package osutil_test
import ( import (
"path/filepath" "path/filepath"
"slices"
"testing" "testing"
"github.com/syncthing/syncthing/lib/build"
"github.com/syncthing/syncthing/lib/fs" "github.com/syncthing/syncthing/lib/fs"
"github.com/syncthing/syncthing/lib/osutil" "github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/rand" "github.com/syncthing/syncthing/lib/rand"
@@ -61,16 +59,11 @@ func TestTraversesSymlink(t *testing.T) {
} }
} }
func TestSymlinkedFSRootTraversal(t *testing.T) { func TestIssue4875(t *testing.T) {
// Normal FS operations should work inside a folder root that is a symlink testFsPath := rand.String(32)
if build.IsWindows { testFs := fs.NewFilesystem(fs.FilesystemTypeFake, testFsPath)
t.Skip("symlinks unavailable")
}
testFsPath := t.TempDir()
testFs := fs.NewFilesystem(fs.FilesystemTypeBasic, testFsPath)
testFs.MkdirAll(filepath.Join("a", "b", "c"), 0o755) testFs.MkdirAll(filepath.Join("a", "b", "c"), 0o755)
if err := testFs.CreateSymlink("b", filepath.Join("a", "l")); err != nil { if err := testFs.CreateSymlink(filepath.Join("a", "b"), filepath.Join("a", "l")); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -83,21 +76,10 @@ func TestSymlinkedFSRootTraversal(t *testing.T) {
t.Fatal("error in setup, a/l/c should be a directory") t.Fatal("error in setup, a/l/c should be a directory")
} }
testFs = fs.NewFilesystem(fs.FilesystemTypeBasic, filepath.Join(testFsPath, "a/l")) testFs = fs.NewFilesystem(fs.FilesystemTypeFake, filepath.Join(testFsPath, "a/l"))
if err := osutil.TraversesSymlink(testFs, "."); err != nil { if err := osutil.TraversesSymlink(testFs, "."); err != nil {
t.Error(`TraversesSymlink on filesystem with symlink at root returned error for ".":`, err) t.Error(`TraversesSymlink on filesystem with symlink at root returned error for ".":`, err)
} }
// We should be able to create and list files within the symlinked root
if _, err := testFs.Create("test"); err != nil {
t.Fatal(err)
}
if names, err := testFs.DirNames("."); err != nil {
t.Fatal(err)
} else if !slices.Contains(names, "test") {
t.Log(names)
t.Error("missing test file in listing")
}
} }
var traversesSymlinkResult error var traversesSymlinkResult error
+10
View File
@@ -389,6 +389,16 @@ func (f FileInfo) HasPermissionBits() bool {
return !f.NoPermissions return !f.NoPermissions
} }
func (f FileInfo) FileSize() int64 {
if f.Deleted {
return 0
}
if f.IsDirectory() || f.IsSymlink() {
return SyntheticDirectorySize
}
return f.Size
}
func (f FileInfo) BlockSize() int { func (f FileInfo) BlockSize() int {
if f.RawBlockSize < MinBlockSize { if f.RawBlockSize < MinBlockSize {
return MinBlockSize return MinBlockSize
+19 -13
View File
@@ -93,25 +93,31 @@ func (e encryptedModel) Request(req *Request) (RequestResponse, error) {
} }
realSize := req.Size - blockOverhead realSize := req.Size - blockOverhead
realOffset := req.Offset - int64(req.BlockNo*blockOverhead) realOffset := req.Offset - int64(req.BlockNo*blockOverhead)
if realOffset < 0 {
panic("bug: realOffset underflow")
}
if req.Size < minPaddedSize { if req.Size < minPaddedSize {
return nil, errors.New("short request") return nil, errors.New("short request")
} }
// Decrypt the block hash. // Attempt to decrypt the block hash; it may be nil depending on what
// type of device the request comes from. Trusted devices with
// encryption enabled know the hash but don't bother to encrypt & send
// it to us. Untrusted devices have the hash from the encrypted index
// data and do send it. The model knows to only verify the hash if it
// actually gets one.
var realHash []byte
fileKey := e.keyGen.FileKey(realName, folderKey) fileKey := e.keyGen.FileKey(realName, folderKey)
var additional [8]byte if len(req.Hash) > 0 {
binary.BigEndian.PutUint64(additional[:], uint64(realOffset)) var additional [8]byte
realHash, err := decryptDeterministic(req.Hash, fileKey, additional[:]) binary.BigEndian.PutUint64(additional[:], uint64(realOffset))
if err != nil { realHash, err = decryptDeterministic(req.Hash, fileKey, additional[:])
// "Legacy", no offset additional data? if err != nil {
realHash, err = decryptDeterministic(req.Hash, fileKey, nil) // "Legacy", no offset additional data?
} realHash, err = decryptDeterministic(req.Hash, fileKey, nil)
if err != nil { }
return nil, fmt.Errorf("decrypting block hash: %w", err) if err != nil {
return nil, fmt.Errorf("decrypting block hash: %w", err)
}
} }
// Perform that request and grab the data. // Perform that request and grab the data.
+14 -30
View File
@@ -56,8 +56,7 @@ const (
// DesiredPerFileBlocks is the number of blocks we aim for per file // DesiredPerFileBlocks is the number of blocks we aim for per file
DesiredPerFileBlocks = 2000 DesiredPerFileBlocks = 2000
// We used to send this size for directories and still accept that for compat. SyntheticDirectorySize = 128
deprecatedSyntheticDirectorySize = 128
// don't bother compressing messages smaller than this many bytes // don't bother compressing messages smaller than this many bytes
compressionThreshold = 128 compressionThreshold = 128
@@ -69,16 +68,15 @@ const (
) )
var ( var (
ErrClosed = errors.New("connection closed") ErrClosed = errors.New("connection closed")
ErrTimeout = errors.New("read timeout") ErrTimeout = errors.New("read timeout")
errNotCompressible = errors.New("not compressible") errNotCompressible = errors.New("not compressible")
errUnknownMessage = errors.New("unknown message") errUnknownMessage = errors.New("unknown message")
errInvalidFilename = errors.New("filename is invalid") errInvalidFilename = errors.New("filename is invalid")
errUncleanFilename = errors.New("filename not in canonical format") errUncleanFilename = errors.New("filename not in canonical format")
errDeletedHasBlocks = errors.New("deleted file with non-empty block list") errDeletedHasBlocks = errors.New("deleted file with non-empty block list")
errNonFileHasBlocks = errors.New("non-file type with non-empty block list") errDirectoryHasBlocks = errors.New("directory with non-empty block list")
errNonFileHasSize = errors.New("non-file type with nonzero size") errFileHasNoBlocks = errors.New("file with empty block list")
errFileHasNoBlocks = errors.New("file with empty block list")
) )
type Model interface { type Model interface {
@@ -485,18 +483,12 @@ func (c *rawConnection) dispatcherLoop() (err error) {
if err := checkFilename(msg.Name); err != nil { if err := checkFilename(msg.Name); err != nil {
return newProtocolError(err, msgContext) return newProtocolError(err, msgContext)
} }
if msg.Size < 0 { if msg.Size <= 0 {
return newProtocolError(fmt.Errorf("request size %d too small", msg.Size), msgContext) return newProtocolError(fmt.Errorf("request size %d too small", msg.Size), msgContext)
} }
if msg.Size > MaxRequestSize { if msg.Size > MaxRequestSize {
return newProtocolError(fmt.Errorf("request size %d exceeds maximum allowed", msg.Size), msgContext) return newProtocolError(fmt.Errorf("request size %d exceeds maximum allowed", msg.Size), msgContext)
} }
if len(msg.Hash) == 0 {
// Syncthing versions older than v1.28.1 omit the hash in
// encrypted requests from trusted devices (a rare config)
// and will run into this.
return newProtocolError(errors.New("request missing block hash"), msgContext)
}
go c.handleRequest(requestFromWire(msg)) go c.handleRequest(requestFromWire(msg))
case *bep.Response: case *bep.Response:
@@ -637,17 +629,9 @@ func checkFileInfoConsistency(f FileInfo) error {
// Deleted files should have no blocks // Deleted files should have no blocks
return errDeletedHasBlocks return errDeletedHasBlocks
case f.Type != FileInfoTypeFile && len(f.Blocks) != 0: case f.Type == FileInfoTypeDirectory && len(f.Blocks) != 0:
// Only files should have blocks // Directories should have no blocks
return errNonFileHasBlocks return errDirectoryHasBlocks
case f.IsDirectory() && f.Size != 0 && f.Size != deprecatedSyntheticDirectorySize:
// Directories should be size zero or the synthetic directory size
return errNonFileHasSize
case f.IsSymlink() && f.Size != 0:
// Symlinks should be size zero
return errNonFileHasSize
case !f.Deleted && !f.IsInvalid() && f.Type == FileInfoTypeFile && len(f.Blocks) == 0: case !f.Deleted && !f.IsInvalid() && f.Type == FileInfoTypeFile && len(f.Blocks) == 0:
// Non-deleted, non-invalid files should have at least one block // Non-deleted, non-invalid files should have at least one block
+1 -88
View File
@@ -451,61 +451,6 @@ func TestCheckConsistency(t *testing.T) {
}, },
ok: false, ok: false,
}, },
{
// directory with zero size
fi: FileInfo{
Name: "foo",
Type: FileInfoTypeDirectory,
},
ok: true,
},
{
// directory with synthetic size
fi: FileInfo{
Name: "foo",
Type: FileInfoTypeDirectory,
Size: deprecatedSyntheticDirectorySize,
},
ok: true,
},
{
// directory with arbitrary size
fi: FileInfo{
Name: "foo",
Type: FileInfoTypeDirectory,
Size: 42,
},
ok: false,
},
{
// symlink with zero size
fi: FileInfo{
Name: "foo",
Type: FileInfoTypeSymlink,
SymlinkTarget: []byte("bar"),
},
ok: true,
},
{
// symlink with synthetic directory size (not permitted)
fi: FileInfo{
Name: "foo",
Type: FileInfoTypeSymlink,
SymlinkTarget: []byte("bar"),
Size: deprecatedSyntheticDirectorySize,
},
ok: false,
},
{
// symlink with arbitrary size
fi: FileInfo{
Name: "foo",
Type: FileInfoTypeSymlink,
SymlinkTarget: []byte("bar"),
Size: 42,
},
ok: false,
},
} }
for _, tc := range cases { for _, tc := range cases {
@@ -599,7 +544,7 @@ func TestDispatcherToCloseDeadlock(t *testing.T) {
} }
func TestRequestMaxSize(t *testing.T) { func TestRequestMaxSize(t *testing.T) {
invalidSize := []int{-65536, -1, MaxRequestSize + 1} invalidSize := []int{-65536, 0, MaxRequestSize + 1}
for _, s := range invalidSize { for _, s := range invalidSize {
t.Run(fmt.Sprintf("invalid/%d", s), func(t *testing.T) { t.Run(fmt.Sprintf("invalid/%d", s), func(t *testing.T) {
m := newTestModel() m := newTestModel()
@@ -615,7 +560,6 @@ func TestRequestMaxSize(t *testing.T) {
Id: 1, Id: 1,
Name: "valid", Name: "valid",
Size: MaxRequestSize, Size: MaxRequestSize,
Hash: []byte{42},
} }
res := <-c.outbox res := <-c.outbox
@@ -629,7 +573,6 @@ func TestRequestMaxSize(t *testing.T) {
Id: 2, Id: 2,
Name: "invalid", Name: "invalid",
Size: int32(s), Size: int32(s),
Hash: []byte{42},
} }
select { select {
@@ -649,35 +592,6 @@ func TestRequestMaxSize(t *testing.T) {
} }
} }
func TestRequestZeroSize(t *testing.T) {
// A zero-sized request should be accepted, since current versions of
// Syncthing send these. See https://github.com/syncthing/syncthing/issues/10709.
m := newTestModel()
rw := testutil.NewBlockingRW()
c := getRawConnection(NewConnection(c0ID, rw, &testutil.NoopRW{}, testutil.NoopCloser{}, m, new(mockedConnectionInfo), CompressionAlways, testKeyGen))
c.Start()
defer closeAndWait(c, rw)
c.inbox <- &bep.ClusterConfig{}
c.inbox <- &bep.Request{
Id: 1,
Name: "valid",
Size: 0,
Hash: []byte{42},
}
select {
case res := <-c.outbox:
if msg, ok := res.msg.(*bep.Response); !ok || msg.Id != 1 {
t.Errorf("bad response %#v", msg)
}
case <-c.dispatcherLoopStopped:
t.Fatal("dispatcher loop terminated, expected zero-sized request to be accepted")
case <-time.After(time.Second):
t.Fatal("timed out waiting for response")
}
}
func TestRequestInvalidFilename(t *testing.T) { func TestRequestInvalidFilename(t *testing.T) {
m := newTestModel() m := newTestModel()
rw := testutil.NewBlockingRW() rw := testutil.NewBlockingRW()
@@ -690,7 +604,6 @@ func TestRequestInvalidFilename(t *testing.T) {
Id: 1, Id: 1,
Name: "../escape", Name: "../escape",
Size: 1024, Size: 1024,
Hash: []byte{42},
} }
select { select {
+16 -3
View File
@@ -120,12 +120,25 @@ func Blocks(ctx context.Context, r io.Reader, blocksize int, sizehint int64, cou
return blocks, nil return blocks, nil
} }
// Validate validates the hash. // Validate validates the hash, if len(hash)>0.
func Validate(buf, hash []byte) bool { func Validate(buf, hash []byte) bool {
hbuf := sha256.Sum256(buf) if len(hash) > 0 {
return bytes.Equal(hbuf[:], hash) hbuf := sha256.Sum256(buf)
return bytes.Equal(hbuf[:], hash)
}
return true
} }
type noopHash struct{}
func (noopHash) Sum32() uint32 { return 0 }
func (noopHash) BlockSize() int { return 0 }
func (noopHash) Size() int { return 0 }
func (noopHash) Reset() {}
func (noopHash) Sum([]byte) []byte { return nil }
func (noopHash) Write([]byte) (int, error) { return 0, nil }
type noopCounter struct{} type noopCounter struct{}
func (*noopCounter) Update(_ int64) {} func (*noopCounter) Update(_ int64) {}
+3 -3
View File
@@ -42,9 +42,9 @@ type testfileList []testfile
var testdata = testfileList{ var testdata = testfileList{
{".stignore", 48, "f60db5c36f642f8a6c1a636024330cd4dba6ab965083542f3629ff7d8c547911"}, {".stignore", 48, "f60db5c36f642f8a6c1a636024330cd4dba6ab965083542f3629ff7d8c547911"},
{"afile", 4, "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c"}, {"afile", 4, "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c"},
{"dir1", 0, ""}, {"dir1", 128, ""},
{filepath.Join("dir1", "dfile"), 5, "49ae93732fcf8d63fe1cce759664982dbd5b23161f007dba8561862adc96d063"}, {filepath.Join("dir1", "dfile"), 5, "49ae93732fcf8d63fe1cce759664982dbd5b23161f007dba8561862adc96d063"},
{"dir2", 0, ""}, {"dir2", 128, ""},
{filepath.Join("dir2", "cfile"), 4, "bf07a7fbb825fc0aae7bf4a1177b2b31fcf8a3feeaf7092761e18c859ee52a9c"}, {filepath.Join("dir2", "cfile"), 4, "bf07a7fbb825fc0aae7bf4a1177b2b31fcf8a3feeaf7092761e18c859ee52a9c"},
{"excludes", 37, "df90b52f0c55dba7a7a940affe482571563b1ac57bd5be4d8a0291e7de928e06"}, {"excludes", 37, "df90b52f0c55dba7a7a940affe482571563b1ac57bd5be4d8a0291e7de928e06"},
{"further-excludes", 5, "7eb0a548094fa6295f7fd9200d69973e5f5ec5c04f2a86d998080ac43ecf89f1"}, {"further-excludes", 5, "7eb0a548094fa6295f7fd9200d69973e5f5ec5c04f2a86d998080ac43ecf89f1"},
@@ -602,7 +602,7 @@ func (l fileList) testfiles() testfileList {
if len(f.Blocks) > 1 { if len(f.Blocks) > 1 {
panic("simple test case stuff only supports a single block per file") panic("simple test case stuff only supports a single block per file")
} }
testfiles[i] = testfile{name: f.Name, length: f.Size} testfiles[i] = testfile{name: f.Name, length: f.FileSize()}
if len(f.Blocks) == 1 { if len(f.Blocks) == 1 {
testfiles[i].hash = fmt.Sprintf("%x", f.Blocks[0].Hash) testfiles[i].hash = fmt.Sprintf("%x", f.Blocks[0].Hash)
} }
+27 -55
View File
@@ -27,8 +27,6 @@ func init() {
factories["external"] = newExternal factories["external"] = newExternal
} }
const unixSpecialChars = "`" + `"'<>;!#$&*? `
type external struct { type external struct {
command string command string
filesystem fs.Filesystem filesystem fs.Filesystem
@@ -70,11 +68,36 @@ func (v external) Archive(filePath string) error {
return errors.New("command is empty, please enter a valid command") return errors.New("command is empty, please enter a valid command")
} }
cmd, err := v.prepareCommand(filePath) words, err := shellquote.Split(v.command)
if err != nil { if err != nil {
return err return fmt.Errorf("command is invalid: %w", err)
} }
context := map[string]string{
"%FOLDER_FILESYSTEM%": string(v.filesystem.Type()),
"%FOLDER_PATH%": v.filesystem.URI(),
"%FILE_PATH%": filePath,
}
for i, word := range words {
for key, val := range context {
word = strings.ReplaceAll(word, key, val)
}
words[i] = word
}
cmd := exec.Command(words[0], words[1:]...)
env := os.Environ()
// filter STGUIAUTH and STGUIAPIKEY from environment variables
var filteredEnv []string
for _, x := range env {
if !strings.HasPrefix(x, "STGUIAUTH=") && !strings.HasPrefix(x, "STGUIAPIKEY=") {
filteredEnv = append(filteredEnv, x)
}
}
cmd.Env = filteredEnv
combinedOutput, err := cmd.CombinedOutput() combinedOutput, err := cmd.CombinedOutput()
l.Debugln("external command output:", string(combinedOutput)) l.Debugln("external command output:", string(combinedOutput))
if err != nil { if err != nil {
@@ -103,54 +126,3 @@ func (external) Restore(_ string, _ time.Time) error {
func (external) Clean(_ context.Context) error { func (external) Clean(_ context.Context) error {
return nil return nil
} }
// prepareCommand returns the command with environment for the given file
// path.
func (v external) prepareCommand(filePath string) (*exec.Cmd, error) {
words, err := shellquote.Split(v.command)
if err != nil {
return nil, fmt.Errorf("command is invalid: %w", err)
}
context := map[string]string{
"%FOLDER_FILESYSTEM%": string(v.filesystem.Type()),
"%FOLDER_PATH%": v.filesystem.URI(),
"%FILE_PATH%": filePath,
}
for i, word := range words {
unsafe := strings.ContainsAny(word, unixSpecialChars)
for key, val := range context {
// If the parameter contains both an unsafe character and a
// template placeholder, we consider it unsafe and reject it.
// Note that the shell splitting will have already removed outer
// quotes, so that a command like `foo "%FILE_PATH%"` is fine
// here, despite the double quote being one of our unsafe
// characters.
if unsafe && strings.Contains(word, key) {
return nil, errors.New("unsafe external versioning command; see https://docs.syncthing.net/users/versioning.html#external-file-versioning")
}
word = strings.ReplaceAll(word, key, val)
}
words[i] = word
}
// filter STGUIAUTH and STGUIAPIKEY from environment variables, and add
// our folder info.
env := os.Environ()
var filteredEnv []string
for _, x := range env {
if !strings.HasPrefix(x, "STGUIAUTH=") && !strings.HasPrefix(x, "STGUIAPIKEY=") {
filteredEnv = append(filteredEnv, x)
}
}
for k, v := range context {
k = strings.Trim(k, "%")
filteredEnv = append(filteredEnv, fmt.Sprintf("%s=%s", k, v))
}
cmd := exec.Command(words[0], words[1:]...) //nolint:gosec // execution with user tainted data, by design
cmd.Env = filteredEnv
return cmd, nil
}
-33
View File
@@ -76,39 +76,6 @@ func TestExternal(t *testing.T) {
} }
} }
func TestExternalCommandSplit(t *testing.T) {
e := external{
filesystem: fs.NewFilesystem(fs.FilesystemTypeFake, "TestExternalCommandSplit"),
}
cases := []struct {
cmd string
safe bool
}{
{`echo %FOLDER_PATH% %FILE_PATH%`, true},
{`echo "%FOLDER_PATH% %FILE_PATH%"`, false},
{`echo %FOLDER_PATH%/%FILE_PATH%`, true},
{`echo "%FOLDER_PATH%/%FILE_PATH%"`, true},
{`echo '%FOLDER_PATH%/%FILE_PATH%'`, true},
{`echo "'%FOLDER_PATH%/%FILE_PATH%'"`, false},
{`sh -c "echo '%FOLDER_PATH%/%FILE_PATH%'"`, false},
{`sh -c "echo %FOLDER_PATH%/%FILE_PATH%"`, false},
}
for _, tc := range cases {
e.command = tc.cmd
res, err := e.prepareCommand("evil file name")
if tc.safe && err != nil {
t.Fatal(err)
}
if !tc.safe && err == nil {
t.Logf("%q", res.Path)
t.Logf("%q", res.Args)
t.Errorf("should be unsafe: %q", tc.cmd)
}
}
}
func prepForRemoval(t *testing.T, file string) { func prepForRemoval(t *testing.T, file string) {
if err := os.RemoveAll("testdata"); err != nil { if err := os.RemoveAll("testdata"); err != nil {
t.Fatal(err) t.Fatal(err)
+2 -2
View File
@@ -155,7 +155,7 @@ func archiveFile(method fs.CopyRangeMethod, srcFs, dstFs fs.Filesystem, filePath
if err != nil { if err != nil {
if fs.IsNotExist(err) { if fs.IsNotExist(err) {
slog.Debug("Creating versions dir") slog.Debug("Creating versions dir")
err := dstFs.MkdirAll(".", fs.ModePerm) err := dstFs.MkdirAll(".", 0o755)
if err != nil { if err != nil {
return err return err
} }
@@ -328,7 +328,7 @@ func restoreFile(method fs.CopyRangeMethod, src, dst fs.Filesystem, filePath str
return err return err
} }
_ = dst.MkdirAll(filepath.Dir(filePath), fs.ModePerm) _ = dst.MkdirAll(filepath.Dir(filePath), 0o755)
err := osutil.RenameOrCopy(method, src, dst, sourceFile, filePath) err := osutil.RenameOrCopy(method, src, dst, sourceFile, filePath)
_ = dst.Chtimes(filePath, sourceMtime, sourceMtime) _ = dst.Chtimes(filePath, sourceMtime, sourceMtime)
return err return err
+1 -1
View File
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
.. ..
.TH "STDISCOSRV" "1" "Jun 08, 2026" "v2.1.0" "Syncthing" .TH "STDISCOSRV" "1" "May 13, 2026" "v2.1.0" "Syncthing"
.SH NAME .SH NAME
stdiscosrv \- Syncthing Discovery Server stdiscosrv \- Syncthing Discovery Server
.SH SYNOPSIS .SH SYNOPSIS
+1 -1
View File
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
.. ..
.TH "STRELAYSRV" "1" "Jun 08, 2026" "v2.1.0" "Syncthing" .TH "STRELAYSRV" "1" "May 13, 2026" "v2.1.0" "Syncthing"
.SH NAME .SH NAME
strelaysrv \- Syncthing Relay Server strelaysrv \- Syncthing Relay Server
.SH SYNOPSIS .SH SYNOPSIS
+1 -1
View File
@@ -28,7 +28,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
.. ..
.TH "SYNCTHING-BEP" "7" "Jun 08, 2026" "v2.1.0" "Syncthing" .TH "SYNCTHING-BEP" "7" "May 13, 2026" "v2.1.0" "Syncthing"
.SH NAME .SH NAME
syncthing-bep \- Block Exchange Protocol v1 syncthing-bep \- Block Exchange Protocol v1
.SH INTRODUCTION AND DEFINITIONS .SH INTRODUCTION AND DEFINITIONS
+6 -5
View File
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
.. ..
.TH "SYNCTHING-CONFIG" "5" "Jun 08, 2026" "v2.1.0" "Syncthing" .TH "SYNCTHING-CONFIG" "5" "May 13, 2026" "v2.1.0" "Syncthing"
.SH NAME .SH NAME
syncthing-config \- Syncthing Configuration syncthing-config \- Syncthing Configuration
.SH OVERVIEW .SH OVERVIEW
@@ -1528,10 +1528,11 @@ addresses to global discovery.
.INDENT 0.0 .INDENT 0.0
.TP .TP
.B sendFullIndexOnUpgrade .B sendFullIndexOnUpgrade
Controls whether all index data is resent when an upgrade has happened. Controls whether all index data is resent when an upgrade has happened,
This used to be the default behavior in older versions, but is mainly useful equivalent to starting Syncthing with \fB\-\-reset\-deltas\fP\&. This used
as a troubleshooting step and causes high database churn. The default is to be the default behavior in older versions, but is mainly useful as a
now \fBfalse\fP\&. troubleshooting step and causes high database churn. The default is now
\fBfalse\fP\&.
.UNINDENT .UNINDENT
.INDENT 0.0 .INDENT 0.0
.TP .TP
+1 -1
View File
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
.. ..
.TH "SYNCTHING-DEVICE-IDS" "7" "Jun 08, 2026" "v2.1.0" "Syncthing" .TH "SYNCTHING-DEVICE-IDS" "7" "May 13, 2026" "v2.1.0" "Syncthing"
.SH NAME .SH NAME
syncthing-device-ids \- Understanding Device IDs syncthing-device-ids \- Understanding Device IDs
.sp .sp
+1 -1
View File
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
.. ..
.TH "SYNCTHING-EVENT-API" "7" "Jun 08, 2026" "v2.1.0" "Syncthing" .TH "SYNCTHING-EVENT-API" "7" "May 13, 2026" "v2.1.0" "Syncthing"
.SH NAME .SH NAME
syncthing-event-api \- Event API syncthing-event-api \- Event API
.SH DESCRIPTION .SH DESCRIPTION
+1 -1
View File
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
.. ..
.TH "SYNCTHING-FAQ" "7" "Jun 08, 2026" "v2.1.0" "Syncthing" .TH "SYNCTHING-FAQ" "7" "May 13, 2026" "v2.1.0" "Syncthing"
.SH NAME .SH NAME
syncthing-faq \- Frequently Asked Questions syncthing-faq \- Frequently Asked Questions
.INDENT 0.0 .INDENT 0.0
+1 -1
View File
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
.. ..
.TH "SYNCTHING-GLOBALDISCO" "7" "Jun 08, 2026" "v2.1.0" "Syncthing" .TH "SYNCTHING-GLOBALDISCO" "7" "May 13, 2026" "v2.1.0" "Syncthing"
.SH NAME .SH NAME
syncthing-globaldisco \- Global Discovery Protocol v3 syncthing-globaldisco \- Global Discovery Protocol v3
.SH ANNOUNCEMENTS .SH ANNOUNCEMENTS
+1 -1
View File
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
.. ..
.TH "SYNCTHING-LOCALDISCO" "7" "Jun 08, 2026" "v2.1.0" "Syncthing" .TH "SYNCTHING-LOCALDISCO" "7" "May 13, 2026" "v2.1.0" "Syncthing"
.SH NAME .SH NAME
syncthing-localdisco \- Local Discovery Protocol v4 syncthing-localdisco \- Local Discovery Protocol v4
.SH MODE OF OPERATION .SH MODE OF OPERATION
+1 -1
View File
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
.. ..
.TH "SYNCTHING-NETWORKING" "7" "Jun 08, 2026" "v2.1.0" "Syncthing" .TH "SYNCTHING-NETWORKING" "7" "May 13, 2026" "v2.1.0" "Syncthing"
.SH NAME .SH NAME
syncthing-networking \- Firewall Setup syncthing-networking \- Firewall Setup
.SH ROUTER SETUP .SH ROUTER SETUP
+1 -1
View File
@@ -28,7 +28,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
.. ..
.TH "SYNCTHING-RELAY" "7" "Jun 08, 2026" "v2.1.0" "Syncthing" .TH "SYNCTHING-RELAY" "7" "May 13, 2026" "v2.1.0" "Syncthing"
.SH NAME .SH NAME
syncthing-relay \- Relay Protocol v1 syncthing-relay \- Relay Protocol v1
.SH WHAT IS A RELAY? .SH WHAT IS A RELAY?
+3 -5
View File
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
.. ..
.TH "SYNCTHING-REST-API" "7" "Jun 08, 2026" "v2.1.0" "Syncthing" .TH "SYNCTHING-REST-API" "7" "May 13, 2026" "v2.1.0" "Syncthing"
.SH NAME .SH NAME
syncthing-rest-api \- REST API syncthing-rest-api \- REST API
.sp .sp
@@ -665,10 +665,8 @@ curl \-X POST \-H \(dqX\-API\-Key: abc123\(dq http://localhost:8384/rest/system/
.UNINDENT .UNINDENT
.UNINDENT .UNINDENT
.sp .sp
Creates \fB\&.stfolder\fP folders in each sync folder if they do not already exist. \fBCaution\fP: See \fB\-\-reset\-database\fP for \fB\&.stfolder\fP creation
\fBCaution\fP: Ensure that all sync folders which are mountpoints are already side\-effect and caution regarding mountpoints.
mounted. Inconsistent versions may result if the mountpoint is later mounted
and contains older versions.
.SS POST /rest/system/restart .SS POST /rest/system/restart
.sp .sp
Post with empty body to immediately restart Syncthing. Post with empty body to immediately restart Syncthing.
+1 -1
View File
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
.. ..
.TH "SYNCTHING-SECURITY" "7" "Jun 08, 2026" "v2.1.0" "Syncthing" .TH "SYNCTHING-SECURITY" "7" "May 13, 2026" "v2.1.0" "Syncthing"
.SH NAME .SH NAME
syncthing-security \- Security Principles syncthing-security \- Security Principles
.sp .sp
+1 -1
View File
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
.. ..
.TH "SYNCTHING-STIGNORE" "5" "Jun 08, 2026" "v2.1.0" "Syncthing" .TH "SYNCTHING-STIGNORE" "5" "May 13, 2026" "v2.1.0" "Syncthing"
.SH NAME .SH NAME
syncthing-stignore \- Prevent files from being synchronized to other nodes syncthing-stignore \- Prevent files from being synchronized to other nodes
.SH SYNOPSIS .SH SYNOPSIS
+1 -1
View File
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
.. ..
.TH "SYNCTHING-VERSIONING" "7" "Jun 08, 2026" "v2.1.0" "Syncthing" .TH "SYNCTHING-VERSIONING" "7" "May 13, 2026" "v2.1.0" "Syncthing"
.SH NAME .SH NAME
syncthing-versioning \- Keep automatic backups of deleted files by other nodes syncthing-versioning \- Keep automatic backups of deleted files by other nodes
.sp .sp
+3 -25
View File
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
.. ..
.TH "SYNCTHING" "1" "Jun 08, 2026" "v2.1.0" "Syncthing" .TH "SYNCTHING" "1" "May 13, 2026" "v2.1.0" "Syncthing"
.SH NAME .SH NAME
syncthing \- Syncthing syncthing \- Syncthing
.SH SYNOPSIS .SH SYNOPSIS
@@ -404,36 +404,14 @@ acknowledge (clear) them.
.TP .TP
.B debug .B debug
Various tools to aid in diagnosing problems or collection information for Various tools to aid in diagnosing problems or collection information for
bug reports. bug reports. Some of these commands access the database directly and can
therefore only work when Syncthing is not running.
.TP .TP
.B \fB\-\fP (a single dash) .B \fB\-\fP (a single dash)
Reads subsequent commands from the standard input stream, without needing to Reads subsequent commands from the standard input stream, without needing to
call the \fBsyncthing cli\fP command over and over. Exits on any invalid call the \fBsyncthing cli\fP command over and over. Exits on any invalid
command or when EOF (end\-of\-file) is received. command or when EOF (end\-of\-file) is received.
.UNINDENT .UNINDENT
.sp
For troubleshooting issues, the \fBdebug\fP subcommand provides some commonly
needed actions for data analysis and repair. Some of these commands access the
database directly and can therefore only work when Syncthing is not running.
Not to be confused with the \fBcli debug\fP subcommand group, which works via the
REST API.
.INDENT 0.0
.TP
.B debug reset\-database
Reset the database, forcing a full rescan and resync. \fBMust only be used
when Syncthing is not running.\fP More granular reset operations are
available on the REST API while Syncthing is running:
\fI\%POST /rest/system/reset\fP
.TP
.B debug database\-statistics
Display database size statistics.
.TP
.B debug database\-counts
Display database folder counts.
.TP
.B debug database\-file
Display database file metadata.
.UNINDENT
.SH PROXIES .SH PROXIES
.sp .sp
Syncthing can use a SOCKS, HTTP, or HTTPS proxy to talk to the outside Syncthing can use a SOCKS, HTTP, or HTTPS proxy to talk to the outside
+48
View File
@@ -0,0 +1,48 @@
# Local Syncthing Patches
Apply the local patches manually after pulling a new upstream Syncthing release:
```bash
git apply patches/sync-stignore.patch patches/webui-build-marker.patch
go run build.go -build-out bin/syncthing-stignore build syncthing
```
The automated release flow uses:
```bash
./scripts/update-custom-release.sh
```
By default the script finds the latest stable upstream tag, creates a local
`custom/<version>-<suffix>` branch, applies all local patches, removes upstream
GitHub/Gitea workflow files from the release commit, tags
`<upstream>-stignore.7`, regenerates embedded GUI assets, runs focused tests,
and writes build artifacts to `dist/`.
Useful options:
```bash
CUSTOM_RELEASE_UPSTREAM_TAG=v2.1.0 ./scripts/update-custom-release.sh
CUSTOM_RELEASE_FORCE=1 ./scripts/update-custom-release.sh
CUSTOM_RELEASE_PATCHES="patches/sync-stignore.patch patches/webui-build-marker.patch" ./scripts/update-custom-release.sh
CUSTOM_RELEASE_PUSH=1 CUSTOM_RELEASE_REMOTE=gitea ./scripts/update-custom-release.sh
CUSTOM_RELEASE_PUSH=1 CUSTOM_RELEASE_PUSH_BRANCH=1 CUSTOM_RELEASE_REMOTE=gitea ./scripts/update-custom-release.sh
CUSTOM_RELEASE_CREATE_GITEA_RELEASE=1 CUSTOM_RELEASE_TEA_REPO=felixfoertsch/syncthing ./scripts/update-custom-release.sh
CUSTOM_RELEASE_BUILDS="darwin/amd64/zip darwin/arm64/zip linux/amd64/tar linux/arm64/tar" ./scripts/update-custom-release.sh
```
The Gitea and GitHub workflows run the script on their respective CI hosts when
the local patchset changes on `main`, on a schedule, and on manual dispatch.
The repository's `upstream` branch stays a clean upstream mirror.
The workflow detects the latest upstream Syncthing stable tag, exits if the
matching custom tag already exists, pushes only the `<upstream>-stignore.7` tag
by default, and publishes release assets on the CI host running the workflow.
The local `custom/<version>` branch is only pushed when
`CUSTOM_RELEASE_PUSH_BRANCH=1` is set. The workflow builds macOS arm64, Linux
amd64, and Linux arm64 archives.
The patch makes root-level `.stignore` sync like regular folder content while
keeping `.stfolder` and `.stversions` protected as internal Syncthing paths.
The web UI patch adds `It syncs .stignore now!` to the GUI footer so the custom
binary is visually distinguishable from upstream builds. The release test fails
if the generated GUI asset bundle does not contain that marker.
+119
View File
@@ -0,0 +1,119 @@
diff --git a/lib/fs/filesystem.go b/lib/fs/filesystem.go
index 31bbb9770..dcece778e 100644
--- a/lib/fs/filesystem.go
+++ b/lib/fs/filesystem.go
@@ -284,8 +284,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
diff --git a/lib/fs/filesystem_test.go b/lib/fs/filesystem_test.go
index 08e45ff50..2d100a2b0 100644
--- a/lib/fs/filesystem_test.go
+++ b/lib/fs/filesystem_test.go
@@ -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},
}
diff --git a/lib/model/folder_recvonly_test.go b/lib/model/folder_recvonly_test.go
index e84d0c49d..fbd442966 100644
--- a/lib/model/folder_recvonly_test.go
+++ b/lib/model/folder_recvonly_test.go
@@ -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 {
diff --git a/lib/model/requests_test.go b/lib/model/requests_test.go
index 288140721..6f8124e97 100644
--- a/lib/model/requests_test.go
+++ b/lib/model/requests_test.go
@@ -974,13 +974,15 @@ func TestIgnoreDeleteUnignore(t *testing.T) {
file := "foobar"
contents := []byte("test file contents\n")
- basicCheck := func(fs []protocol.FileInfo) {
+ basicCheck := func(fs []protocol.FileInfo) protocol.FileInfo {
t.Helper()
- if len(fs) != 1 {
- t.Fatal("expected a single index entry, got", len(fs))
- } else if fs[0].Name != file {
- t.Fatalf("expected a index entry for %v, got one for %v", file, fs[0].Name)
+ for _, f := range fs {
+ if f.Name == file {
+ return f
+ }
}
+ t.Fatalf("expected an index entry for %v, got %v", file, fs)
+ return protocol.FileInfo{}
}
done := make(chan struct{})
@@ -1001,8 +1003,7 @@ func TestIgnoreDeleteUnignore(t *testing.T) {
done = make(chan struct{})
fc.setIndexFn(func(_ context.Context, folder string, fs []protocol.FileInfo) error {
- basicCheck(fs)
- f := fs[0]
+ f := basicCheck(fs)
if !f.IsInvalid() {
t.Errorf("Received non-invalid index update")
}
@@ -1022,8 +1023,7 @@ func TestIgnoreDeleteUnignore(t *testing.T) {
done = make(chan struct{})
fc.setIndexFn(func(_ context.Context, folder string, fs []protocol.FileInfo) error {
- basicCheck(fs)
- f := fs[0]
+ f := basicCheck(fs)
if f.IsInvalid() {
t.Errorf("Received invalid index update")
}
diff --git a/lib/scanner/walk_test.go b/lib/scanner/walk_test.go
index d75dddaf3..3693ae4db 100644
--- a/lib/scanner/walk_test.go
+++ b/lib/scanner/walk_test.go
@@ -40,6 +40,7 @@ type testfile struct {
type testfileList []testfile
var testdata = testfileList{
+ {".stignore", 48, "f60db5c36f642f8a6c1a636024330cd4dba6ab965083542f3629ff7d8c547911"},
{"afile", 4, "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c"},
{"dir1", 128, ""},
{filepath.Join("dir1", "dfile"), 5, "49ae93732fcf8d63fe1cce759664982dbd5b23161f007dba8561862adc96d063"},
+62
View File
@@ -0,0 +1,62 @@
diff --git a/gui/default/index.html b/gui/default/index.html
index e3e573c9a..5ec76e740 100644
--- a/gui/default/index.html
+++ b/gui/default/index.html
@@ -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>
diff --git a/lib/api/auto/custom_marker_test.go b/lib/api/auto/custom_marker_test.go
new file mode 100644
index 000000000..73eae3db7
--- /dev/null
+++ b/lib/api/auto/custom_marker_test.go
@@ -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)
+ }
+}
+1 -1
View File
@@ -93,7 +93,7 @@ func main() {
} }
bs = authorsRe.ReplaceAll(bs, []byte("id=\"contributor-list\">\n"+replacement+"\n </div>")) bs = authorsRe.ReplaceAll(bs, []byte("id=\"contributor-list\">\n"+replacement+"\n </div>"))
if err := os.WriteFile(htmlFile, bs, 0o666); err != nil { if err := os.WriteFile(htmlFile, bs, 0o644); err != nil {
log.Fatal(err) log.Fatal(err)
} }
+1 -1
View File
@@ -265,7 +265,7 @@ func readAll(path string) []byte {
} }
func writeFile(path string, data string) { func writeFile(path string, data string) {
err := os.WriteFile(path, []byte(data), 0o666) err := os.WriteFile(path, []byte(data), 0o644)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@@ -0,0 +1,195 @@
#!/usr/bin/env bats
setup() {
REPO_ROOT="$(git rev-parse --show-toplevel)"
WORKFLOW="$REPO_ROOT/.gitea/workflows/custom-release.yml"
GITHUB_WORKFLOW="$REPO_ROOT/.github/workflows/custom-release.yml"
RELEASE_SCRIPT="$REPO_ROOT/scripts/update-custom-release.sh"
}
@test "github custom release uses a github macos runner and github releases" {
run rg -n 'runs-on:[[:space:]]*macos-14' "$GITHUB_WORKFLOW"
[ "$status" -eq 0 ]
run rg -n 'gh release create' "$GITHUB_WORKFLOW"
[ "$status" -eq 0 ]
run rg -n 'CUSTOM_RELEASE_CREATE_GITEA_RELEASE: "0"' "$GITHUB_WORKFLOW"
[ "$status" -eq 0 ]
run rg -n 'tea|ffmini_macos_arm64' "$GITHUB_WORKFLOW"
[ "$status" -ne 0 ]
}
@test "custom release runs as one job on ffmini macos runner" {
run rg -n 'runs-on:[[:space:]]*ffmini_macos_arm64' "$WORKFLOW"
[ "$status" -eq 0 ]
run rg -n 'ubuntu-latest' "$WORKFLOW"
[ "$status" -ne 0 ]
run rg -n 'actions/upload-artifact' "$WORKFLOW"
[ "$status" -ne 0 ]
}
@test "custom release tea login setup is idempotent on persistent host runner" {
run rg -n 'tea" logins delete actions >/dev/null 2>&1 \|\| true' "$WORKFLOW"
[ "$status" -eq 0 ]
run rg -n 'tea" logins add --name actions' "$WORKFLOW"
[ "$status" -eq 0 ]
}
@test "custom release workflow imports developer id signing material into temporary keychain" {
run rg -n 'DEVELOPER_ID_APPLICATION_P12_BASE64' "$WORKFLOW"
[ "$status" -eq 0 ]
run rg -n 'DEVELOPER_ID_APPLICATION_P12_PASSWORD' "$WORKFLOW"
[ "$status" -eq 0 ]
run rg -n 'DEVELOPER_ID_APPLICATION_P12_BASE64 secret is required' "$WORKFLOW"
[ "$status" -eq 0 ]
run rg -n 'DEVELOPER_ID_APPLICATION_P12_PASSWORD secret is required' "$WORKFLOW"
[ "$status" -ne 0 ]
run rg -n 'security import .* -P "\$DEVELOPER_ID_APPLICATION_P12_PASSWORD"' "$WORKFLOW"
[ "$status" -eq 0 ]
run rg -n 'security import .* -A ' "$WORKFLOW"
[ "$status" -eq 0 ]
run rg -n 'security find-identity -v -p codesigning "\$keychain_path"' "$WORKFLOW"
[ "$status" -eq 0 ]
run rg -n 'security default-keychain -d user -s "\$keychain_path"' "$WORKFLOW"
[ "$status" -eq 0 ]
run rg -n 'previous_default_keychain=' "$WORKFLOW"
[ "$status" -eq 0 ]
run rg -n 'CUSTOM_RELEASE_PREVIOUS_DEFAULT_KEYCHAIN=\$previous_default_keychain' "$WORKFLOW"
[ "$status" -eq 0 ]
run rg -n 'CUSTOM_RELEASE_PREVIOUS_DYNAMIC_DEFAULT_KEYCHAIN' "$WORKFLOW"
[ "$status" -ne 0 ]
run rg -n 'security default-keychain -s "\$CUSTOM_RELEASE_PREVIOUS_DEFAULT_KEYCHAIN"' "$WORKFLOW"
[ "$status" -ne 0 ]
run rg -n 'security default-keychain -d dynamic' "$WORKFLOW"
[ "$status" -ne 0 ]
run rg -n 'security find-identity -v -p codesigning$' "$WORKFLOW" "$RELEASE_SCRIPT"
[ "$status" -eq 0 ]
run rg -n 'codesign_identity_sha1=' "$WORKFLOW"
[ "$status" -eq 0 ]
run rg -n 'sed -n .*Developer ID Application' "$WORKFLOW"
[ "$status" -eq 0 ]
run rg -n 'CUSTOM_RELEASE_CODESIGN_IDENTITY=\$codesign_identity' "$WORKFLOW"
[ "$status" -eq 0 ]
run rg -n 'CUSTOM_RELEASE_CODESIGN_IDENTITY_SHA1=\$codesign_identity_sha1' "$WORKFLOW"
[ "$status" -eq 0 ]
run rg -n 'CUSTOM_RELEASE_KEYCHAIN_PASSWORD=\$keychain_password' "$WORKFLOW"
[ "$status" -eq 0 ]
run rg -n 'codesign --force --dryrun --sign "\$codesign_identity" --keychain "\$keychain_path" --options runtime --timestamp "\$probe_binary"' "$WORKFLOW"
[ "$status" -eq 0 ]
run rg -n 'CUSTOM_RELEASE_CODESIGN_IDENTITY: "Developer ID Application' "$WORKFLOW"
[ "$status" -ne 0 ]
run rg -n 'security create-keychain' "$WORKFLOW"
[ "$status" -eq 0 ]
run rg -n 'keychain_dir="\$HOME/Library/Keychains"' "$WORKFLOW"
[ "$status" -eq 0 ]
run rg -n 'rm -f "\$keychain_path"' "$WORKFLOW"
[ "$status" -eq 0 ]
run rg -n 'existing_keychains=\(\)' "$WORKFLOW"
[ "$status" -eq 0 ]
run rg -n 'security list-keychains -s "\$keychain_path"' "$WORKFLOW"
[ "$status" -eq 0 ]
run rg -n 'security list-keychains -d dynamic' "$WORKFLOW"
[ "$status" -ne 0 ]
run rg -n 'security import' "$WORKFLOW"
[ "$status" -eq 0 ]
run rg -n 'security delete-keychain' "$WORKFLOW"
[ "$status" -eq 0 ]
}
@test "custom release carries per-target cgo mode" {
run rg -n 'darwin/arm64/zip/1' "$WORKFLOW" "$RELEASE_SCRIPT"
[ "$status" -eq 0 ]
run rg -n 'linux/amd64/tar/0' "$WORKFLOW" "$RELEASE_SCRIPT"
[ "$status" -eq 0 ]
run rg -n 'CUSTOM_RELEASE_CGO_ENABLED' "$WORKFLOW"
[ "$status" -ne 0 ]
}
@test "custom release signs darwin assets with hardened runtime and timestamp" {
run rg -n 'codesign_args=\(--force --sign "\$codesign_identity"\)' "$RELEASE_SCRIPT"
[ "$status" -eq 0 ]
run rg -n -- '--keychain "\$CUSTOM_RELEASE_KEYCHAIN_PATH"' "$RELEASE_SCRIPT"
[ "$status" -eq 0 ]
run rg -n 'codesign_args\+=\(--options runtime --timestamp\)' "$RELEASE_SCRIPT"
[ "$status" -eq 0 ]
run rg -n 'security unlock-keychain -p "\$CUSTOM_RELEASE_KEYCHAIN_PASSWORD" "\$CUSTOM_RELEASE_KEYCHAIN_PATH"' "$RELEASE_SCRIPT"
[ "$status" -eq 0 ]
run rg -n 'security find-identity -v -p codesigning "\$CUSTOM_RELEASE_KEYCHAIN_PATH"' "$RELEASE_SCRIPT"
[ "$status" -eq 0 ]
run awk '
/codesign_args=\(--force --sign "\$codesign_identity"\)/ { sign = NR }
/codesign_args\+=\(--keychain "\$CUSTOM_RELEASE_KEYCHAIN_PATH"\)/ { keychain = NR }
/codesign_args\+=\(--options runtime --timestamp\)/ { options = NR }
END { exit !(sign && keychain && options && sign < keychain && keychain < options) }
' "$RELEASE_SCRIPT"
[ "$status" -eq 0 ]
run rg -n 'Developer ID Application' "$WORKFLOW" "$RELEASE_SCRIPT"
[ "$status" -eq 0 ]
run rg -n 'CUSTOM_RELEASE_CODESIGN_IDENTITY' "$WORKFLOW" "$RELEASE_SCRIPT"
[ "$status" -eq 0 ]
}
@test "custom release validates signed darwin binaries before publishing" {
run rg -n 'modernc-sqlite' "$RELEASE_SCRIPT"
[ "$status" -eq 0 ]
run rg -n 'codesign --verify --strict --verbose=2' "$RELEASE_SCRIPT"
[ "$status" -eq 0 ]
run rg -n 'TeamIdentifier=NG5W75WE8U|TeamIdentifier.*NG5W75WE8U' "$RELEASE_SCRIPT"
[ "$status" -eq 0 ]
run rg -n 'CUSTOM_RELEASE_REQUIRE_GATEKEEPER_ASSESSMENT: "0"' "$WORKFLOW"
[ "$status" -eq 0 ]
run rg -n 'require_gatekeeper_assessment="\$\{CUSTOM_RELEASE_REQUIRE_GATEKEEPER_ASSESSMENT:-0\}"' "$RELEASE_SCRIPT"
[ "$status" -eq 0 ]
run rg -n 'if \[\[ "\$require_gatekeeper_assessment" == "1" \]\]; then' "$RELEASE_SCRIPT"
[ "$status" -eq 0 ]
run rg -n 'spctl -a -vv --type execute' "$RELEASE_SCRIPT"
[ "$status" -eq 0 ]
}
+416
View File
@@ -0,0 +1,416 @@
#!/usr/bin/env bash
set -euo pipefail
# Description: create a patched Syncthing release from the latest upstream tag.
# Usage: ./scripts/update-custom-release.sh
upstream_url="${CUSTOM_RELEASE_UPSTREAM_URL:-https://github.com/syncthing/syncthing.git}"
upstream_tag="${CUSTOM_RELEASE_UPSTREAM_TAG:-}"
suffix="${CUSTOM_RELEASE_SUFFIX:-stignore.7}"
branch_prefix="${CUSTOM_RELEASE_BRANCH_PREFIX:-custom}"
dist_dir="${CUSTOM_RELEASE_DIST_DIR:-dist}"
target="${CUSTOM_RELEASE_TARGET:-syncthing}"
archive_kind="${CUSTOM_RELEASE_ARCHIVE:-tar}"
build_specs="${CUSTOM_RELEASE_BUILDS:-}"
default_cgo_enabled="${CUSTOM_RELEASE_CGO_ENABLED:-0}"
codesign_identity="${CUSTOM_RELEASE_CODESIGN_IDENTITY:-}"
codesign_team_id="${CUSTOM_RELEASE_CODESIGN_TEAM_ID:-NG5W75WE8U}"
require_gatekeeper_assessment="${CUSTOM_RELEASE_REQUIRE_GATEKEEPER_ASSESSMENT:-0}"
push_release="${CUSTOM_RELEASE_PUSH:-0}"
push_branch="${CUSTOM_RELEASE_PUSH_BRANCH:-0}"
push_remote="${CUSTOM_RELEASE_REMOTE:-origin}"
run_tests="${CUSTOM_RELEASE_TEST:-1}"
force="${CUSTOM_RELEASE_FORCE:-0}"
publish_gitea_release="${CUSTOM_RELEASE_CREATE_GITEA_RELEASE:-0}"
tea_repo="${CUSTOM_RELEASE_TEA_REPO:-}"
patch_tmp_dir=""
patch_files=()
assets_rebuilt=0
if [[ -n "${CUSTOM_RELEASE_PATCHES:-}" ]]; then
read -r -a patch_files <<< "$CUSTOM_RELEASE_PATCHES"
elif [[ -n "${CUSTOM_RELEASE_PATCH:-}" ]]; then
patch_files=("$CUSTOM_RELEASE_PATCH")
else
patch_files=(
patches/sync-stignore.patch
patches/webui-build-marker.patch
)
fi
log() {
printf '%s\n' "$*"
}
die() {
printf 'error: %s\n' "$*" >&2
exit 1
}
require_clean_worktree() {
if [[ -n "$(git status --porcelain)" ]]; then
die "working tree has uncommitted changes"
fi
}
latest_stable_tag() {
git ls-remote --refs --tags --sort='version:refname' "$upstream_url" 'v[0-9]*' \
| awk '{ tag = $2; sub("refs/tags/", "", tag); if (tag ~ /^v[0-9]+\.[0-9]+\.[0-9]+$/) latest = tag } END { print latest }'
}
tag_exists() {
local tag="$1"
if git rev-parse --quiet --verify "refs/tags/$tag" >/dev/null; then
return 0
fi
git ls-remote --quiet --exit-code --tags "$push_remote" "refs/tags/$tag" >/dev/null 2>&1
}
copy_patches_to_temp() {
local patch_tmp_dir="$1"
local patch_file
local patch_name
local patch_index=0
mkdir -p "$patch_tmp_dir"
for patch_file in "${patch_files[@]}"; do
[[ -f "$patch_file" ]] || die "patch file not found: $patch_file"
printf -v patch_name '%03d-%s' "$patch_index" "$(basename "$patch_file")"
cp "$patch_file" "$patch_tmp_dir/$patch_name"
patch_index=$((patch_index + 1))
done
}
fetch_upstream_tag() {
local tag="$1"
git fetch --force "$upstream_url" "refs/tags/$tag:refs/tags/$tag"
}
create_release_commit() {
local tag="$1"
local custom_tag="$2"
local branch="$3"
local patch_tmp_dir="$4"
local patch_file
git checkout -B "$branch" "$tag"
rm -rf "$dist_dir"
for patch_file in "$patch_tmp_dir"/*; do
[[ -f "$patch_file" ]] || continue
log "Applying $(basename "$patch_file")"
git apply --3way "$patch_file"
done
remove_upstream_workflows
git add -A
git commit -m "apply local Syncthing patches for $tag"
git tag -a "$custom_tag" -m "Syncthing $tag with local patches"
}
remove_upstream_workflows() {
local workflow_dir
for workflow_dir in .github/workflows .gitea/workflows; do
if [[ -e "$workflow_dir" ]]; then
log "Removing upstream workflow directory $workflow_dir from release commit"
rm -rf "$workflow_dir"
fi
done
}
delete_local_tag_if_forced() {
local tag="$1"
if [[ "$force" != "1" ]]; then
return
fi
if git rev-parse --quiet --verify "refs/tags/$tag" >/dev/null; then
git tag -d "$tag"
fi
}
test_release() {
if [[ "$run_tests" != "1" ]]; then
log "Skipping tests because CUSTOM_RELEASE_TEST=$run_tests"
return
fi
rebuild_assets_once
go test ./lib/api/auto ./lib/fs ./lib/ignore ./lib/scanner ./lib/model
}
rebuild_assets_once() {
if [[ "$assets_rebuilt" == "1" ]]; then
return
fi
log "Regenerating embedded GUI assets"
go run build.go assets
assets_rebuilt=1
}
format_build_specs() {
local spec
for spec in $build_specs; do
printf -- '- %s\n' "$spec"
done
}
build_release() {
local custom_tag="$1"
rebuild_assets_once
rm -rf "$dist_dir"
mkdir -p "$dist_dir"
if [[ -z "$build_specs" ]]; then
build_specs="$(go env GOOS)/$(go env GOARCH)/$archive_kind"
fi
local spec
for spec in $build_specs; do
build_one "$custom_tag" "$spec"
done
cat > "$dist_dir/release-notes.md" <<EOF
# $custom_tag
Syncthing $upstream_tag with the local patches applied.
Builds:
$(format_build_specs)
Patches:
$(printf -- '- %s\n' "${patch_files[@]}")
Patch behavior:
- Root-level .stignore syncs like regular folder content.
- .stfolder and .stversions stay protected as Syncthing internals.
- The web GUI footer includes "It syncs .stignore now!" as a custom build marker.
EOF
(
cd "$dist_dir"
if command -v sha256sum >/dev/null 2>&1; then
sha256sum ./* > SHA256SUMS
else
shasum -a 256 ./* > SHA256SUMS
fi
)
}
build_one() {
local custom_tag="$1"
local spec="$2"
local goos
local goarch
local kind
local cgo_enabled
IFS=/ read -r goos goarch kind cgo_enabled <<< "$spec"
[[ -n "$goos" && -n "$goarch" && -n "$kind" ]] || die "invalid build spec: $spec"
cgo_enabled="${cgo_enabled:-$default_cgo_enabled}"
log "Building $target for $goos/$goarch as $kind with CGO_ENABLED=$cgo_enabled"
case "$kind" in
tar|zip)
local archive
archive="$(CGO_ENABLED="$cgo_enabled" go run build.go -version "$custom_tag" -goos "$goos" -goarch "$goarch" "$kind" "$target" | tail -n 1)"
if [[ "$goos" == "darwin" ]]; then
sign_and_validate_darwin_archive "$archive" "$kind"
fi
mv "$archive" "$dist_dir/"
;;
binary)
CGO_ENABLED="$cgo_enabled" go run build.go -version "$custom_tag" -goos "$goos" -goarch "$goarch" -build-out "$dist_dir/$target-$goos-$goarch" build "$target"
if [[ "$goos" == "darwin" ]]; then
sign_and_validate_darwin_binary "$dist_dir/$target-$goos-$goarch"
fi
;;
*)
die "unknown build archive kind in $spec"
;;
esac
}
find_release_binary() {
local root="$1"
local candidate
while IFS= read -r candidate; do
if [[ -x "$candidate" ]]; then
printf '%s\n' "$candidate"
return
fi
done < <(find "$root" -type f -name "$target")
die "could not find executable $target in $root"
}
sign_and_validate_darwin_archive() {
local archive="$1"
local kind="$2"
local tmp
local archive_abs
local binary
[[ -n "$codesign_identity" ]] || die "CUSTOM_RELEASE_CODESIGN_IDENTITY is required for darwin builds"
tmp="$(mktemp -d)"
archive_abs="$(cd "$(dirname "$archive")" && pwd -P)/$(basename "$archive")"
case "$kind" in
zip)
unzip -q "$archive_abs" -d "$tmp"
;;
tar)
tar -xf "$archive_abs" -C "$tmp"
;;
*)
die "cannot sign archive kind $kind"
;;
esac
binary="$(find_release_binary "$tmp")"
sign_and_validate_darwin_binary "$binary"
rm -f "$archive_abs"
case "$kind" in
zip)
(
cd "$tmp"
zip -qr "$archive_abs" .
)
;;
tar)
(
cd "$tmp"
tar -czf "$archive_abs" .
)
;;
esac
rm -rf "$tmp"
}
sign_and_validate_darwin_binary() {
local binary="$1"
local version_output
local codesign_details
local codesign_args
[[ -n "$codesign_identity" ]] || die "CUSTOM_RELEASE_CODESIGN_IDENTITY is required for darwin builds"
codesign_args=(--force --sign "$codesign_identity")
if [[ -n "${CUSTOM_RELEASE_KEYCHAIN_PATH:-}" ]]; then
if [[ -n "${CUSTOM_RELEASE_KEYCHAIN_PASSWORD:-}" ]]; then
security unlock-keychain -p "$CUSTOM_RELEASE_KEYCHAIN_PASSWORD" "$CUSTOM_RELEASE_KEYCHAIN_PATH"
fi
security find-identity -v -p codesigning "$CUSTOM_RELEASE_KEYCHAIN_PATH"
security find-identity -v -p codesigning
codesign_args+=(--keychain "$CUSTOM_RELEASE_KEYCHAIN_PATH")
fi
codesign_args+=(--options runtime --timestamp)
codesign "${codesign_args[@]}" "$binary"
version_output="$("$binary" --version)"
if [[ "$version_output" == *modernc-sqlite* ]]; then
die "darwin build unexpectedly reports [modernc-sqlite]: $version_output"
fi
codesign --verify --strict --verbose=2 "$binary"
codesign_details="$(codesign -dv --verbose=4 "$binary" 2>&1)"
if [[ "$codesign_team_id" == "NG5W75WE8U" && "$codesign_details" != *"TeamIdentifier=NG5W75WE8U"* ]]; then
printf '%s\n' "$codesign_details" >&2
die "darwin build is not signed by TeamIdentifier=NG5W75WE8U"
fi
if [[ "$codesign_details" != *"TeamIdentifier=$codesign_team_id"* ]]; then
printf '%s\n' "$codesign_details" >&2
die "darwin build is not signed by TeamIdentifier=$codesign_team_id"
fi
if [[ "$require_gatekeeper_assessment" == "1" ]]; then
spctl -a -vv --type execute "$binary"
else
log "Skipping Gatekeeper assessment because CUSTOM_RELEASE_REQUIRE_GATEKEEPER_ASSESSMENT=$require_gatekeeper_assessment"
fi
}
push_refs() {
local branch="$1"
local custom_tag="$2"
if [[ "$push_release" != "1" ]]; then
log "Skipping push because CUSTOM_RELEASE_PUSH=$push_release"
return
fi
if [[ "$push_branch" == "1" ]]; then
git push "$push_remote" "$branch"
else
log "Skipping release branch push because CUSTOM_RELEASE_PUSH_BRANCH=$push_branch"
fi
git push "$push_remote" "$custom_tag"
}
publish_release() {
local custom_tag="$1"
if [[ "$publish_gitea_release" != "1" ]]; then
log "Skipping Gitea release publishing because CUSTOM_RELEASE_CREATE_GITEA_RELEASE=$publish_gitea_release"
return
fi
command -v tea >/dev/null 2>&1 || die "tea is required for Gitea release publishing"
local args=(releases create "$custom_tag" --title "$custom_tag" --note-file "$dist_dir/release-notes.md")
if [[ -n "$tea_repo" ]]; then
args+=(--repo "$tea_repo")
else
args+=(--remote "$push_remote")
fi
local asset
for asset in "$dist_dir"/*; do
[[ -f "$asset" ]] || continue
[[ "$(basename "$asset")" == "release-notes.md" ]] && continue
args+=(--asset "$asset")
done
tea "${args[@]}"
}
main() {
require_clean_worktree
if [[ -z "$upstream_tag" ]]; then
upstream_tag="$(latest_stable_tag)"
fi
[[ -n "$upstream_tag" ]] || die "could not determine latest upstream tag"
local custom_tag="${upstream_tag}-${suffix}"
local branch="${branch_prefix}/${upstream_tag#v}-${suffix}"
if tag_exists "$custom_tag" && [[ "$force" != "1" ]]; then
log "Custom release $custom_tag already exists; nothing to do."
return
fi
patch_tmp_dir="$(mktemp -d)"
trap 'rm -rf "$patch_tmp_dir"' EXIT
copy_patches_to_temp "$patch_tmp_dir"
fetch_upstream_tag "$upstream_tag"
delete_local_tag_if_forced "$custom_tag"
create_release_commit "$upstream_tag" "$custom_tag" "$branch" "$patch_tmp_dir"
test_release
build_release "$custom_tag"
push_refs "$branch" "$custom_tag"
publish_release "$custom_tag"
log "Built custom release $custom_tag from upstream $upstream_tag"
log "Artifacts are in $dist_dir/"
}
main "$@"