Compare commits

...
Author SHA1 Message Date
Syncthing .stignore Fork d46d87fac0 apply stignore synchronization patch
custom release / build-custom-release (push) Successful in 3m12s
2026-08-11 11:17:09 +00:00
Jakob Borg 058bcd7334 fix: open GUI when relaunched instead of printing error (fixes #10727) (#10852)
This is primarily to improve the experience in environments that start
Syncthing from a graphical environment, e.g., Windows.

When already running, instead of printing an error we open the GUI. If
the GUI is not available (but the lockfile indicates we are running),
print an error and exit.

If --no-browser is given or STNOBROWSER is set, act like before.

Closes #10736. Marking as `fix` because this does not deserve to trigger
a minor release on its own.

---------

Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-08-11 11:17:09 +00:00
Syncthing Release Automation 42ea7231c5 chore(gui, man, authors): update docs, translations, and contributors 2026-08-10 04:17:57 +00:00
Jakob Borg 328d910aee fix(api): correctly return metrics, support bundle (fixes #10847) (#10849)
Our faked request wasn't good enough; improve it, adding a test.

Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-08-05 18:35:17 +00:00
Jakob Borg d7df27a367 fix(model): correctly handle receive-only changed directories (fixes #8004) (#10843)
When adding a folder in receive-only mode where the contents were
already identical to a remote device, directories would remain as
locally changed when everything else had consolidated. The reason this
happened is that we only did the matching between locally changed files
and their global equivalent for changed items, but directories are
typically not "changed" much as we don't track their mtime, so they
wouldn't pass through this stage when scanning.

Now, instead, do the check when we're anyway walking all the files in
phase two of scanning. This catches all cases of identical items, files
or directories, regardless of how they came to be in the index.

Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-08-05 20:23:53 +02:00
Jakob Borg 8ea09c0094 chore: style fixes from go fix (#10846)
Just `go fix ./...`

Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-08-05 20:23:22 +02:00
181 changed files with 1713 additions and 425 deletions
+142
View File
@@ -0,0 +1,142 @@
name: custom release
permissions:
contents: write
releases: write
on:
push:
branches:
- main
paths:
- ".gitea/workflows/custom-release.yml"
- "patches/**"
- "scripts/update-custom-release.sh"
- "scripts/sync-upstream.sh"
workflow_dispatch:
inputs:
upstream_tag:
description: "Optional upstream Syncthing tag, for example v2.1.0"
required: false
suffix:
description: "Optional custom release suffix, for example stignore.7"
required: false
schedule:
- cron: "17 04 * * *"
jobs:
build-custom-release:
runs-on: ffmini_macos_arm64
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: false
- name: Configure Git author
run: |
git config user.name "Gitea Actions"
git config user.email "actions@git.felixfoertsch.de"
- name: Mirror upstream and rebuild patched main
run: ./scripts/sync-upstream.sh
env:
SYNC_REMOTE: origin
- name: Set up tea
run: |
go install code.gitea.io/tea@v0.14.1
echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH"
"$(go env GOPATH)/bin/tea" logins delete actions >/dev/null 2>&1 || true
"$(go env GOPATH)/bin/tea" logins add --name actions --url https://git.felixfoertsch.de --token "$GITEA_TOKEN" --no-version-check
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
- name: Import Developer ID certificate
run: |
set -euo pipefail
keychain_dir="$HOME/Library/Keychains"
mkdir -p "$keychain_dir"
keychain_path="$keychain_dir/syncthing-release-signing-${GITHUB_RUN_ID:-$$}.keychain-db"
keychain_password="$(openssl rand -hex 24)"
certificate_path="$RUNNER_TEMP/developer-id-application.p12"
previous_default_keychain="$(security default-keychain -d user 2>/dev/null | sed 's/[ "]//g' || true)"
echo "CUSTOM_RELEASE_KEYCHAIN_PATH=$keychain_path" >> "$GITHUB_ENV"
echo "CUSTOM_RELEASE_KEYCHAIN_PASSWORD=$keychain_password" >> "$GITHUB_ENV"
echo "CUSTOM_RELEASE_PREVIOUS_DEFAULT_KEYCHAIN=$previous_default_keychain" >> "$GITHUB_ENV"
if [ -z "$DEVELOPER_ID_APPLICATION_P12_BASE64" ]; then
echo "DEVELOPER_ID_APPLICATION_P12_BASE64 secret is required" >&2
exit 1
fi
printf '%s' "$DEVELOPER_ID_APPLICATION_P12_BASE64" | base64 -D > "$certificate_path"
rm -f "$keychain_path"
security create-keychain -p "$keychain_password" "$keychain_path"
security set-keychain-settings -lut 21600 "$keychain_path"
security unlock-keychain -p "$keychain_password" "$keychain_path"
security import "$certificate_path" -k "$keychain_path" -P "$DEVELOPER_ID_APPLICATION_P12_PASSWORD" -A -T /usr/bin/codesign -T /usr/bin/security
existing_keychains=()
while IFS= read -r existing_keychain; do
existing_keychain="$(printf '%s' "$existing_keychain" | sed 's/[ "]//g')"
if [ -n "$existing_keychain" ] && [ -e "$existing_keychain" ] && [[ "$existing_keychain" != *"/syncthing-release-signing-"*".keychain-db" ]]; then
existing_keychains+=("$existing_keychain")
fi
done < <(security list-keychains)
security list-keychains -s "$keychain_path" "${existing_keychains[@]}"
security list-keychains -d user -s "$keychain_path" "${existing_keychains[@]}" || true
security default-keychain -d user -s "$keychain_path" || true
security list-keychains
security list-keychains -d user || true
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$keychain_password" "$keychain_path"
identity_output="$(security find-identity -v -p codesigning "$keychain_path")"
printf '%s\n' "$identity_output"
security find-identity -v -p codesigning
codesign_identity_sha1="$(printf '%s\n' "$identity_output" | awk '/"Developer ID Application:/ { print $2; exit }')"
codesign_identity="$(printf '%s\n' "$identity_output" | sed -n 's/.*"\(Developer ID Application:[^"]*\)".*/\1/p' | head -n 1)"
if [ -z "$codesign_identity" ]; then
echo "Developer ID Application signing identity is required in DEVELOPER_ID_APPLICATION_P12_BASE64" >&2
exit 1
fi
probe_binary="$RUNNER_TEMP/codesign-probe"
cp /usr/bin/true "$probe_binary"
codesign --force --dryrun --sign "$codesign_identity" --keychain "$keychain_path" --options runtime --timestamp "$probe_binary"
echo "CUSTOM_RELEASE_CODESIGN_IDENTITY=$codesign_identity" >> "$GITHUB_ENV"
echo "CUSTOM_RELEASE_CODESIGN_IDENTITY_SHA1=$codesign_identity_sha1" >> "$GITHUB_ENV"
env:
DEVELOPER_ID_APPLICATION_P12_BASE64: ${{ secrets.DEVELOPER_ID_APPLICATION_P12_BASE64 }}
DEVELOPER_ID_APPLICATION_P12_PASSWORD: ${{ secrets.DEVELOPER_ID_APPLICATION_P12_PASSWORD }}
- name: Build patched Syncthing release
run: ./scripts/update-custom-release.sh
env:
CUSTOM_RELEASE_UPSTREAM_TAG: ${{ github.event.inputs.upstream_tag }}
CUSTOM_RELEASE_SUFFIX: ${{ github.event.inputs.suffix }}
CUSTOM_RELEASE_PUSH: "1"
CUSTOM_RELEASE_PUSH_BRANCH: "0"
CUSTOM_RELEASE_REMOTE: origin
CUSTOM_RELEASE_BUILDS: "darwin/arm64/zip/1 linux/amd64/tar/0 linux/arm64/tar/0"
CUSTOM_RELEASE_CODESIGN_TEAM_ID: "NG5W75WE8U"
CUSTOM_RELEASE_SIGN_DARWIN: "1"
CUSTOM_RELEASE_REQUIRE_GATEKEEPER_ASSESSMENT: "0"
CUSTOM_RELEASE_CREATE_GITEA_RELEASE: "1"
CUSTOM_RELEASE_TEA_REPO: felixfoertsch/syncthing
- name: Delete temporary keychain
if: always()
run: |
if [ -n "${CUSTOM_RELEASE_PREVIOUS_DEFAULT_KEYCHAIN:-}" ] && [ -e "$CUSTOM_RELEASE_PREVIOUS_DEFAULT_KEYCHAIN" ]; then
security default-keychain -d user -s "$CUSTOM_RELEASE_PREVIOUS_DEFAULT_KEYCHAIN" || true
fi
if [ -n "${CUSTOM_RELEASE_KEYCHAIN_PATH:-}" ]; then
security delete-keychain "$CUSTOM_RELEASE_KEYCHAIN_PATH" || true
fi
+150
View File
@@ -0,0 +1,150 @@
name: custom release
permissions:
contents: write
on:
push:
branches:
- main
paths:
- ".github/workflows/custom-release.yml"
- "patches/**"
- "scripts/update-custom-release.sh"
- "scripts/sync-upstream.sh"
workflow_dispatch:
inputs:
upstream_tag:
description: "Optional upstream Syncthing tag, for example v2.1.3"
required: false
suffix:
description: "Optional custom release suffix, for example stignore.7"
required: false
schedule:
- cron: "17 04 * * *"
jobs:
build-custom-release:
runs-on: macos-14
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: false
- name: Configure Git author
run: |
git config user.name "GitHub Actions"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Mirror upstream and rebuild patched main
run: ./scripts/sync-upstream.sh
env:
SYNC_REMOTE: origin
- name: Import Developer ID certificate
run: |
set -euo pipefail
keychain_path="$RUNNER_TEMP/syncthing-release-signing.keychain-db"
keychain_password="$(openssl rand -hex 24)"
certificate_path="$RUNNER_TEMP/developer-id-application.p12"
previous_default_keychain="$(security default-keychain -d user 2>/dev/null | sed 's/[ "]//g' || true)"
echo "::add-mask::$keychain_password"
echo "CUSTOM_RELEASE_KEYCHAIN_PATH=$keychain_path" >> "$GITHUB_ENV"
echo "CUSTOM_RELEASE_KEYCHAIN_PASSWORD=$keychain_password" >> "$GITHUB_ENV"
echo "CUSTOM_RELEASE_CERTIFICATE_PATH=$certificate_path" >> "$GITHUB_ENV"
echo "CUSTOM_RELEASE_PREVIOUS_DEFAULT_KEYCHAIN=$previous_default_keychain" >> "$GITHUB_ENV"
if [ -z "$DEVELOPER_ID_APPLICATION_P12_BASE64" ]; then
echo "DEVELOPER_ID_APPLICATION_P12_BASE64 secret is required" >&2
exit 1
fi
if [ -z "$DEVELOPER_ID_APPLICATION_P12_PASSWORD" ]; then
echo "DEVELOPER_ID_APPLICATION_P12_PASSWORD secret is required" >&2
exit 1
fi
printf '%s' "$DEVELOPER_ID_APPLICATION_P12_BASE64" | base64 -D > "$certificate_path"
security create-keychain -p "$keychain_password" "$keychain_path"
security set-keychain-settings -lut 21600 "$keychain_path"
security unlock-keychain -p "$keychain_password" "$keychain_path"
security import "$certificate_path" -k "$keychain_path" -P "$DEVELOPER_ID_APPLICATION_P12_PASSWORD" -A -T /usr/bin/codesign -T /usr/bin/security
security list-keychains -d user -s "$keychain_path"
security default-keychain -d user -s "$keychain_path"
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$keychain_password" "$keychain_path"
identity_output="$(security find-identity -v -p codesigning "$keychain_path")"
printf '%s\n' "$identity_output"
codesign_identity="$(printf '%s\n' "$identity_output" | sed -n 's/.*"\(Developer ID Application:[^"]*\)".*/\1/p' | head -n 1)"
if [ -z "$codesign_identity" ]; then
echo "Developer ID Application signing identity is required in DEVELOPER_ID_APPLICATION_P12_BASE64" >&2
exit 1
fi
probe_binary="$RUNNER_TEMP/codesign-probe"
cp /usr/bin/true "$probe_binary"
codesign --force --dryrun --sign "$codesign_identity" --keychain "$keychain_path" --options runtime --timestamp "$probe_binary"
echo "CUSTOM_RELEASE_CODESIGN_IDENTITY=$codesign_identity" >> "$GITHUB_ENV"
env:
DEVELOPER_ID_APPLICATION_P12_BASE64: ${{ secrets.DEVELOPER_ID_APPLICATION_P12_BASE64 }}
DEVELOPER_ID_APPLICATION_P12_PASSWORD: ${{ secrets.DEVELOPER_ID_APPLICATION_P12_PASSWORD }}
- name: Build patched Syncthing release
run: |
case "$(uname -m)" in
arm64) darwin_arch=arm64 ;;
x86_64) darwin_arch=amd64 ;;
*) echo "Unsupported macOS runner architecture: $(uname -m)" >&2; exit 1 ;;
esac
export CUSTOM_RELEASE_BUILDS="darwin/$darwin_arch/zip/1 linux/amd64/tar/0 linux/arm64/tar/0"
./scripts/update-custom-release.sh
env:
CUSTOM_RELEASE_UPSTREAM_TAG: ${{ github.event.inputs.upstream_tag }}
CUSTOM_RELEASE_SUFFIX: ${{ github.event.inputs.suffix }}
CUSTOM_RELEASE_PUSH: "1"
CUSTOM_RELEASE_PUSH_BRANCH: "0"
CUSTOM_RELEASE_REMOTE: origin
CUSTOM_RELEASE_CODESIGN_TEAM_ID: "NG5W75WE8U"
CUSTOM_RELEASE_SIGN_DARWIN: "1"
CUSTOM_RELEASE_REQUIRE_GATEKEEPER_ASSESSMENT: "0"
CUSTOM_RELEASE_CREATE_GITEA_RELEASE: "0"
GH_TOKEN: ${{ github.token }}
- name: Publish GitHub release
run: |
tag="${CUSTOM_RELEASE_UPSTREAM_TAG:-$(git ls-remote --refs --tags --sort='version:refname' https://github.com/syncthing/syncthing.git 'v[0-9]*' | awk '{ tag = $2; sub("refs/tags/", "", tag); if (tag ~ /^v[0-9]+\.[0-9]+\.[0-9]+$/) latest = tag } END { print latest }')}-$CUSTOM_RELEASE_SUFFIX"
if gh release view "$tag" >/dev/null 2>&1; then
echo "GitHub release $tag already exists; nothing to do."
exit 0
fi
assets=()
for asset in dist/*; do
[ -f "$asset" ] || continue
[ "$(basename "$asset")" = release-notes.md ] && continue
assets+=("$asset")
done
gh release create "$tag" "${assets[@]}" --title "$tag" --notes-file dist/release-notes.md --verify-tag
env:
CUSTOM_RELEASE_UPSTREAM_TAG: ${{ github.event.inputs.upstream_tag }}
CUSTOM_RELEASE_SUFFIX: ${{ github.event.inputs.suffix || 'stignore.7' }}
GH_TOKEN: ${{ github.token }}
- name: Delete temporary keychain
if: always()
run: |
if [ -n "${CUSTOM_RELEASE_PREVIOUS_DEFAULT_KEYCHAIN:-}" ] && [ -e "$CUSTOM_RELEASE_PREVIOUS_DEFAULT_KEYCHAIN" ]; then
security default-keychain -d user -s "$CUSTOM_RELEASE_PREVIOUS_DEFAULT_KEYCHAIN" || true
fi
if [ -n "${CUSTOM_RELEASE_KEYCHAIN_PATH:-}" ]; then
security delete-keychain "$CUSTOM_RELEASE_KEYCHAIN_PATH" || true
fi
if [ -n "${CUSTOM_RELEASE_CERTIFICATE_PATH:-}" ]; then
rm -f "$CUSTOM_RELEASE_CERTIFICATE_PATH"
fi
+8
View File
@@ -1,3 +1,11 @@
# Syncthing with `.stignore` synchronization
This fork synchronizes the root-level `.stignore` file as regular folder
content, while keeping `.stfolder` and `.stversions` protected as Syncthing
internals. The `upstream` branch mirrors the official Syncthing `main` branch;
this fork's `main` branch and releases apply the `.stignore` synchronization
patch.
[![Syncthing][14]][15]
---
+4 -4
View File
@@ -17,10 +17,10 @@ import (
)
type event struct {
ID int `json:"id"`
Type string `json:"type"`
Time time.Time `json:"time"`
Data map[string]interface{} `json:"data"`
ID int `json:"id"`
Type string `json:"type"`
Time time.Time `json:"time"`
Data map[string]any `json:"data"`
}
func main() {
-1
View File
@@ -60,7 +60,6 @@ func checkServers(deviceID protocol.DeviceID, servers ...string) {
t0 := time.Now()
resc := make(chan checkResult)
for _, srv := range servers {
srv := srv
go func() {
res := checkServer(deviceID, srv)
res.server = srv
+2 -5
View File
@@ -34,7 +34,7 @@ func generateFiles(dir string, files, maxexp int, srcname string) error {
return err
}
for i := 0; i < files; i++ {
for range files {
n := randomName()
if rand.Float64() < 0.05 {
@@ -51,10 +51,7 @@ func generateFiles(dir string, files, maxexp int, srcname string) error {
p1 := filepath.Join(p0, n)
s := int64(1 << uint(rand.Intn(maxexp)))
a := int64(128 * 1024)
if a > s {
a = s
}
a := min(int64(128*1024), s)
s += rand.Int63n(a)
if err := generateOneFile(fd, p1, s); err != nil {
+2 -2
View File
@@ -138,7 +138,7 @@ func printProgress(prefix string, count *atomic.Int64) {
}
}
func saveCert(priv interface{}, derBytes []byte) {
func saveCert(priv any, derBytes []byte) {
certOut, err := os.Create("cert.pem")
if err != nil {
fmt.Println(err)
@@ -179,7 +179,7 @@ func saveCert(priv interface{}, derBytes []byte) {
}
}
func pemBlockForKey(priv interface{}) (*pem.Block, error) {
func pemBlockForKey(priv any) (*pem.Block, error) {
switch k := priv.(type) {
case *rsa.PrivateKey:
return &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(k)}, nil
+1 -1
View File
@@ -203,7 +203,7 @@ func loadIgnorePatterns(path string) (*ignorePatterns, error) {
}
var patterns []*regexp.Regexp
for _, line := range strings.Split(string(bs), "\n") {
for line := range strings.SplitSeq(string(bs), "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
@@ -5,7 +5,6 @@
// You can obtain one at https://mozilla.org/MPL/2.0/.
//go:build noassets
// +build noassets
package auto
+1 -1
View File
@@ -587,7 +587,7 @@ func loadRelays(file string, geoip *geoip.Provider) []*relay {
}
var relays []*relay
for _, line := range strings.Split(string(content), "\n") {
for line := range strings.SplitSeq(string(content), "\n") {
if line == "" {
continue
}
+1 -1
View File
@@ -17,7 +17,7 @@ import (
)
func init() {
for i := 0; i < 10; i++ {
for i := range 10 {
u := fmt.Sprintf("permanent%d", i)
permanentRelays = append(permanentRelays, &relay{URL: u})
}
+1 -1
View File
@@ -188,7 +188,7 @@ func (p *proxy) ServeHTTP(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(resp.StatusCode)
if strings.HasPrefix(ct, "application/json") {
// Special JSON handling; clean it up a bit.
var v interface{}
var v any
if err := json.NewDecoder(resp.Body).Decode(&v); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
+1 -4
View File
@@ -402,10 +402,7 @@ func (s *apiSrv) certificateBytes(req *http.Request) ([]byte, error) {
b.WriteByte('\n')
for i := 0; i < len(cert); i += 64 {
end := i + 64
if end > len(cert) {
end = len(cert)
}
end := min(i+64, len(cert))
b.WriteString(cert[i:end])
b.WriteByte('\n')
}
+3 -8
View File
@@ -7,7 +7,6 @@
package main
import (
"context"
"crypto/tls"
"fmt"
"io"
@@ -120,7 +119,7 @@ func TestRetryAfterSHistogram(t *testing.T) {
numBuckets := (notFoundRetryUnknownMaxSeconds + bucketSize - 1) / bucketSize
buckets := make([]int, numBuckets)
for i := 0; i < n; i++ {
for range n {
v := tracker.retryAfterS()
if v < notFoundRetryUnknownMinSeconds || v > notFoundRetryUnknownMaxSeconds {
t.Fatalf("retryAfterS() = %d, out of range [%d, %d]", v, notFoundRetryUnknownMinSeconds, notFoundRetryUnknownMaxSeconds)
@@ -142,10 +141,7 @@ func TestRetryAfterSHistogram(t *testing.T) {
barWidth := 60
for i, c := range buckets {
lo := i*bucketSize + 1
hi := (i + 1) * bucketSize
if hi > notFoundRetryUnknownMaxSeconds {
hi = notFoundRetryUnknownMaxSeconds
}
hi := min((i+1)*bucketSize, notFoundRetryUnknownMaxSeconds)
bar := ""
if maxCount > 0 {
bar = strings.Repeat("#", c*barWidth/maxCount)
@@ -156,8 +152,7 @@ func TestRetryAfterSHistogram(t *testing.T) {
func BenchmarkAPIRequests(b *testing.B) {
db := newInMemoryStore(b.TempDir(), 0, nil)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ctx := b.Context()
go db.Serve(ctx)
api := newAPISrv("127.0.0.1:0", tls.Certificate{}, db, nil, true, true, 1000, 1000)
srv := httptest.NewServer(http.HandlerFunc(api.handler))
+4 -4
View File
@@ -18,7 +18,7 @@ import (
var (
outboxesMut = sync.RWMutex{}
outboxes = make(map[syncthingprotocol.DeviceID]chan interface{})
outboxes = make(map[syncthingprotocol.DeviceID]chan any)
numConnections atomic.Int64
)
@@ -97,9 +97,9 @@ func protocolConnectionHandler(tcpConn net.Conn, config *tls.Config, token strin
id := syncthingprotocol.NewDeviceID(certs[0].Raw)
messages := make(chan interface{})
messages := make(chan any)
errors := make(chan error, 1)
outbox := make(chan interface{})
outbox := make(chan any)
// Read messages from the connection and send them on the messages
// channel. When there is an error, send it on the error channel and
@@ -364,7 +364,7 @@ func sessionConnectionHandler(conn net.Conn) {
}
}
func messageReader(conn net.Conn, messages chan<- interface{}, errors chan<- error) {
func messageReader(conn net.Conn, messages chan<- any, errors chan<- error) {
numConnections.Add(1)
defer numConnections.Add(-1)
+1 -4
View File
@@ -330,10 +330,7 @@ func take(tokens int, ls ...*rate.Limiter) {
for tokens > 0 {
// chunk is how many tokens we can consume at a time
chunk := tokens
if chunk > minBurst {
chunk = minBurst
}
chunk := min(tokens, minBurst)
// maxDelay is the longest delay mandated by any of the limiters for
// the chosen chunk size.
+2 -2
View File
@@ -38,7 +38,7 @@ func statusService(addr string) {
func getStatus(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
status := make(map[string]interface{})
status := make(map[string]any)
sessionMut.Lock()
// This can potentially be double the number of pending sessions, as each session has two keys, one for each side.
@@ -67,7 +67,7 @@ func getStatus(w http.ResponseWriter, _ *http.Request) {
rc.rate(30*60/10) * 8 / 1000,
rc.rate(60*60/10) * 8 / 1000,
}
status["options"] = map[string]interface{}{
status["options"] = map[string]any{
"network-timeout": networkTimeout / time.Second,
"ping-interval": pingInterval / time.Second,
"message-timeout": messageTimeout / time.Second,
+3 -3
View File
@@ -27,7 +27,7 @@ import (
type APIClient interface {
Get(url string) (*http.Response, error)
Post(url, body string) (*http.Response, error)
PutJSON(url string, o interface{}) (*http.Response, error)
PutJSON(url string, o any) (*http.Response, error)
}
type apiClient struct {
@@ -134,7 +134,7 @@ func (c *apiClient) RequestString(url, method, data string) (*http.Response, err
return c.Request(url, method, bytes.NewBufferString(data))
}
func (c *apiClient) RequestJSON(url, method string, o interface{}) (*http.Response, error) {
func (c *apiClient) RequestJSON(url, method string, o any) (*http.Response, error) {
data, err := json.Marshal(o)
if err != nil {
return nil, err
@@ -150,7 +150,7 @@ func (c *apiClient) Post(url, body string) (*http.Response, error) {
return c.RequestString(url, "POST", body)
}
func (c *apiClient) PutJSON(url string, o interface{}) (*http.Response, error) {
func (c *apiClient) PutJSON(url string, o any) (*http.Response, error) {
return c.RequestJSON(url, "PUT", o)
}
+1 -1
View File
@@ -81,7 +81,7 @@ func (c *configCommand) Run(ctx Context, outerCtx *kong.Context) error {
app.Name = "syncthing cli config"
app.HelpName = "syncthing cli config"
app.Description = outerCtx.Selected().Help
app.Metadata = map[string]interface{}{
app.Metadata = map[string]any{
"clientFactory": ctx.clientFactory,
}
app.CustomAppHelpTemplate = customAppHelpTemplate
+2 -2
View File
@@ -112,7 +112,7 @@ func getConfig(c APIClient) (config.Configuration, error) {
return cfg, nil
}
func prettyPrintJSON(data interface{}) error {
func prettyPrintJSON(data any) error {
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
return enc.Encode(data)
@@ -123,7 +123,7 @@ func prettyPrintResponse(response *http.Response) error {
if err != nil {
return err
}
var data interface{}
var data any
if err := json.Unmarshal(bytes, &data); err != nil {
return err
}
+1 -1
View File
@@ -125,7 +125,7 @@ func uploadPanicLog(ctx context.Context, urlBase, file string) error {
func filterLogLines(data []byte) []byte {
filtered := data[:0]
matched := false
for _, line := range bytes.Split(data, []byte("\n")) {
for line := range bytes.SplitSeq(data, []byte("\n")) {
switch {
case !matched && bytes.HasPrefix(line, []byte("Panic ")):
// This begins the panic trace, set the matched flag and append.
-1
View File
@@ -5,7 +5,6 @@
// You can obtain one at https://mozilla.org/MPL/2.0/.
//go:build !windows
// +build !windows
package main
+55 -35
View File
@@ -313,21 +313,6 @@ func (c *serveCmd) Run() error {
return nil
}
func openGUI() error {
cfg, err := loadOrDefaultConfig()
if err != nil {
return err
}
if guiCfg := cfg.GUI(); guiCfg.Enabled {
if err := openURL(guiCfg.URL()); err != nil {
return err
}
} else {
slog.Error("Browser: GUI is currently disabled")
}
return nil
}
func logPackages() string {
packages := slogutil.PackageDescrs()
@@ -418,6 +403,28 @@ func upgradeViaRest() error {
}
func (c *serveCmd) syncthingMain() {
// Ensure we are the only running instance
lf := flock.New(locations.Get(locations.LockFile))
locked, err := lf.TryLock()
switch {
case err != nil:
slog.Error("Failed to acquire lock", slogutil.Error(err))
os.Exit(svcutil.ExitError.AsInt())
case !locked && c.NoBrowser:
slog.Error("Failed to acquire lock: is another Syncthing instance already running?")
os.Exit(svcutil.ExitError.AsInt())
case !locked:
slog.Info("Seems to already be running, launching GUI instead (use --no-browser to prevent)")
cmd := browserCmd{Verify: true}
if err := cmd.Run(); err != nil {
slog.Error("Failed to open browser", slogutil.Error(err))
os.Exit(svcutil.ExitNoRestart.AsInt())
}
return
}
if c.DebugProfileBlock {
startBlockProfiler()
}
@@ -436,18 +443,7 @@ func (c *serveCmd) syncthingMain() {
)
if err != nil {
slog.Error("Failed to load/generate certificate", slogutil.Error(err))
os.Exit(1)
}
// Ensure we are the only running instance
lf := flock.New(locations.Get(locations.LockFile))
locked, err := lf.TryLock()
if err != nil {
slog.Error("Failed to acquire lock", slogutil.Error(err))
os.Exit(1)
} else if !locked {
slog.Error("Failed to acquire lock: is another Syncthing instance already running?")
os.Exit(1)
os.Exit(svcutil.ExitError.AsInt())
}
ctx, cancel := context.WithCancel(context.Background())
@@ -502,13 +498,13 @@ func (c *serveCmd) syncthingMain() {
if err := syncthing.TryMigrateDatabase(ctx, c.DBDeleteRetentionInterval); err != nil {
slog.Error("Failed to migrate old-style database", slogutil.Error(err))
os.Exit(1)
os.Exit(svcutil.ExitError.AsInt())
}
sdb, err := syncthing.OpenDatabase(locations.Get(locations.Database), c.DBDeleteRetentionInterval)
if err != nil {
slog.Error("Error opening database", slogutil.Error(err))
os.Exit(1)
os.Exit(svcutil.ExitError.AsInt())
}
if c.DebugPerfStats {
@@ -915,7 +911,7 @@ func (u upgradeCmd) Run() error {
switch {
case err != nil && !os.IsNotExist(err):
slog.Error("Failed to lock for upgrade", slogutil.Error(err))
os.Exit(1)
os.Exit(svcutil.ExitError.AsInt())
case locked || os.IsNotExist(err):
// We got the lock, or the config directory didn't exist, so we
// can do a direct upgrade
@@ -935,14 +931,38 @@ func (u upgradeCmd) Run() error {
return nil
}
type browserCmd struct{}
type browserCmd struct {
Verify bool `help:"Verify that the GUI is reachable before launching browser"`
}
func (browserCmd) Run() error {
if err := openGUI(); err != nil {
slog.Error("Failed to open web UI", slogutil.Error(err))
func (c browserCmd) Run() error {
cfg, err := loadOrDefaultConfig()
if err != nil {
return err
}
guiCfg := cfg.GUI()
if !guiCfg.Enabled {
slog.Error("Browser: GUI is currently disabled")
os.Exit(svcutil.ExitError.AsInt())
}
return nil
url := guiCfg.URL()
if c.Verify {
// Do an HTTP request to verify the GUI/API is up and available
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return err
}
_, err = http.DefaultClient.Do(req) //nolint:bodyclose // we're exiting in a millisecond
if err != nil {
slog.Error("GUI not available", slogutil.Error(err))
os.Exit(svcutil.ExitError.AsInt()) //nolint:gocritic // deferred cancel
}
}
return openURL(url)
}
type debugCmd struct {
+8 -3
View File
@@ -171,10 +171,11 @@ func (c *serveCmd) monitorMain() {
exiterr := &exec.ExitError{}
if errors.As(err, &exiterr) {
exitCode := exiterr.ExitCode()
if stopped || c.NoRestart {
switch {
case stopped || c.NoRestart:
os.Exit(exitCode)
}
if exitCode == svcutil.ExitUpgrade.AsInt() {
case exitCode == svcutil.ExitUpgrade.AsInt():
// Restart the monitor process to release the .old
// binary as part of the upgrade process.
slog.Info("Restarting monitor...")
@@ -182,6 +183,10 @@ func (c *serveCmd) monitorMain() {
slog.Error("Failed to restart monitor", slogutil.Error(err))
}
os.Exit(exitCode)
case exitCode == svcutil.ExitNoRestart.AsInt():
// Requested to not restart the child
os.Exit(exitCode)
}
}
-1
View File
@@ -5,7 +5,6 @@
// You can obtain one at https://mozilla.org/MPL/2.0/.
//go:build !windows
// +build !windows
package main
-1
View File
@@ -5,7 +5,6 @@
// You can obtain one at https://mozilla.org/MPL/2.0/.
//go:build windows
// +build windows
package main
-1
View File
@@ -5,7 +5,6 @@
// You can obtain one at https://mozilla.org/MPL/2.0/.
//go:build !solaris && !windows
// +build !solaris,!windows
package main
-1
View File
@@ -5,7 +5,6 @@
// You can obtain one at https://mozilla.org/MPL/2.0/.
//go:build solaris || windows
// +build solaris windows
package main
-1
View File
@@ -5,7 +5,6 @@
// You can obtain one at https://mozilla.org/MPL/2.0/.
//go:build go1.7
// +build go1.7
package main
+2
View File
@@ -125,6 +125,8 @@
"Discovery Status": "حالة الاكتشاف",
"Dismiss": "رفض",
"Do not add it to the ignore list, so this notification may recur.": "لا تقم بإضافته إلى قائمة التجاهل، لذلك قد يتكرر هذا الإشعار.",
"Do not add it to the ignore list, so this notification will reappear if the device connects again.": "لا تقم بإضافته إلى قائمة التجاهل، حتى يتم اخطارك عند اعادة اتصال الجهاز.",
"Do not add it to the ignore list, so this notification will reappear if the device offering this folder connects again.": "لا تقم بإضافته إلى قائمة التجاهل، حتى يتم اخطارك عند اعادة اتصال الجهاز المزوّد لهذا المجلد.",
"Do not restore": "الغاء الاستعادة",
"Do not restore all": "الغاء استعادة الكل",
"Do you want to enable watching for changes for all your folders?": "هل تريد تفعيل مراقبة التغيرات على كل المجلدات؟",
+2
View File
@@ -125,6 +125,8 @@
"Discovery Status": "Discovery Status",
"Dismiss": "Dismiss",
"Do not add it to the ignore list, so this notification may recur.": "Do not add it to the ignore list, so this notification may recur.",
"Do not add it to the ignore list, so this notification will reappear if the device connects again.": "Do not add it to the ignore list, so this notification will reappear if the device connects again.",
"Do not add it to the ignore list, so this notification will reappear if the device offering this folder connects again.": "Do not add it to the ignore list, so this notification will reappear if the device offering this folder connects again.",
"Do not restore": "Do not restore",
"Do not restore all": "Do not restore all",
"Do you want to enable watching for changes for all your folders?": "Do you want to enable watching for changes for all your folders?",
+3
View File
@@ -125,6 +125,8 @@
"Discovery Status": "탐지 현황",
"Dismiss": "나중에",
"Do not add it to the ignore list, so this notification may recur.": "무시 항목에 추가되지 않으니 이 알림이 다시 표시될 수 있습니다.",
"Do not add it to the ignore list, so this notification will reappear if the device connects again.": "무시 항목에 추가되지 않으니 해당 기기가 다시 연결되면 이 알림은 다시 표시될 것입니다.",
"Do not add it to the ignore list, so this notification will reappear if the device offering this folder connects again.": "무시 항목에 추가되지 않으니 이 폴더를 공유하는 기가가 연결되면 이 알림은 다시 표시될 것입니다.",
"Do not restore": "복구하지 않기",
"Do not restore all": "모두 복구하지 않기",
"Do you want to enable watching for changes for all your folders?": "변경 항목 감시를 모든 폴더에서 활성화하시겠습니까?",
@@ -395,6 +397,7 @@
"Staggered": "시차제",
"Staggered File Versioning": "시차제 파일 버전 관리",
"Start Browser": "브라우저 열기",
"Starting": "시작 중",
"Statistics": "통계",
"Stay logged in": "로그인 상태 유지",
"Stopped": "중지됨",
+2 -2
View File
@@ -125,8 +125,8 @@
"Discovery Status": "Stan odnajdywania",
"Dismiss": "Odrzuć",
"Do not add it to the ignore list, so this notification may recur.": "Nie dodaje do listy ignorowanych, więc powiadomienie to może się powtórzyć.",
"Do not add it to the ignore list, so this notification will reappear if the device connects again.": "Nie dodaje do listy ignorowanych, więc powiadomienie pojawi się ponownie, gdy urządzenie połączy się ponownie.",
"Do not add it to the ignore list, so this notification will reappear if the device offering this folder connects again.": "Nie dodaje do listy ignorowanych, więc powiadomienie pojawi się ponownie, gdy urządzenie oferujące ten folder ponownie się połączy.",
"Do not add it to the ignore list, so this notification will reappear if the device connects again.": "Nie dodaje do listy ignorowanych, więc powiadomienie pojawi się ponownie, gdy urządzenie po raz kolejny się połączy.",
"Do not add it to the ignore list, so this notification will reappear if the device offering this folder connects again.": "Nie dodaje do listy ignorowanych, więc powiadomienie pojawi się ponownie, gdy urządzenie oferujące ten folder po raz kolejny się połączy.",
"Do not restore": "Nie przywracaj",
"Do not restore all": "Nie przywracaj wszystkich",
"Do you want to enable watching for changes for all your folders?": "Czy chcesz włączyć obserwowanie zmian we wszystkich folderach?",
+2 -1
View File
@@ -137,7 +137,7 @@
"Edit Device Defaults": "Изменить настройки устройств",
"Edit Folder": "Редактирование папки",
"Edit Folder Defaults": "Изменить настройки папок",
"Editing {%path%}.": "Правка {{path}}.",
"Editing {%path%}.": "Редактирование {{path}}.",
"Enable Crash Reporting": "Включить отчёты о сбоях",
"Enable NAT traversal": "Использовать обход NAT",
"Enable Relaying": "Использовать ретрансляторы",
@@ -396,6 +396,7 @@
"Staggered": "Ступенчато",
"Staggered File Versioning": "Ступенчатое управление версиями файлов",
"Start Browser": "Запускать браузер",
"Starting": "Запуск",
"Statistics": "Статистика",
"Stay logged in": "Оставаться в системе",
"Stopped": "Остановлено",
+3
View File
@@ -1017,6 +1017,9 @@
</div> <!-- /row -->
</div> <!-- /container -->
<footer class="container text-center text-muted small" aria-label="Custom build marker">
It syncs .stignore now!
</footer>
</div> <!-- /ng-cloak -->
<ng-include src="'syncthing/core/networkErrorDialogView.html'"></ng-include>
+1 -1
View File
@@ -313,7 +313,7 @@ nextScript:
// files on lines containing only a semicolon and execute them
// separately. We require it on a separate line because there are
// also statement-internal semicolons in the triggers.
for _, stmt := range strings.Split(string(bs), "\n;") {
for stmt := range strings.SplitSeq(string(bs), "\n;") {
if _, err := tx.Exec(s.expandTemplateVars(stmt)); err != nil {
if strings.Contains(stmt, "syncthing:ignore-failure") {
// We're ok with this failing. Just note it.
+1 -4
View File
@@ -474,10 +474,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.IsInvalid() })
if globIdx < 0 {
globIdx = 0
}
globIdx := max(slices.IndexFunc(es, func(e fileRow) bool { return !e.IsInvalid() }), 0)
global = es[globIdx]
// We "have" the file if the position in the list of versions is at the
+2 -2
View File
@@ -41,8 +41,8 @@ func SetDefaultLevel(level slog.Level) {
}
func SetLevelOverrides(sttrace string) {
pkgs := strings.Split(sttrace, ",")
for _, pkg := range pkgs {
pkgs := strings.SplitSeq(sttrace, ",")
for pkg := range pkgs {
pkg = strings.TrimSpace(pkg)
if pkg == "" {
continue
+2 -2
View File
@@ -45,11 +45,11 @@ type adapter struct {
l *slog.Logger
}
func (a adapter) Debugln(vals ...interface{}) {
func (a adapter) Debugln(vals ...any) {
a.log(strings.TrimSpace(fmt.Sprintln(vals...)), slog.LevelDebug)
}
func (a adapter) Debugf(format string, vals ...interface{}) {
func (a adapter) Debugf(format string, vals ...any) {
a.log(fmt.Sprintf(format, vals...), slog.LevelDebug)
}
+28 -20
View File
@@ -202,7 +202,7 @@ func (s *service) getListener(guiCfg config.GUIConfiguration) (net.Listener, err
return listener, nil
}
func sendJSON(w http.ResponseWriter, jsonObject interface{}) {
func sendJSON(w http.ResponseWriter, jsonObject any) {
w.Header().Set("Content-Type", "application/json")
// Marshalling might fail, in which case we should return a 500 with the
// actual error.
@@ -696,7 +696,7 @@ func (*service) getSystemPaths(w http.ResponseWriter, _ *http.Request) {
}
func (s *service) getJSMetadata(w http.ResponseWriter, _ *http.Request) {
meta, _ := json.Marshal(map[string]interface{}{
meta, _ := json.Marshal(map[string]any{
"deviceID": s.id.String(),
"deviceIDShort": s.id.Short().String(),
"authenticated": true,
@@ -706,7 +706,7 @@ func (s *service) getJSMetadata(w http.ResponseWriter, _ *http.Request) {
}
func (*service) getSystemVersion(w http.ResponseWriter, _ *http.Request) {
sendJSON(w, map[string]interface{}{
sendJSON(w, map[string]any{
"version": build.Version,
"codename": build.Codename,
"longVersion": build.LongVersion,
@@ -838,7 +838,7 @@ func (s *service) getDBNeed(w http.ResponseWriter, r *http.Request) {
}
// Convert the struct to a more loose structure, and inject the size.
sendJSON(w, map[string]interface{}{
sendJSON(w, map[string]any{
"progress": toJsonFileInfoSlice(progress),
"queued": toJsonFileInfoSlice(queued),
"rest": toJsonFileInfoSlice(rest),
@@ -866,7 +866,7 @@ func (s *service) getDBRemoteNeed(w http.ResponseWriter, r *http.Request) {
return
}
sendJSON(w, map[string]interface{}{
sendJSON(w, map[string]any{
"files": toJsonFileInfoSlice(files),
"page": page,
"perpage": perpage,
@@ -886,7 +886,7 @@ func (s *service) getDBLocalChanged(w http.ResponseWriter, r *http.Request) {
return
}
sendJSON(w, map[string]interface{}{
sendJSON(w, map[string]any{
"files": toJsonFileInfoSlice(files),
"page": page,
"perpage": perpage,
@@ -951,7 +951,7 @@ func (s *service) getDBFile(w http.ResponseWriter, r *http.Request) {
return
}
sendJSON(w, map[string]interface{}{
sendJSON(w, map[string]any{
"global": jsonFileInfo(gf),
"local": jsonFileInfo(lf),
"availability": av,
@@ -967,7 +967,7 @@ func (s *service) getDebugFile(w http.ResponseWriter, r *http.Request) {
gf, _, _ := s.model.CurrentGlobalFile(folder, file)
av, _ := s.model.Availability(folder, protocol.FileInfo{Name: file}, protocol.BlockInfo{})
sendJSON(w, map[string]interface{}{
sendJSON(w, map[string]any{
"global": jsonFileInfo(gf),
"local": jsonFileInfo(lf),
"availability": av,
@@ -1037,7 +1037,7 @@ func (s *service) getSystemStatus(w http.ResponseWriter, _ *http.Request) {
runtime.ReadMemStats(&m)
tilde, _ := fs.ExpandTilde("~")
res := make(map[string]interface{})
res := make(map[string]any)
res["myID"] = s.id.String()
res["goroutines"] = runtime.NumGoroutine()
res["alloc"] = m.Alloc
@@ -1184,10 +1184,7 @@ func (s *service) getSupportBundle(w http.ResponseWriter, r *http.Request) {
}
// Metrics data as text
var metricsBuf bytes.Buffer
wr := bufferedResponseWriter{Writer: &metricsBuf}
promhttp.Handler().ServeHTTP(wr, &http.Request{Method: http.MethodGet})
files = append(files, fileEntry{name: "metrics.txt", data: metricsBuf.Bytes()})
files = append(files, fileEntry{name: "metrics.txt", data: prometheusMetrics()})
// Connection data as JSON
connStats := s.model.ConnectionStats()
@@ -1258,6 +1255,16 @@ func (s *service) getSupportBundle(w http.ResponseWriter, r *http.Request) {
io.Copy(w, &zipFilesBuffer)
}
func prometheusMetrics() []byte {
var metricsBuf bytes.Buffer
wr := bufferedResponseWriter{Writer: &metricsBuf}
promhttp.Handler().ServeHTTP(wr, &http.Request{
Method: http.MethodGet,
URL: &url.URL{Scheme: "http://", Host: "localhost", Path: "/metrics"},
})
return metricsBuf.Bytes()
}
func (s *service) getSystemDiscovery(w http.ResponseWriter, _ *http.Request) {
devices := make(map[string]discover.CacheEntry)
@@ -1302,7 +1309,7 @@ func (s *service) getDBIgnores(w http.ResponseWriter, r *http.Request) {
folder := qs.Get("folder")
lines, patterns, err := s.model.LoadIgnores(folder)
sendJSON(w, map[string]interface{}{
sendJSON(w, map[string]any{
"ignore": lines,
"expanded": patterns,
"error": errorString(err),
@@ -1411,7 +1418,7 @@ func (s *service) getSystemUpgrade(w http.ResponseWriter, _ *http.Request) {
httpError(w, err)
return
}
res := make(map[string]interface{})
res := make(map[string]any)
res["running"] = build.Version
res["latest"] = rel.Tag
res["newer"] = upgrade.CompareVersions(rel.Tag, build.Version) == upgrade.Newer
@@ -1439,7 +1446,7 @@ func (*service) getDeviceID(w http.ResponseWriter, r *http.Request) {
func (*service) getLang(w http.ResponseWriter, r *http.Request) {
lang := r.Header.Get("Accept-Language")
weights := make(map[string]float64)
for _, l := range strings.Split(lang, ",") {
for l := range strings.SplitSeq(lang, ",") {
parts := strings.SplitN(l, ";", 2)
code := strings.ToLower(strings.TrimSpace(parts[0]))
weights[code] = 1.0
@@ -1638,7 +1645,7 @@ func (s *service) getFolderErrors(w http.ResponseWriter, r *http.Request) {
}
}
sendJSON(w, map[string]interface{}{
sendJSON(w, map[string]any{
"folder": folder,
"errors": errors,
"page": page,
@@ -1770,8 +1777,8 @@ func (f jsonFileInfo) MarshalJSON() ([]byte, error) {
return json.Marshal(m)
}
func fileIntfJSONMap(f protocol.FileInfo) map[string]interface{} {
out := map[string]interface{}{
func fileIntfJSONMap(f protocol.FileInfo) map[string]any {
out := map[string]any{
"name": f.FileName(),
"type": f.FileType().String(),
"size": f.Size,
@@ -1946,7 +1953,8 @@ func sanitizedHostname(name string) (string, error) {
return r > unicode.MaxASCII ||
!unicode.IsLetter(r) && !unicode.IsNumber(r) &&
r != '.' && r != '-'
})))
})),
)
name, _, err := transform.String(t, name)
if err != nil {
return "", err
+1 -4
View File
@@ -311,10 +311,7 @@ func authLDAP(username string, password string, cfg config.LDAPConfiguration) bo
func formatOptionalPercentS(template string, username string) string {
var replacements []any
nReps := strings.Count(template, "%s") - strings.Count(template, "%%s")
if nReps < 0 {
nReps = 0
}
nReps := max(strings.Count(template, "%s")-strings.Count(template, "%%s"), 0)
for range nReps {
replacements = append(replacements, username)
}
+9 -2
View File
@@ -433,7 +433,6 @@ func TestAPIServiceRequests(t *testing.T) {
}
for _, tc := range cases {
tc := tc
t.Run(tc.URL, func(t *testing.T) {
t.Parallel()
testHTTPRequest(t, baseURL, tc, testAPIKey)
@@ -1723,7 +1722,7 @@ func TestConfigChanges(t *testing.T) {
return resp
}
mod := func(method, path string, data interface{}) {
mod := func(method, path string, data any) {
t.Helper()
bs, err := json.Marshal(data)
if err != nil {
@@ -1830,6 +1829,14 @@ func TestSanitizedHostname(t *testing.T) {
}
}
func TestPrometheusMetrics(t *testing.T) {
// We should get some form of reasonable metrics response
bs := prometheusMetrics()
if !bytes.Contains(bs, []byte("TYPE go_info gauge")) {
t.Error("metrics should include go_info gauge")
}
}
// runningInContainer returns true if we are inside Docker or LXC. It might
// be prone to false negatives if things change in the future, but likely
// not false positives.
+42
View File
@@ -0,0 +1,42 @@
// Copyright (C) 2026 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
package auto
import (
"compress/gzip"
"io"
"strings"
"testing"
)
const customBuildMarker = "It syncs .stignore now!"
func TestCustomBuildMarkerIsEmbedded(t *testing.T) {
asset, ok := Assets()["default/index.html"]
if !ok {
t.Fatal("default/index.html is missing from embedded GUI assets")
}
content := asset.Content
if asset.Gzipped {
reader, err := gzip.NewReader(strings.NewReader(asset.Content))
if err != nil {
t.Fatal(err)
}
defer reader.Close()
data, err := io.ReadAll(reader)
if err != nil {
t.Fatal(err)
}
content = string(data)
}
if !strings.Contains(content, customBuildMarker) {
t.Fatalf("embedded GUI assets do not contain custom build marker %q", customBuildMarker)
}
}
-1
View File
@@ -5,7 +5,6 @@
// You can obtain one at https://mozilla.org/MPL/2.0/.
//go:build noassets
// +build noassets
package auto
+1 -1
View File
@@ -441,7 +441,7 @@ func (c *configMuxBuilder) adjustLDAP(w http.ResponseWriter, r *http.Request, ld
}
// Unmarshals the content of the given body and stores it in to (i.e. to must be a pointer).
func unmarshalTo(body io.ReadCloser, to interface{}) error {
func unmarshalTo(body io.ReadCloser, to any) error {
bs, err := io.ReadAll(body)
body.Close()
if err != nil {
-1
View File
@@ -5,7 +5,6 @@
// You can obtain one at https://mozilla.org/MPL/2.0/.
//go:build race
// +build race
package build
+2 -2
View File
@@ -664,7 +664,7 @@ func (defaults *Defaults) prepare(myID protocol.DeviceID, existingDevices map[pr
defaults.Device.prepare(nil)
}
func ensureZeroForNodefault(empty interface{}, target interface{}) {
func ensureZeroForNodefault(empty any, target any) {
copyMatchingTag(empty, target, "nodefault", func(v string) bool {
if len(v) > 0 && v != "true" {
panic(fmt.Sprintf(`unexpected tag value: %s. expected untagged or "true"`, v))
@@ -674,7 +674,7 @@ func ensureZeroForNodefault(empty interface{}, target interface{}) {
}
// copyMatchingTag copies fields tagged tag:"value" from "from" struct onto "to" struct.
func copyMatchingTag(from interface{}, to interface{}, tag string, shouldCopy func(value string) bool) {
func copyMatchingTag(from any, to any, tag string, shouldCopy func(value string) bool) {
fromStruct := reflect.ValueOf(from).Elem()
fromType := fromStruct.Type()
+6 -6
View File
@@ -1158,8 +1158,8 @@ func TestInvalidDeviceIDRejected(t *testing.T) {
// Change the device ID of the first device to "invalid". Fast and loose
// with the type assertions as we know what the JSON decoder returns.
devs := cfg["devices"].([]interface{})
dev0 := devs[0].(map[string]interface{})
devs := cfg["devices"].([]any)
dev0 := devs[0].(map[string]any)
dev0["deviceID"] = tc.id
devs[0] = dev0
@@ -1197,8 +1197,8 @@ func TestInvalidFolderIDRejected(t *testing.T) {
// Change the folder ID of the first folder to the empty string.
// Fast and loose with the type assertions as we know what the JSON
// decoder returns.
devs := cfg["folders"].([]interface{})
dev0 := devs[0].(map[string]interface{})
devs := cfg["folders"].([]any)
dev0 := devs[0].(map[string]any)
dev0["id"] = tc.id
devs[0] = dev0
@@ -1324,7 +1324,7 @@ func adjustFolderConfiguration(cfg *FolderConfiguration, id, label string, fsTyp
// defaultConfigAsMap returns a valid default config as a JSON-decoded
// map[string]interface{}. This is useful to override random elements and
// re-encode into JSON.
func defaultConfigAsMap() map[string]interface{} {
func defaultConfigAsMap() map[string]any {
cfg := New(device1)
dev := cfg.Defaults.Device.Copy()
adjustDeviceConfiguration(&dev, device2, "name")
@@ -1337,7 +1337,7 @@ func defaultConfigAsMap() map[string]interface{} {
// can't happen
panic(err)
}
var tmp map[string]interface{}
var tmp map[string]any
if err := json.Unmarshal(bs, &tmp); err != nil {
// can't happen
panic(err)
+2 -3
View File
@@ -9,6 +9,7 @@ package config
import (
"encoding/json"
"encoding/xml"
"maps"
"slices"
"strings"
@@ -45,9 +46,7 @@ type internalParam struct {
func (c VersioningConfiguration) Copy() VersioningConfiguration {
cp := c
cp.Params = make(map[string]string, len(c.Params))
for k, v := range c.Params {
cp.Params[k] = v
}
maps.Copy(cp.Params, c.Params)
return cp
}
+2 -2
View File
@@ -401,7 +401,7 @@ func TestConnectionEstablishment(t *testing.T) {
}
}
func withConnectionPair(b interface{ Fatal(...interface{}) }, connUri string, h func(client, server internalConn)) {
func withConnectionPair(b interface{ Fatal(...any) }, connUri string, h func(client, server internalConn)) {
// Root of the service tree.
supervisor := suture.New("main", suture.Spec{
PassThroughPanics: true,
@@ -494,7 +494,7 @@ func withConnectionPair(b interface{ Fatal(...interface{}) }, connUri string, h
_ = serverConn.Close()
}
func mustGetCert(b interface{ Fatal(...interface{}) }) tls.Certificate {
func mustGetCert(b interface{ Fatal(...any) }) tls.Certificate {
cert, err := tlsutil.NewCertificateInMemory("bench", 10)
if err != nil {
b.Fatal(err)
+2 -2
View File
@@ -54,7 +54,7 @@ func TestDialQueueSort(t *testing.T) {
var seen1, seen2 int
for i := 0; i < 100; i++ {
for range 100 {
queue.Sort()
res := shortDevices(queue)
if reflect.DeepEqual(res, expected1) {
@@ -90,7 +90,7 @@ func TestDialQueueSort(t *testing.T) {
var seen1, seen2 int
for i := 0; i < 100; i++ {
for range 100 {
queue.Sort()
res := shortDevices(queue)
if reflect.DeepEqual(res, expected1) {
+5 -9
View File
@@ -256,18 +256,14 @@ func (w *limitedWriter) Write(buf []byte) (int, error) {
// try to be a bit adaptable. We range from the minimum write size of 1
// KiB up to the limiter burst size, aiming for about a write every
// 10ms.
singleWriteSize := int(w.waiter.Limit() / 100) // 10ms worth of data
singleWriteSize = ((singleWriteSize / 1024) + 1) * 1024 // round up to the next kibibyte
if singleWriteSize > limiterBurstSize {
singleWriteSize = limiterBurstSize
}
singleWriteSize := int(w.waiter.Limit() / 100) // 10ms worth of data
singleWriteSize = min(
// round up to the next kibibyte
((singleWriteSize/1024)+1)*1024, limiterBurstSize)
written := 0
for written < len(buf) {
toWrite := singleWriteSize
if toWrite > len(buf)-written {
toWrite = len(buf) - written
}
toWrite := min(singleWriteSize, len(buf)-written)
w.take(toWrite)
n, err := w.writer.Write(buf[written : written+toWrite])
written += n
-1
View File
@@ -5,7 +5,6 @@
// You can obtain one at https://mozilla.org/MPL/2.0/.
//go:build go1.15 && !noquic
// +build go1.15,!noquic
package connections
-1
View File
@@ -5,7 +5,6 @@
// You can obtain one at http://mozilla.org/MPL/2.0/.
//go:build !noquic
// +build !noquic
package connections
-1
View File
@@ -5,7 +5,6 @@
// You can obtain one at http://mozilla.org/MPL/2.0/.
//go:build !noquic
// +build !noquic
package connections
-1
View File
@@ -5,7 +5,6 @@
// You can obtain one at http://mozilla.org/MPL/2.0/.
//go:build noquic
// +build noquic
package connections
+6 -6
View File
@@ -18,23 +18,23 @@ import (
type Registry struct {
mut sync.Mutex
available map[string][]interface{}
available map[string][]any
}
func New() *Registry {
return &Registry{
available: make(map[string][]interface{}),
available: make(map[string][]any),
}
}
func (r *Registry) Register(scheme string, item interface{}) {
func (r *Registry) Register(scheme string, item any) {
r.mut.Lock()
defer r.mut.Unlock()
r.available[scheme] = append(r.available[scheme], item)
}
func (r *Registry) Unregister(scheme string, item interface{}) {
func (r *Registry) Unregister(scheme string, item any) {
r.mut.Lock()
defer r.mut.Unlock()
@@ -49,12 +49,12 @@ func (r *Registry) Unregister(scheme string, item interface{}) {
// Get returns an item for a schema compatible with the given scheme.
// If any item satisfies preferred, that has precedence over other items.
func (r *Registry) Get(scheme string, preferred func(interface{}) bool) interface{} {
func (r *Registry) Get(scheme string, preferred func(any) bool) any {
r.mut.Lock()
defer r.mut.Unlock()
var (
best interface{}
best any
bestPref bool
bestScheme string
)
+4 -4
View File
@@ -14,8 +14,8 @@ import (
func TestRegistry(t *testing.T) {
r := New()
want := func(i int) func(interface{}) bool {
return func(x interface{}) bool { return x.(int) == i }
want := func(i int) func(any) bool {
return func(x any) bool { return x.(int) == i }
}
if res := r.Get("int", want(1)); res != nil {
@@ -73,7 +73,7 @@ func TestShortSchemeFirst(t *testing.T) {
r.Register("foobar", 1)
// If we don't care about the value, we should get the one with "foo".
res := r.Get("foo", func(interface{}) bool { return false })
res := r.Get("foo", func(any) bool { return false })
if res != 0 {
t.Error("unexpected", res)
}
@@ -89,7 +89,7 @@ func BenchmarkGet(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
r.Get("tcp", func(x interface{}) bool {
r.Get("tcp", func(x any) bool {
return x.(*net.TCPAddr).IP.IsUnspecified()
})
}
+3 -4
View File
@@ -19,6 +19,7 @@ import (
"fmt"
"io"
"log/slog"
"maps"
"math"
"net"
"net/url"
@@ -817,7 +818,7 @@ func (s *service) createListener(factory listenerFactory, uri *url.URL) bool {
}
func (s *service) logListenAddressesChangedEvent(l ListenerAddresses) {
s.evLogger.Log(events.ListenAddressesChanged, map[string]interface{}{
s.evLogger.Log(events.ListenAddressesChanged, map[string]any{
"address": l.URI,
"lan": l.LANAddresses,
"wan": l.WANAddresses,
@@ -995,9 +996,7 @@ func newConnectionStatusHandler() connectionStatusHandler {
func (s *connectionStatusHandler) ConnectionStatus() map[string]ConnectionStatusEntry {
result := make(map[string]ConnectionStatusEntry)
s.connectionStatusMut.RLock()
for k, v := range s.connectionStatus {
result[k] = v
}
maps.Copy(result, s.connectionStatus)
s.connectionStatusMut.RUnlock()
return result
}
-1
View File
@@ -5,7 +5,6 @@
// You can obtain one at https://mozilla.org/MPL/2.0/.
//go:build !solaris && !windows
// +build !solaris,!windows
package dialer
-1
View File
@@ -5,7 +5,6 @@
// You can obtain one at https://mozilla.org/MPL/2.0/.
//go:build solaris
// +build solaris
package dialer
-1
View File
@@ -5,7 +5,6 @@
// You can obtain one at https://mozilla.org/MPL/2.0/.
//go:build windows
// +build windows
package dialer
+1 -1
View File
@@ -111,7 +111,7 @@ func DialContextReusePortFunc(registry *registry.Registry) func(ctx context.Cont
return DialContext(ctx, network, addr)
}
localAddrInterface := registry.Get(network, func(addr interface{}) bool {
localAddrInterface := registry.Get(network, func(addr any) bool {
return addr.(*net.TCPAddr).IP.IsUnspecified()
})
if localAddrInterface == nil {
+2 -3
View File
@@ -7,6 +7,7 @@
package discover
import (
"maps"
"sync"
"time"
@@ -60,9 +61,7 @@ func (c *cache) Get(id protocol.DeviceID) (CacheEntry, bool) {
func (c *cache) Cache() map[protocol.DeviceID]CacheEntry {
c.mut.Lock()
m := make(map[protocol.DeviceID]CacheEntry, len(c.entries))
for k, v := range c.entries {
m[k] = v
}
maps.Copy(m, c.entries)
c.mut.Unlock()
return m
}
+1 -1
View File
@@ -294,7 +294,7 @@ func (c *localClient) registerDevice(src net.Addr, device *discoproto.Announce)
})
if isNewDevice {
c.evLogger.Log(events.DeviceDiscovered, map[string]interface{}{
c.evLogger.Log(events.DeviceDiscovered, map[string]any{
"device": id.String(),
"addrs": validAddresses,
})
+7 -7
View File
@@ -235,7 +235,7 @@ const BufferSize = 64
type Logger interface {
suture.Service
Log(t EventType, data interface{})
Log(t EventType, data any)
Subscribe(mask EventType) Subscription
}
@@ -253,10 +253,10 @@ type Event struct {
// Per-subscription sequential event ID. Named "id" for backwards compatibility with the REST API
SubscriptionID int `json:"id"`
// Global ID of the event across all subscriptions
GlobalID int `json:"globalID"`
Time time.Time `json:"time"`
Type EventType `json:"type"`
Data interface{} `json:"data"`
GlobalID int `json:"globalID"`
Time time.Time `json:"time"`
Type EventType `json:"type"`
Data any `json:"data"`
}
type Subscription interface {
@@ -325,7 +325,7 @@ loop:
return nil
}
func (l *logger) Log(t EventType, data interface{}) {
func (l *logger) Log(t EventType, data any) {
l.events <- Event{
Time: time.Now(), // intentionally high precision
Type: t,
@@ -559,7 +559,7 @@ var NoopLogger Logger = &noopLogger{}
func (*noopLogger) Serve(_ context.Context) error { return nil }
func (*noopLogger) Log(_ EventType, _ interface{}) {}
func (*noopLogger) Log(_ EventType, _ any) {}
func (*noopLogger) Subscribe(_ EventType) Subscription {
return &noopSubscription{}
+4 -4
View File
@@ -127,7 +127,7 @@ func TestBufferOverflow(t *testing.T) {
t0 := time.Now()
const nEvents = BufferSize * 2
for i := 0; i < nEvents; i++ {
for range nEvents {
l.Log(DeviceConnected, "foo")
}
if d := time.Since(t0); d > 15*time.Second {
@@ -237,7 +237,7 @@ func TestBufferedSub(t *testing.T) {
bs := NewBufferedSubscription(s, 10*BufferSize)
go func() {
for i := 0; i < 10*BufferSize; i++ {
for i := range 10 * BufferSize {
l.Log(DeviceConnected, fmt.Sprintf("event-%d", i))
if i%30 == 0 {
// Give the buffer routine time to pick up the events
@@ -378,7 +378,7 @@ func TestUnsubscribeContention(t *testing.T) {
stopListeners := make(chan struct{})
var listenerWg sync.WaitGroup
for i := 0; i < listeners; i++ {
for range listeners {
listenerWg.Go(func() {
s := l.Subscribe(AllEvents)
defer s.Unsubscribe()
@@ -400,7 +400,7 @@ func TestUnsubscribeContention(t *testing.T) {
stopSenders := make(chan struct{})
defer close(stopSenders)
var senderWg sync.WaitGroup
for i := 0; i < senders; i++ {
for range senders {
senderWg.Go(func() {
t := time.NewTicker(time.Millisecond)
@@ -5,7 +5,6 @@
// You can obtain one at https://mozilla.org/MPL/2.0/.
//go:build linux
// +build linux
package fs
@@ -5,7 +5,6 @@
// You can obtain one at https://mozilla.org/MPL/2.0/.
//go:build windows
// +build windows
package fs
-1
View File
@@ -5,7 +5,6 @@
// You can obtain one at https://mozilla.org/MPL/2.0/.
//go:build linux
// +build linux
package fs
-1
View File
@@ -5,7 +5,6 @@
// You can obtain one at https://mozilla.org/MPL/2.0/.
//go:build linux || solaris
// +build linux solaris
package fs
-1
View File
@@ -5,7 +5,6 @@
// You can obtain one at https://mozilla.org/MPL/2.0/.
//go:build !windows
// +build !windows
package fs
-1
View File
@@ -5,7 +5,6 @@
// You can obtain one at https://mozilla.org/MPL/2.0/.
//go:build linux || android
// +build linux android
package fs
-1
View File
@@ -5,7 +5,6 @@
// You can obtain one at https://mozilla.org/MPL/2.0/.
//go:build !linux && !android && !windows
// +build !linux,!android,!windows
package fs
-1
View File
@@ -5,7 +5,6 @@
// You can obtain one at https://mozilla.org/MPL/2.0/.
//go:build windows
// +build windows
package fs
-1
View File
@@ -5,7 +5,6 @@
// You can obtain one at https://mozilla.org/MPL/2.0/.
//go:build !windows
// +build !windows
package fs
+1 -1
View File
@@ -580,7 +580,7 @@ func TestXattr(t *testing.T) {
// Create a set of random attributes that we will set and read back
var attrs []protocol.Xattr
for i := 0; i < 10; i++ {
for i := range 10 {
key := fmt.Sprintf("user.test-%d", i)
value := make([]byte, xattrSize())
rand.Read(value)
-1
View File
@@ -5,7 +5,6 @@
// You can obtain one at https://mozilla.org/MPL/2.0/.
//go:build !windows
// +build !windows
package fs
-1
View File
@@ -5,7 +5,6 @@
// You can obtain one at http://mozilla.org/MPL/2.0/.
//go:build linux
// +build linux
package fs
-1
View File
@@ -5,7 +5,6 @@
// You can obtain one at http://mozilla.org/MPL/2.0/.
//go:build !linux
// +build !linux
package fs
@@ -5,7 +5,6 @@
// You can obtain one at http://mozilla.org/MPL/2.0/.
//go:build darwin && !kqueue && cgo && !ios
// +build darwin,!kqueue,cgo,!ios
package fs
-1
View File
@@ -5,7 +5,6 @@
// You can obtain one at http://mozilla.org/MPL/2.0/.
//go:build solaris && cgo
// +build solaris,cgo
package fs
@@ -5,7 +5,6 @@
// You can obtain one at http://mozilla.org/MPL/2.0/.
//go:build linux
// +build linux
package fs
@@ -5,7 +5,6 @@
// You can obtain one at http://mozilla.org/MPL/2.0/.
//go:build dragonfly || freebsd || netbsd || openbsd || ios || kqueue
// +build dragonfly freebsd netbsd openbsd ios kqueue
package fs
-1
View File
@@ -5,7 +5,6 @@
// You can obtain one at http://mozilla.org/MPL/2.0/.
//go:build !linux && !windows && !dragonfly && !freebsd && !netbsd && !openbsd && !solaris && !darwin && !cgo && !ios
// +build !linux,!windows,!dragonfly,!freebsd,!netbsd,!openbsd,!solaris,!darwin,!cgo,!ios
// Catch all platforms that are not specifically handled to use the generic
// event types.
@@ -5,7 +5,6 @@
// You can obtain one at http://mozilla.org/MPL/2.0/.
//go:build windows
// +build windows
package fs
-1
View File
@@ -5,7 +5,6 @@
// You can obtain one at http://mozilla.org/MPL/2.0/.
//go:build !dragonfly && !freebsd && !netbsd && !openbsd && !kqueue && !ios
// +build !dragonfly,!freebsd,!netbsd,!openbsd,!kqueue,!ios
package fs
+2 -4
View File
@@ -5,7 +5,6 @@
// You can obtain one at http://mozilla.org/MPL/2.0/.
//go:build (!solaris && !darwin) || (solaris && cgo) || (darwin && cgo)
// +build !solaris,!darwin solaris,cgo darwin,cgo
package fs
@@ -364,8 +363,7 @@ func TestWatchSymlinkedRoot(t *testing.T) {
linkedFs := NewFilesystem(FilesystemTypeBasic, filepath.Join(testFs.URI(), link))
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ctx := t.Context()
if _, _, err := linkedFs.Watch(".", fakeMatcher{}, ctx, false); err != nil {
panic(err)
}
@@ -635,6 +633,6 @@ func (fakeEventInfo) Event() notify.Event {
return notify.Write
}
func (fakeEventInfo) Sys() interface{} {
func (fakeEventInfo) Sys() any {
return nil
}
-1
View File
@@ -5,7 +5,6 @@
// You can obtain one at http://mozilla.org/MPL/2.0/.
//go:build (solaris && !cgo) || (darwin && !cgo) || (darwin && kqueue)
// +build solaris,!cgo darwin,!cgo darwin,kqueue
package fs
-1
View File
@@ -5,7 +5,6 @@
// You can obtain one at https://mozilla.org/MPL/2.0/.
//go:build windows
// +build windows
package fs
-1
View File
@@ -5,7 +5,6 @@
// You can obtain one at https://mozilla.org/MPL/2.0/.
//go:build freebsd || netbsd
// +build freebsd netbsd
package fs
-1
View File
@@ -5,7 +5,6 @@
// You can obtain one at https://mozilla.org/MPL/2.0/.
//go:build linux || darwin
// +build linux darwin
package fs
-1
View File
@@ -5,7 +5,6 @@
// You can obtain one at https://mozilla.org/MPL/2.0/.
//go:build !windows && !dragonfly && !illumos && !solaris && !openbsd
// +build !windows,!dragonfly,!illumos,!solaris,!openbsd
package fs
-1
View File
@@ -5,7 +5,6 @@
// You can obtain one at https://mozilla.org/MPL/2.0/.
//go:build windows || dragonfly || illumos || solaris || openbsd
// +build windows dragonfly illumos solaris openbsd
package fs
+1 -1
View File
@@ -1017,6 +1017,6 @@ func (f *fakeFileInfo) Group() int {
return f.gid
}
func (*fakeFileInfo) Sys() interface{} {
func (*fakeFileInfo) Sys() any {
return nil
}
+3 -3
View File
@@ -95,7 +95,7 @@ type FileInfo interface {
Size() int64
ModTime() time.Time
IsDir() bool
Sys() interface{}
Sys() any
// Extensions
IsRegular() bool
IsSymlink() bool
@@ -295,8 +295,8 @@ func NewFilesystem(fsType FilesystemType, uri string, opts ...Option) Filesystem
}
// fs cannot import config or versioner, so we hard code .stfolder
// (config.DefaultMarkerName) and .stversions (versioner.DefaultPath)
var internals = []string{".stfolder", ".stignore", ".stversions"}
// (config.DefaultMarkerName) and .stversions (versioner.DefaultPath).
var internals = []string{".stfolder", ".stversions"}
// IsInternal returns true if the file, as a path relative to the folder
// root, represents an internal file that should always be ignored. The file
+2 -2
View File
@@ -21,10 +21,8 @@ func TestIsInternal(t *testing.T) {
internal bool
}{
{".stfolder", true},
{".stignore", true},
{".stversions", true},
{".stfolder/foo", true},
{".stignore/foo", true},
{".stversions/foo", true},
{".stfolderfoo", false},
@@ -34,6 +32,8 @@ func TestIsInternal(t *testing.T) {
{"foo.stignore", false},
{"foo.stversions", false},
{"foo/.stfolder", false},
{".stignore", false},
{".stignore/foo", false},
{"foo/.stignore", false},
{"foo/.stversions", false},
}
+1 -4
View File
@@ -192,10 +192,7 @@ func CommonPrefix(first, second string) string {
isAbs := filepath.IsAbs(first) && filepath.IsAbs(second)
count := len(firstParts)
if len(secondParts) < len(firstParts) {
count = len(secondParts)
}
count := min(len(secondParts), len(firstParts))
common := make([]string, 0, count)
for i := range count {
+1 -1
View File
@@ -106,7 +106,7 @@ func TestSanitizePath(t *testing.T) {
func TestSanitizePathFuzz(t *testing.T) {
buf := make([]byte, 128)
for i := 0; i < 100; i++ {
for range 100 {
rand.Read(buf)
path := SanitizePath(string(buf))
if !utf8.ValidString(path) {
+2 -3
View File
@@ -11,6 +11,7 @@ import (
"compress/gzip"
"context"
"fmt"
"maps"
"net/http"
"strconv"
"strings"
@@ -43,9 +44,7 @@ type recordedResponse struct {
}
func (resp *recordedResponse) ServeHTTP(w http.ResponseWriter, r *http.Request) {
for k, v := range resp.header {
w.Header()[k] = v
}
maps.Copy(w.Header(), resp.header)
w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", int(resp.keep.Seconds())))

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