Compare commits

...
Author SHA1 Message Date
Jakob BorgandGitHub c5a29b5b26 fix(model): don't clobber local flags when receiving index (#10190) 2025-06-20 07:08:06 +00:00
Marcus B SpencerandGitHub 4c64843d60 feat(connections, nat): add UDP portmapping/pinhole for QUIC (fixes #7403) (#10171)
Fixes #7403.

Tested by enabling UPnP on the router, and checking on the router page
that the external ports of the UDP mappings match what is shown in the
logs and the internal ports matching the QUIC listening port.
2025-06-20 04:24:45 +00:00
Jakob Borg b4ff96d754 chore(model): log folder removal
Relevant to #10189, #8416
2025-06-18 19:33:41 +02:00
Jakob Borg 21c5ac2161 chore: remove bad comment in authors script
Closes #10185
2025-06-17 12:27:12 +02:00
tomasz1986andGitHub 6fc0b41f97 feat(gui): add option to limit bandwidth in LAN to Settings (ref #10046) (#10182)
gui: Add option to limit bandwidth in LAN to Settings (ref #10046)

Currently, the option to limit bandwidth in LAN is available only via
the Advanced Configuration, which makes it difficult to discover by new
users, who are confused why their bandwidth limits are not working.

For this reason, make the option easily discoverable by adding it
directly to Connections in the normal Settings in the Web GUI.

Ref:
https://github.com/syncthing/syncthing/issues/10046
https://github.com/syncthing/syncthing/issues/2046
https://github.com/syncthing/syncthing/issues/2569
https://forum.syncthing.net/t/upload-rate-not-work/19025

https://forum.syncthing.net/t/rate-limits-dont-work-over-lan-in-v1-20-1/18521

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

### Screenshots


![image](https://github.com/user-attachments/assets/bd38b3ab-a053-4992-ba86-7fb8b3ba09d9)

Signed-off-by: Tomasz Wilczyński <twilczynski@naver.com>
2025-06-17 09:46:38 +02:00
0b0b2143ed fix(protocol): slightly loosen/correct ownership comparison criteria (fixes #9879) (#10176)
Only Require either matching UID & GID OR matching Names.

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

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

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

Co-authored-by: Jakob Borg <jakob@kastelo.net>
2025-06-16 15:12:33 +00:00
Jakob BorgandGitHub af64140c61 fix(model): avoid flashing "Sync Waiting" unnecessarily (#10181) 2025-06-16 12:53:02 +02:00
Syncthing Release Automation 1c68062231 chore(gui, man, authors): update docs, translations, and contributors 2025-06-16 04:00:36 +00:00
Jakob Borg 4d92855d76 build: release job needs full checkout with tags 2025-06-15 10:37:37 +02:00
Jakob Borg 1c6f542cb7 build: use proper ref for build action 2025-06-15 10:21:03 +02:00
Jakob Borg b28066c85d build: use access token to trigger release builds 2025-06-15 10:14:13 +02:00
Simon FreiandGitHub 71c8a2c36f fix(db): remove invalid member from FileMetadata (#10180) 2025-06-15 09:12:25 +02:00
Simon FreiandGitHub e4ab7b4ff3 fix(watchaggregator): properly handle sub-second watch durations (fixes #9927) (#10179)
I'll let Audrius words from the ticket explain this :)

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


https://github.com/syncthing/syncthing/issues/9927#issuecomment-2967736106
2025-06-14 00:16:22 +02:00
8b978d4712 chore: add migration for remote invalid local flag (#10174)
Follow to resp. migration for the change in this commit:

7b319111d3

---------

Co-authored-by: Jakob Borg <jakob@kastelo.net>
2025-06-13 21:28:07 +00:00
Simon FreiandGitHub 7b319111d3 fix: track invalid files in LocalFlags to fix global count (#10170)
Move the "invalid" bit to a local flag, making it easier to track in counts etc.
2025-06-13 07:33:31 +02:00
Simon FreiandGitHub cb7cea93a2 chore(model): remove redundant removal of internal fields in indexsender (#10173)
While it doesn't hurt, it's unnecessary since the big protobuf
modernisation, that also introduced types separate from the generated
ones for internal use. Those fields are already dropped when converting
to the wire in protocol.
2025-06-12 22:08:21 +00:00
Jakob Borg 20257faf54 build: compat entry for Go 1.25 2025-06-11 22:40:20 +02:00
ardevdandGitHub c14abebd68 refactor(syncthing): use named constant for SIGHUP (#10168) 2025-06-09 20:31:01 +00:00
ardevdandGitHub b1a1a90045 chore(syncthing): ensure response body is closed in upgrade request (#10169) 2025-06-09 22:10:17 +02:00
Jakob BorgandGitHub 8afc9855f2 feat: use Ed25519 keys for sync connections (#10162)
This updates our key generation to use Ed25519 keys/certificates for
sync connections. Certificates for browser use remain ECDSA for wider
compatibility.

Ed25519 is more modern and has fewer concerns for the future than the
ECDSA curves we used previously. It is supported from Go 1.13 and
forwards, which is Syncthing 1.3.0 (October 2019).
2025-06-09 05:48:01 +00:00
4215058911 fix(gui): don't show dial errors for paused devices (fixes #10166) (#10167)
### Purpose

Fixes the problem where paused devices show errors from dials from
previous disconnections (#10166).

### Testing

Tested manually by reproducing using the steps in the mentioned issue.

### Screenshots

Before:

![dial
errors](https://github.com/user-attachments/assets/c12a5566-beb2-4b27-9fcb-5a6d1397028d)

After:

![no dial
errors](https://github.com/user-attachments/assets/7a31e1e6-243a-4b15-b3a1-cf226b58abe0)

Co-authored-by: Jakob Borg <jakob@kastelo.net>
2025-06-09 05:38:07 +00:00
Jakob Borg 9fb1a18dbf fix(strelaypoolsrv): trivial error handling fix 2025-06-09 07:25:31 +02:00
Ross Smith IIandGitHub 064213ceb8 fix(fs): check for unsupported error on modern Windows (fixes #10164) (#10165)
### Purpose

Locally, on Windows 11, and on the windows-2025 GitHub runner (go 1.23
and 1.24), the `TestCopyRange` test is failing with `The request is not
supported.`

On windows-2022 and windows-2019:
```go
err == syscall.ENOTSUP
```
worked, but on Windows 11 and windows-2025, we need:
```go
errors.Is(err, errors.ErrUnsupported)
```

### Testing

Tested on Windows 11, windows-2019, windows-2022, and
[windows-2025](https://github.com/rasa/syncthing/actions/runs/15525123437/job/43703630634#step:7:2811).
2025-06-09 07:20:46 +02:00
Syncthing Release Automation 0211251b34 chore(gui, man, authors): update docs, translations, and contributors 2025-06-09 04:00:17 +00:00
Jakob BorgandGitHub 1903da569b build: explicitly trigger build after pushing release tag (#10160)
Because just pushing the tag with a non-Actions token doesn't suffice,
apparently
2025-06-07 14:22:39 +00:00
Jakob BorgandGitHub b6a7beca1f build: build both Debian armel and armhf (though they are the same for us) (#10159)
Current "arm" is wrong
2025-06-07 16:04:28 +02:00
Jakob Borg 7b83e7403e build: limit golangci-lint to pull requests
Since all the existing code isn't clean
2025-06-07 15:04:30 +02:00
Jakob Borg 1915a470e9 chore: update AUTHORS 2025-06-07 14:31:58 +02:00
Jakob Borg 0b100296e1 Merge branch 'v2'
* v2: (62 commits)
  build: add dependency for next-version script
  docs: add release note mention of platforms no longer built
  build: streamline gathering of facts, checkouts (#10158)
  build: more resilient pushes to releases
  chore(etc): add option dash to upstart config
  chore(fs): linter complaints
  chore(model): the easier linter complaints
  chore(internal): linter complaints
  chore(sqlite): linter complaints
  build: allow v2 into APT candidate channel
  docs: link to Docker image, APT, in release notes
  build: refactor builds for forks/PRs
  build: use same GitHub token for releases as for translation etc pushes
  refactor(sqlite): move deleted flag into logical order in schema
  feat(config): enable multiple connections by default (#10151)
  docs: mention subcommands in release notes, use for all 2.0 releases
  docs: adjust release notes for v2.0.0
  docs: add relnotes for v2.0.0
  build: upgrade setup-zig action (#10134)
  fix(versioner): correct fs creation in test
  ...
2025-06-07 14:18:48 +02:00
Jakob Borg e6ed3acf5f build: add dependency for next-version script 2025-06-07 14:18:12 +02:00
Jakob Borg 9f95bf3573 docs: add release note mention of platforms no longer built 2025-06-07 13:35:21 +02:00
Jakob Borg b05ece0681 build: more resilient pushes to releases 2025-06-07 13:18:58 +02:00
Jakob BorgandGitHub 5381178c46 build: streamline gathering of facts, checkouts (#10158) 2025-06-07 13:11:52 +02:00
Jakob Borg e7f4f8306c Merge branch 'main' into v2
* main:
  build: more resilient pushes to releases
2025-06-07 09:35:00 +02:00
Jakob Borg 9922a3abd9 build: more resilient pushes to releases 2025-06-07 09:34:49 +02:00
Jakob Borg 40ab668a73 chore(etc): add option dash to upstart config 2025-06-07 08:20:48 +02:00
Jakob Borg 10d20c4800 chore(fs): linter complaints 2025-06-06 13:45:44 +02:00
Jakob Borg 700bb75016 chore(model): the easier linter complaints 2025-06-06 13:45:44 +02:00
Jakob Borg e25de22705 chore(internal): linter complaints 2025-06-06 13:45:44 +02:00
Jakob Borg ef6d561c66 chore(sqlite): linter complaints 2025-06-06 13:45:44 +02:00
99 changed files with 720 additions and 431 deletions
+127 -121
View File
@@ -37,6 +37,51 @@ env:
jobs:
#
# Source
#
facts:
name: Gather common facts
runs-on: ubuntu-latest
outputs:
version: ${{ steps.get-version.outputs.version }}
release-kind: ${{ steps.get-version.outputs.release-kind }}
go-version: ${{ steps.get-go.outputs.go-version }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.ref }} # https://github.com/actions/checkout/issues/882
- uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
cache: false
check-latest: true
- name: Get Syncthing version
id: get-version
run: |
version=$(go run build.go version)
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "Version: $version"
kind=stable
if [[ $version == *-rc.[0-9] || $version == *-rc.[0-9][0-9] ]] ; then
kind=candidate
elif [[ $version == *-* ]] ; then
kind=nightly
fi
echo "release-kind=$kind" >> "$GITHUB_OUTPUT"
echo "Release kind: $kind"
- name: Get Go version
id: get-go
run: |
go version
echo "go-version=$(go version | sed 's#^.*go##;s# .*##')" >> $GITHUB_OUTPUT
#
# Tests for all platforms. Runs a matrix build on Windows, Linux and Mac,
# with the list of expected supported Go versions (current, previous).
@@ -123,17 +168,18 @@ jobs:
package-windows:
name: Package for Windows
runs-on: ubuntu-latest
needs:
- facts
env:
VERSION: ${{ needs.facts.outputs.version }}
RELEASE_KIND: ${{ needs.facts.outputs.release-kind }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.ref }} # https://github.com/actions/checkout/issues/882
- uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
go-version: ${{ needs.facts.outputs.go-version }}
cache: false
check-latest: true
- uses: mlugg/setup-zig@v2
@@ -142,7 +188,7 @@ jobs:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-package-windows-${{ hashFiles('**/go.sum') }}
key: ${{ runner.os }}-go-${{ needs.facts.outputs.go-version }}-package-windows-${{ hashFiles('**/go.sum') }}
- name: Install dependencies
run: |
@@ -231,22 +277,18 @@ jobs:
package-linux:
name: Package for Linux
runs-on: ubuntu-latest
needs:
- facts
env:
VERSION: ${{ needs.facts.outputs.version }}
RELEASE_KIND: ${{ needs.facts.outputs.release-kind }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.ref }} # https://github.com/actions/checkout/issues/882
- uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
go-version: ${{ needs.facts.outputs.go-version }}
cache: false
check-latest: true
- name: Get actual Go version
run: |
go version
echo "GO_VERSION=$(go version | sed 's#^.*go##;s# .*##')" >> $GITHUB_ENV
- uses: mlugg/setup-zig@v2
@@ -255,7 +297,7 @@ jobs:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-package-${{ hashFiles('**/go.sum') }}
key: ${{ runner.os }}-go-${{ needs.facts.outputs.go-version }}-package-${{ hashFiles('**/go.sum') }}
- name: Create packages
run: |
@@ -297,32 +339,27 @@ jobs:
name: Package for macOS
if: github.repository_owner == 'syncthing' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/release-nightly' || startsWith(github.ref, 'refs/tags/v'))
environment: release
runs-on: macos-latest
needs:
- facts
env:
CODESIGN_IDENTITY: ${{ secrets.CODESIGN_IDENTITY }}
runs-on: macos-latest
VERSION: ${{ needs.facts.outputs.version }}
RELEASE_KIND: ${{ needs.facts.outputs.release-kind }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.ref }} # https://github.com/actions/checkout/issues/882
- uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
go-version: ${{ needs.facts.outputs.go-version }}
cache: false
check-latest: true
- name: Get actual Go version
run: |
go version
echo "GO_VERSION=$(go version | sed 's#^.*go##;s# .*##')" >> $GITHUB_ENV
- uses: actions/cache@v4
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-package-${{ hashFiles('**/go.sum') }}
key: ${{ runner.os }}-go-${{ needs.facts.outputs.go-version }}-package-${{ hashFiles('**/go.sum') }}
- name: Import signing certificate
if: env.CODESIGN_IDENTITY != ''
@@ -432,29 +469,25 @@ jobs:
package-cross:
name: Package cross compiled
runs-on: ubuntu-latest
needs:
- facts
env:
VERSION: ${{ needs.facts.outputs.version }}
RELEASE_KIND: ${{ needs.facts.outputs.release-kind }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.ref }} # https://github.com/actions/checkout/issues/882
- uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
go-version: ${{ needs.facts.outputs.go-version }}
cache: false
check-latest: true
- name: Get actual Go version
run: |
go version
echo "GO_VERSION=$(go version | sed 's#^.*go##;s# .*##')" >> $GITHUB_ENV
- uses: actions/cache@v4
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-cross-${{ hashFiles('**/go.sum') }}
key: ${{ runner.os }}-go-${{ needs.facts.outputs.go-version }}-cross-${{ hashFiles('**/go.sum') }}
- name: Create packages
run: |
@@ -502,33 +535,33 @@ jobs:
package-source:
name: Package source code
runs-on: ubuntu-latest
needs:
- facts
env:
VERSION: ${{ needs.facts.outputs.version }}
RELEASE_KIND: ${{ needs.facts.outputs.release-kind }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.ref }} # https://github.com/actions/checkout/issues/882
- uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
go-version: ${{ needs.facts.outputs.go-version }}
cache: false
check-latest: true
- name: Package source
run: |
version=$(go run build.go version)
echo "$version" > RELEASE
echo "$VERSION" > RELEASE
go mod vendor
go run build.go assets
cd ..
tar c -z -f "syncthing-source-$version.tar.gz" \
tar c -z -f "syncthing-source-$VERSION.tar.gz" \
--exclude .git \
syncthing
mv "syncthing-source-$version.tar.gz" syncthing
mv "syncthing-source-$VERSION.tar.gz" syncthing
- name: Archive artifacts
uses: actions/upload-artifact@v4
@@ -550,27 +583,26 @@ jobs:
- package-macos
- package-cross
- package-source
- facts
env:
VERSION: ${{ needs.facts.outputs.version }}
RELEASE_KIND: ${{ needs.facts.outputs.release-kind }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.ref }} # https://github.com/actions/checkout/issues/882
- uses: actions/checkout@v4
with:
repository: syncthing/release-tools
path: tools
fetch-depth: 0
- name: Download artifacts
uses: actions/download-artifact@v4
- uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
go-version: ${{ needs.facts.outputs.go-version }}
cache: false
check-latest: true
- name: Install signing tool
run: |
@@ -597,9 +629,6 @@ jobs:
sha256sum "${files[@]}" > sha256sum.txt
popd
version=$(go run build.go version)
echo "VERSION=$version" >> $GITHUB_ENV
- name: Sign shasum files
uses: docker://ghcr.io/kastelo/ezapt:latest
with:
@@ -635,22 +664,18 @@ jobs:
package-debian:
name: Package for Debian
runs-on: ubuntu-latest
needs:
- facts
env:
VERSION: ${{ needs.facts.outputs.version }}
RELEASE_KIND: ${{ needs.facts.outputs.release-kind }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.ref }} # https://github.com/actions/checkout/issues/882
- uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
go-version: ${{ needs.facts.outputs.go-version }}
cache: false
check-latest: true
- name: Get actual Go version
run: |
go version
echo "GO_VERSION=$(go version | sed 's#^.*go##;s# .*##')" >> $GITHUB_ENV
- uses: ruby/setup-ruby@v1
with:
@@ -667,13 +692,14 @@ jobs:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-debian-${{ hashFiles('**/go.sum') }}
key: ${{ runner.os }}-go-${{ needs.facts.outputs.go-version }}-debian-${{ hashFiles('**/go.sum') }}
- name: Package for Debian (CGO)
run: |
for tgt in syncthing stdiscosrv strelaysrv ; do
go run build.go -no-upgrade -installsuffix=no-upgrade -tags "${{env.TAGS}}" -goos linux -goarch amd64 -cc "zig cc -target x86_64-linux-musl" deb "$tgt"
go run build.go -no-upgrade -installsuffix=no-upgrade -tags "${{env.TAGS}}" -goos linux -goarch arm -cc "zig cc -target arm-linux-musleabi -mcpu=arm1136j_s" deb "$tgt"
go run build.go -no-upgrade -installsuffix=no-upgrade -tags "${{env.TAGS}}" -goos linux -goarch armel -cc "zig cc -target arm-linux-musleabi -mcpu=arm1136j_s" deb "$tgt"
go run build.go -no-upgrade -installsuffix=no-upgrade -tags "${{env.TAGS}}" -goos linux -goarch armhf -cc "zig cc -target arm-linux-musleabi -mcpu=arm1136j_s" deb "$tgt"
go run build.go -no-upgrade -installsuffix=no-upgrade -tags "${{env.TAGS}}" -goos linux -goarch arm64 -cc "zig cc -target aarch64-linux-musl" deb "$tgt"
done
env:
@@ -697,13 +723,13 @@ jobs:
environment: release
needs:
- sign-for-upgrade
- facts
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
repository: syncthing/release-tools
path: tools
fetch-depth: 0
- name: Download artifacts
uses: actions/download-artifact@v4
@@ -713,9 +739,8 @@ jobs:
- uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
go-version: ${{ needs.facts.outputs.go-version }}
cache: false
check-latest: true
- name: Create release json
run: |
@@ -735,7 +760,7 @@ jobs:
RCLONE_CONFIG_OBJSTORE_REGION: ${{ secrets.S3_REGION }}
RCLONE_CONFIG_OBJSTORE_ACL: public-read
with:
args: sync -v packages objstore:nightly
args: sync -v --no-update-modtime packages objstore:nightly
#
# Push release artifacts to Spaces
@@ -750,6 +775,10 @@ jobs:
needs:
- sign-for-upgrade
- package-debian
- facts
env:
VERSION: ${{ needs.facts.outputs.version }}
RELEASE_KIND: ${{ needs.facts.outputs.release-kind }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -771,14 +800,8 @@ jobs:
- uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
go-version: ${{ needs.facts.outputs.go-version }}
cache: false
check-latest: true
- name: Set version
run: |
version=$(go run build.go version)
echo "VERSION=$version" >> $GITHUB_ENV
- name: Push to object store (${{ env.VERSION }})
uses: docker://docker.io/rclone/rclone:latest
@@ -791,7 +814,7 @@ jobs:
RCLONE_CONFIG_OBJSTORE_REGION: ${{ secrets.S3_REGION }}
RCLONE_CONFIG_OBJSTORE_ACL: public-read
with:
args: sync -v packages objstore:release/${{ env.VERSION }}
args: sync -v --no-update-modtime packages objstore:release/${{ env.VERSION }}
- name: Push to object store (latest)
uses: docker://docker.io/rclone/rclone:latest
@@ -804,7 +827,7 @@ jobs:
RCLONE_CONFIG_OBJSTORE_REGION: ${{ secrets.S3_REGION }}
RCLONE_CONFIG_OBJSTORE_ACL: public-read
with:
args: sync -v objstore:release/${{ env.VERSION }} objstore:release/latest
args: sync -v --no-update-modtime objstore:release/${{ env.VERSION }} objstore:release/latest
- name: Create GitHub releases and push binaries
run: |
@@ -819,7 +842,7 @@ jobs:
--title "$VERSION" \
--notes-from-tag
fi
gh release upload "$VERSION" \
gh release upload --clobber "$VERSION" \
packages/*.asc packages/*.json \
packages/syncthing-*.tar.gz \
packages/syncthing-*.zip \
@@ -835,7 +858,7 @@ jobs:
--title "$VERSION" \
--notes "https://github.com/syncthing/syncthing/releases/tag/$VERSION"
fi
gh release upload "$VERSION" \
gh release upload --clobber "$VERSION" \
$PKGS/*.asc \
$PKGS/*${repo}*
done
@@ -852,12 +875,13 @@ jobs:
environment: release
needs:
- package-debian
- facts
env:
VERSION: ${{ needs.facts.outputs.version }}
RELEASE_KIND: ${{ needs.facts.outputs.release-kind }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.ref }} # https://github.com/actions/checkout/issues/882
- name: Download packages
uses: actions/download-artifact@v4
@@ -865,24 +889,11 @@ jobs:
name: debian-packages
path: packages
- name: Set version
run: |
version=$(go run build.go version)
echo "Version: $version"
echo "VERSION=$version" >> $GITHUB_ENV
# Decide whether packages should go to stable, candidate or nightly
- name: Prepare packages
run: |
kind=stable
if [[ $VERSION == *-rc.[0-9] || $VERSION == *-rc.[0-9][0-9] ]] ; then
kind=candidate
elif [[ $VERSION == *-* ]] ; then
kind=nightly
fi
echo "Kind: $kind"
mkdir -p packages/syncthing/$kind
mv packages/*.deb packages/syncthing/$kind
mkdir -p packages/syncthing/$RELEASE_KIND
mv packages/*.deb packages/syncthing/$RELEASE_KIND
- name: Pull archive
uses: docker://docker.io/rclone/rclone:latest
@@ -918,7 +929,7 @@ jobs:
RCLONE_CONFIG_OBJSTORE_REGION: ${{ secrets.S3_REGION }}
RCLONE_CONFIG_OBJSTORE_ACL: public-read
with:
args: sync -v dists objstore:apt/dists
args: sync -v --no-update-modtime dists objstore:apt/dists
#
# Build and push (except for PRs) to GHCR.
@@ -930,6 +941,11 @@ jobs:
permissions:
contents: read
packages: write
needs:
- facts
env:
VERSION: ${{ needs.facts.outputs.version }}
RELEASE_KIND: ${{ needs.facts.outputs.release-kind }}
strategy:
matrix:
pkg:
@@ -948,20 +964,11 @@ jobs:
image: discosrv
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.ref }} # https://github.com/actions/checkout/issues/882
- uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
go-version: ${{ needs.facts.outputs.go-version }}
cache: false
check-latest: true
- name: Get actual Go version
run: |
go version
echo "GO_VERSION=$(go version | sed 's#^.*go##;s# .*##')" >> $GITHUB_ENV
- uses: mlugg/setup-zig@v2
@@ -970,7 +977,7 @@ jobs:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-docker-${{ matrix.pkg }}-${{ hashFiles('**/go.sum') }}
key: ${{ runner.os }}-go-${{ needs.facts.outputs.go-version }}-docker-${{ matrix.pkg }}-${{ hashFiles('**/go.sum') }}
- name: Build binaries (CGO)
run: |
@@ -1002,8 +1009,7 @@ jobs:
- name: Set version tags
run: |
version=$(go run build.go version)
version=${version#v}
version=${VERSION#v}
repo=ghcr.io/${{ github.repository_owner }}/${{ matrix.image }}
ref="${{github.ref_name}}"
ref=${ref//\//-} # slashes to dashes
@@ -1022,9 +1028,7 @@ jobs:
fi
echo Pushing to $tags
echo "DOCKER_TAGS=$tags" >> $GITHUB_ENV
echo "VERSION=$version" >> $GITHUB_ENV
- name: Build and push Docker image
uses: docker/build-push-action@v5
@@ -1069,14 +1073,15 @@ jobs:
govulncheck:
runs-on: ubuntu-latest
name: Run govulncheck
needs:
- facts
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
go-version: ${{ needs.facts.outputs.go-version }}
cache: false
check-latest: true
- name: run govulncheck
run: |
@@ -1091,6 +1096,7 @@ jobs:
golangci:
runs-on: ubuntu-latest
name: Run golangci-lint
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
+7
View File
@@ -51,3 +51,10 @@ jobs:
git config --global user.email 'release@syncthing.net'
git tag -a -F notes.md --cleanup=whitespace "$NEXT"
git push origin "$NEXT"
- name: Trigger the build
uses: benc-uk/workflow-dispatch@v1
with:
workflow: build-syncthing.yaml
ref: refs/tags/${{ env.NEXT }}
token: ${{ secrets.ACTIONS_GITHUB_TOKEN }}
+3
View File
@@ -8,12 +8,14 @@ linters:
- exhaustive
- exhaustruct
- forbidigo
- funcorder
- funlen
- gochecknoglobals
- gochecknoinits
- gocognit
- goconst
- gocyclo
- godot
- godox
- gomoddirectives
- inamedparam
@@ -49,6 +51,7 @@ linters:
- std-error-handling
paths:
- internal/gen
- internal/db/olddb
- cmd/dev
- repos
- third_party$
+4
View File
@@ -56,10 +56,12 @@ Anthony Goeckner <agoeckner@users.noreply.github.com>
Antoine Lamielle (0x010C) <antoine.lamielle@0x010c.fr> <gh@0x010c.fr>
Anur <anurnomeru@163.com>
Aranjedeath <Aranjedeath@users.noreply.github.com>
ardevd <ardevd@users.noreply.github.com>
Arkadiusz Tymiński <gevleeog@gmail.com>
Aroun <login@b-vo.fr>
Arthur Axel fREW Schmidt (frioux) <frew@afoolishmanifesto.com> <frioux@gmail.com>
Artur Zubilewicz <AkaZecik@users.noreply.github.com>
Ashish Bhate <bhate.ashish@gmail.com>
Aurélien Rainone <476650+arl@users.noreply.github.com>
BAHADIR YILMAZ <bahadiryilmaz32@gmail.com>
Bart De Vries (mogwa1) <devriesb@gmail.com>
@@ -192,6 +194,7 @@ Luke Hamburg <1992842+luckman212@users.noreply.github.com>
luzpaz <luzpaz@users.noreply.github.com>
Majed Abdulaziz (majedev) <majed.alhajry@gmail.com>
Marc Laporte (marclaporte) <marc@marclaporte.com> <marc@laporte.name>
Marcel Meyer <mm.marcelmeyer@gmail.com>
Marcin Dziadus (marcindziadus) <dziadus.marcin@gmail.com>
Marcus B Spencer <marcus@marcusspencer.xyz> <marcus@marcusspencer.us>
Marcus Legendre <marcus.legendre@gmail.com>
@@ -252,6 +255,7 @@ Philippe Schommers (filoozoom) <philippe@schommers.be>
Phill Luby (pluby) <phill.luby@newredo.com>
Piotr Bejda (piobpl) <piotrb10@gmail.com>
polyfloyd <polyfloyd@users.noreply.github.com>
pullmerge <166967364+pullmerge@users.noreply.github.com>
Quentin Hibon <qh.public@yahoo.com>
Rahmi Pruitt <rjpruitt16@gmail.com>
red_led <red-led@users.noreply.github.com>
+4 -1
View File
@@ -922,6 +922,9 @@ func rmr(paths ...string) {
}
func getReleaseVersion() (string, error) {
if ver := os.Getenv("VERSION"); ver != "" {
return strings.TrimSpace(ver), nil
}
bs, err := os.ReadFile("RELEASE")
if err != nil {
return "", err
@@ -966,7 +969,7 @@ func getGitVersion() (string, error) {
}
func getVersion() string {
// First try for a RELEASE file,
// First try for a RELEASE file or $VERSION env var,
if ver, err := getReleaseVersion(); err == nil {
return ver
}
+1 -1
View File
@@ -620,7 +620,7 @@ func createTestCertificate() tls.Certificate {
}
certFile, keyFile := filepath.Join(tmpDir, "cert.pem"), filepath.Join(tmpDir, "key.pem")
cert, err := tlsutil.NewCertificate(certFile, keyFile, "relaypoolsrv", 20*365)
cert, err := tlsutil.NewCertificate(certFile, keyFile, "relaypoolsrv", 20*365, false)
if err != nil {
log.Fatalln("Failed to create test X509 key pair:", err)
}
+1 -1
View File
@@ -173,7 +173,7 @@ func fetchStats(relay *relay) *stats {
var stats stats
if json.NewDecoder(response.Body).Decode(&stats); err != nil {
if err := json.NewDecoder(response.Body).Decode(&stats); err != nil {
return nil
}
return &stats
+1 -1
View File
@@ -115,7 +115,7 @@ func BenchmarkAPIRequests(b *testing.B) {
srv := httptest.NewServer(http.HandlerFunc(api.handler))
kf := b.TempDir() + "/cert"
crt, err := tlsutil.NewCertificate(kf+".crt", kf+".key", "localhost", 7)
crt, err := tlsutil.NewCertificate(kf+".crt", kf+".key", "localhost", 7, true)
if err != nil {
b.Fatal(err)
}
+1 -1
View File
@@ -107,7 +107,7 @@ func main() {
cert, err = tls.LoadX509KeyPair(cli.Cert, cli.Key)
if os.IsNotExist(err) {
log.Println("Failed to load keypair. Generating one, this might take a while...")
cert, err = tlsutil.NewCertificate(cli.Cert, cli.Key, "stdiscosrv", 20*365)
cert, err = tlsutil.NewCertificate(cli.Cert, cli.Key, "stdiscosrv", 20*365, false)
if err != nil {
log.Fatalln("Failed to generate X509 key pair:", err)
}
+1 -1
View File
@@ -157,7 +157,7 @@ func main() {
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
log.Println("Failed to load keypair. Generating one, this might take a while...")
cert, err = tlsutil.NewCertificate(certFile, keyFile, "strelaysrv", 20*365)
cert, err = tlsutil.NewCertificate(certFile, keyFile, "strelaysrv", 20*365, false)
if err != nil {
log.Fatalln("Failed to generate X509 key pair:", err)
}
+4 -3
View File
@@ -403,9 +403,11 @@ func upgradeViaRest() error {
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
bs, err := io.ReadAll(resp.Body)
defer resp.Body.Close()
if err != nil {
return err
}
@@ -611,8 +613,7 @@ func setupSignalHandling(app *syncthing.App) {
// Exit cleanly with "restarting" code on SIGHUP.
restartSign := make(chan os.Signal, 1)
sigHup := syscall.Signal(1)
signal.Notify(restartSign, sigHup)
signal.Notify(restartSign, syscall.SIGHUP)
go func() {
<-restartSign
app.Stop(svcutil.ExitRestart)
+7
View File
@@ -32,3 +32,10 @@
darwin: "20"
linux: "3.2"
windows: "10.0"
- runtime: go1.25
requirements:
# macOS 12 (Monterey) per https://tip.golang.org/doc/go1.25#darwin
darwin: "21"
linux: "3.2"
windows: "10.0"
+1 -1
View File
@@ -18,4 +18,4 @@ env STNORESTART=yes
respawn
# the syncthing command Upstart is to execute when it is started up
exec $SYNCTHING_EXE -no-browser
exec $SYNCTHING_EXE --no-browser
+1 -1
View File
@@ -10,6 +10,7 @@ require (
github.com/calmh/incontainer v1.0.0
github.com/calmh/xdr v1.2.0
github.com/ccding/go-stun v0.1.5
github.com/coreos/go-semver v0.3.1
github.com/d4l3k/messagediff v1.2.1
github.com/getsentry/raven-go v0.2.0
github.com/go-ldap/ldap/v3 v3.4.11
@@ -61,7 +62,6 @@ require (
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/coreos/go-semver v0.3.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
+2
View File
@@ -27,6 +27,7 @@
"Allowed Networks": "Xarxes permeses",
"Alphabetic": "Alfabètic",
"Altered by ignoring deletes.": "S'ha alterat ignorant les supressions.",
"Always turned on when the folder type is \"{%foldertype%}\".": "Sempre activat quan el tipus de carpeta és \"{{foldertype}}\".",
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "Una ordre externa gestiona la versió. Ha d'eliminar el fitxer de la carpeta compartida. Si el camí a l'aplicació conté espais, s'ha de citar.",
"Anonymous Usage Reporting": "Informe anònim d'ús",
"Anonymous usage report format has changed. Would you like to move to the new format?": "El format de l'informe d'ús anònim ha canviat. Voleu canviar a aquest nou format?",
@@ -52,6 +53,7 @@
"Body:": "Cos de text:",
"Bugs": "Errors (Bugs)",
"Cancel": "Cancel·la",
"Cannot be enabled when the folder type is \"{%foldertype%}\".": "No es pot habilitar quan el tipus de carpeta és \"{{foldertype}}\".",
"Changelog": "Historial de canvis",
"Clean out after": "Netejar després",
"Cleaning Versions": "Netejant versions",
+1
View File
@@ -227,6 +227,7 @@
"Learn more": "Learn more",
"Learn more at {%url%}": "Learn more at {{url}}",
"Limit": "Limit",
"Limit Bandwidth in LAN": "Limit Bandwidth in LAN",
"Listener Failures": "Listener Failures",
"Listener Status": "Listener Status",
"Listeners": "Listeners",
+2
View File
@@ -27,6 +27,7 @@
"Allowed Networks": "רשתות מורשות",
"Alphabetic": "אלפביתי",
"Altered by ignoring deletes.": "השתנה על ידי התעלמות ממחיקות.",
"Always turned on when the folder type is \"{%foldertype%}\".": "תמיד מופעל כאשר סוג התיקייה הוא \"{{foldertype}}\".",
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "פקודה חיצונית מטפלת בניהול הגרסאות. היא חייבת להסיר את הקובץ מהתיקייה המשותפת. אם הנתיב ליישום מכיל רווחים, יש לצטט אותו.",
"Anonymous Usage Reporting": "דיווח שימוש אנונימי",
"Anonymous usage report format has changed. Would you like to move to the new format?": "פורמט דוח שימוש אנונימי השתנה. האם ברצונך לעבור לפורמט החדש?",
@@ -52,6 +53,7 @@
"Body:": "גוף:",
"Bugs": "באגים",
"Cancel": "ביטול",
"Cannot be enabled when the folder type is \"{%foldertype%}\".": "לא ניתן לאפשור כאשר סוג התיקייה הוא \"{{foldertype}}\".",
"Changelog": "יומן שינויים",
"Clean out after": "נקה לאחר",
"Cleaning Versions": "מנקה גרסאות",
+3
View File
@@ -27,6 +27,7 @@
"Allowed Networks": "Toegestane netwerken",
"Alphabetic": "Alfabetisch",
"Altered by ignoring deletes.": "Veranderd door het negeren van verwijderingen.",
"Always turned on when the folder type is \"{%foldertype%}\".": "Altijd aanzetten als het map-type \"{{foldertype}}\" is.",
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "Een externe opdracht regelt het versiebeheer. Hij moet het bestand verwijderen uit de gedeelde map. Als het pad naar de toepassing spaties bevat, moet dit tussen aanhalingstekens geplaatst worden.",
"Anonymous Usage Reporting": "Anonieme gebruikersstatistieken",
"Anonymous usage report format has changed. Would you like to move to the new format?": "Het formaat voor anonieme gebruikersrapporten is gewijzigd. Wil je naar het nieuwe formaat overschakelen?",
@@ -52,6 +53,7 @@
"Body:": "Inhoud:",
"Bugs": "Bugs",
"Cancel": "Annuleren",
"Cannot be enabled when the folder type is \"{%foldertype%}\".": "Kan niet aangezet worden als het map-type \"{{foldertype}}\" is.",
"Changelog": "Wijzigingenlogboek",
"Clean out after": "Opruimen na",
"Cleaning Versions": "Versies opruimen",
@@ -386,6 +388,7 @@
"Staggered File Versioning": "Gespreid versiebeheer",
"Start Browser": "Browser starten",
"Statistics": "Statistieken",
"Stay logged in": "Blijf aangemeld",
"Stopped": "Gestopt",
"Stores and syncs only encrypted data. Folders on all connected devices need to be set up with the same password or be of type \"{%receiveEncrypted%}\" too.": "Bewaart en synchroniseert alleen versleutelde gegevens. Mappen op alle verbonden apparaten moeten met hetzelfde wachtwoord ingesteld worden of ook van het type \"{{receiveEncrypted}}\" zijn.",
"Subject:": "Onderwerp:",
+2 -2
View File
@@ -874,11 +874,11 @@
<td ng-if="!connections[deviceCfg.deviceID].connected" class="text-right">
<span ng-repeat="addr in deviceCfg.addresses">
<span tooltip data-original-title="{{'Configured' | translate}}">{{addr}}</span><br>
<small ng-if="system.lastDialStatus[addr].error" tooltip data-original-title="{{system.lastDialStatus[addr].error}}" class="text-danger">{{abbreviatedError(addr)}}<br></small>
<small ng-if="system.lastDialStatus[addr].error && !deviceCfg.paused" tooltip data-original-title="{{system.lastDialStatus[addr].error}}" class="text-danger">{{abbreviatedError(addr)}}<br></small>
</span>
<span ng-repeat="addr in discoveryCache[deviceCfg.deviceID].addresses">
<span tooltip data-original-title="{{'Discovered' | translate}}">{{addr}}</span><br>
<small ng-if="system.lastDialStatus[addr].error" tooltip data-original-title="{{system.lastDialStatus[addr].error}}" class="text-danger">{{abbreviatedError(addr)}}<br></small>
<small ng-if="system.lastDialStatus[addr].error && !deviceCfg.paused" tooltip data-original-title="{{system.lastDialStatus[addr].error}}" class="text-danger">{{abbreviatedError(addr)}}<br></small>
</span>
</td>
</tr>
@@ -30,7 +30,7 @@
<h4 class="text-center" translate>The Syncthing Authors</h4>
<div class="row">
<div class="col-md-12" id="contributor-list">
Jakob Borg, Audrius Butkevicius, Simon Frei, Tomasz Wilczyński, Alexander Graf, Alexandre Viau, Anderson Mesquita, André Colomb, Antony Male, Ben Schulz, bt90, Caleb Callaway, Daniel Harte, Emil Lundberg, Eric P, Evgeny Kuznetsov, greatroar, Lars K.W. Gohlke, Lode Hoste, Michael Ploujnikov, Ross Smith II, Stefan Tatschner, 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, Arkadiusz Tymiński, Aroun, Arthur Axel fREW Schmidt, Artur Zubilewicz, Aurélien Rainone, BAHADIR YILMAZ, Bart De Vries, Beat Reichenbach, 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_, Cyprien Devillez, d-volution, Dan, Daniel Barczyk, Daniel Bergmann, Daniel Martí, Daniel Padrta, 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, 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, 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, Lukas Lihotzki, Luke Hamburg, luzpaz, Majed Abdulaziz, Marc Laporte, Marcin Dziadus, Marcus B Spencer, 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, Michael Jephcote, Michael Rienstra, 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, Quentin Hibon, Rahmi Pruitt, 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, Simon Mwepu, Simon Pickup, Sly_tom_cat, Sonu Kumar Saw, Stefan Kuntz, Steven Eckhoff, Suhas Gundimeda, Sven Bachmann, Sébastien WENSKE, Taylor Khan, Terrance, TheCreeper, Thomas, Thomas Hipp, Tim Abell, Tim Howes, Tobias Frölich, Tobias Klauser, Tobias Nygren, Tobias Tom, Tom Jakubowski, Tommy van der Vorst, Tully Robinson, Tyler Brazier, Tyler Kropp, Unrud, vapatel2, Veeti Paananen, Victor Buinsky, Vik, Vil Brekin, villekalliomaki, Vladimir Rusinov, wangguoliang, WangXi, Will Rouesnel, William A. Kennington III, wouter bolsterlee, xarx00, Xavier O., xjtdy888, Yannic A., 佛跳墙, 落心
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, Michael Ploujnikov, Ross Smith II, Stefan Tatschner, 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 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_, Cyprien Devillez, d-volution, Dan, Daniel Barczyk, Daniel Bergmann, Daniel Martí, Daniel Padrta, 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, 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, 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, Lukas Lihotzki, Luke Hamburg, luzpaz, Majed Abdulaziz, Marc Laporte, Marcel Meyer, Marcin Dziadus, Marcus B Spencer, 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, Michael Jephcote, Michael Rienstra, 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, pullmerge, Quentin Hibon, Rahmi Pruitt, 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, Simon Mwepu, Simon Pickup, Sly_tom_cat, Sonu Kumar Saw, Stefan Kuntz, Steven Eckhoff, Suhas Gundimeda, Sven Bachmann, Sébastien WENSKE, Taylor Khan, Terrance, TheCreeper, Thomas, Thomas Hipp, Tim Abell, Tim Howes, Tobias Frölich, Tobias Klauser, Tobias Nygren, Tobias Tom, Tom Jakubowski, Tommy van der Vorst, Tully Robinson, Tyler Brazier, Tyler Kropp, Unrud, vapatel2, Veeti Paananen, Victor Buinsky, Vik, Vil Brekin, villekalliomaki, Vladimir Rusinov, wangguoliang, WangXi, Will Rouesnel, William A. Kennington III, wouter bolsterlee, xarx00, Xavier O., xjtdy888, Yannic A., 佛跳墙, 落心
</div>
</div>
</div>
@@ -58,7 +58,6 @@ Jakob Borg, Audrius Butkevicius, Simon Frei, Tomasz Wilczyński, Alexander Graf,
<li><a href="https://github.com/calmh/xdr">calmh/xdr</a>, Copyright &copy; 2014 Jakob Borg.</li>
<li><a href="https://github.com/ccding/go-stun">ccding/go-stun</a>, Copyright &copy; 2016 Cong Ding.</li>
<li><a href="https://github.com/cespare/xxhash/v2">cespare/xxhash/v2</a>, Copyright &copy; 2016 Caleb Spare.</li>
<li><a href="https://github.com/chmduquesne/rollinghash">chmduquesne/rollinghash</a>, Copyright &copy; 2015 Christophe-Marie Duquesne.</li>
<li><a href="https://github.com/cpuguy83/go-md2man/v2">cpuguy83/go-md2man/v2</a>, Copyright &copy; 2014 Brian Goff.</li>
<li><a href="https://github.com/davecgh/go-spew">davecgh/go-spew</a>, Copyright &copy; 2012-2016 Dave Collins.</li>
<li><a href="https://github.com/go-asn1-ber/asn1-ber">go-asn1-ber/asn1-ber</a>, Copyright &copy; 2011-2015 Michael Mitton (mmitton@gmail.com).</li>
@@ -70,14 +69,15 @@ Jakob Borg, Audrius Butkevicius, Simon Frei, Tomasz Wilczyński, Alexander Graf,
<li><a href="https://github.com/protocolbuffers/protobuf-go">google.golang.org/protobuf</a>, Copyright &copy; 2018 The Go Authors.</li>
<li><a href="https://github.com/google/uuid">google/uuid</a>, Copyright &copy; 2009,2014 Google Inc.</li>
<li><a href="https://gopkg.in/yaml.v3">gopkg.in/yaml.v3</a>, Copyright &copy; 2025, the gopkg.in/yaml.v3 authors.</li>
<li><a href="https://github.com/greatroar/blobloom">greatroar/blobloom</a>, Copyright &copy; 2020-2024 the Blobloom authors.</li>
<li><a href="https://github.com/hashicorp/errwrap">hashicorp/errwrap</a>, Copyright &copy; 2014 HashiCorp, Inc.</li>
<li><a href="https://github.com/hashicorp/go-multierror">hashicorp/go-multierror</a>, Copyright &copy; 2014 HashiCorp, Inc.</li>
<li><a href="https://github.com/hashicorp/golang-lru">hashicorp/golang-lru</a>, Copyright &copy; 2014 HashiCorp, Inc.</li>
<li><a href="https://github.com/jackpal/gateway">jackpal/gateway</a>, Copyright &copy; 2010 Jack Palevich.</li>
<li><a href="https://github.com/jackpal/go-nat-pmp">jackpal/go-nat-pmp</a>, Copyright 2013 John Howard Palevich.</li>
<li><a href="https://github.com/jmoiron/sqlx">jmoiron/sqlx</a>, Copyright &copy; 2013, Jason Moiron.</li>
<li><a href="https://github.com/julienschmidt/httprouter">julienschmidt/httprouter</a>, Copyright &copy; 2013, Julien Schmidt.</li>
<li><a href="https://github.com/kballard/go-shellquote">kballard/go-shellquote</a>, Copyright &copy; 2014 Kevin Ballard.</li>
<li><a href="https://github.com/mattn/go-sqlite3">mattn/go-sqlite3</a>, Copyright &copy; 2014 Yasuhiro Matsumoto.</li>
<li><a href="https://github.com/miscreant/miscreant.go">miscreant/miscreant.go</a>, Copyright &copy; 2017-2019 The Miscreant Developers.</li>
<li><a href="https://github.com/munnerz/goautoneg">munnerz/goautoneg</a>, Copyright &copy; 2011, Open Knowledge Foundation Ltd.</li>
<li><a href="https://github.com/pierrec/lz4">pierrec/lz4</a>, Copyright &copy; 2015 Pierre Curto.</li>
@@ -210,6 +210,17 @@
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<div class="checkbox">
<label>
<input id="LimitBandwidthInLan" type="checkbox" ng-model="tmpOptions.limitBandwidthInLan" /> <span translate>Limit Bandwidth in LAN</span>
</label>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
+3 -3
View File
@@ -19,9 +19,9 @@ type Counts struct {
Symlinks int
Deleted int
Bytes int64
Sequence int64 // zero for the global state
DeviceID protocol.DeviceID // device ID for remote devices, or special values for local/global
LocalFlags uint32 // the local flag for this count bucket
Sequence int64 // zero for the global state
DeviceID protocol.DeviceID // device ID for remote devices, or special values for local/global
LocalFlags protocol.FlagLocal // the local flag for this count bucket
}
func (c Counts) Add(other Counts) Counts {
+5 -2
View File
@@ -100,10 +100,9 @@ type FileMetadata struct {
Sequence int64
ModNanos int64
Size int64
LocalFlags int64
LocalFlags protocol.FlagLocal
Type protocol.FileInfoType
Deleted bool
Invalid bool
}
func (f *FileMetadata) ModTime() time.Time {
@@ -121,3 +120,7 @@ func (f *FileMetadata) IsDirectory() bool {
func (f *FileMetadata) ShouldConflict() bool {
return f.LocalFlags&protocol.LocalConflictFlags != 0
}
func (f *FileMetadata) IsInvalid() bool {
return f.LocalFlags.IsInvalid()
}
+1
View File
@@ -21,6 +21,7 @@ var (
Namespace: "syncthing",
Subsystem: "db",
Name: "operations_current",
Help: "Number of database operations currently ongoing, per folder and operation",
}, []string{"folder", "operation"})
metricTotalOperationSeconds = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: "syncthing",
+25 -20
View File
@@ -22,7 +22,7 @@ import (
"github.com/syncthing/syncthing/lib/protocol"
)
const currentSchemaVersion = 1
const currentSchemaVersion = 3
//go:embed sql/**
var embedded embed.FS
@@ -62,6 +62,17 @@ func openBase(path string, maxConns int, pragmas, schemaScripts, migrationScript
baseName: filepath.Base(path),
sql: sqlDB,
statements: make(map[string]*sqlx.Stmt),
tplInput: map[string]any{
"FlagLocalUnsupported": protocol.FlagLocalUnsupported,
"FlagLocalIgnored": protocol.FlagLocalIgnored,
"FlagLocalMustRescan": protocol.FlagLocalMustRescan,
"FlagLocalReceiveOnly": protocol.FlagLocalReceiveOnly,
"FlagLocalGlobal": protocol.FlagLocalGlobal,
"FlagLocalNeeded": protocol.FlagLocalNeeded,
"FlagLocalRemoteInvalid": protocol.FlagLocalRemoteInvalid,
"LocalInvalidFlags": protocol.LocalInvalidFlags,
"SyncthingVersion": build.LongVersion,
},
}
for _, script := range schemaScripts {
@@ -96,16 +107,6 @@ func openBase(path string, maxConns int, pragmas, schemaScripts, migrationScript
return nil, wrap(err)
}
db.tplInput = map[string]any{
"FlagLocalUnsupported": protocol.FlagLocalUnsupported,
"FlagLocalIgnored": protocol.FlagLocalIgnored,
"FlagLocalMustRescan": protocol.FlagLocalMustRescan,
"FlagLocalReceiveOnly": protocol.FlagLocalReceiveOnly,
"FlagLocalGlobal": protocol.FlagLocalGlobal,
"FlagLocalNeeded": protocol.FlagLocalNeeded,
"SyncthingVersion": build.LongVersion,
}
return db, nil
}
@@ -151,15 +152,8 @@ func (s *baseDB) stmt(tpl string) stmt {
return stmt
}
// Apply template expansions
var sb strings.Builder
compTpl := template.Must(template.New("tpl").Funcs(tplFuncs).Parse(tpl))
if err := compTpl.Execute(&sb, s.tplInput); err != nil {
panic("bug: bad template: " + err.Error())
}
// Prepare and cache
stmt, err := s.sql.Preparex(sb.String())
stmt, err := s.sql.Preparex(s.expandTemplateVars(tpl))
if err != nil {
return failedStmt{err}
}
@@ -167,6 +161,17 @@ func (s *baseDB) stmt(tpl string) stmt {
return stmt
}
// expandTemplateVars just applies template expansions to the template
// string, or dies trying
func (s *baseDB) expandTemplateVars(tpl string) string {
var sb strings.Builder
compTpl := template.Must(template.New("tpl").Funcs(tplFuncs).Parse(tpl))
if err := compTpl.Execute(&sb, s.tplInput); err != nil {
panic("bug: bad template: " + err.Error())
}
return sb.String()
}
type stmt interface {
Exec(args ...any) (sql.Result, error)
Get(dest any, args ...any) error
@@ -211,7 +216,7 @@ nextScript:
// separately. We require it on a separate line because there are
// also statement-internal semicolons in the triggers.
for _, stmt := range strings.Split(string(bs), "\n;") {
if _, err := tx.Exec(stmt); err != nil {
if _, err := tx.Exec(s.expandTemplateVars(stmt)); err != nil {
return wrap(err, stmt)
}
}
+1 -1
View File
@@ -255,7 +255,7 @@ func (s *DB) ListDevicesForFolder(folder string) ([]protocol.DeviceID, error) {
func (s *DB) RemoteSequences(folder string) (map[protocol.DeviceID]int64, error) {
fdb, err := s.getFolderDB(folder, false)
if errors.Is(err, errNoSuchFolder) {
return nil, nil
return nil, nil //nolint:nilnil
}
if err != nil {
return nil, err
+51
View File
@@ -268,6 +268,57 @@ func TestDontNeedIgnored(t *testing.T) {
}
}
func TestDontNeedRemoteInvalid(t *testing.T) {
t.Parallel()
db, err := OpenTemp()
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := db.Close(); err != nil {
t.Fatal(err)
}
})
// A remote file with the invalid bit set
files := []protocol.FileInfo{
genFile("test1", 1, 103),
}
files[0].LocalFlags = protocol.FlagLocalRemoteInvalid
err = db.Update(folderID, protocol.DeviceID{42}, files)
if err != nil {
t.Fatal(err)
}
// It's not part of the global size
s, err := db.CountGlobal(folderID)
if err != nil {
t.Fatal(err)
}
if s.Bytes != 0 || s.Files != 0 {
t.Log(s)
t.Error("bad global")
}
// We don't need it
s, err = db.CountNeed(folderID, protocol.LocalDeviceID)
if err != nil {
t.Fatal(err)
}
if s.Bytes != 0 || s.Files != 0 {
t.Log(s)
t.Error("bad need")
}
// It shouldn't show up in the need list
names := mustCollect[protocol.FileInfo](t)(db.AllNeededGlobalFiles(folderID, protocol.LocalDeviceID, config.PullOrderAlphabetic, 0, 0))
if len(names) != 0 {
t.Log(names)
t.Error("need no files")
}
}
func TestRemoteDontNeedLocalIgnored(t *testing.T) {
t.Parallel()
+12 -4
View File
@@ -50,10 +50,14 @@ func Open(path string, opts ...Option) (*DB, error) {
"sql/schema/common/*",
"sql/schema/main/*",
}
migrations := []string{
"sql/migrations/common/*",
"sql/migrations/main/*",
}
os.MkdirAll(path, 0o700)
_ = os.MkdirAll(path, 0o700)
mainPath := filepath.Join(path, "main.db")
mainBase, err := openBase(mainPath, maxDBConns, pragmas, schemas, nil)
mainBase, err := openBase(mainPath, maxDBConns, pragmas, schemas, migrations)
if err != nil {
return nil, err
}
@@ -88,10 +92,14 @@ func OpenForMigration(path string) (*DB, error) {
"sql/schema/common/*",
"sql/schema/main/*",
}
migrations := []string{
"sql/migrations/common/*",
"sql/migrations/main/*",
}
os.MkdirAll(path, 0o700)
_ = os.MkdirAll(path, 0o700)
mainPath := filepath.Join(path, "main.db")
mainBase, err := openBase(mainPath, 1, pragmas, schemas, nil)
mainBase, err := openBase(mainPath, 1, pragmas, schemas, migrations)
if err != nil {
return nil, err
}
+5 -3
View File
@@ -84,8 +84,11 @@ func (s *Service) periodic(ctx context.Context) error {
defer func() { l.Debugln("Periodic done in", time.Since(t1), "+", t1.Sub(t0)) }()
s.sdb.updateLock.Lock()
tidy(ctx, s.sdb.sql)
err := tidy(ctx, s.sdb.sql)
s.sdb.updateLock.Unlock()
if err != nil {
return err
}
return wrap(s.sdb.forEachFolder(func(fdb *folderDB) error {
fdb.updateLock.Lock()
@@ -97,8 +100,7 @@ func (s *Service) periodic(ctx context.Context) error {
if err := garbageCollectBlocklistsAndBlocksLocked(ctx, fdb); err != nil {
return wrap(err)
}
tidy(ctx, fdb.sql)
return nil
return tidy(ctx, fdb.sql)
}))
}
+4 -7
View File
@@ -39,13 +39,10 @@ func (s *folderDB) CountNeed(device protocol.DeviceID) (db.Counts, error) {
}
func (s *folderDB) CountGlobal() (db.Counts, error) {
// Exclude ignored and receive-only changed files from the global count
// (legacy expectation? it's a bit weird since those files can in fact
// be global and you can get them with GetGlobal etc.)
var res []countsRow
err := s.stmt(`
SELECT s.type, s.count, s.size, s.local_flags, s.deleted FROM counts s
WHERE s.local_flags & {{.FlagLocalGlobal}} != 0 AND s.local_flags & {{or .FlagLocalReceiveOnly .FlagLocalIgnored}} = 0
WHERE s.local_flags & {{.FlagLocalGlobal}} != 0 AND s.local_flags & {{.LocalInvalidFlags}} = 0
`).Select(&res)
if err != nil {
return db.Counts{}, wrap(err)
@@ -84,7 +81,7 @@ func (s *folderDB) needSizeRemote(device protocol.DeviceID) (db.Counts, error) {
// See neededGlobalFilesRemote for commentary as that is the same query without summing
if err := s.stmt(`
SELECT g.type, count(*) as count, sum(g.size) as size, g.local_flags, g.deleted FROM files g
WHERE g.local_flags & {{.FlagLocalGlobal}} != 0 AND NOT g.deleted AND NOT g.invalid AND NOT EXISTS (
WHERE g.local_flags & {{.FlagLocalGlobal}} != 0 AND NOT g.deleted AND g.local_flags & {{.LocalInvalidFlags}} = 0 AND NOT EXISTS (
SELECT 1 FROM FILES f
INNER JOIN devices d ON d.idx = f.device_idx
WHERE f.name = g.name AND f.version = g.version AND d.device_id = ?
@@ -94,10 +91,10 @@ func (s *folderDB) needSizeRemote(device protocol.DeviceID) (db.Counts, error) {
UNION ALL
SELECT g.type, count(*) as count, sum(g.size) as size, g.local_flags, g.deleted FROM files g
WHERE g.local_flags & {{.FlagLocalGlobal}} != 0 AND g.deleted AND NOT g.invalid AND EXISTS (
WHERE g.local_flags & {{.FlagLocalGlobal}} != 0 AND g.deleted AND g.local_flags & {{.LocalInvalidFlags}} = 0 AND EXISTS (
SELECT 1 FROM FILES f
INNER JOIN devices d ON d.idx = f.device_idx
WHERE f.name = g.name AND d.device_id = ? AND NOT f.deleted AND NOT f.invalid
WHERE f.name = g.name AND d.device_id = ? AND NOT f.deleted AND f.local_flags & {{.LocalInvalidFlags}} = 0
)
GROUP BY g.type, g.local_flags, g.deleted
`).Select(&res, device.String(),
+5 -5
View File
@@ -74,7 +74,7 @@ func (s *folderDB) GetGlobalAvailability(file string) ([]protocol.DeviceID, erro
func (s *folderDB) AllGlobalFiles() (iter.Seq[db.FileMetadata], func() error) {
it, errFn := iterStructs[db.FileMetadata](s.stmt(`
SELECT f.sequence, f.name, f.type, f.modified as modnanos, f.size, f.deleted, f.invalid, f.local_flags as localflags FROM files f
SELECT f.sequence, f.name, f.type, f.modified as modnanos, f.size, f.deleted, f.local_flags as localflags FROM files f
WHERE f.local_flags & {{.FlagLocalGlobal}} != 0
ORDER BY f.name
`).Queryx())
@@ -93,7 +93,7 @@ func (s *folderDB) AllGlobalFilesPrefix(prefix string) (iter.Seq[db.FileMetadata
end := prefixEnd(prefix)
it, errFn := iterStructs[db.FileMetadata](s.stmt(`
SELECT f.sequence, f.name, f.type, f.modified as modnanos, f.size, f.deleted, f.invalid, f.local_flags as localflags FROM files f
SELECT f.sequence, f.name, f.type, f.modified as modnanos, f.size, f.deleted, f.local_flags as localflags FROM files f
WHERE f.name >= ? AND f.name < ? AND f.local_flags & {{.FlagLocalGlobal}} != 0
ORDER BY f.name
`).Queryx(prefix, end))
@@ -158,7 +158,7 @@ func (s *folderDB) neededGlobalFilesRemote(device protocol.DeviceID, selectOpts
SELECT fi.fiprotobuf, bl.blprotobuf, g.name, g.size, g.modified FROM fileinfos fi
INNER JOIN files g on fi.sequence = g.sequence
LEFT JOIN blocklists bl ON bl.blocklist_hash = g.blocklist_hash
WHERE g.local_flags & {{.FlagLocalGlobal}} != 0 AND NOT g.deleted AND NOT g.invalid AND NOT EXISTS (
WHERE g.local_flags & {{.FlagLocalGlobal}} != 0 AND NOT g.deleted AND g.local_flags & {{.LocalInvalidFlags}} = 0 AND NOT EXISTS (
SELECT 1 FROM FILES f
INNER JOIN devices d ON d.idx = f.device_idx
WHERE f.name = g.name AND f.version = g.version AND d.device_id = ?
@@ -169,10 +169,10 @@ func (s *folderDB) neededGlobalFilesRemote(device protocol.DeviceID, selectOpts
SELECT fi.fiprotobuf, bl.blprotobuf, g.name, g.size, g.modified FROM fileinfos fi
INNER JOIN files g on fi.sequence = g.sequence
LEFT JOIN blocklists bl ON bl.blocklist_hash = g.blocklist_hash
WHERE g.local_flags & {{.FlagLocalGlobal}} != 0 AND g.deleted AND NOT g.invalid AND EXISTS (
WHERE g.local_flags & {{.FlagLocalGlobal}} != 0 AND g.deleted AND g.local_flags & {{.LocalInvalidFlags}} = 0 AND EXISTS (
SELECT 1 FROM FILES f
INNER JOIN devices d ON d.idx = f.device_idx
WHERE f.name = g.name AND d.device_id = ? AND NOT f.deleted AND NOT f.invalid
WHERE f.name = g.name AND d.device_id = ? AND NOT f.deleted AND f.local_flags & {{.LocalInvalidFlags}} = 0
)
`+selectOpts).Queryx(
device.String(),
+1 -1
View File
@@ -89,7 +89,7 @@ func (s *folderDB) AllLocalFilesWithPrefix(device protocol.DeviceID, prefix stri
func (s *folderDB) AllLocalFilesWithBlocksHash(h []byte) (iter.Seq[db.FileMetadata], func() error) {
return iterStructs[db.FileMetadata](s.stmt(`
SELECT f.sequence, f.name, f.type, f.modified as modnanos, f.size, f.deleted, f.invalid, f.local_flags as localflags FROM files f
SELECT f.sequence, f.name, f.type, f.modified as modnanos, f.size, f.deleted, f.local_flags as localflags FROM files f
WHERE f.device_idx = {{.LocalDeviceIdx}} AND f.blocklist_hash = ?
`).Queryx(h))
}
+5 -1
View File
@@ -32,8 +32,12 @@ func openFolderDB(folder, path string, deleteRetention time.Duration) (*folderDB
"sql/schema/common/*",
"sql/schema/folder/*",
}
migrations := []string{
"sql/migrations/common/*",
"sql/migrations/folder/*",
}
base, err := openBase(path, maxDBConns, pragmas, schemas, nil)
base, err := openBase(path, maxDBConns, pragmas, schemas, migrations)
if err != nil {
return nil, err
}
+20 -16
View File
@@ -46,8 +46,8 @@ func (s *folderDB) Update(device protocol.DeviceID, fs []protocol.FileInfo) erro
//nolint:sqlclosecheck
insertFileStmt, err := txp.Preparex(`
INSERT OR REPLACE INTO files (device_idx, remote_sequence, name, type, modified, size, version, deleted, invalid, local_flags, blocklist_hash)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
INSERT OR REPLACE INTO files (device_idx, remote_sequence, name, type, modified, size, version, deleted, local_flags, blocklist_hash)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
RETURNING sequence
`)
if err != nil {
@@ -101,7 +101,7 @@ func (s *folderDB) Update(device protocol.DeviceID, fs []protocol.FileInfo) erro
remoteSeq = &f.Sequence
}
var localSeq int64
if err := insertFileStmt.Get(&localSeq, deviceIdx, remoteSeq, f.Name, f.Type, f.ModTime().UnixNano(), f.Size, f.Version.String(), f.IsDeleted(), f.IsInvalid(), f.LocalFlags, blockshash); err != nil {
if err := insertFileStmt.Get(&localSeq, deviceIdx, remoteSeq, f.Name, f.Type, f.ModTime().UnixNano(), f.Size, f.Version.String(), f.IsDeleted(), f.LocalFlags, blockshash); err != nil {
return wrap(err, "insert file")
}
@@ -329,7 +329,7 @@ func (s *folderDB) recalcGlobalForFolder(txp *txPreparedStmts) error {
func (s *folderDB) recalcGlobalForFile(txp *txPreparedStmts, file string) error {
//nolint:sqlclosecheck
selStmt, err := txp.Preparex(`
SELECT name, device_idx, sequence, modified, version, deleted, invalid, local_flags FROM files
SELECT name, device_idx, sequence, modified, version, deleted, local_flags FROM files
WHERE name = ?
`)
if err != nil {
@@ -350,7 +350,7 @@ func (s *folderDB) recalcGlobalForFile(txp *txPreparedStmts, file string) error
// The global version is the first one in the list that is not invalid,
// or just the first one in the list if all are invalid.
var global fileRow
globIdx := slices.IndexFunc(es, func(e fileRow) bool { return !e.Invalid })
globIdx := slices.IndexFunc(es, func(e fileRow) bool { return !e.IsInvalid() })
if globIdx < 0 {
globIdx = 0
}
@@ -368,7 +368,7 @@ func (s *folderDB) recalcGlobalForFile(txp *txPreparedStmts, file string) error
// Set the global flag on the global entry. Set the need flag if the
// local device needs this file, unless it's invalid.
global.LocalFlags |= protocol.FlagLocalGlobal
if hasLocal || global.Invalid {
if hasLocal || global.IsInvalid() {
global.LocalFlags &= ^protocol.FlagLocalNeeded
} else {
global.LocalFlags |= protocol.FlagLocalNeeded
@@ -426,18 +426,17 @@ type fileRow struct {
Sequence int64
Modified int64
Size int64
LocalFlags int64 `db:"local_flags"`
LocalFlags protocol.FlagLocal `db:"local_flags"`
Deleted bool
Invalid bool
}
func (e fileRow) Compare(other fileRow) int {
// From FileInfo.WinsConflict
vc := e.Version.Vector.Compare(other.Version.Vector)
vc := e.Version.Compare(other.Version.Vector)
switch vc {
case protocol.Equal:
if e.Invalid != other.Invalid {
if e.Invalid {
if e.IsInvalid() != other.IsInvalid() {
if e.IsInvalid() {
return 1
}
return -1
@@ -453,8 +452,8 @@ func (e fileRow) Compare(other fileRow) int {
case protocol.Lesser: // we are older
return 1
case protocol.ConcurrentGreater, protocol.ConcurrentLesser: // there is a conflict
if e.Invalid != other.Invalid {
if e.Invalid { // we are invalid, we lose
if e.IsInvalid() != other.IsInvalid() {
if e.IsInvalid() { // we are invalid, we lose
return 1
}
return -1 // they are invalid, we win
@@ -477,6 +476,10 @@ func (e fileRow) Compare(other fileRow) int {
}
}
func (e fileRow) IsInvalid() bool {
return e.LocalFlags.IsInvalid()
}
func (s *folderDB) periodicCheckpointLocked(fs []protocol.FileInfo) {
// Induce periodic checkpoints. We add points for each file and block,
// and checkpoint when we've written more than a threshold of points.
@@ -519,11 +522,12 @@ func (s *folderDB) periodicCheckpointLocked(fs []protocol.FileInfo) {
// failed, we'll keep trying it until we succeed. Increase it faster
// when we fail to checkpoint, as it's more likely the WAL is
// growing and will need truncation when we get out of this state.
if res == 1 {
switch {
case res == 1:
s.checkpointsCount += 10
} else if res == 0 && checkpointType == "TRUNCATE" {
case res == 0 && checkpointType == "TRUNCATE":
s.checkpointsCount = 0
} else {
default:
s.checkpointsCount++
}
s.updatePoints = 0
@@ -1,7 +0,0 @@
-- Copyright (C) 2025 The Syncthing Authors.
--
-- This Source Code Form is subject to the terms of the Mozilla Public
-- License, v. 2.0. If a copy of the MPL was not distributed with this file,
-- You can obtain one at https://mozilla.org/MPL/2.0/.
-- The next migration should be number two.
@@ -0,0 +1,20 @@
-- Copyright (C) 2025 The Syncthing Authors.
--
-- This Source Code Form is subject to the terms of the Mozilla Public
-- License, v. 2.0. If a copy of the MPL was not distributed with this file,
-- You can obtain one at https://mozilla.org/MPL/2.0/.
-- Remote files with the invalid bit instead gain the RemoteInvalid local
-- flag.
UPDATE files
SET local_flags = local_flags | {{.FlagLocalRemoteInvalid}}
FROM (
SELECT idx FROM devices
WHERE device_id = '7777777-777777N-7777777-777777N-7777777-777777N-7777777-77777Q4'
) AS local_device
WHERE invalid AND device_idx != local_device.idx
;
-- The invalid column goes away.
ALTER TABLE files DROP COLUMN invalid
;
@@ -0,0 +1,17 @@
-- Copyright (C) 2025 The Syncthing Authors.
--
-- This Source Code Form is subject to the terms of the Mozilla Public
-- License, v. 2.0. If a copy of the MPL was not distributed with this file,
-- You can obtain one at https://mozilla.org/MPL/2.0/.
-- Remove broken file entries in the database.
DELETE FROM files
WHERE type == 0 -- files
AND NOT deleted -- that are not deleted
AND blocklist_hash IS null -- with no blocks
AND local_flags & {{.LocalInvalidFlags}} == 0 -- and not invalid
;
-- Force a new index transmission.
DELETE FROM indexids
;
@@ -31,7 +31,6 @@ CREATE TABLE IF NOT EXISTS files (
size INTEGER NOT NULL,
version TEXT NOT NULL COLLATE BINARY,
deleted INTEGER NOT NULL, -- boolean
invalid INTEGER NOT NULL, -- boolean
local_flags INTEGER NOT NULL,
blocklist_hash BLOB, -- null when there are no blocks
FOREIGN KEY(device_idx) REFERENCES devices(idx) ON DELETE CASCADE
+2 -2
View File
@@ -37,7 +37,7 @@ func NewTyped(db KV, prefix string) *Typed {
// is overwritten.
func (n *Typed) PutInt64(key string, val int64) error {
var valBs [8]byte
binary.BigEndian.PutUint64(valBs[:], uint64(val))
binary.BigEndian.PutUint64(valBs[:], uint64(val)) //nolint:gosec
return n.db.PutKV(n.prefixedKey(key), valBs[:])
}
@@ -49,7 +49,7 @@ func (n *Typed) Int64(key string) (int64, bool, error) {
return 0, false, filterNotFound(err)
}
val := binary.BigEndian.Uint64(valBs)
return int64(val), true, nil
return int64(val), true, nil //nolint:gosec
}
// PutTime stores a new time.Time. Any existing value (even if of another
+4 -2
View File
@@ -7,14 +7,16 @@
package protoutil
import (
"fmt"
"errors"
"google.golang.org/protobuf/proto"
)
var errBufferTooSmall = errors.New("buffer too small")
func MarshalTo(buf []byte, pb proto.Message) (int, error) {
if sz := proto.Size(pb); len(buf) < sz {
return 0, fmt.Errorf("buffer too small")
return 0, errBufferTooSmall
} else if sz == 0 {
return 0, nil
}
+1 -1
View File
@@ -166,7 +166,7 @@ func (s *service) getListener(guiCfg config.GUIConfiguration) (net.Listener, err
name = s.tlsDefaultCommonName
}
cert, err = tlsutil.NewCertificate(httpsCertFile, httpsKeyFile, name, httpsCertLifetimeDays)
cert, err = tlsutil.NewCertificate(httpsCertFile, httpsKeyFile, name, httpsCertLifetimeDays, true)
}
if err != nil {
return nil, err
+27 -4
View File
@@ -49,9 +49,11 @@ type quicListener struct {
registry *registry.Registry
lanChecker *lanChecker
address *url.URL
laddr net.Addr
mut sync.Mutex
address *url.URL
natService *nat.Service
mapping *nat.Mapping
laddr net.Addr
mut sync.Mutex
}
func (t *quicListener) OnNATTypeChanged(natType stun.NATType) {
@@ -126,7 +128,24 @@ func (t *quicListener) serve(ctx context.Context) error {
l.Infof("QUIC listener (%v) starting", udpConn.LocalAddr())
defer l.Infof("QUIC listener (%v) shutting down", udpConn.LocalAddr())
var ipVersion nat.IPVersion
switch t.uri.Scheme {
case "quic4":
ipVersion = nat.IPv4Only
case "quic6":
ipVersion = nat.IPv6Only
default:
ipVersion = nat.IPvAny
}
mapping := t.natService.NewMapping(nat.UDP, ipVersion, udpAddr.IP, udpAddr.Port)
mapping.OnChanged(func() {
t.notifyAddressesChanged(t)
})
// Should be called after t.mapping is nil'ed out.
defer t.natService.RemoveMapping(mapping)
t.mut.Lock()
t.mapping = mapping
t.laddr = udpConn.LocalAddr()
t.mut.Unlock()
defer func() {
@@ -196,6 +215,9 @@ func (t *quicListener) WANAddresses() []*url.URL {
if t.address != nil {
uris = append(uris, t.address)
}
uris = append(uris, portMappingURIs(t.mapping, *t.uri)...)
t.mut.Unlock()
return uris
}
@@ -232,12 +254,13 @@ func (*quicListenerFactory) Valid(config.Configuration) error {
return nil
}
func (f *quicListenerFactory) New(uri *url.URL, cfg config.Wrapper, tlsCfg *tls.Config, conns chan internalConn, _ *nat.Service, registry *registry.Registry, lanChecker *lanChecker) genericListener {
func (f *quicListenerFactory) New(uri *url.URL, cfg config.Wrapper, tlsCfg *tls.Config, conns chan internalConn, natService *nat.Service, registry *registry.Registry, lanChecker *lanChecker) genericListener {
l := &quicListener{
uri: fixupPort(uri, config.DefaultQUICPort),
cfg: cfg,
tlsCfg: tlsCfg,
conns: conns,
natService: natService,
factory: f,
registry: registry,
lanChecker: lanChecker,
+2 -17
View File
@@ -175,24 +175,9 @@ func (t *tcpListener) WANAddresses() []*url.URL {
uris := []*url.URL{
maybeReplacePort(t.uri, t.laddr),
}
if t.mapping != nil {
addrs := t.mapping.ExternalAddresses()
for _, addr := range addrs {
uri := *t.uri
// Does net.JoinHostPort internally
uri.Host = addr.String()
uris = append(uris, &uri)
// For every address with a specified IP, add one without an IP,
// just in case the specified IP is still internal (router behind DMZ).
if len(addr.IP) != 0 && !addr.IP.IsUnspecified() {
zeroUri := *t.uri
addr.IP = nil
zeroUri.Host = addr.String()
uris = append(uris, &zeroUri)
}
}
}
uris = append(uris, portMappingURIs(t.mapping, *t.uri)...)
t.mut.RUnlock()
// If we support ReusePort, add an unspecified zero port address, which will be resolved by the discovery server
+25
View File
@@ -12,6 +12,7 @@ import (
"strconv"
"strings"
"github.com/syncthing/syncthing/lib/nat"
"github.com/syncthing/syncthing/lib/osutil"
)
@@ -130,3 +131,27 @@ func maybeReplacePort(uri *url.URL, laddr net.Addr) *url.URL {
uriCopy.Host = net.JoinHostPort(host, lportStr)
return &uriCopy
}
func portMappingURIs(mapping *nat.Mapping, listener_uri url.URL) []*url.URL {
var uris []*url.URL
if mapping != nil {
addrs := mapping.ExternalAddresses()
for _, addr := range addrs {
uri := listener_uri
// Does net.JoinHostPort internally
uri.Host = addr.String()
uris = append(uris, &uri)
// For every address with a specified IP, add one without an IP,
// just in case the specified IP is still internal (router behind DMZ).
if len(addr.IP) != 0 && !addr.IP.IsUnspecified() {
zeroUri := listener_uri
addr.IP = nil
zeroUri.Host = addr.String()
uris = append(uris, &zeroUri)
}
}
}
return uris
}
+4 -4
View File
@@ -379,12 +379,12 @@ func longFilenameSupport(path string) string {
return path
}
type ErrWatchEventOutsideRoot struct{ msg string }
type WatchEventOutsideRootError struct{ msg string }
func (e *ErrWatchEventOutsideRoot) Error() string {
func (e *WatchEventOutsideRootError) Error() string {
return e.msg
}
func (f *BasicFilesystem) newErrWatchEventOutsideRoot(absPath string, roots []string) *ErrWatchEventOutsideRoot {
return &ErrWatchEventOutsideRoot{fmt.Sprintf("Watching for changes encountered an event outside of the filesystem root: f.root==%v, roots==%v, path==%v. This should never happen, please report this message to forum.syncthing.net.", f.root, roots, absPath)}
func (f *BasicFilesystem) newErrWatchEventOutsideRoot(absPath string, roots []string) *WatchEventOutsideRootError {
return &WatchEventOutsideRootError{fmt.Sprintf("Watching for changes encountered an event outside of the filesystem root: f.root==%v, roots==%v, path==%v. This should never happen, please report this message to forum.syncthing.net.", f.root, roots, absPath)}
}
+1 -1
View File
@@ -15,7 +15,7 @@ import (
)
func (fi basicFileInfo) InodeChangeTime() time.Time {
if sys, ok := fi.FileInfo.Sys().(*syscall.Stat_t); ok {
if sys, ok := fi.Sys().(*syscall.Stat_t); ok {
return time.Unix(0, sys.Ctimespec.Nano())
}
return time.Time{}
+1 -1
View File
@@ -86,7 +86,7 @@ func (f *BasicFilesystem) Remove(name string) error {
// unrooted) or an error if the given path is not a subpath and handles the
// special case when the given path is the folder root without a trailing
// pathseparator.
func (f *BasicFilesystem) unrootedChecked(absPath string, roots []string) (string, *ErrWatchEventOutsideRoot) {
func (f *BasicFilesystem) unrootedChecked(absPath string, roots []string) (string, *WatchEventOutsideRootError) {
for _, root := range roots {
// Make sure the root ends with precisely one path separator, to
// ease prefix comparisons.
+1 -1
View File
@@ -69,7 +69,7 @@ var xattrBufPool = sync.Pool{
}
func getXattr(path, name string) ([]byte, error) {
buf := xattrBufPool.Get().([]byte)
buf := xattrBufPool.Get().([]byte) //nolint:forcetypeassert
defer func() {
// Put the buffer back in the pool, or not if we're not supposed to
// (we returned it to the caller).
+5 -5
View File
@@ -24,16 +24,16 @@ const (
caseCacheItemLimit = 4 << 10
)
type ErrCaseConflict struct {
type CaseConflictError struct {
Given, Real string
}
func (e *ErrCaseConflict) Error() string {
func (e *CaseConflictError) Error() string {
return fmt.Sprintf(`remote "%v" uses different upper or lowercase characters than local "%v"; change the casing on either side to match the other`, e.Given, e.Real)
}
func IsErrCaseConflict(err error) bool {
e := &ErrCaseConflict{}
e := &CaseConflictError{}
return errors.As(err, &e)
}
@@ -239,7 +239,7 @@ func (f *caseFilesystem) Rename(oldpath, newpath string) error {
}
if err := f.checkCase(newpath); err != nil {
// Case-only rename is ok
e := &ErrCaseConflict{}
e := &CaseConflictError{}
if !errors.As(err, &e) || e.Real != oldpath {
return err
}
@@ -389,7 +389,7 @@ func (f *caseFilesystem) checkCaseExisting(name string) error {
// comparing, as we don't want to treat a normalization difference as a
// case conflict.
if norm.NFC.String(realName) != norm.NFC.String(name) {
return &ErrCaseConflict{name, realName}
return &CaseConflictError{name, realName}
}
return nil
}
+12 -10
View File
@@ -147,29 +147,29 @@ func newFakeFilesystem(rootURI string, _ ...Option) *fakeFS {
// *look* like file I/O, but they are not. Do not worry that they
// might fail.
rng := rand.New(rand.NewSource(int64(seed)))
rng := rand.New(rand.NewSource(int64(seed))) //nolint:gosec
var createdFiles int
var writtenData int64
for (files == 0 || createdFiles < files) && (maxsize == 0 || writtenData>>20 < int64(maxsize)) {
dir := filepath.Join(fmt.Sprintf("%02x", rng.Intn(255)), fmt.Sprintf("%02x", rng.Intn(255)))
file := fmt.Sprintf("%016x", rng.Int63())
fs.MkdirAll(dir, 0o755)
_ = fs.MkdirAll(dir, 0o755)
fd, _ := fs.Create(filepath.Join(dir, file))
createdFiles++
fsize := int64(sizeavg/2 + rng.Intn(sizeavg))
fd.Truncate(fsize)
_ = fd.Truncate(fsize)
writtenData += fsize
ftime := time.Unix(1000000000+rng.Int63n(10*365*86400), 0)
fs.Chtimes(filepath.Join(dir, file), ftime, ftime)
_ = fs.Chtimes(filepath.Join(dir, file), ftime, ftime)
}
}
if !nostfolder {
// Also create a default folder marker for good measure
fs.Mkdir(".stfolder", 0o700)
_ = fs.Mkdir(".stfolder", 0o700)
}
// We only set the latency after doing the operations required to create
@@ -284,9 +284,10 @@ func (fs *fakeFS) create(name string) (*fakeEntry, error) {
time.Sleep(fs.latency)
if entry := fs.entryForName(name); entry != nil {
if entry.entryType == fakeEntryTypeDir {
switch entry.entryType {
case fakeEntryTypeDir:
return nil, os.ErrExist
} else if entry.entryType == fakeEntryTypeSymlink {
case fakeEntryTypeSymlink:
return nil, errors.New("following symlink not supported")
}
entry.size = 0
@@ -731,6 +732,7 @@ func (fs *fakeFS) resetCounters() {
}
func (fs *fakeFS) reportMetricsPerOp(b *testing.B) {
b.Helper()
fs.reportMetricsPer(b, 1, "op")
}
@@ -829,7 +831,7 @@ func (f *fakeFile) readShortAt(p []byte, offs int64) (int, error) {
if f.seed == 0 {
hf := fnv.New64()
hf.Write([]byte(f.name))
f.seed = int64(hf.Sum64())
f.seed = int64(hf.Sum64()) //nolint:gosec
}
// Check whether the read is a continuation of an RNG we already have or
@@ -839,14 +841,14 @@ func (f *fakeFile) readShortAt(p []byte, offs int64) (int, error) {
nextBlockOffs := (seedNo + 1) << randomBlockShift
if f.rng == nil || f.offset != offs || seedNo != f.seedOffs {
// This is not a straight read continuing from a previous one
f.rng = rand.New(rand.NewSource(f.seed + seedNo))
f.rng = rand.New(rand.NewSource(f.seed + seedNo)) //nolint:gosec
// If the read is not at the start of the block, discard data
// accordingly.
diff := offs - minOffs
if diff > 0 {
lr := io.LimitReader(f.rng, diff)
io.Copy(io.Discard, lr)
_, _ = io.Copy(io.Discard, lr)
}
f.offset = offs
+5 -4
View File
@@ -180,7 +180,7 @@ const (
// SkipDir is used as a return value from WalkFuncs to indicate that
// the directory named in the call is to be skipped. It is not returned
// as an error by any function.
var SkipDir = filepath.SkipDir
var SkipDir = filepath.SkipDir //nolint:errname
func IsExist(err error) bool {
return errors.Is(err, ErrExist)
@@ -259,15 +259,16 @@ func NewFilesystem(fsType FilesystemType, uri string, opts ...Option) Filesystem
// attributed to the calling function.
layersAboveWalkFilesystem++
}
if l.ShouldDebug("walkfs") {
switch {
case l.ShouldDebug("walkfs"):
// A walkFilesystem is not a layer to skip, it embeds the underlying
// filesystem, passing calls directly trough. Except for calls made
// during walking, however those are truly originating in the walk
// filesystem.
fs = NewWalkFilesystem(newLogFilesystem(fs, layersAboveWalkFilesystem))
} else if l.ShouldDebug("fs") {
case l.ShouldDebug("fs"):
fs = newLogFilesystem(NewWalkFilesystem(fs), layersAboveWalkFilesystem)
} else {
default:
fs = NewWalkFilesystem(fs)
}
+2 -1
View File
@@ -7,6 +7,7 @@
package fs
import (
"errors"
"io"
)
@@ -28,7 +29,7 @@ func copyRangeStandard(src, dst File, srcOffset, dstOffset, size int64) error {
}
n, err := src.ReadAt(buf, srcOffset)
if err != nil {
if err == io.EOF {
if errors.Is(err, io.EOF) {
return io.ErrUnexpectedEOF
}
return err
+2 -1
View File
@@ -8,6 +8,7 @@ package fs
import (
"bytes"
"errors"
"io"
"math/rand"
"os"
@@ -329,7 +330,7 @@ func TestCopyRange(tttt *testing.T) {
t.Fatal("dst file is not a basic file")
}
if err := impl(srcBasic, dstBasic, testCase.srcOffset, testCase.dstOffset, testCase.copySize); err != nil {
if err == syscall.ENOTSUP {
if errors.Is(err, errors.ErrUnsupported) {
// Test runner can adjust directory in which to run the tests, that allow broader tests.
t.Skip("Not supported on the current filesystem, set STFSTESTPATH env var.")
}
+1 -1
View File
@@ -68,7 +68,7 @@ func (*optionMtime) String() string {
func (f *mtimeFS) Chtimes(name string, atime, mtime time.Time) error {
// Do a normal Chtimes call, don't care if it succeeds or not.
f.chtimes(name, atime, mtime)
_ = f.chtimes(name, atime, mtime)
// Stat the file to see what happened. Here we *do* return an error,
// because it might be "does not exist" or similar.
+4 -2
View File
@@ -26,8 +26,10 @@ type Option interface {
type FilesystemFactory func(string, ...Option) (Filesystem, error)
// For each registered file system type, a function to construct a file system.
var filesystemFactories map[FilesystemType]FilesystemFactory = make(map[FilesystemType]FilesystemFactory)
var filesystemFactoriesMutex sync.Mutex = sync.Mutex{}
var (
filesystemFactories map[FilesystemType]FilesystemFactory = make(map[FilesystemType]FilesystemFactory)
filesystemFactoriesMutex sync.Mutex = sync.Mutex{}
)
// Register a function to be called when a filesystem is to be constructed with
// the specified fsType. The function will receive the URI for the file system as well
+1 -1
View File
@@ -192,7 +192,7 @@ func CommonPrefix(first, second string) string {
}
common := make([]string, 0, count)
for i := 0; i < count; i++ {
for i := range count {
if firstParts[i] != secondParts[i] {
break
}
+3 -3
View File
@@ -89,7 +89,7 @@ func (f *walkFilesystem) walk(path string, info FileInfo, walkFn WalkFunc, ances
err = walkFn(path, info, nil)
if err != nil {
if info.IsDir() && err == SkipDir {
if info.IsDir() && errors.Is(err, SkipDir) {
return nil
}
return err
@@ -117,13 +117,13 @@ func (f *walkFilesystem) walk(path string, info FileInfo, walkFn WalkFunc, ances
filename := filepath.Join(path, name)
fileInfo, err := f.Lstat(filename)
if err != nil {
if err := walkFn(filename, fileInfo, err); err != nil && err != SkipDir {
if err := walkFn(filename, fileInfo, err); err != nil && !errors.Is(err, SkipDir) {
return err
}
} else {
err = f.walk(filename, fileInfo, walkFn, ancestors)
if err != nil {
if !fileInfo.IsDir() || err != SkipDir {
if !fileInfo.IsDir() || !errors.Is(err, SkipDir) {
return err
}
}
+2 -2
View File
@@ -51,7 +51,7 @@ type standardBlockPullReorderer struct {
}
func newStandardBlockPullReorderer(id protocol.DeviceID, otherDevices []protocol.DeviceID) *standardBlockPullReorderer {
allDevices := append(otherDevices, id)
allDevices := append(otherDevices, id) //nolint:gocritic
slices.SortFunc(allDevices, func(a, b protocol.DeviceID) int {
return a.Compare(b)
})
@@ -92,7 +92,7 @@ func (p *standardBlockPullReorderer) Reorder(blocks []protocol.BlockInfo) []prot
// The rest of the chunks we fetch in a random order in whole chunks.
// Generate chunk index slice and shuffle it
indexes := make([]int, 0, len(chunks)-1)
for i := range len(chunks) {
for i := range chunks {
if i != p.myIndex {
indexes = append(indexes, i)
}
+4 -3
View File
@@ -56,17 +56,18 @@ func (p *deviceFolderDownloadState) Update(updates []protocol.FileDownloadProgre
if update.UpdateType == protocol.FileDownloadProgressUpdateTypeForget && ok && local.version.Equal(update.Version) {
delete(p.files, update.Name)
} else if update.UpdateType == protocol.FileDownloadProgressUpdateTypeAppend {
if !ok {
switch {
case !ok:
local = deviceFolderFileDownloadState{
blockIndexes: update.BlockIndexes,
version: update.Version,
blockSize: update.BlockSize,
}
} else if !local.version.Equal(update.Version) {
case !local.version.Equal(update.Version):
local.blockIndexes = append(local.blockIndexes[:0], update.BlockIndexes...)
local.version = update.Version
local.blockSize = update.BlockSize
} else {
default:
local.blockIndexes = append(local.blockIndexes, update.BlockIndexes...)
}
p.files[update.Name] = local
+1 -1
View File
@@ -74,7 +74,7 @@ func (f *fakeConnection) DownloadProgress(_ context.Context, dp *protocol.Downlo
})
}
func (f *fakeConnection) addFileLocked(name string, flags uint32, ftype protocol.FileInfoType, data []byte, version protocol.Vector, localFlags uint32) {
func (f *fakeConnection) addFileLocked(name string, flags uint32, ftype protocol.FileInfoType, data []byte, version protocol.Vector, localFlags protocol.FlagLocal) {
blockSize := protocol.BlockSize(int64(len(data)))
blocks, _ := scanner.Blocks(context.TODO(), bytes.NewReader(data), blockSize, int64(len(data)), nil)
+12 -8
View File
@@ -44,7 +44,7 @@ type folder struct {
*stats.FolderStatisticsReference
ioLimiter *semaphore.Semaphore
localFlags uint32
localFlags protocol.FlagLocal
model *model
shortID protocol.ShortID
@@ -52,7 +52,7 @@ type folder struct {
ignores *ignore.Matcher
mtimefs fs.Filesystem
modTimeWindow time.Duration
ctx context.Context // used internally, only accessible on serve lifetime
ctx context.Context //nolint:containedctx // used internally, only accessible on serve lifetime
done chan struct{} // used externally, accessible regardless of serve
scanInterval time.Duration
@@ -188,8 +188,12 @@ func (f *folder) Serve(ctx context.Context) error {
case <-f.pullScheduled:
if f.PullerDelayS > 0 {
// Wait for incoming updates to settle before doing the
// actual pull
f.setState(FolderSyncWaiting)
// actual pull. Only set the state to SyncWaiting if we have
// reason to believe there is something to sync, to avoid
// unnecessary flashing in the GUI.
if needCount, err := f.db.CountNeed(f.folderID, protocol.LocalDeviceID); err == nil && needCount.TotalItems() > 0 {
f.setState(FolderSyncWaiting)
}
pullTimer.Reset(time.Duration(float64(time.Second) * f.PullerDelayS))
} else {
_, err = f.pull()
@@ -322,7 +326,7 @@ func (f *folder) Reschedule() {
return
}
// Sleep a random time between 3/4 and 5/4 of the configured interval.
sleepNanos := (f.scanInterval.Nanoseconds()*3 + rand.Int63n(2*f.scanInterval.Nanoseconds())) / 4
sleepNanos := (f.scanInterval.Nanoseconds()*3 + rand.Int63n(2*f.scanInterval.Nanoseconds())) / 4 //nolint:gosec
interval := time.Duration(sleepNanos) * time.Nanosecond
l.Debugln(f, "next rescan in", interval)
f.scanTimer.Reset(interval)
@@ -1077,7 +1081,7 @@ func (f *folder) monitorWatch(ctx context.Context) {
f.setWatchError(err, next)
// This error was previously a panic and should never occur, so generate
// a warning, but don't do it repetitively.
var errOutside *fs.ErrWatchEventOutsideRoot
var errOutside *fs.WatchEventOutsideRootError
if errors.As(err, &errOutside) {
if !warnedOutside {
l.Warnln(err)
@@ -1111,7 +1115,7 @@ func (f *folder) setWatchError(err error, nextTryIn time.Duration) {
prevErr := f.watchErr
f.watchErr = err
f.watchMut.Unlock()
if err != prevErr {
if err != prevErr { //nolint:errorlint
data := map[string]interface{}{
"folder": f.ID,
}
@@ -1127,7 +1131,7 @@ func (f *folder) setWatchError(err error, nextTryIn time.Duration) {
return
}
msg := fmt.Sprintf("Error while trying to start filesystem watcher for folder %s, trying again in %v: %v", f.Description(), nextTryIn, err)
if prevErr != err {
if prevErr != err { //nolint:errorlint
l.Infof(msg)
return
}
+1 -1
View File
@@ -28,7 +28,7 @@ func newSendOnlyFolder(model *model, ignores *ignore.Matcher, cfg config.FolderC
f := &sendOnlyFolder{
folder: newFolder(model, ignores, cfg, evLogger, ioLimiter, nil),
}
f.folder.puller = f
f.puller = f
return f
}
+14 -14
View File
@@ -136,7 +136,7 @@ func newSendReceiveFolder(model *model, ignores *ignore.Matcher, cfg config.Fold
blockPullReorderer: newBlockPullReorderer(cfg.BlockPullOrder, model.id, cfg.DeviceIDs()),
writeLimiter: semaphore.New(cfg.MaxConcurrentWrites),
}
f.folder.puller = f
f.puller = f
if f.Copiers == 0 {
f.Copiers = defaultCopiers
@@ -359,13 +359,14 @@ loop:
}
case file.IsDeleted():
if file.IsDirectory() {
switch {
case file.IsDirectory():
// Perform directory deletions at the end, as we may have
// files to delete inside them before we get to that point.
dirDeletions = append(dirDeletions, file)
} else if file.IsSymlink() {
case file.IsSymlink():
f.deleteFile(file, dbUpdateChan, scanChan)
} else {
default:
df, ok, err := f.model.sdb.GetDeviceFile(f.folderID, protocol.LocalDeviceID, file.Name)
if err != nil {
return changed, nil, nil, err
@@ -976,7 +977,7 @@ func (f *sendReceiveFolder) renameFile(cur, source, target protocol.FileInfo, db
}
switch stat, serr := f.mtimefs.Lstat(target.Name); {
case serr != nil:
var caseErr *fs.ErrCaseConflict
var caseErr *fs.CaseConflictError
switch {
case errors.As(serr, &caseErr):
if caseErr.Real != source.Name {
@@ -1023,7 +1024,7 @@ func (f *sendReceiveFolder) renameFile(cur, source, target protocol.FileInfo, db
tempName := fs.TempName(target.Name)
if f.versioner != nil {
err = f.CheckAvailableSpace(uint64(source.Size))
err = f.CheckAvailableSpace(uint64(source.Size)) //nolint:gosec
if err == nil {
err = osutil.Copy(f.CopyRangeMethod.ToFS(), f.mtimefs, f.mtimefs, source.Name, tempName)
if err == nil {
@@ -1148,7 +1149,7 @@ func (f *sendReceiveFolder) reuseBlocks(blocks []protocol.BlockInfo, reused []in
// reuse.
tempBlocks, err := scanner.HashFile(f.ctx, f.ID, f.mtimefs, tempName, file.BlockSize(), nil)
if err != nil {
var caseErr *fs.ErrCaseConflict
var caseErr *fs.CaseConflictError
if errors.As(err, &caseErr) {
if rerr := f.mtimefs.Rename(caseErr.Real, tempName); rerr == nil {
tempBlocks, err = scanner.HashFile(f.ctx, f.ID, f.mtimefs, tempName, file.BlockSize(), nil)
@@ -1295,7 +1296,7 @@ func (f *sendReceiveFolder) copierRoutine(in <-chan copyBlocksState, pullChan ch
}
for state := range in {
if err := f.CheckAvailableSpace(uint64(state.file.Size)); err != nil {
if err := f.CheckAvailableSpace(uint64(state.file.Size)); err != nil { //nolint:gosec
state.fail(err)
// Nothing more to do for this failed file, since it would use to much disk space
out <- state.sharedPullerState
@@ -1461,7 +1462,7 @@ func (f *sendReceiveFolder) copyBlockFromFile(srcName string, srcOffset int64, s
}
func (*sendReceiveFolder) verifyBuffer(buf []byte, block protocol.BlockInfo) error {
if len(buf) != int(block.Size) {
if len(buf) != block.Size {
return fmt.Errorf("length mismatch %d != %d", len(buf), block.Size)
}
@@ -1489,8 +1490,7 @@ func (f *sendReceiveFolder) pullerRoutine(in <-chan pullBlockState, out chan<- *
// ongoing at any given time, based on the size of the blocks
// themselves.
state := state
bytes := int(state.block.Size)
bytes := state.block.Size
if err := requestLimiter.TakeWithContext(f.ctx, bytes); err != nil {
state.fail(err)
@@ -1713,7 +1713,7 @@ func (f *sendReceiveFolder) dbUpdaterRoutine(dbUpdateChan <-chan dbUpdateJob) {
// sync directories
for dir := range changedDirs {
delete(changedDirs, dir)
if !f.FolderConfiguration.DisableFsync {
if !f.DisableFsync {
fd, err := f.mtimefs.Open(dir)
if err != nil {
l.Debugf("fsync %q failed: %v", dir, err)
@@ -1996,7 +1996,7 @@ func (f *sendReceiveFolder) deleteDirOnDiskHandleChildren(dir string, scanChan c
// Lets just assume the file has changed.
scanChan <- path
hasToBeScanned = true
return nil
return nil //nolint:nilerr
}
if !cf.IsEquivalentOptional(diskFile, protocol.FileInfoComparison{
ModTimeWindow: f.modTimeWindow,
@@ -2055,7 +2055,7 @@ func (f *sendReceiveFolder) deleteDirOnDiskHandleChildren(dir string, scanChan c
// not changed.
func (f *sendReceiveFolder) scanIfItemChanged(name string, stat fs.FileInfo, item protocol.FileInfo, hasItem bool, fromDelete bool, scanChan chan<- string) (err error) {
defer func() {
if err == errModified {
if errors.Is(err, errModified) {
scanChan <- name
}
}()
+1 -1
View File
@@ -1014,7 +1014,7 @@ func TestPullCaseOnlyPerformFinish(t *testing.T) {
default:
}
var caseErr *fs.ErrCaseConflict
var caseErr *fs.CaseConflictError
if !errors.As(err, &caseErr) {
t.Error("Expected case conflict error, got", err)
}
+5 -4
View File
@@ -11,6 +11,7 @@ package model
import (
"context"
"errors"
"fmt"
"strings"
"time"
@@ -125,7 +126,7 @@ func (c *folderSummaryService) Summary(folder string) (*FolderSummary, error) {
var local, global, need, ro db.Counts
var ourSeq int64
var remoteSeq map[protocol.DeviceID]int64
errors, err := c.model.FolderErrors(folder)
errs, err := c.model.FolderErrors(folder)
if err == nil {
global, _ = c.model.GlobalSize(folder)
local, _ = c.model.LocalSize(folder, protocol.LocalDeviceID)
@@ -137,12 +138,12 @@ func (c *folderSummaryService) Summary(folder string) (*FolderSummary, error) {
// For API backwards compatibility (SyncTrayzor needs it) an empty folder
// summary is returned for not running folders, an error might actually be
// more appropriate
if err != nil && err != ErrFolderPaused && err != ErrFolderNotRunning {
if err != nil && !errors.Is(err, ErrFolderPaused) && !errors.Is(err, ErrFolderNotRunning) {
return nil, err
}
res.Errors = len(errors)
res.PullErrors = len(errors) // deprecated
res.Errors = len(errs)
res.PullErrors = len(errs) // deprecated
res.Invalid = "" // Deprecated, retains external API for now
+8 -18
View File
@@ -66,7 +66,8 @@ func newIndexHandler(conn protocol.Connection, downloads *deviceDownloadState, f
// about us. Lets check to see if we can start sending index
// updates directly or need to send the index from start...
if startInfo.local.IndexID == myIndexID {
switch startInfo.local.IndexID {
case myIndexID:
// They say they've seen our index ID before, so we can
// send a delta update only.
@@ -83,15 +84,17 @@ func newIndexHandler(conn protocol.Connection, downloads *deviceDownloadState, f
l.Debugf("Device %v folder %s is delta index compatible (mlv=%d)", conn.DeviceID().Short(), folder.Description(), startInfo.local.MaxSequence)
startSequence = startInfo.local.MaxSequence
}
} else if startInfo.local.IndexID != 0 {
case 0:
l.Debugf("Device %v folder %s has no index ID for us", conn.DeviceID().Short(), folder.Description())
default:
// They say they've seen an index ID from us, but it's
// not the right one. Either they are confused or we
// must have reset our database since last talking to
// them. We'll start with a full index transfer.
l.Infof("Device %v folder %s has mismatching index ID for us (%v != %v)", conn.DeviceID().Short(), folder.Description(), startInfo.local.IndexID, myIndexID)
startSequence = 0
} else {
l.Debugf("Device %v folder %s has no index ID for us", conn.DeviceID().Short(), folder.Description())
}
// This is the other side's description of themselves. We
@@ -418,11 +421,6 @@ func (s *indexHandler) receive(fs []protocol.FileInfo, update bool, op string, p
"precedingSeq": fs[i-1].Sequence,
})
}
// The local attributes should never be transmitted over the wire.
// Make sure they look like they weren't.
fs[i].LocalFlags = 0
fs[i].VersionHash = nil
}
// Verify the claimed last sequence number
@@ -478,22 +476,14 @@ func (s *indexHandler) logSequenceAnomaly(msg string, extra map[string]any) {
}
func prepareFileInfoForIndex(f protocol.FileInfo) protocol.FileInfo {
// Mark the file as invalid if any of the local bad stuff flags are set.
f.RawInvalid = f.IsInvalid()
// If the file is marked LocalReceive (i.e., changed locally on a
// receive only folder) we do not want it to ever become the
// globally best version, invalid or not.
if f.IsReceiveOnlyChanged() {
f.Version = protocol.Vector{}
}
// The trailer with the encrypted fileinfo is device local, don't send info
// about that to remotes
// The trailer with the encrypted fileinfo is device local, announce the size without it to remotes.
f.Size -= int64(f.EncryptionTrailerSize)
f.EncryptionTrailerSize = 0
// never sent externally
f.LocalFlags = 0
f.VersionHash = nil
f.InodeChangeNs = 0
return f
}
+1 -1
View File
@@ -18,7 +18,7 @@ var (
Name: "folder_state",
Help: "Current folder state",
}, []string{"folder"})
metricFolderSummary = promauto.NewGaugeVec(prometheus.GaugeOpts{
metricFolderSummary = promauto.NewGaugeVec(prometheus.GaugeOpts{ //nolint:promlinter
Namespace: "syncthing",
Subsystem: "model",
Name: "folder_summary",
+29 -22
View File
@@ -195,7 +195,7 @@ var (
ErrFolderMissing = errors.New("no such folder")
errNoVersioner = errors.New("folder has no versioner")
// errors about why a connection is closed
errStopped = errors.New("Syncthing is being stopped")
errStopped = errors.New("Syncthing is being stopped") //nolint:staticcheck
errEncryptionInvConfigLocal = errors.New("can't encrypt outgoing data because local data is encrypted (folder-type receive-encrypted)")
errEncryptionInvConfigRemote = errors.New("remote has encrypted data and encrypts that data for us - this is impossible")
errEncryptionNotEncryptedLocal = errors.New("remote expects to exchange encrypted data, but is configured for plain data")
@@ -460,6 +460,9 @@ func (m *model) warnAboutOverwritingProtectedFiles(cfg config.FolderConfiguratio
}
func (m *model) removeFolder(cfg config.FolderConfiguration) {
l.Infoln("Removing folder", cfg.Description())
defer l.Infoln("Removed folder", cfg.Description())
m.mut.RLock()
wait := m.folderRunners.StopAndWaitChan(cfg.ID, 0)
m.mut.RUnlock()
@@ -622,24 +625,26 @@ func (m *model) UsageReportingStats(report *contract.Report, version int, previe
for _, line := range lines {
// Allow prefixes to be specified in any order, but only once.
loop:
for {
if strings.HasPrefix(line, "!") && !seenPrefix[0] {
switch {
case strings.HasPrefix(line, "!") && !seenPrefix[0]:
seenPrefix[0] = true
line = line[1:]
report.IgnoreStats.Inverts++
} else if strings.HasPrefix(line, "(?i)") && !seenPrefix[1] {
case strings.HasPrefix(line, "(?i)") && !seenPrefix[1]:
seenPrefix[1] = true
line = line[4:]
report.IgnoreStats.Folded++
} else if strings.HasPrefix(line, "(?d)") && !seenPrefix[2] {
case strings.HasPrefix(line, "(?d)") && !seenPrefix[2]:
seenPrefix[2] = true
line = line[4:]
report.IgnoreStats.Deletable++
} else {
default:
seenPrefix[0] = false
seenPrefix[1] = false
seenPrefix[2] = false
break
break loop
}
}
@@ -1227,9 +1232,10 @@ func (m *model) ClusterConfig(conn protocol.Connection, cm *protocol.ClusterConf
for _, folder := range cm.Folders {
info := &clusterConfigDeviceInfo{}
for _, dev := range folder.Devices {
if dev.ID == m.id {
switch dev.ID {
case m.id:
info.local = dev
} else if dev.ID == deviceID {
case deviceID:
info.remote = dev
}
if info.local.ID != protocol.EmptyDeviceID && info.remote.ID != protocol.EmptyDeviceID {
@@ -1451,7 +1457,7 @@ func (m *model) ccHandleFolders(folders []protocol.Folder, deviceCfg config.Devi
sameError := false
m.mut.Lock()
if devs, ok := m.folderEncryptionFailures[folder.ID]; ok {
sameError = devs[deviceID] == err
sameError = devs[deviceID] == err //nolint:errorlint
} else {
m.folderEncryptionFailures[folder.ID] = make(map[protocol.DeviceID]error)
}
@@ -1461,7 +1467,8 @@ func (m *model) ccHandleFolders(folders []protocol.Folder, deviceCfg config.Devi
if sameError {
l.Debugln(msg)
} else {
if rerr, ok := err.(*redactedError); ok {
var rerr *redactedError
if errors.As(err, &rerr) {
err = rerr.redacted
}
m.evLogger.Log(events.Failure, err.Error())
@@ -2015,7 +2022,7 @@ func (m *model) Request(conn protocol.Connection, req *protocol.Request) (out pr
// The requestResponse releases the bytes to the buffer pool and the
// limiters when its Close method is called.
res := newLimitedRequestResponse(int(req.Size), limiter, m.globalRequestLimiter)
res := newLimitedRequestResponse(req.Size, limiter, m.globalRequestLimiter)
defer func() {
// Close it ourselves if it isn't returned due to an error
@@ -2061,16 +2068,17 @@ func (m *model) Request(conn protocol.Connection, req *protocol.Request) (out pr
}
n, err := readOffsetIntoBuf(folderFs, req.Name, req.Offset, res.data)
if fs.IsNotExist(err) {
switch {
case fs.IsNotExist(err):
l.Debugf("%v REQ(in) file doesn't exist: %s: %q / %q o=%d s=%d", m, deviceID.Short(), req.Folder, req.Name, req.Offset, req.Size)
return nil, protocol.ErrNoSuchFile
} else if err == io.EOF {
case errors.Is(err, io.EOF):
// Read beyond end of file. This might indicate a problem, or it
// might be a short block that gets padded when read for encrypted
// folders. We ignore the error and let the hash validation in the
// next step take care of it, by only hashing the part we actually
// managed to read.
} else if err != nil {
case err != nil:
l.Debugf("%v REQ(in) failed reading file (%v): %s: %q / %q o=%d s=%d", m, err, deviceID.Short(), req.Folder, req.Name, req.Offset, req.Size)
return nil, protocol.ErrGeneric
}
@@ -2230,13 +2238,13 @@ func (m *model) SetIgnores(folder string, content []string) error {
func (m *model) setIgnores(cfg config.FolderConfiguration, content []string) error {
err := cfg.CheckPath()
if err == config.ErrPathMissing {
if errors.Is(err, config.ErrPathMissing) {
if err = cfg.CreateRoot(); err != nil {
return fmt.Errorf("failed to create folder root: %w", err)
}
err = cfg.CheckPath()
}
if err != nil && err != config.ErrMarkerMissing {
if err != nil && !errors.Is(err, config.ErrMarkerMissing) {
return err
}
@@ -2498,7 +2506,6 @@ func (m *model) ScanFolders() map[string]error {
wg := sync.NewWaitGroup()
wg.Add(len(folders))
for _, folder := range folders {
folder := folder
go func() {
err := m.ScanFolder(folder)
if err != nil {
@@ -2677,7 +2684,7 @@ func (m *model) WatchError(folder string) error {
runner, _ := m.folderRunners.Get(folder)
m.mut.RUnlock()
if err != nil {
return nil // If the folder isn't running, there's no error to report.
return nil //nolint:nilerr // If the folder isn't running, there's no error to report.
}
return runner.WatchError()
}
@@ -2744,7 +2751,7 @@ func (m *model) GlobalDirectoryTree(folder, prefix string, levels int, dirsOnly
prefix = osutil.NativeFilename(prefix)
if prefix != "" && !strings.HasSuffix(prefix, sep) {
prefix = prefix + sep
prefix += sep
}
for f, err := range itererr.Zip(m.sdb.AllGlobalFilesPrefix(folder, prefix)) {
@@ -2753,7 +2760,7 @@ func (m *model) GlobalDirectoryTree(folder, prefix string, levels int, dirsOnly
}
// Don't include the prefix itself.
if f.Invalid || f.Deleted || strings.HasPrefix(prefix, f.Name) {
if f.IsInvalid() || f.Deleted || strings.HasPrefix(prefix, f.Name) {
continue
}
@@ -3456,8 +3463,8 @@ type updatedPendingFolder struct {
// redactPathError checks if the error is actually a os.PathError, and if yes
// returns a redactedError with the path removed.
func redactPathError(err error) (error, bool) {
perr, ok := err.(*os.PathError)
if !ok {
var perr *os.PathError
if !errors.As(err, &perr) {
return nil, false
}
return &redactedError{
+3 -2
View File
@@ -3498,6 +3498,7 @@ func TestScanDeletedROChangedOnSR(t *testing.T) {
}
// A remote must have the file, otherwise the deletion below is
// automatically resolved as not a ro-changed item.
file.LocalFlags = 0 // clear as we're skipping the code path where it would otherwise be cleared naturally
must(t, m.IndexUpdate(conn, &protocol.IndexUpdate{Folder: fcfg.ID, Files: []protocol.FileInfo{file}}))
must(t, ffs.Remove(name))
@@ -3609,7 +3610,7 @@ func TestIssue6961(t *testing.T) {
// Remote, valid and existing file
must(t, m.Index(conn1, &protocol.Index{Folder: fcfg.ID, Files: []protocol.FileInfo{{Name: name, Version: version, Sequence: 1}}}))
// Remote, invalid (receive-only) and existing file
must(t, m.Index(conn2, &protocol.Index{Folder: fcfg.ID, Files: []protocol.FileInfo{{Name: name, RawInvalid: true, Sequence: 1}}}))
must(t, m.Index(conn2, &protocol.Index{Folder: fcfg.ID, Files: []protocol.FileInfo{{Name: name, LocalFlags: protocol.FlagLocalRemoteInvalid, Sequence: 1}}}))
// Create a local file
if fd, err := tfs.OpenFile(name, fs.OptCreate, 0o666); err != nil {
t.Fatal(err)
@@ -3635,7 +3636,7 @@ func TestIssue6961(t *testing.T) {
m.ScanFolders()
// Drop the remote index, add some other file.
must(t, m.Index(conn2, &protocol.Index{Folder: fcfg.ID, Files: []protocol.FileInfo{{Name: "bar", RawInvalid: true, Sequence: 1}}}))
must(t, m.Index(conn2, &protocol.Index{Folder: fcfg.ID, Files: []protocol.FileInfo{{Name: "bar", LocalFlags: protocol.FlagLocalRemoteInvalid, Sequence: 1}}}))
// Pause and unpause folder to create new db.FileSet and thus recalculate everything
pauseFolder(t, wcfg, fcfg.ID, true)
+1 -1
View File
@@ -384,7 +384,7 @@ func writeEncryptionTrailer(file protocol.FileInfo, writer io.WriterAt) (int64,
if err != nil {
return 0, err
}
binary.BigEndian.PutUint32(bs[n:], uint32(n))
binary.BigEndian.PutUint32(bs[n:], uint32(n)) //nolint:gosec
bs = bs[:n+4]
if _, err := writer.WriteAt(bs, wireFile.Size); err != nil {
+6 -6
View File
@@ -257,7 +257,7 @@ func (s *Service) verifyExistingLocked(ctx context.Context, mapping *Mapping, na
// extAddrs either contains one IPv4 address, or possibly several
// IPv6 addresses all using the same port. Therefore the first
// entry always has the external port.
responseAddrs, err := s.tryNATDevice(ctx, nat, mapping.address, extAddrs[0].Port, leaseTime)
responseAddrs, err := s.tryNATDevice(ctx, nat, mapping.address, extAddrs[0].Port, mapping.protocol, leaseTime)
if err != nil {
l.Infof("Failed to renew %s -> %v open port on %s: %s", mapping, extAddrs, id, err)
mapping.removeAddressLocked(id)
@@ -309,7 +309,7 @@ func (s *Service) acquireNewLocked(ctx context.Context, mapping *Mapping, nats m
continue
}
addrs, err := s.tryNATDevice(ctx, nat, mapping.address, 0, leaseTime)
addrs, err := s.tryNATDevice(ctx, nat, mapping.address, 0, mapping.protocol, leaseTime)
if err != nil {
l.Infof("Failed to acquire %s open port on %s: %s", mapping, id, err)
continue
@@ -325,14 +325,14 @@ func (s *Service) acquireNewLocked(ctx context.Context, mapping *Mapping, nats m
// tryNATDevice tries to acquire a port mapping for the given internal address to
// the given external port. If external port is 0, picks a pseudo-random port.
func (s *Service) tryNATDevice(ctx context.Context, natd Device, intAddr Address, extPort int, leaseTime time.Duration) ([]Address, error) {
func (s *Service) tryNATDevice(ctx context.Context, natd Device, intAddr Address, extPort int, protocol Protocol, leaseTime time.Duration) ([]Address, error) {
var err error
var port int
// For IPv6, we just try to create the pinhole. If it fails, nothing can be done (probably no IGDv2 support).
// If it already exists, the relevant UPnP standard requires that the gateway recognizes this and updates the lease time.
// Since we usually have a global unicast IPv6 address so no conflicting mappings, we just request the port we're running on
if natd.SupportsIPVersion(IPv6Only) {
ipaddrs, err := natd.AddPinhole(ctx, TCP, intAddr, leaseTime)
ipaddrs, err := natd.AddPinhole(ctx, protocol, intAddr, leaseTime)
var addrs []Address
for _, ipaddr := range ipaddrs {
addrs = append(addrs, Address{
@@ -354,7 +354,7 @@ func (s *Service) tryNATDevice(ctx context.Context, natd Device, intAddr Address
if extPort != 0 {
// First try renewing our existing mapping, if we have one.
name := fmt.Sprintf("syncthing-%d", extPort)
port, err = natd.AddPortMapping(ctx, TCP, intAddr.Port, extPort, name, leaseTime)
port, err = natd.AddPortMapping(ctx, protocol, intAddr.Port, extPort, name, leaseTime)
if err == nil {
extPort = port
goto findIP
@@ -372,7 +372,7 @@ func (s *Service) tryNATDevice(ctx context.Context, natd Device, intAddr Address
// Then try up to ten random ports.
extPort = 1024 + predictableRand.Intn(65535-1024)
name := fmt.Sprintf("syncthing-%d", extPort)
port, err = natd.AddPortMapping(ctx, TCP, intAddr.Port, extPort, name, leaseTime)
port, err = natd.AddPortMapping(ctx, protocol, intAddr.Port, extPort, name, leaseTime)
if err == nil {
extPort = port
goto findIP
+42 -27
View File
@@ -17,26 +17,33 @@ import (
"github.com/syncthing/syncthing/lib/build"
)
type FlagLocal uint32
// FileInfo.LocalFlags flags
const (
FlagLocalUnsupported = 1 << 0 // 1: The kind is unsupported, e.g. symlinks on Windows
FlagLocalIgnored = 1 << 1 // 2: Matches local ignore patterns
FlagLocalMustRescan = 1 << 2 // 4: Doesn't match content on disk, must be rechecked fully
FlagLocalReceiveOnly = 1 << 3 // 8: Change detected on receive only folder
FlagLocalGlobal = 1 << 4 // 16: This is the global file version
FlagLocalNeeded = 1 << 5 // 32: We need this file
FlagLocalUnsupported FlagLocal = 1 << 0 // 1: The kind is unsupported, e.g. symlinks on Windows
FlagLocalIgnored FlagLocal = 1 << 1 // 2: Matches local ignore patterns
FlagLocalMustRescan FlagLocal = 1 << 2 // 4: Doesn't match content on disk, must be rechecked fully
FlagLocalReceiveOnly FlagLocal = 1 << 3 // 8: Change detected on receive only folder
FlagLocalGlobal FlagLocal = 1 << 4 // 16: This is the global file version
FlagLocalNeeded FlagLocal = 1 << 5 // 32: We need this file
FlagLocalRemoteInvalid FlagLocal = 1 << 6 // 64: The remote marked this as invalid
// Flags that should result in the Invalid bit on outgoing updates
LocalInvalidFlags = FlagLocalUnsupported | FlagLocalIgnored | FlagLocalMustRescan | FlagLocalReceiveOnly
// Flags that should result in the Invalid bit on outgoing updates (or had it on ingoing ones)
LocalInvalidFlags = FlagLocalUnsupported | FlagLocalIgnored | FlagLocalMustRescan | FlagLocalReceiveOnly | FlagLocalRemoteInvalid
// Flags that should result in a file being in conflict with its
// successor, due to us not having an up to date picture of its state on
// disk.
LocalConflictFlags = FlagLocalUnsupported | FlagLocalIgnored | FlagLocalReceiveOnly
LocalAllFlags = FlagLocalUnsupported | FlagLocalIgnored | FlagLocalMustRescan | FlagLocalReceiveOnly | FlagLocalGlobal | FlagLocalNeeded
LocalAllFlags = FlagLocalUnsupported | FlagLocalIgnored | FlagLocalMustRescan | FlagLocalReceiveOnly | FlagLocalGlobal | FlagLocalNeeded | FlagLocalRemoteInvalid
)
func (f FlagLocal) IsInvalid() bool {
return f&LocalInvalidFlags != 0
}
// BlockSizes is the list of valid block sizes, from min to max
var BlockSizes []int
@@ -82,7 +89,9 @@ type FileInfo struct {
// host only. It is not part of the protocol, doesn't get sent or
// received (we make sure to zero it), nonetheless we need it on our
// struct and to be able to serialize it to/from the database.
LocalFlags uint32
// It does carry the info to decide if the file is invalid, which is part of
// the protocol.
LocalFlags FlagLocal
// The version_hash is an implementation detail and not part of the wire
// format.
@@ -97,7 +106,6 @@ type FileInfo struct {
EncryptionTrailerSize int
Deleted bool
RawInvalid bool
NoPermissions bool
truncated bool // was created from a truncated file info without blocks
@@ -128,11 +136,11 @@ func (f *FileInfo) ToWire(withInternalFields bool) *bep.FileInfo {
BlockSize: f.RawBlockSize,
Platform: f.Platform.toWire(),
Deleted: f.Deleted,
Invalid: f.RawInvalid,
Invalid: f.IsInvalid(),
NoPermissions: f.NoPermissions,
}
if withInternalFields {
w.LocalFlags = f.LocalFlags
w.LocalFlags = uint32(f.LocalFlags)
w.VersionHash = f.VersionHash
w.InodeChangeNs = f.InodeChangeNs
w.EncryptionTrailerSize = int32(f.EncryptionTrailerSize)
@@ -207,6 +215,10 @@ type FileInfoWithoutBlocks interface {
}
func fileInfoFromWireWithBlocks(w FileInfoWithoutBlocks, blocks []BlockInfo) FileInfo {
var localFlags FlagLocal
if w.GetInvalid() {
localFlags = FlagLocalRemoteInvalid
}
return FileInfo{
Name: w.GetName(),
Size: w.GetSize(),
@@ -224,14 +236,14 @@ func fileInfoFromWireWithBlocks(w FileInfoWithoutBlocks, blocks []BlockInfo) Fil
RawBlockSize: w.GetBlockSize(),
Platform: platformDataFromWire(w.GetPlatform()),
Deleted: w.GetDeleted(),
RawInvalid: w.GetInvalid(),
LocalFlags: localFlags,
NoPermissions: w.GetNoPermissions(),
}
}
func FileInfoFromDB(w *bep.FileInfo) FileInfo {
f := FileInfoFromWire(w)
f.LocalFlags = w.LocalFlags
f.LocalFlags = FlagLocal(w.LocalFlags)
f.VersionHash = w.VersionHash
f.InodeChangeNs = w.InodeChangeNs
f.EncryptionTrailerSize = int(w.EncryptionTrailerSize)
@@ -240,7 +252,7 @@ func FileInfoFromDB(w *bep.FileInfo) FileInfo {
func FileInfoFromDBTruncated(w FileInfoWithoutBlocks) FileInfo {
f := fileInfoFromWireWithBlocks(w, nil)
f.LocalFlags = w.GetLocalFlags()
f.LocalFlags = FlagLocal(w.GetLocalFlags())
f.VersionHash = w.GetVersionHash()
f.InodeChangeNs = w.GetInodeChangeNs()
f.EncryptionTrailerSize = int(w.GetEncryptionTrailerSize())
@@ -252,13 +264,13 @@ func (f FileInfo) String() string {
switch f.Type {
case FileInfoTypeDirectory:
return fmt.Sprintf("Directory{Name:%q, Sequence:%d, Permissions:0%o, ModTime:%v, Version:%v, VersionHash:%x, Deleted:%v, Invalid:%v, LocalFlags:0x%x, NoPermissions:%v, Platform:%v, InodeChangeTime:%v}",
f.Name, f.Sequence, f.Permissions, f.ModTime(), f.Version, f.VersionHash, f.Deleted, f.RawInvalid, f.LocalFlags, f.NoPermissions, f.Platform, f.InodeChangeTime())
f.Name, f.Sequence, f.Permissions, f.ModTime(), f.Version, f.VersionHash, f.Deleted, f.IsInvalid(), f.LocalFlags, f.NoPermissions, f.Platform, f.InodeChangeTime())
case FileInfoTypeFile:
return fmt.Sprintf("File{Name:%q, Sequence:%d, Permissions:0%o, ModTime:%v, Version:%v, VersionHash:%x, Length:%d, Deleted:%v, Invalid:%v, LocalFlags:0x%x, NoPermissions:%v, BlockSize:%d, NumBlocks:%d, BlocksHash:%x, Platform:%v, InodeChangeTime:%v}",
f.Name, f.Sequence, f.Permissions, f.ModTime(), f.Version, f.VersionHash, f.Size, f.Deleted, f.RawInvalid, f.LocalFlags, f.NoPermissions, f.RawBlockSize, len(f.Blocks), f.BlocksHash, f.Platform, f.InodeChangeTime())
f.Name, f.Sequence, f.Permissions, f.ModTime(), f.Version, f.VersionHash, f.Size, f.Deleted, f.IsInvalid(), f.LocalFlags, f.NoPermissions, f.RawBlockSize, len(f.Blocks), f.BlocksHash, f.Platform, f.InodeChangeTime())
case FileInfoTypeSymlink, FileInfoTypeSymlinkDirectory, FileInfoTypeSymlinkFile:
return fmt.Sprintf("Symlink{Name:%q, Type:%v, Sequence:%d, Version:%v, VersionHash:%x, Deleted:%v, Invalid:%v, LocalFlags:0x%x, NoPermissions:%v, SymlinkTarget:%q, Platform:%v, InodeChangeTime:%v}",
f.Name, f.Type, f.Sequence, f.Version, f.VersionHash, f.Deleted, f.RawInvalid, f.LocalFlags, f.NoPermissions, f.SymlinkTarget, f.Platform, f.InodeChangeTime())
f.Name, f.Type, f.Sequence, f.Version, f.VersionHash, f.Deleted, f.IsInvalid(), f.LocalFlags, f.NoPermissions, f.SymlinkTarget, f.Platform, f.InodeChangeTime())
default:
panic("mystery file type detected")
}
@@ -269,7 +281,7 @@ func (f FileInfo) IsDeleted() bool {
}
func (f FileInfo) IsInvalid() bool {
return f.RawInvalid || f.LocalFlags&LocalInvalidFlags != 0
return f.LocalFlags.IsInvalid()
}
func (f FileInfo) IsUnsupported() bool {
@@ -342,7 +354,7 @@ func (f FileInfo) FileName() string {
return f.Name
}
func (f FileInfo) FileLocalFlags() uint32 {
func (f FileInfo) FileLocalFlags() FlagLocal {
return f.LocalFlags
}
@@ -386,7 +398,7 @@ type FileInfoComparison struct {
ModTimeWindow time.Duration
IgnorePerms bool
IgnoreBlocks bool
IgnoreFlags uint32
IgnoreFlags FlagLocal
IgnoreOwnership bool
IgnoreXattrs bool
}
@@ -533,8 +545,7 @@ func (f *FileInfo) SetDeleted(by ShortID) {
f.setNoContent()
}
func (f *FileInfo) setLocalFlags(flags uint32) {
f.RawInvalid = false
func (f *FileInfo) setLocalFlags(flags FlagLocal) {
f.LocalFlags = flags
f.setNoContent()
}
@@ -847,9 +858,13 @@ func unixOwnershipEqual(a, b *UnixData) bool {
if a == nil || b == nil {
return false
}
ownerEqual := a.OwnerName == "" || b.OwnerName == "" || a.OwnerName == b.OwnerName
groupEqual := a.GroupName == "" || b.GroupName == "" || a.GroupName == b.GroupName
return a.UID == b.UID && a.GID == b.GID && ownerEqual && groupEqual
if a.UID == b.UID && a.GID == b.GID {
return true
}
if a.OwnerName == b.OwnerName && a.GroupName == b.GroupName {
return true
}
return false
}
func windowsOwnershipEqual(a, b *WindowsData) bool {
+42 -6
View File
@@ -45,7 +45,7 @@ func TestIsEquivalent(t *testing.T) {
b FileInfo
ignPerms *bool // nil means should not matter, we'll test both variants
ignBlocks *bool
ignFlags uint32
ignFlags FlagLocal
eq bool
}
cases := []testCase{
@@ -75,8 +75,8 @@ func TestIsEquivalent(t *testing.T) {
eq: false,
},
{
a: FileInfo{RawInvalid: false},
b: FileInfo{RawInvalid: true},
a: FileInfo{LocalFlags: 0},
b: FileInfo{LocalFlags: FlagLocalRemoteInvalid},
eq: false,
},
{
@@ -100,8 +100,8 @@ func TestIsEquivalent(t *testing.T) {
eq: false,
},
{
a: FileInfo{RawInvalid: true},
b: FileInfo{RawInvalid: true},
a: FileInfo{LocalFlags: FlagLocalRemoteInvalid},
b: FileInfo{LocalFlags: FlagLocalRemoteInvalid},
eq: true,
},
{
@@ -110,7 +110,7 @@ func TestIsEquivalent(t *testing.T) {
eq: true,
},
{
a: FileInfo{RawInvalid: true},
a: FileInfo{LocalFlags: FlagLocalRemoteInvalid},
b: FileInfo{LocalFlags: FlagLocalUnsupported},
eq: true,
},
@@ -196,6 +196,42 @@ func TestIsEquivalent(t *testing.T) {
b: FileInfo{Type: FileInfoTypeFile, SymlinkTarget: []byte("b")},
eq: true,
},
// Unix Ownership should be the same
{
a: FileInfo{Platform: PlatformData{Unix: &UnixData{OwnerName: "A", GroupName: "A", UID: 1000, GID: 1000}}},
b: FileInfo{Platform: PlatformData{Unix: &UnixData{OwnerName: "A", GroupName: "A", UID: 1000, GID: 1000}}},
eq: true,
},
// ... but matching ID is enough
{
a: FileInfo{Platform: PlatformData{Unix: &UnixData{OwnerName: "A", GroupName: "A", UID: 1000, GID: 1000}}},
b: FileInfo{Platform: PlatformData{Unix: &UnixData{OwnerName: "B", GroupName: "B", UID: 1000, GID: 1000}}},
eq: true,
},
// ... or matching name
{
a: FileInfo{Platform: PlatformData{Unix: &UnixData{OwnerName: "A", GroupName: "A", UID: 1000, GID: 1000}}},
b: FileInfo{Platform: PlatformData{Unix: &UnixData{OwnerName: "A", GroupName: "A", UID: 1001, GID: 1001}}},
eq: true,
},
// ... or empty name
{
a: FileInfo{Platform: PlatformData{Unix: &UnixData{OwnerName: "A", GroupName: "A", UID: 1000, GID: 1000}}},
b: FileInfo{Platform: PlatformData{Unix: &UnixData{OwnerName: "", GroupName: "", UID: 1000, GID: 1000}}},
eq: true,
},
// ... but not different ownership
{
a: FileInfo{Platform: PlatformData{Unix: &UnixData{OwnerName: "A", GroupName: "A", UID: 1000, GID: 1000}}},
b: FileInfo{Platform: PlatformData{Unix: &UnixData{OwnerName: "B", GroupName: "B", UID: 1001, GID: 1001}}},
eq: false,
},
// or missing ownership
{
a: FileInfo{Platform: PlatformData{Unix: &UnixData{OwnerName: "A", GroupName: "A", UID: 1000, GID: 1000}}},
b: FileInfo{Platform: PlatformData{}},
eq: false,
},
}
if build.IsWindows {
+1 -1
View File
@@ -15,7 +15,7 @@ func TestWinsConflict(t *testing.T) {
// The first should always win over the second
{{ModifiedS: 42}, {ModifiedS: 41}},
{{ModifiedS: 41}, {ModifiedS: 42, Deleted: true}},
{{Deleted: true}, {ModifiedS: 10, RawInvalid: true}},
{{Deleted: true}, {ModifiedS: 10, LocalFlags: FlagLocalRemoteInvalid}},
{{ModifiedS: 41, Version: Vector{Counters: []Counter{{ID: 42, Value: 2}, {ID: 43, Value: 1}}}}, {ModifiedS: 41, Version: Vector{Counters: []Counter{{ID: 42, Value: 1}, {ID: 43, Value: 2}}}}},
}
+3 -1
View File
@@ -357,11 +357,13 @@ func encryptFileInfo(keyGen *KeyGenerator, fi FileInfo, folderKey *[keySize]byte
Permissions: 0o644,
ModifiedS: 1234567890, // Sat Feb 14 00:31:30 CET 2009
Deleted: fi.Deleted,
RawInvalid: fi.IsInvalid(),
Version: version,
Sequence: fi.Sequence,
Encrypted: encryptedFI,
}
if fi.IsInvalid() {
enc.LocalFlags = FlagLocalRemoteInvalid
}
if typ == FileInfoTypeFile {
enc.Size = offset // new total file size
enc.Blocks = blocks
+1 -1
View File
@@ -54,7 +54,7 @@ type Config struct {
// events are emitted. Negative number means disabled.
ProgressTickIntervalS int
// Local flags to set on scanned files
LocalFlags uint32
LocalFlags protocol.FlagLocal
// Modification time is to be considered unchanged if the difference is lower.
ModTimeWindow time.Duration
// Event logger to which the scan progress events are sent
+1 -1
View File
@@ -567,7 +567,7 @@ func TestScanOwnershipWindows(t *testing.T) {
}
}
func walkDir(fs fs.Filesystem, dir string, cfiler CurrentFiler, matcher *ignore.Matcher, localFlags uint32) []protocol.FileInfo {
func walkDir(fs fs.Filesystem, dir string, cfiler CurrentFiler, matcher *ignore.Matcher, localFlags protocol.FlagLocal) []protocol.FileInfo {
cfg, cancel := testConfig()
defer cancel()
cfg.Filesystem = fs
+2 -2
View File
@@ -60,8 +60,8 @@ func LoadOrGenerateCertificate(certFile, keyFile string) (tls.Certificate, error
}
func GenerateCertificate(certFile, keyFile string) (tls.Certificate, error) {
l.Infof("Generating ECDSA key and certificate for %s...", tlsDefaultCommonName)
return tlsutil.NewCertificate(certFile, keyFile, tlsDefaultCommonName, deviceCertLifetimeDays)
l.Infof("Generating key and certificate for %s...", tlsDefaultCommonName)
return tlsutil.NewCertificate(certFile, keyFile, tlsDefaultCommonName, deviceCertLifetimeDays, false)
}
func DefaultConfig(path string, myID protocol.DeviceID, evLogger events.Logger, skipPortProbing bool) (config.Wrapper, error) {
+41 -11
View File
@@ -8,6 +8,7 @@ package tlsutil
import (
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rsa"
"crypto/tls"
@@ -87,9 +88,28 @@ func SecureDefaultWithTLS12() *tls.Config {
}
}
// generateCertificate generates a PEM formatted key pair and self-signed certificate in memory.
func generateCertificate(commonName string, lifetimeDays int) (*pem.Block, *pem.Block, error) {
priv, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
// generateCertificate generates a PEM formatted key pair and self-signed
// certificate in memory. The compatible flag indicates whether we aim for
// compatibility (browsers) or maximum efficiency/security (sync
// connections).
func generateCertificate(commonName string, lifetimeDays int, compatible bool) (*pem.Block, *pem.Block, error) {
var pub, priv any
var err error
var sigAlgo x509.SignatureAlgorithm
if compatible {
// For browser connections we prefer ECDSA-P256
sigAlgo = x509.ECDSAWithSHA256
var pk *ecdsa.PrivateKey
pk, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err == nil {
priv = pk
pub = pk.Public()
}
} else {
// For sync connections we use Ed25519
sigAlgo = x509.PureEd25519
pub, priv, err = ed25519.GenerateKey(rand.Reader)
}
if err != nil {
return nil, nil, fmt.Errorf("generate key: %w", err)
}
@@ -110,13 +130,13 @@ func generateCertificate(commonName string, lifetimeDays int) (*pem.Block, *pem.
DNSNames: []string{commonName},
NotBefore: notBefore,
NotAfter: notAfter,
SignatureAlgorithm: x509.ECDSAWithSHA256,
SignatureAlgorithm: sigAlgo,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
BasicConstraintsValid: true,
}
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, priv.Public(), priv)
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, pub, priv)
if err != nil {
return nil, nil, fmt.Errorf("create cert: %w", err)
}
@@ -130,9 +150,12 @@ func generateCertificate(commonName string, lifetimeDays int) (*pem.Block, *pem.
return certBlock, keyBlock, nil
}
// NewCertificate generates and returns a new TLS certificate, saved to the given PEM files.
func NewCertificate(certFile, keyFile string, commonName string, lifetimeDays int) (tls.Certificate, error) {
certBlock, keyBlock, err := generateCertificate(commonName, lifetimeDays)
// NewCertificate generates and returns a new TLS certificate, saved to the
// given PEM files. The compatible flag indicates whether we aim for
// compatibility (browsers) or maximum efficiency/security (sync
// connections).
func NewCertificate(certFile, keyFile string, commonName string, lifetimeDays int, compatible bool) (tls.Certificate, error) {
certBlock, keyBlock, err := generateCertificate(commonName, lifetimeDays, compatible)
if err != nil {
return tls.Certificate{}, err
}
@@ -162,9 +185,10 @@ func NewCertificate(certFile, keyFile string, commonName string, lifetimeDays in
return tls.X509KeyPair(pem.EncodeToMemory(certBlock), pem.EncodeToMemory(keyBlock))
}
// NewCertificateInMemory generates and returns a new TLS certificate, kept only in memory.
// NewCertificateInMemory generates and returns a new TLS certificate, kept
// only in memory.
func NewCertificateInMemory(commonName string, lifetimeDays int) (tls.Certificate, error) {
certBlock, keyBlock, err := generateCertificate(commonName, lifetimeDays)
certBlock, keyBlock, err := generateCertificate(commonName, lifetimeDays, false)
if err != nil {
return tls.Certificate{}, err
}
@@ -246,7 +270,13 @@ func pemBlockForKey(priv interface{}) (*pem.Block, error) {
return nil, err
}
return &pem.Block{Type: "EC PRIVATE KEY", Bytes: b}, nil
case ed25519.PrivateKey:
bs, err := x509.MarshalPKCS8PrivateKey(k)
if err != nil {
return nil, err
}
return &pem.Block{Type: "PRIVATE KEY", Bytes: bs}, nil
default:
return nil, errors.New("unknown key type")
return nil, fmt.Errorf("unknown key type: %T", priv)
}
}
+3 -3
View File
@@ -440,7 +440,7 @@ func (a *aggregator) updateConfig(folderCfg config.FolderConfiguration) {
if maxDelay := folderCfg.FSWatcherTimeoutS; maxDelay > 0 {
// FSWatcherTimeoutS is set explicitly so use that, but it also
// can't be lower than FSWatcherDelayS
a.notifyTimeout = time.Duration(max(maxDelay, folderCfg.FSWatcherDelayS)) * time.Second
a.notifyTimeout = time.Duration(max(maxDelay, folderCfg.FSWatcherDelayS) * float64(time.Second))
} else {
// Use the default FSWatcherTimeoutS calculation
a.notifyTimeout = notifyTimeout(folderCfg.FSWatcherDelayS)
@@ -471,10 +471,10 @@ func notifyTimeout(eventDelayS float64) time.Duration {
longDelayTimeout = time.Minute
)
if eventDelayS < shortDelayS {
return time.Duration(eventDelayS*shortDelayMultiplicator) * time.Second
return time.Duration(eventDelayS * shortDelayMultiplicator * float64(time.Second))
}
if eventDelayS < longDelayS {
return longDelayTimeout
}
return time.Duration(eventDelayS) * time.Second
return time.Duration(eventDelayS * float64(time.Second))
}
+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]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
.TH "STDISCOSRV" "1" "Jun 01, 2025" "v1.29.6" "Syncthing"
.TH "STDISCOSRV" "1" "Jun 14, 2025" "v1.29.7" "Syncthing"
.SH NAME
stdiscosrv \- Syncthing Discovery Server
.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]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
.TH "STRELAYSRV" "1" "Jun 01, 2025" "v1.29.6" "Syncthing"
.TH "STRELAYSRV" "1" "Jun 14, 2025" "v1.29.7" "Syncthing"
.SH NAME
strelaysrv \- Syncthing Relay Server
.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]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
.TH "SYNCTHING-BEP" "7" "Jun 01, 2025" "v1.29.6" "Syncthing"
.TH "SYNCTHING-BEP" "7" "Jun 14, 2025" "v1.29.7" "Syncthing"
.SH NAME
syncthing-bep \- Block Exchange Protocol v1
.SH INTRODUCTION AND DEFINITIONS
+4 -3
View File
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
.TH "SYNCTHING-CONFIG" "5" "Jun 01, 2025" "v1.29.6" "Syncthing"
.TH "SYNCTHING-CONFIG" "5" "Jun 14, 2025" "v1.29.7" "Syncthing"
.SH NAME
syncthing-config \- Syncthing Configuration
.SH SYNOPSIS
@@ -600,8 +600,9 @@ to \fB\-1\fP to always use weak hash. Default is \fB25\fP\&.
.INDENT 0.0
.TP
.B markerName
Name of a directory or file in the folder root to be used as
\fI\%How do I serve a folder from a read only filesystem?\fP\&. Default is \fB\&.stfolder\fP\&.
Name of a directory or file in the folder root to be used as a marker \- see
\fI\%marker FAQ\fP for its purpose.
A marker directory is only created by Syncthing for the default \fB\&.stfolder\fP, not otherwise.
.UNINDENT
.INDENT 0.0
.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]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
.TH "SYNCTHING-DEVICE-IDS" "7" "Jun 01, 2025" "v1.29.6" "Syncthing"
.TH "SYNCTHING-DEVICE-IDS" "7" "Jun 14, 2025" "v1.29.7" "Syncthing"
.SH NAME
syncthing-device-ids \- Understanding Device IDs
.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]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
.TH "SYNCTHING-EVENT-API" "7" "Jun 01, 2025" "v1.29.6" "Syncthing"
.TH "SYNCTHING-EVENT-API" "7" "Jun 14, 2025" "v1.29.7" "Syncthing"
.SH NAME
syncthing-event-api \- Event API
.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]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
.TH "SYNCTHING-FAQ" "7" "Jun 01, 2025" "v1.29.6" "Syncthing"
.TH "SYNCTHING-FAQ" "7" "Jun 14, 2025" "v1.29.7" "Syncthing"
.SH NAME
syncthing-faq \- Frequently Asked Questions
.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]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
.TH "SYNCTHING-GLOBALDISCO" "7" "Jun 01, 2025" "v1.29.6" "Syncthing"
.TH "SYNCTHING-GLOBALDISCO" "7" "Jun 14, 2025" "v1.29.7" "Syncthing"
.SH NAME
syncthing-globaldisco \- Global Discovery Protocol v3
.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]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
.TH "SYNCTHING-LOCALDISCO" "7" "Jun 01, 2025" "v1.29.6" "Syncthing"
.TH "SYNCTHING-LOCALDISCO" "7" "Jun 14, 2025" "v1.29.7" "Syncthing"
.SH NAME
syncthing-localdisco \- Local Discovery Protocol v4
.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]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
.TH "SYNCTHING-NETWORKING" "7" "Jun 01, 2025" "v1.29.6" "Syncthing"
.TH "SYNCTHING-NETWORKING" "7" "Jun 14, 2025" "v1.29.7" "Syncthing"
.SH NAME
syncthing-networking \- Firewall 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]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
.TH "SYNCTHING-RELAY" "7" "Jun 01, 2025" "v1.29.6" "Syncthing"
.TH "SYNCTHING-RELAY" "7" "Jun 14, 2025" "v1.29.7" "Syncthing"
.SH NAME
syncthing-relay \- Relay Protocol v1
.SH WHAT IS A RELAY?
+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]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
.TH "SYNCTHING-REST-API" "7" "Jun 01, 2025" "v1.29.6" "Syncthing"
.TH "SYNCTHING-REST-API" "7" "Jun 14, 2025" "v1.29.7" "Syncthing"
.SH NAME
syncthing-rest-api \- REST API
.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]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
.TH "SYNCTHING-SECURITY" "7" "Jun 01, 2025" "v1.29.6" "Syncthing"
.TH "SYNCTHING-SECURITY" "7" "Jun 14, 2025" "v1.29.7" "Syncthing"
.SH NAME
syncthing-security \- Security Principles
.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]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
.TH "SYNCTHING-STIGNORE" "5" "Jun 01, 2025" "v1.29.6" "Syncthing"
.TH "SYNCTHING-STIGNORE" "5" "Jun 14, 2025" "v1.29.7" "Syncthing"
.SH NAME
syncthing-stignore \- Prevent files from being synchronized to other nodes
.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]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
.TH "SYNCTHING-VERSIONING" "7" "Jun 01, 2025" "v1.29.6" "Syncthing"
.TH "SYNCTHING-VERSIONING" "7" "Jun 14, 2025" "v1.29.7" "Syncthing"
.SH NAME
syncthing-versioning \- Keep automatic backups of deleted files by other nodes
.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]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
.TH "SYNCTHING" "1" "Jun 01, 2025" "v1.29.6" "Syncthing"
.TH "SYNCTHING" "1" "Jun 14, 2025" "v1.29.7" "Syncthing"
.SH NAME
syncthing \- Syncthing
.SH SYNOPSIS
+11
View File
@@ -25,3 +25,14 @@
- Multiple connections are now used by default between v2 devices. The new
default value is to use three connections: one for index metadata and two
for data exchange.
- The following platforms unfortunately no longer get prebuilt binaries for
download at syncthing.net and on GitHub, due to complexities related to
cross compilation with SQLite:
- dragonfly/amd64
- illumos/amd64 and solaris/amd64
- linux/ppc64
- netbsd/*
- openbsd/386 and openbsd/arm
- windows/arm
-7
View File
@@ -99,11 +99,6 @@ func main() {
// Write AUTHORS file
// Sort by author name
slices.SortFunc(authors, func(a, b author) int {
return strings.Compare(strings.ToLower(a.name), strings.ToLower(b.name))
})
out, err := os.Create("AUTHORS")
if err != nil {
log.Fatal(err)
@@ -176,8 +171,6 @@ var excludeCommits = stringSetFromStrings([]string{
"4dfb9d7c83ed172f12ae19408517961f4a49beeb",
})
// allAuthors returns the set of authors in the git commit log, except those
// in excluded commits.
func addAuthors(authors *authorSet) {
// All existing source-tracked files
bs, err := exec.Command("git", "ls-tree", "-r", "HEAD", "--name-only").CombinedOutput()
+1
View File
@@ -9,6 +9,7 @@ package tools
import (
_ "github.com/calmh/xdr"
_ "github.com/coreos/go-semver/semver"
_ "github.com/maxbrunsfeld/counterfeiter/v6"
_ "golang.org/x/tools/cmd/goimports"
)