Compare commits
33
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee275fee65 | ||
|
|
6606e6b4d3 | ||
|
|
f6428af4c8 | ||
|
|
a5cbeeafea | ||
|
|
5dbf809a4c | ||
|
|
e16b8013a6 | ||
|
|
6df85dc95c | ||
|
|
f93306c819 | ||
|
|
9cdff04877 | ||
|
|
9bb5f9b4dd | ||
|
|
8961f642c6 | ||
|
|
d91f8849a2 | ||
|
|
6be1ff8480 | ||
|
|
3ec73403c1 | ||
|
|
8ca3cca0a0 | ||
|
|
0c489f4ae2 | ||
|
|
deb1e5b38a | ||
|
|
49a2688caa | ||
|
|
05b4f6abda | ||
|
|
9152d7fb2f | ||
|
|
4404b4dfb4 | ||
|
|
b537090d91 | ||
|
|
79423edbdf | ||
|
|
33075974cb | ||
|
|
8a3a06f7ca | ||
|
|
d0b35021c6 | ||
|
|
6322091462 | ||
|
|
5464970c5d | ||
|
|
3962a23723 | ||
|
|
feaa90408e | ||
|
|
a8ed6e4855 | ||
|
|
5b1e1c0520 | ||
|
|
c17be06192 |
@@ -22,6 +22,7 @@ jobs:
|
||||
if: github.repository_owner == 'syncthing'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
pkg:
|
||||
- stcrashreceiver
|
||||
|
||||
@@ -164,7 +164,7 @@ jobs:
|
||||
needs:
|
||||
- build-test
|
||||
- package-linux
|
||||
# - package-illumos
|
||||
- package-illumos
|
||||
- package-cross
|
||||
- package-source
|
||||
- package-debian
|
||||
@@ -646,7 +646,7 @@ jobs:
|
||||
needs:
|
||||
- codesign-windows
|
||||
- package-linux
|
||||
# - package-illumos
|
||||
- package-illumos
|
||||
- package-macos
|
||||
- package-cross
|
||||
- package-source
|
||||
|
||||
@@ -116,6 +116,7 @@ Dmitry Saveliev (dsaveliev) <d.e.saveliev@gmail.com>
|
||||
domain <32405309+szu17dmy@users.noreply.github.com>
|
||||
Domenic Horner <domenic@tgxn.net>
|
||||
Dominik Heidler (asdil12) <dominik@heidler.eu>
|
||||
Elias <1elias.bauer@gmail.com>
|
||||
Elias Jarlebring (jarlebring) <jarlebring@gmail.com>
|
||||
Elliot Huffman <thelich2@gmail.com>
|
||||
Emil Hessman (ceh) <emil@hessman.se>
|
||||
@@ -216,6 +217,7 @@ Matic Potočnik <hairyfotr@gmail.com>
|
||||
Matt Burke (burkemw3) <mburke@amplify.com> <burkemw3@gmail.com>
|
||||
Matt Robenolt <matt@ydekproductions.com>
|
||||
Matteo Ruina <matteo.ruina@gmail.com>
|
||||
mattn <mattn.jp@gmail.com>
|
||||
Maurizio Tomasi <ziotom78@gmail.com>
|
||||
Max <github@germancoding.com>
|
||||
Max Schulze (kralo) <max.schulze@online.de> <kralo@users.noreply.github.com>
|
||||
@@ -286,6 +288,7 @@ Sergey Mishin (ralder) <ralder@yandex.ru>
|
||||
Sertonix <83883937+Sertonix@users.noreply.github.com>
|
||||
Severin von Wnuck-Lipinski <ss7@live.de>
|
||||
Shaarad Dalvi <60266155+shaaraddalvi@users.noreply.github.com> <shdalv@microsoft.com>
|
||||
Shablone <20610621+Shablone@users.noreply.github.com>
|
||||
Shivam Kumar <155747305+maishivamhoo123@users.noreply.github.com>
|
||||
Simon Mwepu <simonmwepu@gmail.com>
|
||||
Simon Pickup <simon@pickupinfinity.com>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<application>
|
||||
<windowsSettings>
|
||||
<consoleAllocationPolicy xmlns="http://schemas.microsoft.com/SMI/2024/WindowsSettings">detached</consoleAllocationPolicy>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
</assembly>
|
||||
@@ -718,7 +718,7 @@ func shouldBuildSyso(dir string) (string, error) {
|
||||
}
|
||||
|
||||
jsonPath := filepath.Join(dir, "versioninfo.json")
|
||||
err = os.WriteFile(jsonPath, bs, 0o644)
|
||||
err = os.WriteFile(jsonPath, bs, 0o666)
|
||||
if err != nil {
|
||||
return "", errors.New("failed to create " + jsonPath + ": " + err.Error())
|
||||
}
|
||||
@@ -732,9 +732,18 @@ func shouldBuildSyso(dir string) (string, error) {
|
||||
sysoPath := filepath.Join(dir, "cmd", "syncthing", "resource.syso")
|
||||
|
||||
// See https://github.com/josephspurrier/goversioninfo#command-line-flags
|
||||
arm := strings.HasPrefix(goarch, "arm")
|
||||
a64 := strings.Contains(goarch, "64")
|
||||
if _, err := runError("goversioninfo", "-o", sysoPath, fmt.Sprintf("-arm=%v", arm), fmt.Sprintf("-64=%v", a64)); err != nil {
|
||||
// For manifest see https://learn.microsoft.com/en-us/windows/console/console-allocation-policy
|
||||
isARM := strings.HasPrefix(goarch, "arm")
|
||||
is64Bit := strings.Contains(goarch, "64")
|
||||
|
||||
args := []string{
|
||||
"-manifest=assets/windows/syncthing.exe.manifest", // console-allocation-policy
|
||||
"-o", sysoPath, // output path
|
||||
fmt.Sprintf("-arm=%v", isARM),
|
||||
fmt.Sprintf("-64=%v", is64Bit),
|
||||
}
|
||||
|
||||
if _, err := runError("goversioninfo", args...); err != nil {
|
||||
return "", errors.New("failed to create " + sysoPath + ": " + err.Error())
|
||||
}
|
||||
|
||||
@@ -774,7 +783,7 @@ func copyFile(src, dst string, perm os.FileMode) error {
|
||||
}
|
||||
|
||||
copy:
|
||||
os.MkdirAll(filepath.Dir(dst), 0o777)
|
||||
os.MkdirAll(filepath.Dir(dst), os.ModePerm)
|
||||
if err := os.WriteFile(dst, in, perm); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1423,7 +1432,7 @@ func writeCompatJSON() {
|
||||
continue
|
||||
}
|
||||
bs, _ := json.MarshalIndent(e, "", " ")
|
||||
if err := os.WriteFile("compat.json", bs, 0o644); err != nil {
|
||||
if err := os.WriteFile("compat.json", bs, 0o666); err != nil {
|
||||
log.Fatal("Writing compat.json:", err)
|
||||
}
|
||||
return
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
2026-05-21 10:03:01 INF syncthing v2.1.1-dev.9.gb3b7d228.dirty-morecrashrep "Hafnium Hornet" (go1.26.3 darwin-arm64) jb@jbo-m3wl72rv 2026-05-21 07:58:11 UTC [stnoupgrade] (log.pkg=main)
|
||||
2026-05-21 10:03:01 INF No automatic upgrades; STNOUPGRADE environment variable defined (log.pkg=main)
|
||||
2026-05-21 10:03:01 INF Calculated our device ID (device=I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU log.pkg=syncthing)
|
||||
2026-05-21 10:03:01 INF Overall rate limit in use (send="is unlimited" recv="is unlimited" log.pkg=connections)
|
||||
2026-05-21 10:03:01 INF Using discovery mechanism (identity="IPv4 local broadcast discovery on port 21027" log.pkg=discover)
|
||||
2026-05-21 10:03:01 INF Using discovery mechanism (identity="IPv6 local multicast discovery on address [ff12::8384]:21027" log.pkg=discover)
|
||||
2026-05-21 10:03:01 INF TCP listener starting (address=127.0.0.1:22001 log.pkg=connections)
|
||||
2026-05-21 10:03:01 INF Ready to synchronize (folder.id=default folder.type=sendreceive log.pkg=model)
|
||||
2026-05-21 10:03:01 INF QUIC listener starting (address=127.0.0.1:22001 log.pkg=connections)
|
||||
2026-05-21 10:03:01 INF GUI and API listening (address=127.0.0.1:8081 log.pkg=api)
|
||||
...
|
||||
2026-05-21 10:03:01 INF Access the GUI via the following URL: http://127.0.0.1:8081/ (log.pkg=api)
|
||||
2026-05-21 10:03:01 INF Loaded configuration (name=s1 log.pkg=syncthing)
|
||||
2026-05-21 10:03:01 INF Loaded peer device configuration (device=MRIW7OK name=s2 address="[tcp://127.0.0.1:22002 quic://127.0.0.1:22002]" log.pkg=syncthing)
|
||||
2026-05-21 10:03:01 INF Completed initial scan (folder.id=default folder.type=sendreceive log.pkg=model)
|
||||
0xee9de1fe260
|
||||
2026-05-21 10:03:02 INF Measured hashing performance (perf="2789.71 MB/s" log.pkg=syncthing)
|
||||
Panic at 2026-05-21T10:03:02+02:00
|
||||
runtime: marked free object in span 0x108b34d20, elemsize=8 freeindex=34 (bad use of unsafe.Pointer or having race conditions? try -d=checkptr or -race)
|
||||
0xee9de1fe000 alloc marked
|
||||
0xee9de1fe008 alloc marked
|
||||
...
|
||||
0xee9de1fe250 free unmarked
|
||||
0xee9de1fe258 free unmarked
|
||||
0xee9de1fe260 free marked zombie
|
||||
7 6 5 4 3 2 1 0 f e d c b a 9 8 0123456789abcdef
|
||||
00000ee9de1fe260: 00000000 00000000 ........
|
||||
0xee9de1fe268 free unmarked
|
||||
0xee9de1fe270 free unmarked
|
||||
...
|
||||
0xee9de1fff60 free unmarked
|
||||
0xee9de1fff68 free unmarked
|
||||
0xee9de1fff70 free unmarked
|
||||
0xee9de1fff78 free unmarked
|
||||
fatal error: found pointer to free object
|
||||
|
||||
runtime stack:
|
||||
runtime.throw({0x105881781?, 0x8?})
|
||||
runtime/panic.go:1229 +0x38 fp=0x16bf82bb0 sp=0x16bf82b80 pc=0x104f0ca48
|
||||
runtime.(*mspan).reportZombies(0x108b34d20)
|
||||
runtime/mgcsweep.go:893 +0x314 fp=0x16bf82c30 sp=0x16bf82bb0 pc=0x104ec10b4
|
||||
runtime.(*sweepLocked).sweep(0x16bf82d88?, 0x0)
|
||||
runtime/mgcsweep.go:673 +0xbd0 fp=0x16bf82d50 sp=0x16bf82c30 pc=0x104ec0840
|
||||
runtime.(*mcentral).uncacheSpan(0x16bf82db8?, 0x104ea4954?)
|
||||
runtime/mcentral.go:237 +0xbc fp=0x16bf82d80 sp=0x16bf82d50 pc=0x104eaac3c
|
||||
runtime.(*mcache).releaseAll(0x1089a85f0)
|
||||
runtime/mcache.go:322 +0x188 fp=0x16bf82df0 sp=0x16bf82d80 pc=0x104eaa4e8
|
||||
runtime.(*mcache).prepareForSweep(0x1089a85f0)
|
||||
runtime/mcache.go:366 +0x4c fp=0x16bf82e20 sp=0x16bf82df0 pc=0x104eaa61c
|
||||
runtime.gcMarkTermination.func4(0xee9de005808)
|
||||
runtime/mgc.go:1546 +0x24 fp=0x16bf82e50 sp=0x16bf82e20 pc=0x104f076e4
|
||||
runtime.forEachPInternal(0x10656f798)
|
||||
runtime/proc.go:2167 +0x178 fp=0x16bf82ee0 sp=0x16bf82e50 pc=0x104eda728
|
||||
runtime.gcMarkTermination.forEachP.func7()
|
||||
runtime/proc.go:2126 +0x40 fp=0x16bf82f10 sp=0x16bf82ee0 pc=0x104eb3130
|
||||
runtime.systemstack(0x7fc000)
|
||||
runtime/asm_arm64.s:399 +0x68 fp=0x16bf82f20 sp=0x16bf82f10 pc=0x104f12888
|
||||
|
||||
goroutine 84 gp=0xee9de45c1e0 m=3 mp=0xee9de019008 [flushing proc caches]:
|
||||
runtime.systemstack_switch()
|
||||
runtime/asm_arm64.s:347 +0x8 fp=0xee9de805c40 sp=0xee9de805c30 pc=0x104f12808
|
||||
runtime.forEachP(...)
|
||||
runtime/proc.go:2112
|
||||
runtime.gcMarkTermination({0xc0?, 0x1331f928480ca?, 0xc?, 0x0?})
|
||||
runtime/mgc.go:1545 +0x5f4 fp=0xee9de805e80 sp=0xee9de805c40 pc=0x104eb28c4
|
||||
runtime.gcMarkDone()
|
||||
runtime/mgc.go:1173 +0x364 fp=0xee9de805f20 sp=0xee9de805e80 pc=0x104eb1bc4
|
||||
runtime.gcBgMarkWorker(0xee9de341810)
|
||||
runtime/mgc.go:1912 +0x29c fp=0xee9de805fb0 sp=0xee9de805f20 pc=0x104eb372c
|
||||
runtime.gcBgMarkStartWorkers.gowrap1()
|
||||
runtime/mgc.go:1695 +0x20 fp=0xee9de805fd0 sp=0xee9de805fb0 pc=0x104eb3470
|
||||
runtime.goexit({})
|
||||
runtime/asm_arm64.s:1447 +0x4 fp=0xee9de805fd0 sp=0xee9de805fd0 pc=0x104f14a04
|
||||
created by runtime.gcBgMarkStartWorkers in goroutine 1
|
||||
runtime/mgc.go:1695 +0x134
|
||||
|
||||
@@ -42,7 +42,7 @@ type currentFile struct {
|
||||
}
|
||||
|
||||
func (d *diskStore) Serve(ctx context.Context) {
|
||||
if err := os.MkdirAll(d.dir, 0o700); err != nil {
|
||||
if err := os.MkdirAll(d.dir, os.ModePerm); err != nil {
|
||||
log.Println("Creating directory:", err)
|
||||
return
|
||||
}
|
||||
@@ -62,7 +62,7 @@ func (d *diskStore) Serve(ctx context.Context) {
|
||||
case entry := <-d.inbox:
|
||||
path := d.fullPath(entry.path)
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
||||
if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
|
||||
log.Println("Creating directory:", err)
|
||||
continue
|
||||
}
|
||||
@@ -77,7 +77,7 @@ func (d *diskStore) Serve(ctx context.Context) {
|
||||
log.Println("Failed to compress crash report:", err)
|
||||
continue
|
||||
}
|
||||
if err := os.WriteFile(path, buf.Bytes(), 0o600); err != nil {
|
||||
if err := os.WriteFile(path, buf.Bytes(), 0o666); err != nil {
|
||||
log.Printf("Failed to write %s: %v", entry.path, err)
|
||||
_ = os.Remove(path)
|
||||
continue
|
||||
@@ -136,15 +136,25 @@ func (d *diskStore) Exists(path string) bool {
|
||||
}
|
||||
|
||||
func (d *diskStore) clean() {
|
||||
for len(d.currentFiles) > 0 && (len(d.currentFiles) > d.maxFiles || d.currentSize > d.maxBytes) {
|
||||
f := d.currentFiles[0]
|
||||
numDeleted := 0
|
||||
for idx := range d.currentFiles {
|
||||
if len(d.currentFiles)-numDeleted < d.maxFiles && d.currentSize < d.maxBytes {
|
||||
break
|
||||
}
|
||||
|
||||
f := d.currentFiles[idx]
|
||||
log.Println("Removing", f.path)
|
||||
if err := os.Remove(f.path); err != nil {
|
||||
log.Println("Failed to remove file:", err)
|
||||
}
|
||||
d.currentFiles = d.currentFiles[1:]
|
||||
d.currentSize -= f.size
|
||||
numDeleted = idx + 1
|
||||
}
|
||||
|
||||
// Compact currentFiles
|
||||
copy(d.currentFiles, d.currentFiles[numDeleted:])
|
||||
d.currentFiles = d.currentFiles[:len(d.currentFiles)-numDeleted]
|
||||
|
||||
var oldest time.Duration
|
||||
if len(d.currentFiles) > 0 {
|
||||
oldest = time.Since(time.Unix(d.currentFiles[0].mtime, 0)).Truncate(time.Minute)
|
||||
@@ -158,7 +168,7 @@ func (d *diskStore) clean() {
|
||||
}
|
||||
|
||||
func (d *diskStore) inventory() error {
|
||||
d.currentFiles = nil
|
||||
d.currentFiles = d.currentFiles[:0]
|
||||
d.currentSize = 0
|
||||
err := filepath.Walk(d.dir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
@@ -29,7 +30,7 @@ import (
|
||||
raven "github.com/getsentry/raven-go"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/syncthing/syncthing/lib/build"
|
||||
"github.com/syncthing/syncthing/lib/ur"
|
||||
"github.com/syncthing/syncthing/lib/ur/contract"
|
||||
)
|
||||
|
||||
const maxRequestSize = 1 << 20 // 1 MiB
|
||||
@@ -89,6 +90,7 @@ func main() {
|
||||
if params.MetricsListen != "" {
|
||||
mmux := http.NewServeMux()
|
||||
mmux.Handle("/metrics", promhttp.Handler())
|
||||
mmux.HandleFunc("/debug/pprof/", pprof.Index)
|
||||
go func() {
|
||||
if err := http.ListenAndServe(params.MetricsListen, mmux); err != nil {
|
||||
log.Fatalln("HTTP serve metrics:", err)
|
||||
@@ -123,12 +125,13 @@ func handleFailureFn(dsn, failureDir string, ignore *ignorePatterns) func(w http
|
||||
return
|
||||
}
|
||||
|
||||
if ignore.match(bs) {
|
||||
if pat, ok := ignore.match(bs); ok {
|
||||
metricIgnoreMatchesTotal.WithLabelValues(pat).Inc()
|
||||
result = "ignored"
|
||||
return
|
||||
}
|
||||
|
||||
var reports []ur.FailureReport
|
||||
var reports []contract.FailureReport
|
||||
err = json.Unmarshal(bs, &reports)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
@@ -176,7 +179,7 @@ func handleFailureFn(dsn, failureDir string, ignore *ignorePatterns) func(w http
|
||||
}
|
||||
}
|
||||
|
||||
func saveFailureWithGoroutines(data ur.FailureData, failureDir string) (string, error) {
|
||||
func saveFailureWithGoroutines(data contract.FailureData, failureDir string) (string, error) {
|
||||
bs := make([]byte, len(data.Description)+len(data.Goroutines))
|
||||
copy(bs, data.Description)
|
||||
copy(bs[len(data.Description):], data.Goroutines)
|
||||
@@ -216,14 +219,14 @@ func loadIgnorePatterns(path string) (*ignorePatterns, error) {
|
||||
return &ignorePatterns{patterns: patterns}, nil
|
||||
}
|
||||
|
||||
func (i *ignorePatterns) match(report []byte) bool {
|
||||
func (i *ignorePatterns) match(report []byte) (string, bool) {
|
||||
if i == nil {
|
||||
return false
|
||||
return "", false
|
||||
}
|
||||
for _, re := range i.patterns {
|
||||
if re.Match(report) {
|
||||
return true
|
||||
return re.String(), true
|
||||
}
|
||||
}
|
||||
return false
|
||||
return "", false
|
||||
}
|
||||
|
||||
@@ -37,4 +37,24 @@ var (
|
||||
Subsystem: "crashreceiver",
|
||||
Name: "diskstore_oldest_age_seconds",
|
||||
})
|
||||
metricSentryReportsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: "syncthing",
|
||||
Subsystem: "crashreceiver",
|
||||
Name: "sentry_reports_total",
|
||||
}, []string{"result"})
|
||||
metricIgnoreMatchesTotal = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: "syncthing",
|
||||
Subsystem: "crashreceiver",
|
||||
Name: "ignore_matches_total",
|
||||
}, []string{"pattern"})
|
||||
metricSourceCodeLoadsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||
Namespace: "syncthing",
|
||||
Subsystem: "crashreceiver",
|
||||
Name: "source_code_loads_total",
|
||||
}, []string{"result"})
|
||||
metricSourceCodeCacheSize = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: "syncthing",
|
||||
Subsystem: "crashreceiver",
|
||||
Name: "source_code_cache_size",
|
||||
})
|
||||
)
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"regexp"
|
||||
@@ -52,11 +53,15 @@ func (s *sentryService) Serve(ctx context.Context) {
|
||||
pkt, err := parseCrashReport(req.reportID, req.data)
|
||||
if err != nil {
|
||||
log.Println("Failed to parse crash report:", err)
|
||||
metricSentryReportsTotal.WithLabelValues("parse_failure").Inc()
|
||||
continue
|
||||
}
|
||||
if err := sendReport(s.dsn, pkt, req.userID); err != nil {
|
||||
log.Println("Failed to send crash report:", err)
|
||||
metricSentryReportsTotal.WithLabelValues("send_failure").Inc()
|
||||
continue
|
||||
}
|
||||
metricSentryReportsTotal.WithLabelValues("success").Inc()
|
||||
|
||||
case <-ctx.Done():
|
||||
return
|
||||
@@ -69,6 +74,7 @@ func (s *sentryService) Send(reportID, userID string, data []byte) bool {
|
||||
case s.inbox <- sentryRequest{reportID, userID, data}:
|
||||
return true
|
||||
default:
|
||||
metricCrashReportsTotal.WithLabelValues("overflow").Inc()
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -108,11 +114,10 @@ func parseCrashReport(path string, report []byte) (*raven.Packet, error) {
|
||||
|
||||
version, err := build.ParseVersion(string(parts[0]))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("%w in %q", err, parts[0])
|
||||
}
|
||||
report = parts[1]
|
||||
|
||||
foundPanic := false
|
||||
var subjectLine []byte
|
||||
for {
|
||||
parts = bytes.SplitN(report, []byte("\n"), 2)
|
||||
@@ -123,14 +128,9 @@ func parseCrashReport(path string, report []byte) (*raven.Packet, error) {
|
||||
line := parts[0]
|
||||
report = parts[1]
|
||||
|
||||
if foundPanic {
|
||||
// The previous line was our "Panic at ..." header. We are now
|
||||
// at the beginning of the real panic trace and this is our
|
||||
// subject line.
|
||||
if bytes.HasPrefix(line, []byte("panic:")) || bytes.HasPrefix(line, []byte("fatal error:")) {
|
||||
subjectLine = line
|
||||
break
|
||||
} else if bytes.HasPrefix(line, []byte("Panic at")) {
|
||||
foundPanic = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,26 +9,33 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseReport(t *testing.T) {
|
||||
bs, err := os.ReadFile("_testdata/panic.log")
|
||||
files, err := filepath.Glob("_testdata/*.log")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, file := range files {
|
||||
bs, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
pkt, err := parseCrashReport("1/2/345", bs)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
pkt, err := parseCrashReport("1/2/345", bs)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
bs, err = pkt.JSON()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Printf("%s\n", bs)
|
||||
}
|
||||
|
||||
bs, err = pkt.JSON()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Printf("%s\n", bs)
|
||||
}
|
||||
|
||||
func TestCrashReportFingerprint(t *testing.T) {
|
||||
|
||||
@@ -15,23 +15,33 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
lru "github.com/hashicorp/golang-lru/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
urlPrefix = "https://raw.githubusercontent.com/syncthing/syncthing/"
|
||||
httpTimeout = 10 * time.Second
|
||||
urlPrefix = "https://raw.githubusercontent.com/syncthing/syncthing/"
|
||||
httpTimeout = 10 * time.Second
|
||||
maxCacheEntries = 1000
|
||||
)
|
||||
|
||||
type cacheKey struct {
|
||||
version string
|
||||
file string
|
||||
}
|
||||
|
||||
type githubSourceCodeLoader struct {
|
||||
mut sync.Mutex
|
||||
version string
|
||||
cache map[string]map[string][][]byte // version -> file -> lines
|
||||
client *http.Client
|
||||
|
||||
cache *lru.TwoQueueCache[cacheKey, [][]byte] // version & file -> lines
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func newGithubSourceCodeLoader() *githubSourceCodeLoader {
|
||||
cache, _ := lru.New2Q[cacheKey, [][]byte](maxCacheEntries)
|
||||
return &githubSourceCodeLoader{
|
||||
cache: make(map[string]map[string][][]byte),
|
||||
cache: cache,
|
||||
client: &http.Client{Timeout: httpTimeout},
|
||||
}
|
||||
}
|
||||
@@ -39,9 +49,6 @@ func newGithubSourceCodeLoader() *githubSourceCodeLoader {
|
||||
func (l *githubSourceCodeLoader) LockWithVersion(version string) {
|
||||
l.mut.Lock()
|
||||
l.version = version
|
||||
if _, ok := l.cache[version]; !ok {
|
||||
l.cache[version] = make(map[string][][]byte)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *githubSourceCodeLoader) Unlock() {
|
||||
@@ -50,11 +57,13 @@ func (l *githubSourceCodeLoader) Unlock() {
|
||||
|
||||
func (l *githubSourceCodeLoader) Load(filename string, line, context int) ([][]byte, int) {
|
||||
filename = filepath.ToSlash(filename)
|
||||
lines, ok := l.cache[l.version][filename]
|
||||
key := cacheKey{version: l.version, file: filename}
|
||||
lines, ok := l.cache.Get(key)
|
||||
if !ok {
|
||||
// Cache whatever we managed to find (or nil if nothing, so we don't try again)
|
||||
defer func() {
|
||||
l.cache[l.version][filename] = lines
|
||||
l.cache.Add(key, lines)
|
||||
metricSourceCodeCacheSize.Set(float64(l.cache.Len()))
|
||||
}()
|
||||
|
||||
knownPrefixes := []string{"/lib/", "/cmd/"}
|
||||
@@ -73,19 +82,25 @@ func (l *githubSourceCodeLoader) Load(filename string, line, context int) ([][]b
|
||||
resp, err := l.client.Get(url)
|
||||
if err != nil {
|
||||
fmt.Println("Loading source:", err)
|
||||
metricSourceCodeLoadsTotal.WithLabelValues("failed").Inc()
|
||||
return nil, 0
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
fmt.Println("Loading source:", resp.Status)
|
||||
metricSourceCodeLoadsTotal.WithLabelValues("failed").Inc()
|
||||
return nil, 0
|
||||
}
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
_ = resp.Body.Close()
|
||||
if err != nil {
|
||||
fmt.Println("Loading source:", err.Error())
|
||||
metricSourceCodeLoadsTotal.WithLabelValues("failed").Inc()
|
||||
return nil, 0
|
||||
}
|
||||
lines = bytes.Split(data, []byte{'\n'})
|
||||
metricSourceCodeLoadsTotal.WithLabelValues("loaded").Inc()
|
||||
} else {
|
||||
metricSourceCodeLoadsTotal.WithLabelValues("cached").Inc()
|
||||
}
|
||||
|
||||
return getLineFromLines(lines, line, context)
|
||||
|
||||
@@ -7,21 +7,18 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type crashReceiver struct {
|
||||
store *diskStore
|
||||
sentry *sentryService
|
||||
ignore *ignorePatterns
|
||||
|
||||
ignoredMut sync.RWMutex
|
||||
ignored map[string]struct{}
|
||||
}
|
||||
|
||||
func (r *crashReceiver) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
@@ -69,12 +66,6 @@ func (r *crashReceiver) serveGet(reportID string, w http.ResponseWriter, _ *http
|
||||
// serveHead responds to HEAD requests by checking if the named report
|
||||
// already exists in the system.
|
||||
func (r *crashReceiver) serveHead(reportID string, w http.ResponseWriter, _ *http.Request) {
|
||||
r.ignoredMut.RLock()
|
||||
_, ignored := r.ignored[reportID]
|
||||
r.ignoredMut.RUnlock()
|
||||
if ignored {
|
||||
return // found
|
||||
}
|
||||
if !r.store.Exists(reportID) {
|
||||
http.Error(w, "Not found", http.StatusNotFound)
|
||||
}
|
||||
@@ -87,17 +78,7 @@ func (r *crashReceiver) servePut(reportID string, w http.ResponseWriter, req *ht
|
||||
metricCrashReportsTotal.WithLabelValues(result).Inc()
|
||||
}()
|
||||
|
||||
r.ignoredMut.RLock()
|
||||
_, ignored := r.ignored[reportID]
|
||||
r.ignoredMut.RUnlock()
|
||||
if ignored {
|
||||
result = "ignored_cached"
|
||||
io.Copy(io.Discard, req.Body)
|
||||
return // found
|
||||
}
|
||||
|
||||
// Read at most maxRequestSize of report data.
|
||||
log.Println("Receiving report", reportID)
|
||||
lr := io.LimitReader(req.Body, maxRequestSize)
|
||||
bs, err := io.ReadAll(lr)
|
||||
if err != nil {
|
||||
@@ -106,14 +87,12 @@ func (r *crashReceiver) servePut(reportID string, w http.ResponseWriter, req *ht
|
||||
return
|
||||
}
|
||||
|
||||
if r.ignore.match(bs) {
|
||||
r.ignoredMut.Lock()
|
||||
if r.ignored == nil {
|
||||
r.ignored = make(map[string]struct{})
|
||||
}
|
||||
r.ignored[reportID] = struct{}{}
|
||||
r.ignoredMut.Unlock()
|
||||
first := string(bytes.TrimSpace(bytes.Split(bs, []byte("\n"))[0]))
|
||||
|
||||
if pat, ok := r.ignore.match(bs); ok {
|
||||
metricIgnoreMatchesTotal.WithLabelValues(pat).Inc()
|
||||
result = "ignored"
|
||||
log.Printf("Ignored report %s, matched: %s (%s)", reportID[:8], pat, first)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -121,13 +100,15 @@ func (r *crashReceiver) servePut(reportID string, w http.ResponseWriter, req *ht
|
||||
|
||||
// Store the report
|
||||
if !r.store.Put(reportID, bs) {
|
||||
log.Println("Failed to store report (queue full):", reportID)
|
||||
log.Println("Failed to store report (queue full):", reportID[:8])
|
||||
result = "queue_failure"
|
||||
}
|
||||
|
||||
// Send the report to Sentry
|
||||
if !r.sentry.Send(reportID, userIDFor(req), bs) {
|
||||
log.Println("Failed to send report to sentry (queue full):", reportID)
|
||||
log.Println("Failed to send report to sentry (queue full):", reportID[:8])
|
||||
result = "sentry_failure"
|
||||
}
|
||||
|
||||
log.Printf("Received report %s (%s)", reportID[:8], first)
|
||||
}
|
||||
|
||||
@@ -52,5 +52,5 @@ func compressAndWrite(bs []byte, fullPath string) error {
|
||||
gw.Close()
|
||||
|
||||
// Create an output file with the compressed report
|
||||
return os.WriteFile(fullPath, buf.Bytes(), 0o644)
|
||||
return os.WriteFile(fullPath, buf.Bytes(), 0o666)
|
||||
}
|
||||
|
||||
@@ -612,7 +612,7 @@ func saveRelays(file string, relays []*relay) error {
|
||||
for _, relay := range relays {
|
||||
content += relay.uri.String() + "\n"
|
||||
}
|
||||
return os.WriteFile(file, []byte(content), 0o777)
|
||||
return os.WriteFile(file, []byte(content), 0o666)
|
||||
}
|
||||
|
||||
func createTestCertificate() tls.Certificate {
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
@@ -77,7 +77,7 @@ func newInMemoryStore(dir string, flushInterval time.Duration, blobs blob.Store)
|
||||
slog.Error("Failed to find database in blob storage", "error", cerr)
|
||||
return s
|
||||
}
|
||||
fd, cerr := os.Create(path.Join(s.dir, "records.db"))
|
||||
fd, cerr := os.Create(filepath.Join(s.dir, "records.db"))
|
||||
if cerr != nil {
|
||||
slog.Error("Failed to create database file", "error", cerr)
|
||||
return s
|
||||
@@ -257,7 +257,7 @@ func (s *inMemoryStore) write() (err error) {
|
||||
}
|
||||
}()
|
||||
|
||||
dbf := path.Join(s.dir, "records.db")
|
||||
dbf := filepath.Join(s.dir, "records.db")
|
||||
fd, err := os.Create(dbf + ".tmp")
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -340,7 +340,7 @@ func (s *inMemoryStore) write() (err error) {
|
||||
}
|
||||
|
||||
func (s *inMemoryStore) read() (int, error) {
|
||||
fd, err := os.Open(path.Join(s.dir, "records.db"))
|
||||
fd, err := os.Open(filepath.Join(s.dir, "records.db"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
+27
-8
@@ -7,6 +7,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
@@ -113,14 +115,11 @@ var (
|
||||
)
|
||||
|
||||
const (
|
||||
dbOpGet = "get"
|
||||
dbOpPut = "put"
|
||||
dbOpMerge = "merge"
|
||||
dbOpDelete = "delete"
|
||||
dbResSuccess = "success"
|
||||
dbResNotFound = "not_found"
|
||||
dbResError = "error"
|
||||
dbResUnmarshalError = "unmarsh_err"
|
||||
dbOpGet = "get"
|
||||
dbOpPut = "put"
|
||||
dbOpMerge = "merge"
|
||||
dbResSuccess = "success"
|
||||
dbResNotFound = "not_found"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -132,4 +131,24 @@ func init() {
|
||||
databaseOperations, databaseOperationSeconds,
|
||||
databaseWriteSeconds, databaseLastWritten,
|
||||
retryAfterLevel)
|
||||
|
||||
// Prewarm important counters so they're available with zero values at
|
||||
// startup
|
||||
|
||||
apiRequestsTotal.WithLabelValues(http.MethodGet, "200")
|
||||
apiRequestsTotal.WithLabelValues(http.MethodGet, "404")
|
||||
apiRequestsTotal.WithLabelValues(http.MethodPost, "204")
|
||||
apiRequestsTotal.WithLabelValues(http.MethodPost, "400")
|
||||
apiRequestsTotal.WithLabelValues(http.MethodPost, "403")
|
||||
|
||||
lookupRequestsTotal.WithLabelValues("success")
|
||||
lookupRequestsTotal.WithLabelValues("not_found_ever")
|
||||
lookupRequestsTotal.WithLabelValues("not_found_recent")
|
||||
|
||||
announceRequestsTotal.WithLabelValues("success")
|
||||
announceRequestsTotal.WithLabelValues("bad_request")
|
||||
announceRequestsTotal.WithLabelValues("no_certificate")
|
||||
|
||||
replicationSendsTotal.WithLabelValues("success")
|
||||
replicationRecvsTotal.WithLabelValues("success")
|
||||
}
|
||||
|
||||
@@ -167,7 +167,7 @@ func (c *CLI) process(srcFs fs.Filesystem, dstFs fs.Filesystem, path string) err
|
||||
|
||||
var plainFd fs.File
|
||||
if dstFs != nil {
|
||||
if err := dstFs.MkdirAll(filepath.Dir(plainFi.Name), 0o700); err != nil {
|
||||
if err := dstFs.MkdirAll(filepath.Dir(plainFi.Name), fs.ModePerm); err != nil {
|
||||
return fmt.Errorf("%s: %w", plainFi.Name, err)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
package main
|
||||
|
||||
type buildSpecificOptions struct {
|
||||
HideConsole bool `name:"no-console" help:"Hide console window" env:"STHIDECONSOLE"`
|
||||
HideConsole bool `name:"no-console" help:"Hide console window (Always enabled on Windows 11 24H2 and later)" env:"STHIDECONSOLE"`
|
||||
}
|
||||
|
||||
@@ -673,7 +673,7 @@ func auditWriter(auditFile string) io.Writer {
|
||||
} else {
|
||||
auditFlags = os.O_WRONLY | os.O_CREATE | os.O_APPEND
|
||||
}
|
||||
fd, err = os.OpenFile(auditFile, auditFlags, 0o600)
|
||||
fd, err = os.OpenFile(auditFile, auditFlags, 0o666)
|
||||
if err != nil {
|
||||
slog.Error("Failed to open audit file", slogutil.Error(err))
|
||||
os.Exit(svcutil.ExitError.AsInt())
|
||||
@@ -915,10 +915,14 @@ func (u upgradeCmd) Run() error {
|
||||
case err != nil && !os.IsNotExist(err):
|
||||
slog.Error("Failed to lock for upgrade", slogutil.Error(err))
|
||||
os.Exit(1)
|
||||
case locked:
|
||||
err = upgradeViaRest()
|
||||
default:
|
||||
case locked || os.IsNotExist(err):
|
||||
// We got the lock, or the config directory didn't exist, so we
|
||||
// can do a direct upgrade
|
||||
err = upgrade.To(release)
|
||||
default:
|
||||
// We didn't get the lock, because Syncthing was running, so
|
||||
// upgrade via REST.
|
||||
err = upgradeViaRest()
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
|
||||
@@ -233,7 +233,7 @@ func copyStderr(stderr io.Reader, dst io.Writer) {
|
||||
|
||||
dst.Write([]byte(line))
|
||||
|
||||
if panicFd == nil && (strings.HasPrefix(line, "panic:") || strings.HasPrefix(line, "fatal error:")) {
|
||||
if panicFd == nil && (strings.HasPrefix(line, "panic:") || strings.HasPrefix(line, "fatal error:") || strings.HasPrefix(line, "runtime:")) {
|
||||
panicFd, err = os.Create(locations.GetTimestamped(locations.PanicLog))
|
||||
if err != nil {
|
||||
slog.Error("Failed to create panic log", slogutil.Error(err))
|
||||
@@ -479,7 +479,7 @@ func (f *autoclosedFile) ensureOpenLocked() error {
|
||||
// We open the file for write only, and create it if it doesn't exist.
|
||||
flags := os.O_WRONLY | os.O_CREATE | os.O_APPEND
|
||||
|
||||
fd, err := os.OpenFile(f.name, flags, 0o644)
|
||||
fd, err := os.OpenFile(f.name, flags, 0o666)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -9,8 +9,27 @@
|
||||
|
||||
package main
|
||||
|
||||
import "os/exec"
|
||||
import "golang.org/x/sys/windows"
|
||||
|
||||
func openURL(url string) error {
|
||||
return exec.Command("cmd.exe", "/C", "start "+url).Run()
|
||||
urlPtr, err := windows.UTF16PtrFromString(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
verbPtr, err := windows.UTF16PtrFromString("open")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = windows.ShellExecute(
|
||||
0, // hwnd
|
||||
verbPtr, // operation
|
||||
urlPtr, // file
|
||||
nil, // parameters
|
||||
nil, // directory
|
||||
windows.SW_SHOWNORMAL,
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -22,17 +22,17 @@ require (
|
||||
github.com/julienschmidt/httprouter v1.3.0
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||
github.com/maruel/panicparse/v2 v2.5.0
|
||||
github.com/mattn/go-sqlite3 v1.14.44
|
||||
github.com/mattn/go-sqlite3 v1.14.45
|
||||
github.com/maxmind/geoipupdate/v6 v6.1.0
|
||||
github.com/miscreant/miscreant.go v0.0.0-20200214223636-26d376326b75
|
||||
github.com/oschwald/geoip2-golang v1.13.0
|
||||
github.com/pierrec/lz4/v4 v4.1.26
|
||||
github.com/pierrec/lz4/v4 v4.1.27
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1
|
||||
github.com/quic-go/quic-go v0.59.0
|
||||
github.com/quic-go/quic-go v0.60.0
|
||||
github.com/rabbitmq/amqp091-go v1.11.0
|
||||
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9
|
||||
github.com/shirou/gopsutil/v4 v4.26.4
|
||||
github.com/shirou/gopsutil/v4 v4.26.5
|
||||
github.com/syncthing/notify v0.0.0-20250528144937-c7027d4f7465
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d
|
||||
github.com/thejerf/suture/v4 v4.0.6
|
||||
@@ -40,14 +40,14 @@ require (
|
||||
github.com/vitrun/qart v0.0.0-20160531060029-bf64b92db6b0
|
||||
github.com/willabides/kongplete v0.4.0
|
||||
github.com/wlynxg/anet v0.0.5
|
||||
golang.org/x/crypto v0.51.0
|
||||
golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a
|
||||
golang.org/x/net v0.54.0
|
||||
golang.org/x/sys v0.44.0
|
||||
golang.org/x/text v0.37.0
|
||||
golang.org/x/crypto v0.53.0
|
||||
golang.org/x/exp v0.0.0-20260611194520-c48552f49976
|
||||
golang.org/x/net v0.56.0
|
||||
golang.org/x/sys v0.46.0
|
||||
golang.org/x/text v0.38.0
|
||||
golang.org/x/time v0.15.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
modernc.org/sqlite v1.50.0
|
||||
modernc.org/sqlite v1.52.0
|
||||
sigs.k8s.io/yaml v1.6.0
|
||||
)
|
||||
|
||||
@@ -94,12 +94,12 @@ require (
|
||||
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
golang.org/x/mod v0.36.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6 // indirect
|
||||
golang.org/x/tools v0.45.0 // indirect
|
||||
golang.org/x/mod v0.37.0 // indirect
|
||||
golang.org/x/sync v0.21.0 // indirect
|
||||
golang.org/x/telemetry v0.0.0-20260610154732-fb80ec83bdd9 // indirect
|
||||
golang.org/x/tools v0.46.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.72.0 // indirect
|
||||
modernc.org/libc v1.72.3 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
)
|
||||
@@ -110,9 +110,6 @@ replace github.com/gobwas/glob v0.2.3 => github.com/calmh/glob v0.0.0-2022061508
|
||||
// https://github.com/jackpal/gateway/pull/49
|
||||
replace github.com/jackpal/gateway v1.1.1 => github.com/marbens-arch/gateway v1.1.2-0.20260308173556-c567cc04e7d4
|
||||
|
||||
// https://github.com/mattn/go-sqlite3/pull/1338
|
||||
replace github.com/mattn/go-sqlite3 v1.14.44 => github.com/calmh/go-sqlite3 v1.14.35-0.20260509063420-822b4765116d
|
||||
|
||||
tool (
|
||||
github.com/calmh/xdr/cmd/genxdr
|
||||
github.com/maxbrunsfeld/counterfeiter/v6
|
||||
|
||||
@@ -19,8 +19,6 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/calmh/glob v0.0.0-20220615080505-1d823af5017b h1:Fjm4GuJ+TGMgqfGHN42IQArJb77CfD/mAwLbDUoJe6g=
|
||||
github.com/calmh/glob v0.0.0-20220615080505-1d823af5017b/go.mod h1:91K7jfEsgJSyfSrX+gmrRfZMtntx6JsHolWubGXDopg=
|
||||
github.com/calmh/go-sqlite3 v1.14.35-0.20260509063420-822b4765116d h1:liVtRMlDBqDyR4qKkMRIFTdUK6+aoG6Oh97DKnhrCHc=
|
||||
github.com/calmh/go-sqlite3 v1.14.35-0.20260509063420-822b4765116d/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
|
||||
github.com/calmh/incontainer v1.0.0 h1:g2cTUtZuFGmMGX8GoykPkN1Judj2uw8/3/aEtq4Z/rg=
|
||||
github.com/calmh/incontainer v1.0.0/go.mod h1:eOhqnw15c9X+4RNBe0W3HlUZFfX16O0EDsCOInTndHY=
|
||||
github.com/calmh/xdr v1.2.0 h1:GaGSNH4ZDw9kNdYqle6+RcAENiaQ8/611Ok+jQbBEeU=
|
||||
@@ -149,6 +147,8 @@ github.com/maruel/panicparse/v2 v2.5.0/go.mod h1:DA2fDiBk63bKfBf4CVZP9gb4fuvzdPb
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.45 h1:6KA/spDguL3KV8rnybG7ezSaE4SeMR3KC9VbUoAQaIk=
|
||||
github.com/mattn/go-sqlite3 v1.14.45/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
|
||||
github.com/maxbrunsfeld/counterfeiter/v6 v6.12.0 h1:aOeI7xAOVdK+R6xbVsZuU9HmCZYmQVmZgPf9xJUd2Sg=
|
||||
github.com/maxbrunsfeld/counterfeiter/v6 v6.12.0/go.mod h1:0hZWbtfeCYUQeAQdPLUzETiBhUSns7O6LDj9vH88xKA=
|
||||
github.com/maxmind/geoipupdate/v6 v6.1.0 h1:sdtTHzzQNJlXF5+fd/EoPTucRHyMonYt/Cok8xzzfqA=
|
||||
@@ -179,8 +179,8 @@ github.com/oschwald/geoip2-golang v1.13.0 h1:Q44/Ldc703pasJeP5V9+aFSZFmBN7DKHbNs
|
||||
github.com/oschwald/geoip2-golang v1.13.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo=
|
||||
github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
|
||||
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
|
||||
github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY=
|
||||
github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
||||
github.com/pierrec/lz4/v4 v4.1.27 h1:+PhzhWDrjRj89TH2sw43nE3+4+W8lSxIuQadEHZyjUk=
|
||||
github.com/pierrec/lz4/v4 v4.1.27/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@@ -200,8 +200,10 @@ github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzM
|
||||
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0=
|
||||
github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk=
|
||||
github.com/quic-go/quic-go v0.60.0 h1:xcQioE8OM66UQLeUMHltK1CCcOu3JbVB4JAQdDQSB+0=
|
||||
github.com/quic-go/quic-go v0.60.0/go.mod h1:wpKpjmPpftl30sL6pFh7REVpjbcCVy4zt2vDyK1TuJk=
|
||||
github.com/rabbitmq/amqp091-go v1.11.0 h1:HxIctVm9Gid/Vtn706necmZ7Wj6pgGI2eqplRbEY8O8=
|
||||
github.com/rabbitmq/amqp091-go v1.11.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg=
|
||||
@@ -216,8 +218,8 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8=
|
||||
github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM=
|
||||
github.com/shirou/gopsutil/v4 v4.26.4 h1:B4SXVbcwTyrocPHEmWBC4uCYr4Xcu3MK1TXqbprAOWY=
|
||||
github.com/shirou/gopsutil/v4 v4.26.4/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
|
||||
github.com/shirou/gopsutil/v4 v4.26.5 h1:RPcBXkpz7kOj9PqGFQOlBPZHsyaPvPVQc098y9RmCNM=
|
||||
github.com/shirou/gopsutil/v4 v4.26.5/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
@@ -265,13 +267,13 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
||||
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
||||
golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a h1:+3jdDGGB8NGb1Zktc737jlt3/A5f6UlwSzmvqUuufxw=
|
||||
golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw=
|
||||
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||
golang.org/x/exp v0.0.0-20260611194520-c48552f49976 h1:X8Hz2ImujgbmetVuW+w2YkyZChE3cBpZi2P158rTG9M=
|
||||
golang.org/x/exp v0.0.0-20260611194520-c48552f49976/go.mod h1:vnf4pv9iKZXY58sQE1L86zmNWJ4159e1RkcWiLCkeEY=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
||||
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
||||
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
|
||||
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
@@ -280,13 +282,13 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
|
||||
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
|
||||
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -309,25 +311,25 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
||||
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6 h1:HjU6IWBiAgRIdAJ9/y1rwCn+UELEmwV+VsTLzj/W4sE=
|
||||
golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6/go.mod h1:Eqhaxk/wZsWEH8CRxLwj6xzEJbz7k1EFGqx7nyCoabE=
|
||||
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/telemetry v0.0.0-20260610154732-fb80ec83bdd9 h1:FjUup8XrRy7lv+XHONi6KKUSizeF2NnVrTnz/HhbohQ=
|
||||
golang.org/x/telemetry v0.0.0-20260610154732-fb80ec83bdd9/go.mod h1:3AWMyWHS+caVoiEXpiq6+tzKA40J4vQT3MYr80ZtQpc=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
||||
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
|
||||
golang.org/x/tools v0.46.0 h1:7jTurBkPZu4moS/Uy4OQT1M+QBlsj3wejyZwsT8Z7rk=
|
||||
golang.org/x/tools v0.46.0/go.mod h1:FrD85F8l+NWL+9XWBSyVSHO6Ne4jutsfIFba7AWQ5Ys=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -358,10 +360,10 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
modernc.org/cc/v4 v4.27.3 h1:uNCgn37E5U09mTv1XgskEVUJ8ADKpmFMPxzGJ0TSo+U=
|
||||
modernc.org/cc/v4 v4.27.3/go.mod h1:3YjcbCqhoTTHPycJDRl2WZKKFj0nwcOIPBfEZK0Hdk8=
|
||||
modernc.org/ccgo/v4 v4.32.4 h1:L5OB8rpEX4ZsXEQwGozRfJyJSFHbbNVOoQ59DU9/KuU=
|
||||
modernc.org/ccgo/v4 v4.32.4/go.mod h1:lY7f+fiTDHfcv6YlRgSkxYfhs+UvOEEzj49jAn2TOx0=
|
||||
modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY=
|
||||
modernc.org/cc/v4 v4.28.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||
modernc.org/ccgo/v4 v4.34.0 h1:yRLPFZieg532OT4rp4JFNIVcquwalMX26G95WQDqwCQ=
|
||||
modernc.org/ccgo/v4 v4.34.0/go.mod h1:AS5WYMyBakQ+fhsHhtP8mWB82KTGPkNNJDGfGQCe0/A=
|
||||
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
@@ -370,18 +372,18 @@ modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
|
||||
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.72.0 h1:IEu559v9a0XWjw0DPoVKtXpO2qt5NVLAnFaBbjq+n8c=
|
||||
modernc.org/libc v1.72.0/go.mod h1:tTU8DL8A+XLVkEY3x5E/tO7s2Q/q42EtnNWda/L5QhQ=
|
||||
modernc.org/libc v1.72.3 h1:ZnDF4tXn4NBXFutMMQC4vtbTFSXhhKzR73fv0beZEAU=
|
||||
modernc.org/libc v1.72.3/go.mod h1:dn0dZNnnn1clLyvRxLxYExxiKRZIRENOfqQ8XEeg4Qs=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
|
||||
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.50.0 h1:eMowQSWLK0MeiQTdmz3lqoF5dqclujdlIKeJA11+7oM=
|
||||
modernc.org/sqlite v1.50.0/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew=
|
||||
modernc.org/sqlite v1.52.0 h1:p4dhYh2tXZCiyaqHwRVJDjIGKWyXayiQpThxgDzJaxo=
|
||||
modernc.org/sqlite v1.52.0/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"Add Device": "Dodaj uređaj",
|
||||
"Add Folder": "Dodaj mapu",
|
||||
"Add Remote Device": "Dodaj udaljeni uređaj",
|
||||
"Add devices from the introducer to our device list, for mutually shared folders.": "Dodaj uređaje od posrednika u našu listu uređaja, za međusobno dijeljene mape.",
|
||||
"Add filter entry": "Dodaj unos filtra",
|
||||
"Add ignore patterns": "Dodaj uzorke zanemarivanja",
|
||||
"Add new folder?": "Dodati novu mapu?",
|
||||
@@ -187,6 +188,7 @@
|
||||
"GUI Authentication Password": "Lozinka za GUI autentifikacju",
|
||||
"GUI Authentication User": "Korisnik za GUI autentifikacju",
|
||||
"GUI Authentication: Set User and Password": "GUI autentifikacija: Postavite korisnika i lozinku",
|
||||
"GUI Listen Address": "GUI adresa za slušanje",
|
||||
"GUI Override Directory": "Direktorij za nadjačavanje GUI-ja",
|
||||
"GUI Theme": "Tema GUI-ja",
|
||||
"General": "Općenito",
|
||||
@@ -214,6 +216,8 @@
|
||||
"Incorrect user name or password.": "Neispravno korisničko ime ili lozinka.",
|
||||
"Info": "Informacije",
|
||||
"Internally used paths:": "Interno korištene staze:",
|
||||
"Introduced By": "Uveden od",
|
||||
"Introducer": "Posrednik",
|
||||
"Introduction": "Uvod",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Inverzija zadanog uvjeta (tj. ne isključuj)",
|
||||
"Keep Versions": "Zadrži verzije",
|
||||
@@ -248,6 +252,7 @@
|
||||
"Log tailing paused. Scroll to the bottom to continue.": "Praćenje zapisa je pauzirano. Pomaknite se na dno kako biste nastavili.",
|
||||
"Login failed, see Syncthing logs for details.": "Prijava nije uspjela. Pogledaj detalje u odjeljku zapisima sinkronizacije.",
|
||||
"Logs": "Zapisi",
|
||||
"Maintain an index of all blocks in the folder, enabling reuse of blocks from other files when syncing changes. Disable to reduce database size at the cost of not being able to reuse blocks across files.": "Održavaj indeks svih blokova u mapi, što omogućava ponovnu upotrebu blokova iz drugih datoteka pri sinkronizaciji promjena. Onemogući da smanjiš veličinu baze podataka, uz cijenu nemogućnosti ponovne upotrebe blokova između datoteka.",
|
||||
"Major Upgrade": "Velika nadogradnja",
|
||||
"Mass actions": "Masovne radnje",
|
||||
"Maximum Age": "Maksimalna starost",
|
||||
@@ -401,6 +406,7 @@
|
||||
"Support Bundle": "Paket podrške",
|
||||
"Sync Extended Attributes": "Sinkroniziraj proširena svojstva",
|
||||
"Sync Ownership": "Sinkroniziraj vlasništvo",
|
||||
"Sync Protocol Listen Addresses": "Adrese za slušanje sinkronizacijskog protokola",
|
||||
"Sync Status": "Stanje sinkronizacije",
|
||||
"Syncing": "Sinkroniziranje",
|
||||
"Syncthing device ID for \"{%devicename%}\"": "Syncthing ID uređaja za „{{devicename}}“",
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<h4 class="text-center" translate>The Syncthing Authors</h4>
|
||||
<div class="row">
|
||||
<div class="col-md-12" id="contributor-list">
|
||||
Jakob Borg, Audrius Butkevicius, Simon Frei, Tomasz Wilczyński, Alexander Graf, Alexandre Viau, Anderson Mesquita, André Colomb, Antony Male, Ben Schulz, bt90, Caleb Callaway, Daniel Harte, Emil Lundberg, Eric P, Evgeny Kuznetsov, greatroar, Lars K.W. Gohlke, Lode Hoste, Marcus B Spencer, Michael Ploujnikov, Ross Smith II, Stefan Tatschner, Tommy van der Vorst, Wulf Weich, Adam Piggott, Adel Qalieh, Aleksey Vasenev, Alessandro G., Alex Ionescu, Alex Lindeman, Alex Xu, Alexander Seiler, Alexandre Alves, Aman Gupta, Andreas Sommer, andresvia, Andrew Rabert, Andrey D, andyleap, Anjan Momi, Anthony Goeckner, Antoine Lamielle, Anur, Aranjedeath, ardevd, Arkadiusz Tymiński, Aroun, Arthur Axel fREW Schmidt, Artur Zubilewicz, Ashish Bhate, Aurélien Rainone, BAHADIR YILMAZ, Bart De Vries, Beat Reichenbach, Ben Norcombe, Ben Shepherd, Ben Sidhom, Benedikt Heine, Benno Fünfstück, Benny Ng, boomsquared, Boqin Qin, Boris Rybalkin, Brendan Long, Catfriend1, Cathryne Linenweaver, Cedric Staniewski, Chih-Hsuan Yen, Choongkyu, Chris Howie, Chris Joel, Christian Kujau, Christian Prescott, chucic, cjc7373, Colin Kennedy, Cromefire_, cui, Cyprien Devillez, d-volution, Dan, Daniel Barczyk, Daniel Bergmann, Daniel Martí, Daniel Padrta, Daniil Gentili, Darshil Chanpura, dashangcun, David Rimmer, DeflateAwning, Denis A., Dennis Wilson, derekriemer, DerRockWolf, desbma, Devon G. Redekopp, digital, Dimitri Papadopoulos Orfanos, Dmitry Saveliev, domain, Domenic Horner, Dominik Heidler, Elias Jarlebring, Elliot Huffman, Emil Hessman, Eng Zer Jun, entity0xfe, Epifeny, epifeny, Eric Lesiuta, Erik Meitner, Evan Spensley, Federico Castagnini, Felix, Felix Ableitner, Felix Lampe, Felix Unterpaintner, Francois-Xavier Gsell, Frank Isemann, Gahl Saraf, georgespatton, ghjklw, Gilli Sigurdsson, Gleb Sinyavskiy, Graham Miln, Greg, guangwu, gudvinr, Gusted, Han Boetes, HansK-p, Harrison Jones, Hazem Krimi, Heiko Zuerker, Hireworks, Hugo Locurcio, Iain Barnett, Ian Johnson, ignacy123, Iskander Sharipov, Jaakko Hannikainen, Jack Croft, Jacob, Jake Peterson, James O'Beirne, James Patterson, Jaroslav Lichtblau, Jaroslav Malec, Jaspitta, Jaya Chithra, Jaya Kumar, Jeffery To, jelle van der Waa, Jens Diemer, Jochen Voss, Johan Vromans, John Rinehart, Jonas Thelemann, Jonathan, Jose Manuel Delicado, JRNitre, jtagcat, Julian Lehrhuber, Jörg Thalheim, Jędrzej Kula, Kapil Sareen, Karol Różycki, Kebin Liu, Keith Harrison, Kelong Cong, Ken'ichi Kamada, Kevin Allen, Kevin Bushiri, Kevin White, Jr., klemens, Kurt Fitzner, kylosus, Lars Lehtonen, Laurent Etiemble, Leo Arias, Liu Siyuan, Lord Landon Agahnim, LSmithx2, Luiz Angelo Daros de Luca, Lukas Lihotzki, Luke Hamburg, luzpaz, Majed Abdulaziz, Marc Laporte, Marcel Meyer, Marcin Dziadus, Marcus Legendre, Mario Majila, Mark Pulford, Martchus, Mateusz Naściszewski, Mateusz Ż, mathias4833, Matic Potočnik, Matt Burke, Matt Robenolt, Matteo Ruina, Maurizio Tomasi, Max, Max Schulze, MaximAL, Maximilian, Maxwell G, Michael Jephcote, Michael Rienstra, Michael Wang 汪東陽, MichaIng, Migelo, Mike Boone, MikeLund, MikolajTwarog, Mingxuan Lin, mv1005, Nate Morrison, nf, Nicholas Rishel, Nick Busey, Nico Stapelbroek, Nicolas Braud-Santoni, Nicolas Perraut, Niels Peter Roest, Nils Jakobi, NinoM4ster, Nitroretro, NoLooseEnds, Oliver Freyermuth, orangekame3, otbutz, overkill, Oyebanji Jacob Mayowa, Pablo, Pascal Jungblut, Paul Brit, Paul Donald, Pawel Palenica, perewa, Peter Badida, Peter Dave Hello, Peter Hoeg, Peter Marquardt, Phani Rithvij, Phil Davis, Philippe Schommers, Phill Luby, Piotr Bejda, polyfloyd, Prathik P Kulkarni, pullmerge, Quentin Hibon, Rahmi Pruitt, RealCharlesChia, red_led, Robert Carosi, Roberto Santalla, Robin Schoonover, Roman Zaynetdinov, rubenbe, Ruslan Yevdokymov, Ryan Qian, Ryan Sullivan, Sacheendra Talluri, Scott Klupfel, sec65, Sergey Mishin, Sertonix, Severin von Wnuck-Lipinski, Shaarad Dalvi, Shivam Kumar, Simon Mwepu, Simon Pickup, Sly_tom_cat, Sonu Kumar Saw, Stefan Kuntz, Steven Eckhoff, Suhas Gundimeda, Sven Bachmann, Sébastien WENSKE, Tao, Taylor Khan, Terrance, TheCreeper, Thomas, Thomas Hipp, Tim Abell, Tim Howes, Tobias Frölich, Tobias Klauser, Tobias Nygren, Tobias Tom, Tom Jakubowski, Tully Robinson, Tyler Brazier, Tyler Kropp, Umer-Azaz, Unrud, Val Markovic, vapatel2, Veeti Paananen, Victor Buinsky, Vik, Vil Brekin, villekalliomaki, Vladimir Rusinov, vvaswani, wangguoliang, WangXi, Will Rouesnel, William A. Kennington III, wouter bolsterlee, xarx00, Xavier O., xjtdy888, Yannic A., yparitcher, 佛跳墙, 落心
|
||||
Jakob Borg, Audrius Butkevicius, Simon Frei, Tomasz Wilczyński, Alexander Graf, Alexandre Viau, Anderson Mesquita, André Colomb, Antony Male, Ben Schulz, bt90, Caleb Callaway, Daniel Harte, Emil Lundberg, Eric P, Evgeny Kuznetsov, greatroar, Lars K.W. Gohlke, Lode Hoste, Marcus B Spencer, Michael Ploujnikov, Ross Smith II, Stefan Tatschner, Tommy van der Vorst, Wulf Weich, Adam Piggott, Adel Qalieh, Aleksey Vasenev, Alessandro G., Alex Ionescu, Alex Lindeman, Alex Xu, Alexander Seiler, Alexandre Alves, Aman Gupta, Andreas Sommer, andresvia, Andrew Rabert, Andrey D, andyleap, Anjan Momi, Anthony Goeckner, Antoine Lamielle, Anur, Aranjedeath, ardevd, Arkadiusz Tymiński, Aroun, Arthur Axel fREW Schmidt, Artur Zubilewicz, Ashish Bhate, Aurélien Rainone, BAHADIR YILMAZ, Bart De Vries, Beat Reichenbach, Ben Norcombe, Ben Shepherd, Ben Sidhom, Benedikt Heine, Benno Fünfstück, Benny Ng, boomsquared, Boqin Qin, Boris Rybalkin, Brendan Long, Catfriend1, Cathryne Linenweaver, Cedric Staniewski, Chih-Hsuan Yen, Choongkyu, Chris Howie, Chris Joel, Christian Kujau, Christian Prescott, chucic, cjc7373, Colin Kennedy, Cromefire_, cui, Cyprien Devillez, d-volution, Dan, Daniel Barczyk, Daniel Bergmann, Daniel Martí, Daniel Padrta, Daniil Gentili, Darshil Chanpura, dashangcun, David Rimmer, DeflateAwning, Denis A., Dennis Wilson, derekriemer, DerRockWolf, desbma, Devon G. Redekopp, digital, Dimitri Papadopoulos Orfanos, Dmitry Saveliev, domain, Domenic Horner, Dominik Heidler, Elias, Elias Jarlebring, Elliot Huffman, Emil Hessman, Eng Zer Jun, entity0xfe, Epifeny, epifeny, Eric Lesiuta, Erik Meitner, Evan Spensley, Federico Castagnini, Felix, Felix Ableitner, Felix Lampe, Felix Unterpaintner, Francois-Xavier Gsell, Frank Isemann, Gahl Saraf, georgespatton, ghjklw, Gilli Sigurdsson, Gleb Sinyavskiy, Graham Miln, Greg, guangwu, gudvinr, Gusted, Han Boetes, HansK-p, Harrison Jones, Hazem Krimi, Heiko Zuerker, Hireworks, Hugo Locurcio, Iain Barnett, Ian Johnson, ignacy123, Iskander Sharipov, Jaakko Hannikainen, Jack Croft, Jacob, Jake Peterson, James O'Beirne, James Patterson, Jaroslav Lichtblau, Jaroslav Malec, Jaspitta, Jaya Chithra, Jaya Kumar, Jeffery To, jelle van der Waa, Jens Diemer, Jochen Voss, Johan Vromans, John Rinehart, Jonas Thelemann, Jonathan, Jose Manuel Delicado, JRNitre, jtagcat, Julian Lehrhuber, Jörg Thalheim, Jędrzej Kula, Kapil Sareen, Karol Różycki, Kebin Liu, Keith Harrison, Kelong Cong, Ken'ichi Kamada, Kevin Allen, Kevin Bushiri, Kevin White, Jr., klemens, Kurt Fitzner, kylosus, Lars Lehtonen, Laurent Etiemble, Leo Arias, Liu Siyuan, Lord Landon Agahnim, LSmithx2, Luiz Angelo Daros de Luca, Lukas Lihotzki, Luke Hamburg, luzpaz, Majed Abdulaziz, Marc Laporte, Marcel Meyer, Marcin Dziadus, Marcus Legendre, Mario Majila, Mark Pulford, Martchus, Mateusz Naściszewski, Mateusz Ż, mathias4833, Matic Potočnik, Matt Burke, Matt Robenolt, Matteo Ruina, mattn, Maurizio Tomasi, Max, Max Schulze, MaximAL, Maximilian, Maxwell G, Michael Jephcote, Michael Rienstra, Michael Wang 汪東陽, MichaIng, Migelo, Mike Boone, MikeLund, MikolajTwarog, Mingxuan Lin, mv1005, Nate Morrison, nf, Nicholas Rishel, Nick Busey, Nico Stapelbroek, Nicolas Braud-Santoni, Nicolas Perraut, Niels Peter Roest, Nils Jakobi, NinoM4ster, Nitroretro, NoLooseEnds, Oliver Freyermuth, orangekame3, otbutz, overkill, Oyebanji Jacob Mayowa, Pablo, Pascal Jungblut, Paul Brit, Paul Donald, Pawel Palenica, perewa, Peter Badida, Peter Dave Hello, Peter Hoeg, Peter Marquardt, Phani Rithvij, Phil Davis, Philippe Schommers, Phill Luby, Piotr Bejda, polyfloyd, Prathik P Kulkarni, pullmerge, Quentin Hibon, Rahmi Pruitt, RealCharlesChia, red_led, Robert Carosi, Roberto Santalla, Robin Schoonover, Roman Zaynetdinov, rubenbe, Ruslan Yevdokymov, Ryan Qian, Ryan Sullivan, Sacheendra Talluri, Scott Klupfel, sec65, Sergey Mishin, Sertonix, Severin von Wnuck-Lipinski, Shaarad Dalvi, Shablone, Shivam Kumar, Simon Mwepu, Simon Pickup, Sly_tom_cat, Sonu Kumar Saw, Stefan Kuntz, Steven Eckhoff, Suhas Gundimeda, Sven Bachmann, Sébastien WENSKE, Tao, Taylor Khan, Terrance, TheCreeper, Thomas, Thomas Hipp, Tim Abell, Tim Howes, Tobias Frölich, Tobias Klauser, Tobias Nygren, Tobias Tom, Tom Jakubowski, Tully Robinson, Tyler Brazier, Tyler Kropp, Umer-Azaz, Unrud, Val Markovic, vapatel2, Veeti Paananen, Victor Buinsky, Vik, Vil Brekin, villekalliomaki, Vladimir Rusinov, vvaswani, wangguoliang, WangXi, Will Rouesnel, William A. Kennington III, wouter bolsterlee, xarx00, Xavier O., xjtdy888, Yannic A., yparitcher, 佛跳墙, 落心
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2997,6 +2997,9 @@ angular.module('syncthing.core')
|
||||
$scope.restoreVersions.tree = $("#restoreTree").fancytree({
|
||||
extensions: ["table", "filter", "glyph"],
|
||||
quicksearch: true,
|
||||
// Node titles are remote-controlled file/path
|
||||
// components; render them as text, not HTML.
|
||||
escapeTitles: true,
|
||||
filter: {
|
||||
hideExpanders: true,
|
||||
mode: "hide"
|
||||
|
||||
@@ -79,6 +79,7 @@ type DB interface {
|
||||
|
||||
// Cleanup
|
||||
DropAllFiles(folder string, device protocol.DeviceID) error
|
||||
DropFolderDevice(folder string, device protocol.DeviceID) error
|
||||
DropDevice(device protocol.DeviceID) error
|
||||
DropFilesNamed(folder string, device protocol.DeviceID, names []string) error
|
||||
DropFolder(folder string) error
|
||||
|
||||
@@ -128,6 +128,11 @@ func (m metricsDB) DropAllFiles(folder string, device protocol.DeviceID) error {
|
||||
return m.DB.DropAllFiles(folder, device)
|
||||
}
|
||||
|
||||
func (m metricsDB) DropFolderDevice(folder string, device protocol.DeviceID) error {
|
||||
defer m.account(folder, "DropFolderDevice")()
|
||||
return m.DB.DropFolderDevice(folder, device)
|
||||
}
|
||||
|
||||
func (m metricsDB) DropDevice(device protocol.DeviceID) error {
|
||||
defer m.account("-", "DropDevice")()
|
||||
return m.DB.DropDevice(device)
|
||||
|
||||
@@ -255,6 +255,17 @@ func (s *DB) DropAllFiles(folder string, device protocol.DeviceID) error {
|
||||
return fdb.DropAllFiles(device)
|
||||
}
|
||||
|
||||
func (s *DB) DropFolderDevice(folder string, device protocol.DeviceID) error {
|
||||
fdb, err := s.getFolderDB(folder, false)
|
||||
if errors.Is(err, errNoSuchFolder) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fdb.DropDevice(device)
|
||||
}
|
||||
|
||||
func (s *DB) DropFilesNamed(folder string, device protocol.DeviceID, names []string) error {
|
||||
fdb, err := s.getFolderDB(folder, false)
|
||||
if errors.Is(err, errNoSuchFolder) {
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"errors"
|
||||
"iter"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
@@ -799,6 +798,13 @@ func TestDropAllFiles(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// The sequence is non-zero before the drop
|
||||
if seq, err := db.GetDeviceSequence("a", protocol.DeviceID{1}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if seq == 0 {
|
||||
t.Error("expected non-zero sequence before drop")
|
||||
}
|
||||
|
||||
// Drop folder A
|
||||
if err := db.DropAllFiles("a", protocol.DeviceID{1}); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -826,6 +832,20 @@ func TestDropAllFiles(t *testing.T) {
|
||||
t.Error("expected count to be two")
|
||||
}
|
||||
|
||||
// The device sequence for the dropped folder is reset to zero.
|
||||
if seq, err := db.GetDeviceSequence("a", protocol.DeviceID{1}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if seq != 0 {
|
||||
t.Log(seq)
|
||||
t.Error("expected sequence to be reset to zero after DropAllFiles")
|
||||
}
|
||||
// Sequence for the untouched folder is unaffected.
|
||||
if seq, err := db.GetDeviceSequence("b", protocol.DeviceID{1}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if seq == 0 {
|
||||
t.Error("expected non-zero sequence for untouched folder")
|
||||
}
|
||||
|
||||
// Drop things that don't exist
|
||||
if err := db.DropAllFiles("a", protocol.DeviceID{99}); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -838,6 +858,67 @@ func TestDropAllFiles(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDropFolderDevice(t *testing.T) {
|
||||
db, err := Open(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := db.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
|
||||
// Files from device 1 in folder a
|
||||
err = db.Update("a", protocol.DeviceID{1}, []protocol.FileInfo{
|
||||
genFile("test1", 1, 101),
|
||||
genFile("test2", 2, 102),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Device 1 has an index ID.
|
||||
if err := db.SetIndexID("a", protocol.DeviceID{1}, protocol.IndexID(0xdeadbeef)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if id, err := db.GetIndexID("a", protocol.DeviceID{1}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if id != protocol.IndexID(0xdeadbeef) {
|
||||
t.Errorf("expected index ID to be set, got %v", id)
|
||||
}
|
||||
|
||||
// Drop device 1 from folder a
|
||||
if err := db.DropFolderDevice("a", protocol.DeviceID{1}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Files for device 1 in folder a are gone
|
||||
if _, ok, err := db.GetDeviceFile("a", protocol.DeviceID{1}, "test1"); err != nil || ok {
|
||||
t.Log(err, ok)
|
||||
t.Error("expected device 1 file in folder A to not exist")
|
||||
}
|
||||
if c, err := db.CountLocal("a", protocol.DeviceID{1}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if c.Files != 0 {
|
||||
t.Log(c)
|
||||
t.Error("expected device 1 count in folder A to be zero")
|
||||
}
|
||||
|
||||
// The index ID for device 1 in folder A is gone.
|
||||
if id, err := db.GetIndexID("a", protocol.DeviceID{1}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if id != 0 {
|
||||
t.Errorf("expected index ID to be cleared, got %v", id)
|
||||
}
|
||||
if seq, err := db.GetDeviceSequence("a", protocol.DeviceID{1}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if seq != 0 {
|
||||
t.Log(seq)
|
||||
t.Error("expected sequence to be zero after DropFolderDevice")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConcurrentUpdate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -1168,7 +1249,7 @@ func TestOpenSpecialName(t *testing.T) {
|
||||
|
||||
// Create a "base" dir that is in the way if the path becomes
|
||||
// incorrectly truncated in the next steps.
|
||||
base := path.Join(dir, "test")
|
||||
base := filepath.Join(dir, "test")
|
||||
if err := os.Mkdir(base, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -235,6 +235,13 @@ func (s *folderDB) DropAllFiles(device protocol.DeviceID) error {
|
||||
defer tx.Rollback() //nolint:errcheck
|
||||
txp := &txPreparedStmts{Tx: tx}
|
||||
|
||||
if _, err := tx.Exec(`
|
||||
UPDATE indexids SET sequence = 0
|
||||
WHERE device_idx = ?
|
||||
`, deviceIdx); err != nil {
|
||||
return wrap(err)
|
||||
}
|
||||
|
||||
// Drop all the file entries
|
||||
|
||||
result, err := tx.Exec(`
|
||||
|
||||
+1
-1
@@ -1248,7 +1248,7 @@ func (s *service) getSupportBundle(w http.ResponseWriter, r *http.Request) {
|
||||
zipFilePath := filepath.Join(locations.GetBaseDir(locations.ConfigBaseDir), zipFileName)
|
||||
|
||||
// Write buffer zip to local zip file (back up)
|
||||
if err := os.WriteFile(zipFilePath, zipFilesBuffer.Bytes(), 0o600); err != nil {
|
||||
if err := os.WriteFile(zipFilePath, zipFilesBuffer.Bytes(), 0o666); err != nil {
|
||||
slog.Warn("Failed to create support bundle zip (file)", slogutil.FilePath(zipFilePath), slogutil.Error(err))
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ import (
|
||||
// or, somewhere along the way the "+" in the version tag disappeared:
|
||||
// syncthing v1.23.7-dev.26.gdf7b56ae.dirty-stversionextra "Fermium Flea" (go1.20.5 darwin-arm64) jb@ok.kastelo.net 2023-07-12 06:55:26 UTC [Some Wrapper, purego, stnoupgrade]
|
||||
var (
|
||||
longVersionRE = regexp.MustCompile(`syncthing\s+(v[^\s]+)\s+"([^"]+)"\s\(([^\s]+)\s+([^-]+)-([^)]+)\)\s+([^\s]+)[^\[]*(?:\[(.+)\])?$`)
|
||||
longVersionRE = regexp.MustCompile(`syncthing\s+(v[^\s]+)\s+"([^"]+)"\s\(([^\s]+)\s+([^-]+)-([^)]+)\)\s+([^\s]+)[^\[]*(?:\[(.+)\])?`)
|
||||
gitExtraRE = regexp.MustCompile(`\.\d+\.g[0-9a-f]+`) // ".1.g6aaae618"
|
||||
gitExtraSepRE = regexp.MustCompile(`[.-]`) // dot or dash
|
||||
)
|
||||
|
||||
@@ -57,6 +57,20 @@ func TestParseVersion(t *testing.T) {
|
||||
Extra: []string{"Some Wrapper", "purego", "stnoupgrade"},
|
||||
},
|
||||
},
|
||||
{
|
||||
longVersion: `2026-05-18 14:53:32 INF syncthing v2.0.3 "Hafnium Hornet" (go1.25.0 darwin-amd64) builder@github.syncthing.net 2025-08-22 07:00:05 UTC [stnoupgrade] (log.pkg=main)`,
|
||||
parsed: VersionParts{
|
||||
Version: "v2.0.3",
|
||||
Tag: "v2.0.3",
|
||||
Commit: "",
|
||||
Codename: "Hafnium Hornet",
|
||||
Runtime: "go1.25.0",
|
||||
GOOS: "darwin",
|
||||
GOARCH: "amd64",
|
||||
Builder: "builder@github.syncthing.net",
|
||||
Extra: []string{"stnoupgrade"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
|
||||
@@ -168,7 +168,7 @@ func (f *FolderConfiguration) CreateMarker() error {
|
||||
ffs := f.Filesystem()
|
||||
|
||||
// Create the marker as a directory
|
||||
err := ffs.Mkdir(DefaultMarkerName, 0o755)
|
||||
err := ffs.Mkdir(DefaultMarkerName, fs.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -176,7 +176,7 @@ func (f *FolderConfiguration) CreateMarker() error {
|
||||
// Create a file inside it, reducing the risk of the marker directory
|
||||
// being removed by automated cleanup tools.
|
||||
markerFile := filepath.Join(DefaultMarkerName, f.markerFilename())
|
||||
if err := fs.WriteFile(ffs, markerFile, f.markerContents(), 0o644); err != nil {
|
||||
if err := fs.WriteFile(ffs, markerFile, f.markerContents(), 0o666); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -246,19 +246,10 @@ func (f *FolderConfiguration) checkFilesystemPath(ffs fs.Filesystem, path string
|
||||
}
|
||||
|
||||
func (f *FolderConfiguration) CreateRoot() (err error) {
|
||||
// Directory permission bits. Will be filtered down to something
|
||||
// sane by umask on Unixes.
|
||||
permBits := fs.FileMode(0o777)
|
||||
if build.IsWindows {
|
||||
// Windows has no umask so we must chose a safer set of bits to
|
||||
// begin with.
|
||||
permBits = 0o700
|
||||
}
|
||||
|
||||
filesystem := f.Filesystem()
|
||||
|
||||
if _, err = filesystem.Stat("."); fs.IsNotExist(err) {
|
||||
err = filesystem.MkdirAll(".", permBits)
|
||||
err = filesystem.MkdirAll(".", fs.ModePerm)
|
||||
}
|
||||
|
||||
return err
|
||||
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/syncthing/syncthing/internal/slogutil"
|
||||
"github.com/syncthing/syncthing/lib/build"
|
||||
"github.com/syncthing/syncthing/lib/fs"
|
||||
"github.com/syncthing/syncthing/lib/netutil"
|
||||
"github.com/syncthing/syncthing/lib/upgrade"
|
||||
@@ -224,27 +223,20 @@ func migrateToConfigV24(cfg *Configuration) {
|
||||
}
|
||||
|
||||
func migrateToConfigV23(cfg *Configuration) {
|
||||
permBits := fs.FileMode(0o777)
|
||||
if build.IsWindows {
|
||||
// Windows has no umask so we must chose a safer set of bits to
|
||||
// begin with.
|
||||
permBits = 0o700
|
||||
}
|
||||
|
||||
// Upgrade code remains hardcoded for .stfolder despite configurable
|
||||
// marker name in later versions.
|
||||
|
||||
for i := range cfg.Folders {
|
||||
fs := cfg.Folders[i].Filesystem()
|
||||
ffs := cfg.Folders[i].Filesystem()
|
||||
// Invalid config posted, or tests.
|
||||
if fs == nil {
|
||||
if ffs == nil {
|
||||
continue
|
||||
}
|
||||
if stat, err := fs.Stat(DefaultMarkerName); err == nil && !stat.IsDir() {
|
||||
err = fs.Remove(DefaultMarkerName)
|
||||
if stat, err := ffs.Stat(DefaultMarkerName); err == nil && !stat.IsDir() {
|
||||
err = ffs.Remove(DefaultMarkerName)
|
||||
if err == nil {
|
||||
err = fs.Mkdir(DefaultMarkerName, permBits)
|
||||
fs.Hide(DefaultMarkerName) // ignore error
|
||||
err = ffs.Mkdir(DefaultMarkerName, fs.ModePerm)
|
||||
ffs.Hide(DefaultMarkerName) // ignore error
|
||||
}
|
||||
if err != nil {
|
||||
slog.Warn("Failed to upgrade folder marker", slogutil.Error(err))
|
||||
|
||||
@@ -827,6 +827,11 @@ func (s *service) logListenAddressesChangedEvent(l ListenerAddresses) {
|
||||
func (s *service) CommitConfiguration(from, to config.Configuration) bool {
|
||||
newDevices := make(map[protocol.DeviceID]bool, len(to.Devices))
|
||||
for _, dev := range to.Devices {
|
||||
if dev.DeviceID == s.myID {
|
||||
// Do not report connection metrics for ourselves
|
||||
continue
|
||||
}
|
||||
|
||||
newDevices[dev.DeviceID] = true
|
||||
registerDeviceMetrics(dev.DeviceID.String())
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/url"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -185,9 +186,10 @@ func (t *tcpListener) WANAddresses() []*url.URL {
|
||||
|
||||
t.mut.RUnlock()
|
||||
|
||||
// If we support ReusePort, add an unspecified zero port address, which will be resolved by the discovery server
|
||||
// in hopes that TCP punch through works.
|
||||
if dialer.SupportsReusePort {
|
||||
// If we support ReusePort, and we are already announcing an unspecified
|
||||
// address, add an unspecified zero port address, which will be resolved
|
||||
// by the discovery server in hopes that TCP punch through works.
|
||||
if dialer.SupportsReusePort && slices.ContainsFunc(uris, func(u *url.URL) bool { return u.Hostname() == "0.0.0.0" }) {
|
||||
uri := *t.uri
|
||||
uri.Host = "0.0.0.0:0"
|
||||
uris = append([]*url.URL{&uri}, uris...)
|
||||
|
||||
@@ -289,7 +289,6 @@ func (c *globalClient) sendAnnouncement(ctx context.Context, timer *time.Timer)
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode > 299 {
|
||||
slog.DebugContext(ctx, "announce POST", "server", c.server, "status", resp.Status)
|
||||
c.setError(errors.New(resp.Status))
|
||||
|
||||
if h := resp.Header.Get("Retry-After"); h != "" {
|
||||
|
||||
+4
-19
@@ -174,7 +174,7 @@ func (f *BasicFilesystem) MkdirAll(path string, perm FileMode) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return f.mkdirAll(path, os.FileMode(perm))
|
||||
return os.MkdirAll(path, os.FileMode(perm))
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) Lstat(name string) (FileInfo, error) {
|
||||
@@ -241,15 +241,7 @@ func (f *BasicFilesystem) DirNames(name string) ([]string, error) {
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) Open(name string) (File, error) {
|
||||
rootedName, err := f.rooted(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fd, err := os.Open(rootedName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return basicFile{fd, name}, err
|
||||
return f.OpenFile(name, os.O_RDONLY, 0)
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) OpenFile(name string, flags int, mode FileMode) (File, error) {
|
||||
@@ -257,6 +249,7 @@ func (f *BasicFilesystem) OpenFile(name string, flags int, mode FileMode) (File,
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
flags |= alwaysOpenFlags // enforce extra bits in flags
|
||||
fd, err := os.OpenFile(rootedName, flags, os.FileMode(mode))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -265,15 +258,7 @@ func (f *BasicFilesystem) OpenFile(name string, flags int, mode FileMode) (File,
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) Create(name string) (File, error) {
|
||||
rootedName, err := f.rooted(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fd, err := os.Create(rootedName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return basicFile{fd, name}, err
|
||||
return f.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o666)
|
||||
}
|
||||
|
||||
func (*BasicFilesystem) Walk(_ string, _ WalkFunc) error {
|
||||
|
||||
@@ -14,8 +14,11 @@ import (
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
const alwaysOpenFlags = syscall.O_NOFOLLOW // never open symlinks as the final path component
|
||||
|
||||
func (f *BasicFilesystem) CreateSymlink(target, name string) error {
|
||||
name, err := f.rooted(name)
|
||||
if err != nil {
|
||||
@@ -32,10 +35,6 @@ func (f *BasicFilesystem) ReadSymlink(name string) (string, error) {
|
||||
return os.Readlink(name)
|
||||
}
|
||||
|
||||
func (*BasicFilesystem) mkdirAll(path string, perm os.FileMode) error {
|
||||
return os.MkdirAll(path, perm)
|
||||
}
|
||||
|
||||
// Unhide is a noop on unix, as unhiding files requires renaming them.
|
||||
// We still check that the relative path does not try to escape the root
|
||||
func (f *BasicFilesystem) Unhide(name string) error {
|
||||
|
||||
@@ -21,6 +21,8 @@ import (
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
const alwaysOpenFlags = 0 // no extra flags
|
||||
|
||||
var errNotSupported = errors.New("symlinks not supported")
|
||||
|
||||
func (BasicFilesystem) ReadSymlink(path string) (string, error) {
|
||||
@@ -31,57 +33,6 @@ func (BasicFilesystem) CreateSymlink(target, name string) error {
|
||||
return errNotSupported
|
||||
}
|
||||
|
||||
// Required due to https://github.com/golang/go/issues/10900
|
||||
func (f *BasicFilesystem) mkdirAll(path string, perm os.FileMode) error {
|
||||
// Fast path: if we can tell whether path is a directory or file, stop with success or error.
|
||||
dir, err := os.Stat(path)
|
||||
if err == nil {
|
||||
if dir.IsDir() {
|
||||
return nil
|
||||
}
|
||||
return &os.PathError{
|
||||
Op: "mkdir",
|
||||
Path: path,
|
||||
Err: syscall.ENOTDIR,
|
||||
}
|
||||
}
|
||||
|
||||
// Slow path: make sure parent exists and then call Mkdir for path.
|
||||
i := len(path)
|
||||
for i > 0 && IsPathSeparator(path[i-1]) { // Skip trailing path separator.
|
||||
i--
|
||||
}
|
||||
|
||||
j := i
|
||||
for j > 0 && !IsPathSeparator(path[j-1]) { // Scan backward over element.
|
||||
j--
|
||||
}
|
||||
|
||||
if j > 1 {
|
||||
// Create parent
|
||||
parent := path[0 : j-1]
|
||||
if parent != filepath.VolumeName(parent) {
|
||||
err = f.mkdirAll(parent, perm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parent now exists; invoke Mkdir and use its result.
|
||||
err = os.Mkdir(path, perm)
|
||||
if err != nil {
|
||||
// Handle arguments like "foo/." by
|
||||
// double-checking that directory doesn't exist.
|
||||
dir, err1 := os.Lstat(path)
|
||||
if err1 == nil && dir.IsDir() {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) Unhide(name string) error {
|
||||
name, err := f.rooted(name)
|
||||
if err != nil {
|
||||
|
||||
+2
-2
@@ -153,7 +153,7 @@ func newFakeFilesystem(rootURI string, _ ...Option) *fakeFS {
|
||||
for (files == 0 || createdFiles < files) && (maxsize == 0 || writtenData>>20 < int64(maxsize)) {
|
||||
dir := filepath.Join(fmt.Sprintf("%02x", rng.Intn(255)), fmt.Sprintf("%02x", rng.Intn(255)))
|
||||
file := fmt.Sprintf("%016x", rng.Int63())
|
||||
_ = fs.MkdirAll(dir, 0o755)
|
||||
_ = fs.MkdirAll(dir, ModePerm)
|
||||
|
||||
fd, _ := fs.Create(filepath.Join(dir, file))
|
||||
createdFiles++
|
||||
@@ -169,7 +169,7 @@ func newFakeFilesystem(rootURI string, _ ...Option) *fakeFS {
|
||||
|
||||
if !nostfolder {
|
||||
// Also create a default folder marker for good measure
|
||||
_ = fs.Mkdir(".stfolder", 0o700)
|
||||
_ = fs.Mkdir(".stfolder", ModePerm)
|
||||
}
|
||||
|
||||
// We only set the latency after doing the operations required to create
|
||||
|
||||
@@ -382,7 +382,7 @@ loop:
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if hasCurFile && file.BlocksEqual(curFile) {
|
||||
if hasCurFile && curFile.Type == file.Type && file.BlocksEqual(curFile) {
|
||||
// We are supposed to copy the entire file, and then fetch nothing. We
|
||||
// are only updating metadata, so we don't actually *need* to make the
|
||||
// copy.
|
||||
@@ -701,7 +701,7 @@ func (f *sendReceiveFolder) checkParent(file string, scanChan chan<- string) boo
|
||||
return true
|
||||
}
|
||||
f.sl.Debug("Creating parent directory", slogutil.FilePath(file))
|
||||
if err := f.mtimefs.MkdirAll(parent, 0o755); err != nil {
|
||||
if err := f.mtimefs.MkdirAll(parent, fs.ModePerm); err != nil {
|
||||
f.newPullError(file, fmt.Errorf("creating parent dir: %w", err))
|
||||
return false
|
||||
}
|
||||
@@ -1345,6 +1345,12 @@ func (f *sendReceiveFolder) copierRoutine(ctx context.Context, in <-chan copyBlo
|
||||
default:
|
||||
}
|
||||
|
||||
if block.Size == 0 {
|
||||
// Copying zero bytes is a no-op.
|
||||
state.copyDone(block)
|
||||
continue
|
||||
}
|
||||
|
||||
if !f.DisableSparseFiles && state.reused == 0 && block.IsEmpty() {
|
||||
// The block is a block of all zeroes, and we are not reusing
|
||||
// a temp file, so there is no need to do anything with it.
|
||||
@@ -1534,6 +1540,13 @@ func (f *sendReceiveFolder) pullerRoutine(ctx context.Context, in <-chan pullBlo
|
||||
|
||||
bytes := state.block.Size
|
||||
|
||||
if bytes == 0 {
|
||||
// Pulling zero bytes is a no-op.
|
||||
state.pullDone(state.block)
|
||||
out <- state.sharedPullerState
|
||||
continue
|
||||
}
|
||||
|
||||
if err := requestLimiter.TakeWithContext(ctx, bytes); err != nil {
|
||||
state.fail(err)
|
||||
out <- state.sharedPullerState
|
||||
|
||||
@@ -891,7 +891,7 @@ func TestPullCtxCancel(t *testing.T) {
|
||||
emptyState := func() pullBlockState {
|
||||
return pullBlockState{
|
||||
sharedPullerState: newSharedPullerState(protocol.FileInfo{}, nil, f.folderID, "", nil, nil, false, false, protocol.FileInfo{}, false, false),
|
||||
block: protocol.BlockInfo{},
|
||||
block: protocol.BlockInfo{Size: 42},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/svcutil"
|
||||
"github.com/syncthing/syncthing/lib/ur"
|
||||
"github.com/syncthing/syncthing/lib/ur/contract"
|
||||
)
|
||||
|
||||
type indexHandler struct {
|
||||
@@ -470,7 +470,7 @@ func (s *indexHandler) logSequenceAnomaly(msg string, extra map[string]any) {
|
||||
extraStrs[k] = fmt.Sprint(v)
|
||||
}
|
||||
|
||||
s.evLogger.Log(events.Failure, ur.FailureData{
|
||||
s.evLogger.Log(events.Failure, contract.FailureData{
|
||||
Description: msg,
|
||||
Extra: extraStrs,
|
||||
})
|
||||
|
||||
+3
-3
@@ -371,7 +371,7 @@ func (m *model) addAndStartFolderLockedWithIgnores(cfg config.FolderConfiguratio
|
||||
for _, available := range devs {
|
||||
if _, ok := expected[available]; !ok {
|
||||
l.Debugln("dropping", folder, "state for", available)
|
||||
_ = m.sdb.DropAllFiles(folder, available)
|
||||
_ = m.sdb.DropFolderDevice(folder, available)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1784,7 +1784,7 @@ func (m *model) handleAutoAccepts(deviceID protocol.DeviceID, folder protocol.Fo
|
||||
|
||||
// Attempt to create it to make sure it does, now.
|
||||
fullPath := filepath.Join(defaultFolderCfg.Path, path)
|
||||
if err := defaultPathFs.MkdirAll(path, 0o700); err != nil {
|
||||
if err := defaultPathFs.MkdirAll(path, fs.ModePerm); err != nil {
|
||||
slog.Error("Failed to create path for auto-accepted folder", folder.LogAttr(), slogutil.FilePath(fullPath), slogutil.Error(err))
|
||||
continue
|
||||
}
|
||||
@@ -2079,7 +2079,7 @@ func (m *model) Request(conn protocol.Connection, req *protocol.Request) (out pr
|
||||
return nil, protocol.ErrGeneric
|
||||
}
|
||||
|
||||
if folderCfg.Type != config.FolderTypeReceiveEncrypted && len(req.Hash) > 0 && !scanner.Validate(res.data[:n], req.Hash) {
|
||||
if folderCfg.Type != config.FolderTypeReceiveEncrypted && !scanner.Validate(res.data[:n], req.Hash) {
|
||||
m.recheckFile(deviceID, req.Folder, req.Name, req.Offset, req.Hash)
|
||||
l.Debugf("%v REQ(in) failed validating data: %s: %q / %q o=%d s=%d", m, deviceID.Short(), req.Folder, req.Name, req.Offset, req.Size)
|
||||
return nil, protocol.ErrNoSuchFile
|
||||
|
||||
+20
-9
@@ -9,6 +9,7 @@ package model
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -93,8 +94,10 @@ func TestRequest(t *testing.T) {
|
||||
|
||||
m.ScanFolder("default")
|
||||
|
||||
foobarHash := sha256.Sum256([]byte("foobar"))
|
||||
|
||||
// Existing, shared file
|
||||
res, err := m.Request(device1Conn, &protocol.Request{Folder: "default", Name: "foo", Size: 6})
|
||||
res, err := m.Request(device1Conn, &protocol.Request{Folder: "default", Name: "foo", Size: 6, Hash: foobarHash[:]})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -104,35 +107,42 @@ func TestRequest(t *testing.T) {
|
||||
}
|
||||
|
||||
// Existing, nonshared file
|
||||
_, err = m.Request(device2Conn, &protocol.Request{Folder: "default", Name: "foo", Size: 6})
|
||||
_, err = m.Request(device2Conn, &protocol.Request{Folder: "default", Name: "foo", Size: 6, Hash: foobarHash[:]})
|
||||
if err == nil {
|
||||
t.Error("Unexpected nil error on insecure file read")
|
||||
}
|
||||
|
||||
// Nonexistent file
|
||||
_, err = m.Request(device1Conn, &protocol.Request{Folder: "default", Name: "nonexistent", Size: 6})
|
||||
_, err = m.Request(device1Conn, &protocol.Request{Folder: "default", Name: "nonexistent", Size: 6, Hash: foobarHash[:]})
|
||||
if err == nil {
|
||||
t.Error("Unexpected nil error on insecure file read")
|
||||
}
|
||||
|
||||
// Shared folder, but disallowed file name
|
||||
_, err = m.Request(device1Conn, &protocol.Request{Folder: "default", Name: "../walk.go", Size: 6})
|
||||
_, err = m.Request(device1Conn, &protocol.Request{Folder: "default", Name: "../walk.go", Size: 6, Hash: foobarHash[:]})
|
||||
if err == nil {
|
||||
t.Error("Unexpected nil error on insecure file read")
|
||||
}
|
||||
|
||||
// Negative size
|
||||
_, err = m.Request(device1Conn, &protocol.Request{Folder: "default", Name: "foo", Size: -4})
|
||||
_, err = m.Request(device1Conn, &protocol.Request{Folder: "default", Name: "foo", Size: -4, Hash: foobarHash[:]})
|
||||
if err == nil {
|
||||
t.Error("Unexpected nil error on insecure file read")
|
||||
}
|
||||
|
||||
// Larger block than available
|
||||
// Missing hash
|
||||
_, err = m.Request(device1Conn, &protocol.Request{Folder: "default", Name: "foo", Size: 6})
|
||||
if err == nil {
|
||||
t.Error("Unexpected nil error on request without hash")
|
||||
}
|
||||
|
||||
// Larger block than available, with a mismatched hash
|
||||
_, err = m.Request(device1Conn, &protocol.Request{Folder: "default", Name: "foo", Size: 42, Hash: []byte("hash necessary but not checked")})
|
||||
if err == nil {
|
||||
t.Error("Unexpected nil error on read past end of file")
|
||||
}
|
||||
_, err = m.Request(device1Conn, &protocol.Request{Folder: "default", Name: "foo", Size: 42})
|
||||
// Larger block than available, with the matching hash of the short read
|
||||
_, err = m.Request(device1Conn, &protocol.Request{Folder: "default", Name: "foo", Size: 42, Hash: foobarHash[:]})
|
||||
if err != nil {
|
||||
t.Error("Unexpected error when large read should be permitted")
|
||||
}
|
||||
@@ -2979,15 +2989,16 @@ func TestRequestLimit(t *testing.T) {
|
||||
defer cleanupModel(m)
|
||||
m.ScanFolder("default")
|
||||
|
||||
emptyHash := sha256.Sum256(nil)
|
||||
befReq := time.Now()
|
||||
first, err := m.Request(conn, &protocol.Request{Folder: "default", Name: file, Size: 2000})
|
||||
first, err := m.Request(conn, &protocol.Request{Folder: "default", Name: file, Size: 2000, Hash: emptyHash[:]})
|
||||
if err != nil {
|
||||
t.Fatalf("First request failed: %v", err)
|
||||
}
|
||||
reqDur := time.Since(befReq)
|
||||
returned := make(chan struct{})
|
||||
go func() {
|
||||
second, err := m.Request(conn, &protocol.Request{Folder: "default", Name: file, Size: 2000})
|
||||
second, err := m.Request(conn, &protocol.Request{Folder: "default", Name: file, Size: 2000, Hash: emptyHash[:]})
|
||||
if err != nil {
|
||||
t.Errorf("Second request failed: %v", err)
|
||||
}
|
||||
|
||||
+13
-19
@@ -93,31 +93,25 @@ func (e encryptedModel) Request(req *Request) (RequestResponse, error) {
|
||||
}
|
||||
realSize := req.Size - blockOverhead
|
||||
realOffset := req.Offset - int64(req.BlockNo*blockOverhead)
|
||||
if realOffset < 0 {
|
||||
panic("bug: realOffset underflow")
|
||||
}
|
||||
|
||||
if req.Size < minPaddedSize {
|
||||
return nil, errors.New("short request")
|
||||
}
|
||||
|
||||
// Attempt to decrypt the block hash; it may be nil depending on what
|
||||
// type of device the request comes from. Trusted devices with
|
||||
// encryption enabled know the hash but don't bother to encrypt & send
|
||||
// it to us. Untrusted devices have the hash from the encrypted index
|
||||
// data and do send it. The model knows to only verify the hash if it
|
||||
// actually gets one.
|
||||
|
||||
var realHash []byte
|
||||
// Decrypt the block hash.
|
||||
fileKey := e.keyGen.FileKey(realName, folderKey)
|
||||
if len(req.Hash) > 0 {
|
||||
var additional [8]byte
|
||||
binary.BigEndian.PutUint64(additional[:], uint64(realOffset))
|
||||
realHash, err = decryptDeterministic(req.Hash, fileKey, additional[:])
|
||||
if err != nil {
|
||||
// "Legacy", no offset additional data?
|
||||
realHash, err = decryptDeterministic(req.Hash, fileKey, nil)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decrypting block hash: %w", err)
|
||||
}
|
||||
var additional [8]byte
|
||||
binary.BigEndian.PutUint64(additional[:], uint64(realOffset))
|
||||
realHash, err := decryptDeterministic(req.Hash, fileKey, additional[:])
|
||||
if err != nil {
|
||||
// "Legacy", no offset additional data?
|
||||
realHash, err = decryptDeterministic(req.Hash, fileKey, nil)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decrypting block hash: %w", err)
|
||||
}
|
||||
|
||||
// Perform that request and grab the data.
|
||||
|
||||
+24
-13
@@ -68,15 +68,16 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
ErrClosed = errors.New("connection closed")
|
||||
ErrTimeout = errors.New("read timeout")
|
||||
errNotCompressible = errors.New("not compressible")
|
||||
errUnknownMessage = errors.New("unknown message")
|
||||
errInvalidFilename = errors.New("filename is invalid")
|
||||
errUncleanFilename = errors.New("filename not in canonical format")
|
||||
errDeletedHasBlocks = errors.New("deleted file with non-empty block list")
|
||||
errDirectoryHasBlocks = errors.New("directory with non-empty block list")
|
||||
errFileHasNoBlocks = errors.New("file with empty block list")
|
||||
ErrClosed = errors.New("connection closed")
|
||||
ErrTimeout = errors.New("read timeout")
|
||||
errNotCompressible = errors.New("not compressible")
|
||||
errUnknownMessage = errors.New("unknown message")
|
||||
errInvalidFilename = errors.New("filename is invalid")
|
||||
errUncleanFilename = errors.New("filename not in canonical format")
|
||||
errDeletedHasBlocks = errors.New("deleted file with non-empty block list")
|
||||
errNonFileHasBlocks = errors.New("non-file type with non-empty block list")
|
||||
errNonFileHasSize = errors.New("non-file type with nonzero size")
|
||||
errFileHasNoBlocks = errors.New("file with empty block list")
|
||||
)
|
||||
|
||||
type Model interface {
|
||||
@@ -483,12 +484,18 @@ func (c *rawConnection) dispatcherLoop() (err error) {
|
||||
if err := checkFilename(msg.Name); err != nil {
|
||||
return newProtocolError(err, msgContext)
|
||||
}
|
||||
if msg.Size <= 0 {
|
||||
if msg.Size < 0 {
|
||||
return newProtocolError(fmt.Errorf("request size %d too small", msg.Size), msgContext)
|
||||
}
|
||||
if msg.Size > MaxRequestSize {
|
||||
return newProtocolError(fmt.Errorf("request size %d exceeds maximum allowed", msg.Size), msgContext)
|
||||
}
|
||||
if len(msg.Hash) == 0 {
|
||||
// Syncthing versions older than v1.28.1 omit the hash in
|
||||
// encrypted requests from trusted devices (a rare config)
|
||||
// and will run into this.
|
||||
return newProtocolError(errors.New("request missing block hash"), msgContext)
|
||||
}
|
||||
go c.handleRequest(requestFromWire(msg))
|
||||
|
||||
case *bep.Response:
|
||||
@@ -629,9 +636,13 @@ func checkFileInfoConsistency(f FileInfo) error {
|
||||
// Deleted files should have no blocks
|
||||
return errDeletedHasBlocks
|
||||
|
||||
case f.Type == FileInfoTypeDirectory && len(f.Blocks) != 0:
|
||||
// Directories should have no blocks
|
||||
return errDirectoryHasBlocks
|
||||
case f.Type != FileInfoTypeFile && len(f.Blocks) != 0:
|
||||
// Only files should have blocks
|
||||
return errNonFileHasBlocks
|
||||
|
||||
case f.Type != FileInfoTypeFile && f.Size != 0:
|
||||
// Only files should have a size
|
||||
return errNonFileHasSize
|
||||
|
||||
case !f.Deleted && !f.IsInvalid() && f.Type == FileInfoTypeFile && len(f.Blocks) == 0:
|
||||
// Non-deleted, non-invalid files should have at least one block
|
||||
|
||||
@@ -544,7 +544,7 @@ func TestDispatcherToCloseDeadlock(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRequestMaxSize(t *testing.T) {
|
||||
invalidSize := []int{-65536, 0, MaxRequestSize + 1}
|
||||
invalidSize := []int{-65536, -1, MaxRequestSize + 1}
|
||||
for _, s := range invalidSize {
|
||||
t.Run(fmt.Sprintf("invalid/%d", s), func(t *testing.T) {
|
||||
m := newTestModel()
|
||||
@@ -560,6 +560,7 @@ func TestRequestMaxSize(t *testing.T) {
|
||||
Id: 1,
|
||||
Name: "valid",
|
||||
Size: MaxRequestSize,
|
||||
Hash: []byte{42},
|
||||
}
|
||||
|
||||
res := <-c.outbox
|
||||
@@ -573,6 +574,7 @@ func TestRequestMaxSize(t *testing.T) {
|
||||
Id: 2,
|
||||
Name: "invalid",
|
||||
Size: int32(s),
|
||||
Hash: []byte{42},
|
||||
}
|
||||
|
||||
select {
|
||||
@@ -592,6 +594,35 @@ func TestRequestMaxSize(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestZeroSize(t *testing.T) {
|
||||
// A zero-sized request should be accepted, since current versions of
|
||||
// Syncthing send these. See https://github.com/syncthing/syncthing/issues/10709.
|
||||
m := newTestModel()
|
||||
rw := testutil.NewBlockingRW()
|
||||
c := getRawConnection(NewConnection(c0ID, rw, &testutil.NoopRW{}, testutil.NoopCloser{}, m, new(mockedConnectionInfo), CompressionAlways, testKeyGen))
|
||||
c.Start()
|
||||
defer closeAndWait(c, rw)
|
||||
|
||||
c.inbox <- &bep.ClusterConfig{}
|
||||
c.inbox <- &bep.Request{
|
||||
Id: 1,
|
||||
Name: "valid",
|
||||
Size: 0,
|
||||
Hash: []byte{42},
|
||||
}
|
||||
|
||||
select {
|
||||
case res := <-c.outbox:
|
||||
if msg, ok := res.msg.(*bep.Response); !ok || msg.Id != 1 {
|
||||
t.Errorf("bad response %#v", msg)
|
||||
}
|
||||
case <-c.dispatcherLoopStopped:
|
||||
t.Fatal("dispatcher loop terminated, expected zero-sized request to be accepted")
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("timed out waiting for response")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestInvalidFilename(t *testing.T) {
|
||||
m := newTestModel()
|
||||
rw := testutil.NewBlockingRW()
|
||||
@@ -604,6 +635,7 @@ func TestRequestInvalidFilename(t *testing.T) {
|
||||
Id: 1,
|
||||
Name: "../escape",
|
||||
Size: 1024,
|
||||
Hash: []byte{42},
|
||||
}
|
||||
|
||||
select {
|
||||
|
||||
+3
-16
@@ -120,25 +120,12 @@ func Blocks(ctx context.Context, r io.Reader, blocksize int, sizehint int64, cou
|
||||
return blocks, nil
|
||||
}
|
||||
|
||||
// Validate validates the hash, if len(hash)>0.
|
||||
// Validate validates the hash.
|
||||
func Validate(buf, hash []byte) bool {
|
||||
if len(hash) > 0 {
|
||||
hbuf := sha256.Sum256(buf)
|
||||
return bytes.Equal(hbuf[:], hash)
|
||||
}
|
||||
|
||||
return true
|
||||
hbuf := sha256.Sum256(buf)
|
||||
return bytes.Equal(hbuf[:], hash)
|
||||
}
|
||||
|
||||
type noopHash struct{}
|
||||
|
||||
func (noopHash) Sum32() uint32 { return 0 }
|
||||
func (noopHash) BlockSize() int { return 0 }
|
||||
func (noopHash) Size() int { return 0 }
|
||||
func (noopHash) Reset() {}
|
||||
func (noopHash) Sum([]byte) []byte { return nil }
|
||||
func (noopHash) Write([]byte) (int, error) { return 0, nil }
|
||||
|
||||
type noopCounter struct{}
|
||||
|
||||
func (*noopCounter) Update(_ int64) {}
|
||||
|
||||
@@ -282,3 +282,16 @@ func clear(v interface{}, since int) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type FailureReport struct {
|
||||
FailureData
|
||||
|
||||
Count int
|
||||
Version string
|
||||
}
|
||||
|
||||
type FailureData struct {
|
||||
Description string
|
||||
Goroutines string
|
||||
Extra map[string]string
|
||||
}
|
||||
|
||||
+14
-26
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
"github.com/syncthing/syncthing/lib/svcutil"
|
||||
"github.com/syncthing/syncthing/lib/tlsutil"
|
||||
"github.com/syncthing/syncthing/lib/ur/contract"
|
||||
|
||||
"github.com/thejerf/suture/v4"
|
||||
)
|
||||
@@ -39,23 +40,10 @@ var (
|
||||
invalidEventDataType = "failure event data is not a string"
|
||||
)
|
||||
|
||||
type FailureReport struct {
|
||||
FailureData
|
||||
|
||||
Count int
|
||||
Version string
|
||||
}
|
||||
|
||||
type FailureData struct {
|
||||
Description string
|
||||
Goroutines string
|
||||
Extra map[string]string
|
||||
}
|
||||
|
||||
func FailureDataWithGoroutines(description string) FailureData {
|
||||
func FailureDataWithGoroutines(description string) contract.FailureData {
|
||||
var buf strings.Builder
|
||||
pprof.Lookup("goroutine").WriteTo(&buf, 1)
|
||||
return FailureData{
|
||||
return contract.FailureData{
|
||||
Description: description,
|
||||
Goroutines: buf.String(),
|
||||
Extra: make(map[string]string),
|
||||
@@ -86,7 +74,7 @@ type failureHandler struct {
|
||||
type failureStat struct {
|
||||
first, last time.Time
|
||||
count int
|
||||
data FailureData
|
||||
data contract.FailureData
|
||||
}
|
||||
|
||||
func (h *failureHandler) Serve(ctx context.Context) error {
|
||||
@@ -105,24 +93,24 @@ func (h *failureHandler) Serve(ctx context.Context) error {
|
||||
if !ok {
|
||||
// Just to be safe - shouldn't ever happen, as
|
||||
// evChan is set to nil when unsubscribing.
|
||||
h.addReport(FailureData{Description: evChanClosed}, time.Now())
|
||||
h.addReport(contract.FailureData{Description: evChanClosed}, time.Now())
|
||||
evChan = nil
|
||||
continue
|
||||
}
|
||||
var data FailureData
|
||||
var data contract.FailureData
|
||||
switch d := e.Data.(type) {
|
||||
case string:
|
||||
data.Description = d
|
||||
case FailureData:
|
||||
case contract.FailureData:
|
||||
data = d
|
||||
default:
|
||||
// Same here, shouldn't ever happen.
|
||||
h.addReport(FailureData{Description: invalidEventDataType}, time.Now())
|
||||
h.addReport(contract.FailureData{Description: invalidEventDataType}, time.Now())
|
||||
continue
|
||||
}
|
||||
h.addReport(data, e.Time)
|
||||
case <-timer.C:
|
||||
reports := make([]FailureReport, 0, len(h.buf))
|
||||
reports := make([]contract.FailureReport, 0, len(h.buf))
|
||||
now := time.Now()
|
||||
for descr, stat := range h.buf {
|
||||
if now.Sub(stat.last) > minDelay || now.Sub(stat.first) > maxDelay {
|
||||
@@ -152,7 +140,7 @@ func (h *failureHandler) Serve(ctx context.Context) error {
|
||||
if sub != nil {
|
||||
sub.Unsubscribe()
|
||||
if len(h.buf) > 0 {
|
||||
reports := make([]FailureReport, 0, len(h.buf))
|
||||
reports := make([]contract.FailureReport, 0, len(h.buf))
|
||||
for _, stat := range h.buf {
|
||||
reports = append(reports, newFailureReport(stat))
|
||||
}
|
||||
@@ -179,7 +167,7 @@ func (h *failureHandler) applyOpts(opts config.OptionsConfiguration, sub events.
|
||||
return url, nil, nil
|
||||
}
|
||||
|
||||
func (h *failureHandler) addReport(data FailureData, evTime time.Time) {
|
||||
func (h *failureHandler) addReport(data contract.FailureData, evTime time.Time) {
|
||||
if stat, ok := h.buf[data.Description]; ok {
|
||||
stat.last = evTime
|
||||
stat.count++
|
||||
@@ -204,7 +192,7 @@ func (*failureHandler) String() string {
|
||||
return "FailureHandler"
|
||||
}
|
||||
|
||||
func sendFailureReports(ctx context.Context, reports []FailureReport, url string) {
|
||||
func sendFailureReports(ctx context.Context, reports []contract.FailureReport, url string) {
|
||||
var b bytes.Buffer
|
||||
if err := json.NewEncoder(&b).Encode(reports); err != nil {
|
||||
panic(err)
|
||||
@@ -235,8 +223,8 @@ func sendFailureReports(ctx context.Context, reports []FailureReport, url string
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
func newFailureReport(stat *failureStat) FailureReport {
|
||||
return FailureReport{
|
||||
func newFailureReport(stat *failureStat) contract.FailureReport {
|
||||
return contract.FailureReport{
|
||||
FailureData: stat.data,
|
||||
Count: stat.count,
|
||||
Version: build.LongVersion,
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
|
||||
"github.com/syncthing/syncthing/lib/build"
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/fs"
|
||||
)
|
||||
|
||||
func TestTaggedFilename(t *testing.T) {
|
||||
@@ -256,3 +257,57 @@ func TestArchiveFoldersCreationPermission(t *testing.T) {
|
||||
t.Errorf("földer2 permissions %v, want %v", folder2VersionsInfo.Mode(), folder2Perms)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDupDirTreeWritePermissions(t *testing.T) {
|
||||
// The structure should be replicated, with user permission bits set along the way
|
||||
|
||||
srcFs := fs.NewFilesystem(fs.FilesystemTypeFake, "TestDupDirTreeWritePermissions/srcFs")
|
||||
dstFs := fs.NewFilesystem(fs.FilesystemTypeFake, "TestDupDirTreeWritePermissions/dstFs")
|
||||
|
||||
// A source dir to duplicate
|
||||
_ = srcFs.Mkdir("foo", 0o444)
|
||||
_ = srcFs.Mkdir("foo/bar", 0o555)
|
||||
_ = srcFs.Mkdir("foo/bar/baz", 0o000)
|
||||
|
||||
// Duplication should succeed
|
||||
if err := dupDirTree(srcFs, dstFs, "foo/bar/baz"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Permissions should be the same, but with read/write/execute bits for
|
||||
// the user
|
||||
if info, err := dstFs.Lstat("foo"); err != nil || info.Mode() != 0o744 {
|
||||
t.Fatalf("foo: 0o%o", info.Mode())
|
||||
}
|
||||
if info, err := dstFs.Lstat("foo/bar"); err != nil || info.Mode() != 0o755 {
|
||||
t.Fatalf("foo/bar: 0o%o", info.Mode())
|
||||
}
|
||||
if info, err := dstFs.Lstat("foo/bar/baz"); err != nil || info.Mode() != 0o700 {
|
||||
t.Fatalf("foo/bar/baz: 0o%o", info.Mode())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDupDirFastPath(t *testing.T) {
|
||||
srcFs := fs.NewFilesystem(fs.FilesystemTypeFake, "TestDupDirFastPath/srcFs")
|
||||
dstFs := fs.NewFilesystem(fs.FilesystemTypeFake, "TestDupDirFastPath/dstFs")
|
||||
|
||||
// A source dir to duplicate
|
||||
_ = srcFs.Mkdir("foo", 0o444)
|
||||
_ = srcFs.Mkdir("foo/bar", 0o555)
|
||||
_ = srcFs.Mkdir("foo/bar/baz", 0o000)
|
||||
|
||||
// The destination exists, but with too few permission bits
|
||||
_ = dstFs.MkdirAll("foo/bar/baz", 0o555)
|
||||
|
||||
// Duplication should succeed
|
||||
if err := dupDirTree(srcFs, dstFs, "foo/bar/baz"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Permissions for the destination should have been updated. (This
|
||||
// differs from what would have been created by the duplication of the
|
||||
// 0o000 dir in the src, because it already existed.)
|
||||
if info, err := dstFs.Lstat("foo/bar/baz"); err != nil || info.Mode() != 0o755 {
|
||||
t.Fatalf("foo/bar/baz: 0o%o", info.Mode())
|
||||
}
|
||||
}
|
||||
|
||||
+68
-38
@@ -155,7 +155,7 @@ func archiveFile(method fs.CopyRangeMethod, srcFs, dstFs fs.Filesystem, filePath
|
||||
if err != nil {
|
||||
if fs.IsNotExist(err) {
|
||||
slog.Debug("Creating versions dir")
|
||||
err := dstFs.MkdirAll(".", 0o755)
|
||||
err := dstFs.MkdirAll(".", fs.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -192,48 +192,78 @@ func archiveFile(method fs.CopyRangeMethod, srcFs, dstFs fs.Filesystem, filePath
|
||||
return err
|
||||
}
|
||||
|
||||
func dupDirTree(srcFs, dstFs fs.Filesystem, folderPath string) error {
|
||||
// Return early if the folder already exists.
|
||||
_, err := dstFs.Stat(folderPath)
|
||||
if err == nil || !fs.IsNotExist(err) {
|
||||
return err
|
||||
// dupDirTree ensures folderPath exists in dstFs, copying permissions mostly
|
||||
// from srcFs. Permissions are altered to have the user read, write, and
|
||||
// execute bits set so that Syncthing file operations are possible within
|
||||
// the destination directory.
|
||||
//
|
||||
// We want to retain the source group and other bits so that we do not
|
||||
// inadvertently open up a directory for users who shouldn't have access to
|
||||
// it, but we do not consider it a security issue to open up the permissions
|
||||
// for the current user.
|
||||
//
|
||||
// This is based on os.MkdirAll with our srcFs adjustments.
|
||||
func dupDirTree(srcFs, dstFs fs.Filesystem, path string) error {
|
||||
const (
|
||||
allPerms = 0o777
|
||||
minDirPerms = 0o700
|
||||
)
|
||||
|
||||
// Fast path: if we can tell whether path is a directory or file, stop with success or error.
|
||||
if dir, err := dstFs.Lstat(path); err == nil {
|
||||
if !dir.IsDir() {
|
||||
return errors.New("destination exists but is not a directory")
|
||||
}
|
||||
if dir.Mode()&minDirPerms != minDirPerms {
|
||||
// We want all the required permission bits set
|
||||
_ = dstFs.Chmod(path, dir.Mode()&allPerms|minDirPerms)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
hadParent := true
|
||||
for i := range folderPath {
|
||||
if os.IsPathSeparator(folderPath[i]) {
|
||||
// If the parent folder didn't exist, then this folder doesn't exist
|
||||
// so we can skip the check
|
||||
if hadParent {
|
||||
_, err := dstFs.Stat(folderPath[:i])
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
if !fs.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
hadParent = false
|
||||
err := dupDirWithPerms(srcFs, dstFs, folderPath[:i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Slow path: make sure parent exists and then call Mkdir for path.
|
||||
|
||||
// Extract the parent folder from path by first removing any trailing
|
||||
// path separator and then scanning backward until finding a path
|
||||
// separator or reaching the beginning of the string.
|
||||
i := len(path) - 1
|
||||
for i >= 0 && os.IsPathSeparator(path[i]) {
|
||||
i--
|
||||
}
|
||||
for i >= 0 && !os.IsPathSeparator(path[i]) {
|
||||
i--
|
||||
}
|
||||
if i < 0 {
|
||||
i = 0
|
||||
}
|
||||
|
||||
// If there is a parent directory, and it is not the volume name,
|
||||
// recurse to ensure parent directory exists.
|
||||
if parent := path[:i]; len(parent) > len(filepath.VolumeName(path)) {
|
||||
if err := dupDirTree(srcFs, dstFs, parent); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return dupDirWithPerms(srcFs, dstFs, folderPath)
|
||||
}
|
||||
|
||||
func dupDirWithPerms(srcFs, dstFs fs.Filesystem, folderPath string) error {
|
||||
srcStat, err := srcFs.Stat(folderPath)
|
||||
if err != nil {
|
||||
// Parent now exists; invoke Mkdir and use its result.
|
||||
srcPerms := fs.FileMode(minDirPerms)
|
||||
if srcDir, err := srcFs.Lstat(path); err == nil {
|
||||
srcPerms = srcDir.Mode()&allPerms | minDirPerms
|
||||
}
|
||||
if err := dstFs.Mkdir(path, srcPerms); err != nil {
|
||||
// Handle arguments like "foo/." by
|
||||
// double-checking that directory doesn't exist.
|
||||
dir, err1 := dstFs.Lstat(path)
|
||||
if err1 == nil && dir.IsDir() {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
// If we call Mkdir with srcStat.Mode(), we won't get the expected perms because of umask
|
||||
// So, we create the folder with 0700, and then change the perms to the srcStat.Mode()
|
||||
err = dstFs.Mkdir(folderPath, 0o700)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return dstFs.Chmod(folderPath, srcStat.Mode())
|
||||
|
||||
// Extra chmod to ensure our permissions override umask
|
||||
_ = dstFs.Chmod(path, srcPerms)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func restoreFile(method fs.CopyRangeMethod, src, dst fs.Filesystem, filePath string, versionTime time.Time, tagger fileTagger) error {
|
||||
@@ -298,7 +328,7 @@ func restoreFile(method fs.CopyRangeMethod, src, dst fs.Filesystem, filePath str
|
||||
return err
|
||||
}
|
||||
|
||||
_ = dst.MkdirAll(filepath.Dir(filePath), 0o755)
|
||||
_ = dst.MkdirAll(filepath.Dir(filePath), fs.ModePerm)
|
||||
err := osutil.RenameOrCopy(method, src, dst, sourceFile, filePath)
|
||||
_ = dst.Chtimes(filePath, sourceMtime, sourceMtime)
|
||||
return err
|
||||
|
||||
+1
-1
@@ -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" "May 13, 2026" "v2.1.0" "Syncthing"
|
||||
.TH "STDISCOSRV" "1" "Jun 07, 2026" "v2.1.0" "Syncthing"
|
||||
.SH NAME
|
||||
stdiscosrv \- Syncthing Discovery Server
|
||||
.SH SYNOPSIS
|
||||
|
||||
+1
-1
@@ -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" "May 13, 2026" "v2.1.0" "Syncthing"
|
||||
.TH "STRELAYSRV" "1" "Jun 07, 2026" "v2.1.0" "Syncthing"
|
||||
.SH NAME
|
||||
strelaysrv \- Syncthing Relay Server
|
||||
.SH SYNOPSIS
|
||||
|
||||
+1
-1
@@ -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" "May 13, 2026" "v2.1.0" "Syncthing"
|
||||
.TH "SYNCTHING-BEP" "7" "Jun 07, 2026" "v2.1.0" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-bep \- Block Exchange Protocol v1
|
||||
.SH INTRODUCTION AND DEFINITIONS
|
||||
|
||||
@@ -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" "May 13, 2026" "v2.1.0" "Syncthing"
|
||||
.TH "SYNCTHING-CONFIG" "5" "Jun 07, 2026" "v2.1.0" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-config \- Syncthing Configuration
|
||||
.SH OVERVIEW
|
||||
|
||||
@@ -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" "May 13, 2026" "v2.1.0" "Syncthing"
|
||||
.TH "SYNCTHING-DEVICE-IDS" "7" "Jun 07, 2026" "v2.1.0" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-device-ids \- Understanding Device IDs
|
||||
.sp
|
||||
|
||||
@@ -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" "May 13, 2026" "v2.1.0" "Syncthing"
|
||||
.TH "SYNCTHING-EVENT-API" "7" "Jun 07, 2026" "v2.1.0" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-event-api \- Event API
|
||||
.SH DESCRIPTION
|
||||
|
||||
+1
-1
@@ -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" "May 13, 2026" "v2.1.0" "Syncthing"
|
||||
.TH "SYNCTHING-FAQ" "7" "Jun 07, 2026" "v2.1.0" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-faq \- Frequently Asked Questions
|
||||
.INDENT 0.0
|
||||
|
||||
@@ -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" "May 13, 2026" "v2.1.0" "Syncthing"
|
||||
.TH "SYNCTHING-GLOBALDISCO" "7" "Jun 07, 2026" "v2.1.0" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-globaldisco \- Global Discovery Protocol v3
|
||||
.SH ANNOUNCEMENTS
|
||||
|
||||
@@ -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" "May 13, 2026" "v2.1.0" "Syncthing"
|
||||
.TH "SYNCTHING-LOCALDISCO" "7" "Jun 07, 2026" "v2.1.0" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-localdisco \- Local Discovery Protocol v4
|
||||
.SH MODE OF OPERATION
|
||||
|
||||
@@ -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" "May 13, 2026" "v2.1.0" "Syncthing"
|
||||
.TH "SYNCTHING-NETWORKING" "7" "Jun 07, 2026" "v2.1.0" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-networking \- Firewall Setup
|
||||
.SH ROUTER SETUP
|
||||
|
||||
@@ -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" "May 13, 2026" "v2.1.0" "Syncthing"
|
||||
.TH "SYNCTHING-RELAY" "7" "Jun 07, 2026" "v2.1.0" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-relay \- Relay Protocol v1
|
||||
.SH WHAT IS A RELAY?
|
||||
|
||||
@@ -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" "May 13, 2026" "v2.1.0" "Syncthing"
|
||||
.TH "SYNCTHING-REST-API" "7" "Jun 07, 2026" "v2.1.0" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-rest-api \- REST API
|
||||
.sp
|
||||
|
||||
@@ -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" "May 13, 2026" "v2.1.0" "Syncthing"
|
||||
.TH "SYNCTHING-SECURITY" "7" "Jun 07, 2026" "v2.1.0" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-security \- Security Principles
|
||||
.sp
|
||||
|
||||
@@ -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" "May 13, 2026" "v2.1.0" "Syncthing"
|
||||
.TH "SYNCTHING-STIGNORE" "5" "Jun 07, 2026" "v2.1.0" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-stignore \- Prevent files from being synchronized to other nodes
|
||||
.SH SYNOPSIS
|
||||
|
||||
@@ -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" "May 13, 2026" "v2.1.0" "Syncthing"
|
||||
.TH "SYNCTHING-VERSIONING" "7" "Jun 07, 2026" "v2.1.0" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-versioning \- Keep automatic backups of deleted files by other nodes
|
||||
.sp
|
||||
|
||||
+1
-1
@@ -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" "May 13, 2026" "v2.1.0" "Syncthing"
|
||||
.TH "SYNCTHING" "1" "Jun 07, 2026" "v2.1.0" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing \- Syncthing
|
||||
.SH SYNOPSIS
|
||||
|
||||
+1
-1
@@ -93,7 +93,7 @@ func main() {
|
||||
}
|
||||
bs = authorsRe.ReplaceAll(bs, []byte("id=\"contributor-list\">\n"+replacement+"\n </div>"))
|
||||
|
||||
if err := os.WriteFile(htmlFile, bs, 0o644); err != nil {
|
||||
if err := os.WriteFile(htmlFile, bs, 0o666); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
|
||||
@@ -265,7 +265,7 @@ func readAll(path string) []byte {
|
||||
}
|
||||
|
||||
func writeFile(path string, data string) {
|
||||
err := os.WriteFile(path, []byte(data), 0o644)
|
||||
err := os.WriteFile(path, []byte(data), 0o666)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user