Compare commits
53
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cba163a1fd | ||
|
|
a8e2c8edb6 | ||
|
|
3e501d9036 | ||
|
|
9ca101756d | ||
|
|
a873d12c65 | ||
|
|
8ff670c564 | ||
|
|
b1ed2802fb | ||
|
|
b70cb580c8 | ||
|
|
28be3ba788 | ||
|
|
d4770ddc77 | ||
|
|
cbe1220680 | ||
|
|
0b95c5fa76 | ||
|
|
0343bca257 | ||
|
|
878016db39 | ||
|
|
1f4fde9525 | ||
|
|
5b9d8a838f | ||
|
|
8b19cb1e11 | ||
|
|
ce1e259bb4 | ||
|
|
2238a288d9 | ||
|
|
c8ee2a5cf6 | ||
|
|
1704827d04 | ||
|
|
a156e88eef | ||
|
|
94d0195b63 | ||
|
|
1616edcee3 | ||
|
|
6505e123bb | ||
|
|
63e4659282 | ||
|
|
f3f5557c8e | ||
|
|
b794726e1f | ||
|
|
3d59740a0a | ||
|
|
66fb65b01f | ||
|
|
5c2fcbfd19 | ||
|
|
f9b72330a8 | ||
|
|
822b6ac36b | ||
|
|
77f7778292 | ||
|
|
aed2c66e52 | ||
|
|
68a1fd010f | ||
|
|
ac8b3342ac | ||
|
|
0ea90dd932 | ||
|
|
718b1ce2b7 | ||
|
|
29f7510f5a | ||
|
|
a7f9ed4a80 | ||
|
|
1baefea410 | ||
|
|
563cec8923 | ||
|
|
cb24638ec9 | ||
|
|
2fb24dc2cc | ||
|
|
9aa2d2c92f | ||
|
|
d1c5100c98 | ||
|
|
42e677c055 | ||
|
|
feff334547 | ||
|
|
713cf357ce | ||
|
|
5342bec1b7 | ||
|
|
7df75e681d | ||
|
|
8dc826b234 |
@@ -7,7 +7,7 @@ on:
|
||||
- infra-*
|
||||
|
||||
env:
|
||||
GO_VERSION: "~1.22.3"
|
||||
GO_VERSION: "~1.23.0"
|
||||
CGO_ENABLED: "0"
|
||||
BUILD_USER: docker
|
||||
BUILD_HOST: github.syncthing.net
|
||||
|
||||
@@ -12,7 +12,7 @@ env:
|
||||
# The go version to use for builds. We set check-latest to true when
|
||||
# installing, so we get the latest patch version that matches the
|
||||
# expression.
|
||||
GO_VERSION: "~1.22.3"
|
||||
GO_VERSION: "~1.23.0"
|
||||
|
||||
# Optimize compatibility on the slow archictures.
|
||||
GO386: softfloat
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
runner: ["windows-latest", "ubuntu-latest", "macos-latest"]
|
||||
# The oldest version in this list should match what we have in our go.mod.
|
||||
# Variables don't seem to be supported here, or we could have done something nice.
|
||||
go: ["~1.21.7", "~1.22.3"]
|
||||
go: ["~1.22.6", "~1.23.0"]
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
- name: Set git to use LF
|
||||
@@ -238,7 +238,9 @@ jobs:
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: packages-linux
|
||||
path: syncthing-linux-*.tar.gz
|
||||
path: |
|
||||
syncthing-linux-*.tar.gz
|
||||
compat.json
|
||||
|
||||
#
|
||||
# macOS
|
||||
@@ -514,7 +516,7 @@ jobs:
|
||||
|
||||
- name: Install signing tool
|
||||
run: |
|
||||
go install ./cmd/stsigtool
|
||||
go install ./cmd/dev/stsigtool
|
||||
|
||||
- name: Sign archives
|
||||
run: |
|
||||
|
||||
@@ -18,3 +18,4 @@ deb
|
||||
/repos
|
||||
/proto/scripts/protoc-gen-gosyncthing
|
||||
/gui/next-gen-gui
|
||||
/compat.json
|
||||
|
||||
@@ -308,6 +308,7 @@ Simon Frei (imsodin) <freisim93@gmail.com>
|
||||
Simon Mwepu <simonmwepu@gmail.com>
|
||||
Simon Pickup <simon@pickupinfinity.com>
|
||||
Sly_tom_cat <slytomcat@mail.ru>
|
||||
Sonu Kumar Saw <31889738+dev-saw99@users.noreply.github.com>
|
||||
Stefan Kuntz (Stefan-Code) <stefan.github@gmail.com> <Stefan.github@gmail.com>
|
||||
Stefan Tatschner (rumpelsepp) <stefan@sevenbyte.org> <rumpelsepp@sevenbyte.org> <stefan@rumpelsepp.org>
|
||||
Steven Eckhoff <steven.eckhoff.opensource@gmail.com>
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
//go:build ignore
|
||||
// +build ignore
|
||||
//go:build tools
|
||||
// +build tools
|
||||
|
||||
package main
|
||||
|
||||
@@ -34,6 +34,8 @@ import (
|
||||
"time"
|
||||
|
||||
buildpkg "github.com/syncthing/syncthing/lib/build"
|
||||
"github.com/syncthing/syncthing/lib/upgrade"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -191,37 +193,37 @@ var targets = map[string]target{
|
||||
debname: "syncthing-relaypoolsrv",
|
||||
debdeps: []string{"libc6"},
|
||||
description: "Syncthing Relay Pool Server",
|
||||
buildPkgs: []string{"github.com/syncthing/syncthing/cmd/strelaypoolsrv"},
|
||||
buildPkgs: []string{"github.com/syncthing/syncthing/cmd/infra/strelaypoolsrv"},
|
||||
binaryName: "strelaypoolsrv", // .exe will be added automatically for Windows builds
|
||||
archiveFiles: []archiveFile{
|
||||
{src: "{{binary}}", dst: "{{binary}}", perm: 0755},
|
||||
{src: "cmd/strelaypoolsrv/README.md", dst: "README.txt", perm: 0644},
|
||||
{src: "cmd/strelaypoolsrv/LICENSE", dst: "LICENSE.txt", perm: 0644},
|
||||
{src: "cmd/infra/strelaypoolsrv/README.md", dst: "README.txt", perm: 0644},
|
||||
{src: "cmd/infra/strelaypoolsrv/LICENSE", dst: "LICENSE.txt", perm: 0644},
|
||||
{src: "AUTHORS", dst: "AUTHORS.txt", perm: 0644},
|
||||
},
|
||||
installationFiles: []archiveFile{
|
||||
{src: "{{binary}}", dst: "deb/usr/bin/{{binary}}", perm: 0755},
|
||||
{src: "cmd/strelaypoolsrv/README.md", dst: "deb/usr/share/doc/syncthing-relaypoolsrv/README.txt", perm: 0644},
|
||||
{src: "cmd/strelaypoolsrv/LICENSE", dst: "deb/usr/share/doc/syncthing-relaypoolsrv/LICENSE.txt", perm: 0644},
|
||||
{src: "cmd/infra/strelaypoolsrv/README.md", dst: "deb/usr/share/doc/syncthing-relaypoolsrv/README.txt", perm: 0644},
|
||||
{src: "cmd/infra/strelaypoolsrv/LICENSE", dst: "deb/usr/share/doc/syncthing-relaypoolsrv/LICENSE.txt", perm: 0644},
|
||||
{src: "AUTHORS", dst: "deb/usr/share/doc/syncthing-relaypoolsrv/AUTHORS.txt", perm: 0644},
|
||||
},
|
||||
},
|
||||
"stupgrades": {
|
||||
name: "stupgrades",
|
||||
description: "Syncthing Upgrade Check Server",
|
||||
buildPkgs: []string{"github.com/syncthing/syncthing/cmd/stupgrades"},
|
||||
buildPkgs: []string{"github.com/syncthing/syncthing/cmd/infra/stupgrades"},
|
||||
binaryName: "stupgrades",
|
||||
},
|
||||
"stcrashreceiver": {
|
||||
name: "stcrashreceiver",
|
||||
description: "Syncthing Crash Server",
|
||||
buildPkgs: []string{"github.com/syncthing/syncthing/cmd/stcrashreceiver"},
|
||||
buildPkgs: []string{"github.com/syncthing/syncthing/cmd/infra/stcrashreceiver"},
|
||||
binaryName: "stcrashreceiver",
|
||||
},
|
||||
"ursrv": {
|
||||
name: "ursrv",
|
||||
description: "Syncthing Usage Reporting Server",
|
||||
buildPkgs: []string{"github.com/syncthing/syncthing/cmd/ursrv"},
|
||||
buildPkgs: []string{"github.com/syncthing/syncthing/cmd/infra/ursrv"},
|
||||
binaryName: "ursrv",
|
||||
},
|
||||
}
|
||||
@@ -230,15 +232,11 @@ func initTargets() {
|
||||
all := targets["all"]
|
||||
pkgs, _ := filepath.Glob("cmd/*")
|
||||
for _, pkg := range pkgs {
|
||||
pkg = filepath.Base(pkg)
|
||||
if strings.HasPrefix(pkg, ".") {
|
||||
// ignore dotfiles
|
||||
if files, err := filepath.Glob(pkg + "/*.go"); err != nil || len(files) == 0 {
|
||||
// No go files in the directory
|
||||
continue
|
||||
}
|
||||
if noupgrade && pkg == "stupgrades" {
|
||||
continue
|
||||
}
|
||||
all.buildPkgs = append(all.buildPkgs, fmt.Sprintf("github.com/syncthing/syncthing/cmd/%s", pkg))
|
||||
all.buildPkgs = append(all.buildPkgs, fmt.Sprintf("github.com/syncthing/syncthing/%s", pkg))
|
||||
}
|
||||
targets["all"] = all
|
||||
|
||||
@@ -342,9 +340,11 @@ func runCommand(cmd string, target target) {
|
||||
|
||||
case "tar":
|
||||
buildTar(target, tags)
|
||||
writeCompatJSON()
|
||||
|
||||
case "zip":
|
||||
buildZip(target, tags)
|
||||
writeCompatJSON()
|
||||
|
||||
case "deb":
|
||||
buildDeb(target)
|
||||
@@ -834,12 +834,12 @@ func listFiles(dir string) []string {
|
||||
|
||||
func rebuildAssets() {
|
||||
os.Setenv("SOURCE_DATE_EPOCH", fmt.Sprint(buildStamp()))
|
||||
runPrint(goCmd, "generate", "github.com/syncthing/syncthing/lib/api/auto", "github.com/syncthing/syncthing/cmd/strelaypoolsrv/auto")
|
||||
runPrint(goCmd, "generate", "github.com/syncthing/syncthing/lib/api/auto", "github.com/syncthing/syncthing/cmd/infra/strelaypoolsrv/auto")
|
||||
}
|
||||
|
||||
func lazyRebuildAssets() {
|
||||
shouldRebuild := shouldRebuildAssets("lib/api/auto/gui.files.go", "gui") ||
|
||||
shouldRebuildAssets("cmd/strelaypoolsrv/auto/gui.files.go", "cmd/strelaypoolsrv/gui")
|
||||
shouldRebuildAssets("cmd/infra/strelaypoolsrv/auto/gui.files.go", "cmd/infra/strelaypoolsrv/gui")
|
||||
|
||||
if withNextGenGUI {
|
||||
shouldRebuild = buildNextGenGUI() || shouldRebuild
|
||||
@@ -1557,3 +1557,29 @@ func nextPatchVersion(ver string) string {
|
||||
digits[len(digits)-1] = strconv.Itoa(n + 1)
|
||||
return strings.Join(digits, ".")
|
||||
}
|
||||
|
||||
func writeCompatJSON() {
|
||||
bs, err := os.ReadFile("compat.yaml")
|
||||
if err != nil {
|
||||
log.Fatal("Reading compat.yaml:", err)
|
||||
}
|
||||
|
||||
var entries []upgrade.ReleaseCompatibility
|
||||
if err := yaml.Unmarshal(bs, &entries); err != nil {
|
||||
log.Fatal("Parsing compat.yaml:", err)
|
||||
}
|
||||
|
||||
rt := runtime.Version()
|
||||
for _, e := range entries {
|
||||
if !strings.HasPrefix(rt, e.Runtime) {
|
||||
continue
|
||||
}
|
||||
bs, _ := json.MarshalIndent(e, "", " ")
|
||||
if err := os.WriteFile("compat.json", bs, 0o644); err != nil {
|
||||
log.Fatal("Writing compat.json:", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
log.Fatalf("runtime %v not found in compat.yaml", rt)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
//go:generate go run ../../../script/genassets.go -o gui.files.go ../gui
|
||||
//go:generate go run ../../../../script/genassets.go -o gui.files.go ../gui
|
||||
|
||||
// Package auto contains auto generated files for web assets.
|
||||
package auto
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
lru "github.com/hashicorp/golang-lru/v2"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/syncthing/syncthing/cmd/strelaypoolsrv/auto"
|
||||
"github.com/syncthing/syncthing/cmd/infra/strelaypoolsrv/auto"
|
||||
"github.com/syncthing/syncthing/lib/assets"
|
||||
_ "github.com/syncthing/syncthing/lib/automaxprocs"
|
||||
"github.com/syncthing/syncthing/lib/geoip"
|
||||
@@ -11,8 +11,8 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/alecthomas/kong"
|
||||
"github.com/syncthing/syncthing/cmd/ursrv/aggregate"
|
||||
"github.com/syncthing/syncthing/cmd/ursrv/serve"
|
||||
"github.com/syncthing/syncthing/cmd/infra/ursrv/aggregate"
|
||||
"github.com/syncthing/syncthing/cmd/infra/ursrv/serve"
|
||||
_ "github.com/syncthing/syncthing/lib/automaxprocs"
|
||||
)
|
||||
|
||||
@@ -72,6 +72,7 @@ var (
|
||||
{regexp.MustCompile(`\sandroid-builder@github\.syncthing\.net`), "Google Play"},
|
||||
{regexp.MustCompile(`\sandroid-.*teamcity@build\.syncthing\.net`), "Google Play"},
|
||||
{regexp.MustCompile(`\sandroid-.*vagrant@basebox-stretch64`), "F-Droid"},
|
||||
{regexp.MustCompile(`\svagrant@bookworm`), "F-Droid"},
|
||||
{regexp.MustCompile(`\svagrant@bullseye`), "F-Droid"},
|
||||
{regexp.MustCompile(`\sbuilduser@(archlinux|svetlemodry)`), "Arch (3rd party)"},
|
||||
{regexp.MustCompile(`\ssyncthing@archlinux`), "Arch (3rd party)"},
|
||||
@@ -80,6 +81,7 @@ var (
|
||||
{regexp.MustCompile(`\sbrew@`), "Homebrew (3rd party)"},
|
||||
{regexp.MustCompile(`\sroot@buildkitsandbox`), "LinuxServer.io (3rd party)"},
|
||||
{regexp.MustCompile(`\sports@freebsd`), "FreeBSD (3rd party)"},
|
||||
{regexp.MustCompile(`\snix@nix`), "Nix (3rd party)"},
|
||||
{regexp.MustCompile(`.`), "Others"},
|
||||
}
|
||||
)
|
||||
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
@@ -270,7 +270,7 @@ found in the LICENSE file.
|
||||
</h4>
|
||||
</div>
|
||||
<div id="collapseTwo" class="panel-collapse collapse">
|
||||
<div class="panel-body">
|
||||
<div class="panel-body less-padding">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<table class="table table-striped">
|
||||
@@ -611,7 +611,7 @@ found in the LICENSE file.
|
||||
</div>
|
||||
<hr>
|
||||
<p>
|
||||
<a href="https://github.com/syncthing/syncthing/tree/main/cmd/ursrv">Source code</a>.
|
||||
<a href="https://github.com/syncthing/syncthing/">Source code</a>.
|
||||
This product includes GeoLite2 data created by MaxMind, available from
|
||||
<a href="http://www.maxmind.com">http://www.maxmind.com</a>.
|
||||
</p>
|
||||
+15
-4
@@ -10,8 +10,10 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
|
||||
amqp "github.com/rabbitmq/amqp091-go"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/thejerf/suture/v4"
|
||||
)
|
||||
|
||||
@@ -49,7 +51,7 @@ func newAMQPReplicator(broker, clientID string, db database) *amqpReplicator {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *amqpReplicator) send(key string, ps []DatabaseAddress, seen int64) {
|
||||
func (s *amqpReplicator) send(key *protocol.DeviceID, ps []DatabaseAddress, seen int64) {
|
||||
s.sender.send(key, ps, seen)
|
||||
}
|
||||
|
||||
@@ -109,9 +111,9 @@ func (s *amqpSender) String() string {
|
||||
return fmt.Sprintf("amqpSender(%q)", s.broker)
|
||||
}
|
||||
|
||||
func (s *amqpSender) send(key string, ps []DatabaseAddress, seen int64) {
|
||||
func (s *amqpSender) send(key *protocol.DeviceID, ps []DatabaseAddress, seen int64) {
|
||||
item := ReplicationRecord{
|
||||
Key: key,
|
||||
Key: key[:],
|
||||
Addresses: ps,
|
||||
Seen: seen,
|
||||
}
|
||||
@@ -161,8 +163,17 @@ func (s *amqpReceiver) Serve(ctx context.Context) error {
|
||||
replicationRecvsTotal.WithLabelValues("error").Inc()
|
||||
return fmt.Errorf("replication unmarshal: %w", err)
|
||||
}
|
||||
id, err := protocol.DeviceIDFromBytes(rec.Key)
|
||||
if err != nil {
|
||||
id, err = protocol.DeviceIDFromString(string(rec.Key))
|
||||
}
|
||||
if err != nil {
|
||||
log.Println("Replication device ID:", err)
|
||||
replicationRecvsTotal.WithLabelValues("error").Inc()
|
||||
continue
|
||||
}
|
||||
|
||||
if err := s.db.merge(rec.Key, rec.Addresses, rec.Seen); err != nil {
|
||||
if err := s.db.merge(&id, rec.Addresses, rec.Seen); err != nil {
|
||||
return fmt.Errorf("replication database merge: %w", err)
|
||||
}
|
||||
|
||||
|
||||
+120
-62
@@ -22,7 +22,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -45,10 +45,14 @@ type apiSrv struct {
|
||||
listener net.Listener
|
||||
repl replicator // optional
|
||||
useHTTP bool
|
||||
missesIncrease int
|
||||
compression bool
|
||||
gzipWriters sync.Pool
|
||||
seenTracker *retryAfterTracker
|
||||
notSeenTracker *retryAfterTracker
|
||||
}
|
||||
|
||||
mapsMut sync.Mutex
|
||||
misses map[string]int32
|
||||
type replicator interface {
|
||||
send(key *protocol.DeviceID, addrs []DatabaseAddress, seen int64)
|
||||
}
|
||||
|
||||
type requestID int64
|
||||
@@ -61,19 +65,30 @@ type contextKey int
|
||||
|
||||
const idKey contextKey = iota
|
||||
|
||||
func newAPISrv(addr string, cert tls.Certificate, db database, repl replicator, useHTTP bool, missesIncrease int) *apiSrv {
|
||||
func newAPISrv(addr string, cert tls.Certificate, db database, repl replicator, useHTTP, compression bool) *apiSrv {
|
||||
return &apiSrv{
|
||||
addr: addr,
|
||||
cert: cert,
|
||||
db: db,
|
||||
repl: repl,
|
||||
useHTTP: useHTTP,
|
||||
misses: make(map[string]int32),
|
||||
missesIncrease: missesIncrease,
|
||||
addr: addr,
|
||||
cert: cert,
|
||||
db: db,
|
||||
repl: repl,
|
||||
useHTTP: useHTTP,
|
||||
compression: compression,
|
||||
seenTracker: &retryAfterTracker{
|
||||
name: "seenTracker",
|
||||
bucketStarts: time.Now(),
|
||||
desiredRate: 250,
|
||||
currentDelay: notFoundRetryUnknownMinSeconds,
|
||||
},
|
||||
notSeenTracker: &retryAfterTracker{
|
||||
name: "notSeenTracker",
|
||||
bucketStarts: time.Now(),
|
||||
desiredRate: 250,
|
||||
currentDelay: notFoundRetryUnknownMaxSeconds / 2,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *apiSrv) Serve(_ context.Context) error {
|
||||
func (s *apiSrv) Serve(ctx context.Context) error {
|
||||
if s.useHTTP {
|
||||
listener, err := net.Listen("tcp", s.addr)
|
||||
if err != nil {
|
||||
@@ -107,6 +122,11 @@ func (s *apiSrv) Serve(_ context.Context) error {
|
||||
ErrorLog: log.New(io.Discard, "", 0),
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
srv.Shutdown(context.Background())
|
||||
}()
|
||||
|
||||
err := srv.Serve(s.listener)
|
||||
if err != nil {
|
||||
log.Println("Serve:", err)
|
||||
@@ -183,8 +203,7 @@ func (s *apiSrv) handleGET(w http.ResponseWriter, req *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
key := deviceID.String()
|
||||
rec, err := s.db.get(key)
|
||||
rec, err := s.db.get(&deviceID)
|
||||
if err != nil {
|
||||
// some sort of internal error
|
||||
lookupRequestsTotal.WithLabelValues("internal_error").Inc()
|
||||
@@ -194,27 +213,14 @@ func (s *apiSrv) handleGET(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
|
||||
if len(rec.Addresses) == 0 {
|
||||
lookupRequestsTotal.WithLabelValues("not_found").Inc()
|
||||
|
||||
s.mapsMut.Lock()
|
||||
misses := s.misses[key]
|
||||
if misses < rec.Misses {
|
||||
misses = rec.Misses
|
||||
var afterS int
|
||||
if rec.Seen == 0 {
|
||||
afterS = s.notSeenTracker.retryAfterS()
|
||||
lookupRequestsTotal.WithLabelValues("not_found_ever").Inc()
|
||||
} else {
|
||||
afterS = s.seenTracker.retryAfterS()
|
||||
lookupRequestsTotal.WithLabelValues("not_found_recent").Inc()
|
||||
}
|
||||
misses += int32(s.missesIncrease)
|
||||
s.misses[key] = misses
|
||||
s.mapsMut.Unlock()
|
||||
|
||||
if misses >= notFoundMissesWriteInterval {
|
||||
rec.Misses = misses
|
||||
rec.Missed = time.Now().UnixNano()
|
||||
rec.Addresses = nil
|
||||
// rec.Seen retained from get
|
||||
s.db.put(key, rec)
|
||||
}
|
||||
|
||||
afterS := notFoundRetryAfterSeconds(int(misses))
|
||||
retryAfterHistogram.Observe(float64(afterS))
|
||||
w.Header().Set("Retry-After", strconv.Itoa(afterS))
|
||||
http.Error(w, "Not Found", http.StatusNotFound)
|
||||
return
|
||||
@@ -226,10 +232,16 @@ func (s *apiSrv) handleGET(w http.ResponseWriter, req *http.Request) {
|
||||
var bw io.Writer = w
|
||||
|
||||
// Use compression if the client asks for it
|
||||
if strings.Contains(req.Header.Get("Accept-Encoding"), "gzip") {
|
||||
if s.compression && strings.Contains(req.Header.Get("Accept-Encoding"), "gzip") {
|
||||
gw, ok := s.gzipWriters.Get().(*gzip.Writer)
|
||||
if ok {
|
||||
gw.Reset(w)
|
||||
} else {
|
||||
gw = gzip.NewWriter(w)
|
||||
}
|
||||
w.Header().Set("Content-Encoding", "gzip")
|
||||
gw := gzip.NewWriter(bw)
|
||||
defer gw.Close()
|
||||
defer s.gzipWriters.Put(gw)
|
||||
bw = gw
|
||||
}
|
||||
|
||||
@@ -292,25 +304,25 @@ func (s *apiSrv) Stop() {
|
||||
}
|
||||
|
||||
func (s *apiSrv) handleAnnounce(deviceID protocol.DeviceID, addresses []string) error {
|
||||
key := deviceID.String()
|
||||
now := time.Now()
|
||||
expire := now.Add(addressExpiryTime).UnixNano()
|
||||
|
||||
// The address slice must always be sorted for database merges to work
|
||||
// properly.
|
||||
slices.Sort(addresses)
|
||||
addresses = slices.Compact(addresses)
|
||||
|
||||
dbAddrs := make([]DatabaseAddress, len(addresses))
|
||||
for i := range addresses {
|
||||
dbAddrs[i].Address = addresses[i]
|
||||
dbAddrs[i].Expires = expire
|
||||
}
|
||||
|
||||
// The address slice must always be sorted for database merges to work
|
||||
// properly.
|
||||
sort.Sort(databaseAddressOrder(dbAddrs))
|
||||
|
||||
seen := now.UnixNano()
|
||||
if s.repl != nil {
|
||||
s.repl.send(key, dbAddrs, seen)
|
||||
s.repl.send(&deviceID, dbAddrs, seen)
|
||||
}
|
||||
return s.db.merge(key, dbAddrs, seen)
|
||||
return s.db.merge(&deviceID, dbAddrs, seen)
|
||||
}
|
||||
|
||||
func handlePing(w http.ResponseWriter, _ *http.Request) {
|
||||
@@ -360,7 +372,7 @@ func certificateBytes(req *http.Request) ([]byte, error) {
|
||||
}
|
||||
|
||||
bs = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: hdr})
|
||||
} else if hdr := req.Header.Get("X-Forwarded-Tls-Client-Cert"); hdr != "" {
|
||||
} else if cert := req.Header.Get("X-Forwarded-Tls-Client-Cert"); cert != "" {
|
||||
// Traefik 2 passtlsclientcert
|
||||
//
|
||||
// The certificate is in PEM format, maybe with URL encoding
|
||||
@@ -368,19 +380,36 @@ func certificateBytes(req *http.Request) ([]byte, error) {
|
||||
// statements. We need to decode, reinstate the newlines every 64
|
||||
// character and add statements for the PEM decoder
|
||||
|
||||
if strings.Contains(hdr, "%") {
|
||||
if unesc, err := url.QueryUnescape(hdr); err == nil {
|
||||
hdr = unesc
|
||||
if strings.Contains(cert, "%") {
|
||||
if unesc, err := url.QueryUnescape(cert); err == nil {
|
||||
cert = unesc
|
||||
}
|
||||
}
|
||||
|
||||
for i := 64; i < len(hdr); i += 65 {
|
||||
hdr = hdr[:i] + "\n" + hdr[i:]
|
||||
const (
|
||||
header = "-----BEGIN CERTIFICATE-----"
|
||||
footer = "-----END CERTIFICATE-----"
|
||||
)
|
||||
|
||||
var b bytes.Buffer
|
||||
b.Grow(len(header) + 1 + len(cert) + len(cert)/64 + 1 + len(footer) + 1)
|
||||
|
||||
b.WriteString(header)
|
||||
b.WriteByte('\n')
|
||||
|
||||
for i := 0; i < len(cert); i += 64 {
|
||||
end := i + 64
|
||||
if end > len(cert) {
|
||||
end = len(cert)
|
||||
}
|
||||
b.WriteString(cert[i:end])
|
||||
b.WriteByte('\n')
|
||||
}
|
||||
|
||||
hdr = "-----BEGIN CERTIFICATE-----\n" + hdr
|
||||
hdr += "\n-----END CERTIFICATE-----\n"
|
||||
bs = []byte(hdr)
|
||||
b.WriteString(footer)
|
||||
b.WriteByte('\n')
|
||||
|
||||
bs = b.Bytes()
|
||||
}
|
||||
|
||||
if bs == nil {
|
||||
@@ -494,15 +523,44 @@ func errorRetryAfterString() string {
|
||||
return strconv.Itoa(errorRetryAfterSeconds + rand.Intn(errorRetryFuzzSeconds))
|
||||
}
|
||||
|
||||
func notFoundRetryAfterSeconds(misses int) int {
|
||||
retryAfterS := notFoundRetryMinSeconds + notFoundRetryIncSeconds*misses
|
||||
if retryAfterS > notFoundRetryMaxSeconds {
|
||||
retryAfterS = notFoundRetryMaxSeconds
|
||||
}
|
||||
retryAfterS += rand.Intn(notFoundRetryFuzzSeconds)
|
||||
return retryAfterS
|
||||
}
|
||||
|
||||
func reannounceAfterString() string {
|
||||
return strconv.Itoa(reannounceAfterSeconds + rand.Intn(reannounzeFuzzSeconds))
|
||||
}
|
||||
|
||||
type retryAfterTracker struct {
|
||||
name string
|
||||
desiredRate float64 // requests per second
|
||||
|
||||
mut sync.Mutex
|
||||
lastCount int // requests in the last bucket
|
||||
curCount int // requests in the current bucket
|
||||
bucketStarts time.Time // start of the current bucket
|
||||
currentDelay int // current delay in seconds
|
||||
}
|
||||
|
||||
func (t *retryAfterTracker) retryAfterS() int {
|
||||
now := time.Now()
|
||||
t.mut.Lock()
|
||||
if durS := now.Sub(t.bucketStarts).Seconds(); durS > float64(t.currentDelay) {
|
||||
t.bucketStarts = now
|
||||
t.lastCount = t.curCount
|
||||
lastRate := float64(t.lastCount) / durS
|
||||
|
||||
switch {
|
||||
case t.currentDelay > notFoundRetryUnknownMinSeconds &&
|
||||
lastRate < 0.75*t.desiredRate:
|
||||
t.currentDelay = max(8*t.currentDelay/10, notFoundRetryUnknownMinSeconds)
|
||||
case t.currentDelay < notFoundRetryUnknownMaxSeconds &&
|
||||
lastRate > 1.25*t.desiredRate:
|
||||
t.currentDelay = min(3*t.currentDelay/2, notFoundRetryUnknownMaxSeconds)
|
||||
}
|
||||
|
||||
t.curCount = 0
|
||||
}
|
||||
if t.curCount == 0 {
|
||||
retryAfterLevel.WithLabelValues(t.name).Set(float64(t.currentDelay))
|
||||
}
|
||||
t.curCount++
|
||||
t.mut.Unlock()
|
||||
return t.currentDelay + rand.Intn(t.currentDelay/4)
|
||||
}
|
||||
|
||||
@@ -7,9 +7,20 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/tlsutil"
|
||||
)
|
||||
|
||||
func TestFixupAddresses(t *testing.T) {
|
||||
@@ -94,3 +105,79 @@ func addr(host string, port int) *net.TCPAddr {
|
||||
Port: port,
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAPIRequests(b *testing.B) {
|
||||
db := newInMemoryStore(b.TempDir(), 0, nil)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
go db.Serve(ctx)
|
||||
api := newAPISrv("127.0.0.1:0", tls.Certificate{}, db, nil, true, true)
|
||||
srv := httptest.NewServer(http.HandlerFunc(api.handler))
|
||||
|
||||
kf := b.TempDir() + "/cert"
|
||||
crt, err := tlsutil.NewCertificate(kf+".crt", kf+".key", "localhost", 7)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
certBs, err := os.ReadFile(kf + ".crt")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
certBs = regexp.MustCompile(`---[^\n]+---\n`).ReplaceAll(certBs, nil)
|
||||
certString := string(strings.ReplaceAll(string(certBs), "\n", " "))
|
||||
|
||||
devID := protocol.NewDeviceID(crt.Certificate[0])
|
||||
devIDString := devID.String()
|
||||
|
||||
b.Run("Announce", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
url := srv.URL + "/v2/?device=" + devIDString
|
||||
for i := 0; i < b.N; i++ {
|
||||
req, _ := http.NewRequest(http.MethodPost, url, strings.NewReader(`{"addresses":["tcp://10.10.10.10:42000"]}`))
|
||||
req.Header.Set("X-Forwarded-Tls-Client-Cert", certString)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusNoContent {
|
||||
b.Fatalf("unexpected status %s", resp.Status)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Lookup", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
url := srv.URL + "/v2/?device=" + devIDString
|
||||
for i := 0; i < b.N; i++ {
|
||||
req, _ := http.NewRequest(http.MethodGet, url, nil)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
io.Copy(io.Discard, resp.Body)
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
b.Fatalf("unexpected status %s", resp.Status)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("LookupNoCompression", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
url := srv.URL + "/v2/?device=" + devIDString
|
||||
for i := 0; i < b.N; i++ {
|
||||
req, _ := http.NewRequest(http.MethodGet, url, nil)
|
||||
req.Header.Set("Accept-Encoding", "identity") // disable compression
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
io.Copy(io.Discard, resp.Body)
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
b.Fatalf("unexpected status %s", resp.Status)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
+313
-299
@@ -10,17 +10,22 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"cmp"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/url"
|
||||
"sort"
|
||||
"os"
|
||||
"path"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/sliceutil"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/storage"
|
||||
"github.com/syndtr/goleveldb/leveldb/util"
|
||||
"github.com/puzpuzpuz/xsync/v3"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
)
|
||||
|
||||
type clock interface {
|
||||
@@ -34,380 +39,389 @@ func (defaultClock) Now() time.Time {
|
||||
}
|
||||
|
||||
type database interface {
|
||||
put(key string, rec DatabaseRecord) error
|
||||
merge(key string, addrs []DatabaseAddress, seen int64) error
|
||||
get(key string) (DatabaseRecord, error)
|
||||
put(key *protocol.DeviceID, rec DatabaseRecord) error
|
||||
merge(key *protocol.DeviceID, addrs []DatabaseAddress, seen int64) error
|
||||
get(key *protocol.DeviceID) (DatabaseRecord, error)
|
||||
}
|
||||
|
||||
type levelDBStore struct {
|
||||
db *leveldb.DB
|
||||
inbox chan func()
|
||||
clock clock
|
||||
marshalBuf []byte
|
||||
type inMemoryStore struct {
|
||||
m *xsync.MapOf[protocol.DeviceID, DatabaseRecord]
|
||||
dir string
|
||||
flushInterval time.Duration
|
||||
s3 *s3Copier
|
||||
clock clock
|
||||
}
|
||||
|
||||
func newLevelDBStore(dir string) (*levelDBStore, error) {
|
||||
db, err := leveldb.OpenFile(dir, levelDBOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
func newInMemoryStore(dir string, flushInterval time.Duration, s3 *s3Copier) *inMemoryStore {
|
||||
s := &inMemoryStore{
|
||||
m: xsync.NewMapOf[protocol.DeviceID, DatabaseRecord](),
|
||||
dir: dir,
|
||||
flushInterval: flushInterval,
|
||||
s3: s3,
|
||||
clock: defaultClock{},
|
||||
}
|
||||
return &levelDBStore{
|
||||
db: db,
|
||||
inbox: make(chan func(), 16),
|
||||
clock: defaultClock{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func newMemoryLevelDBStore() (*levelDBStore, error) {
|
||||
db, err := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &levelDBStore{
|
||||
db: db,
|
||||
inbox: make(chan func(), 16),
|
||||
clock: defaultClock{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *levelDBStore) put(key string, rec DatabaseRecord) error {
|
||||
t0 := time.Now()
|
||||
defer func() {
|
||||
databaseOperationSeconds.WithLabelValues(dbOpPut).Observe(time.Since(t0).Seconds())
|
||||
}()
|
||||
|
||||
rc := make(chan error)
|
||||
|
||||
s.inbox <- func() {
|
||||
size := rec.Size()
|
||||
if len(s.marshalBuf) < size {
|
||||
s.marshalBuf = make([]byte, size)
|
||||
nr, err := s.read()
|
||||
if os.IsNotExist(err) && s3 != nil {
|
||||
// Try to read from AWS
|
||||
fd, cerr := os.Create(path.Join(s.dir, "records.db"))
|
||||
if cerr != nil {
|
||||
log.Println("Error creating database file:", err)
|
||||
return s
|
||||
}
|
||||
n, _ := rec.MarshalTo(s.marshalBuf)
|
||||
rc <- s.db.Put([]byte(key), s.marshalBuf[:n], nil)
|
||||
if err := s3.downloadLatest(fd); err != nil {
|
||||
log.Printf("Error reading database from S3: %v", err)
|
||||
}
|
||||
_ = fd.Close()
|
||||
nr, err = s.read()
|
||||
}
|
||||
|
||||
err := <-rc
|
||||
if err != nil {
|
||||
databaseOperations.WithLabelValues(dbOpPut, dbResError).Inc()
|
||||
} else {
|
||||
databaseOperations.WithLabelValues(dbOpPut, dbResSuccess).Inc()
|
||||
log.Println("Error reading database:", err)
|
||||
}
|
||||
|
||||
return err
|
||||
log.Printf("Read %d records from database", nr)
|
||||
s.expireAndCalculateStatistics()
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *levelDBStore) merge(key string, addrs []DatabaseAddress, seen int64) error {
|
||||
func (s *inMemoryStore) put(key *protocol.DeviceID, rec DatabaseRecord) error {
|
||||
t0 := time.Now()
|
||||
s.m.Store(*key, rec)
|
||||
databaseOperations.WithLabelValues(dbOpPut, dbResSuccess).Inc()
|
||||
databaseOperationSeconds.WithLabelValues(dbOpPut).Observe(time.Since(t0).Seconds())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *inMemoryStore) merge(key *protocol.DeviceID, addrs []DatabaseAddress, seen int64) error {
|
||||
t0 := time.Now()
|
||||
defer func() {
|
||||
databaseOperationSeconds.WithLabelValues(dbOpMerge).Observe(time.Since(t0).Seconds())
|
||||
}()
|
||||
|
||||
rc := make(chan error)
|
||||
newRec := DatabaseRecord{
|
||||
Addresses: addrs,
|
||||
Seen: seen,
|
||||
}
|
||||
|
||||
s.inbox <- func() {
|
||||
// grab the existing record
|
||||
oldRec, err := s.get(key)
|
||||
if err != nil {
|
||||
// "not found" is not an error from get, so this is serious
|
||||
// stuff only
|
||||
rc <- err
|
||||
return
|
||||
}
|
||||
newRec = merge(newRec, oldRec)
|
||||
oldRec, _ := s.m.Load(*key)
|
||||
newRec = merge(oldRec, newRec)
|
||||
s.m.Store(*key, newRec)
|
||||
|
||||
// We replicate s.put() functionality here ourselves instead of
|
||||
// calling it because we want to serialize our get above together
|
||||
// with the put in the same function.
|
||||
size := newRec.Size()
|
||||
if len(s.marshalBuf) < size {
|
||||
s.marshalBuf = make([]byte, size)
|
||||
}
|
||||
n, _ := newRec.MarshalTo(s.marshalBuf)
|
||||
rc <- s.db.Put([]byte(key), s.marshalBuf[:n], nil)
|
||||
}
|
||||
databaseOperations.WithLabelValues(dbOpMerge, dbResSuccess).Inc()
|
||||
databaseOperationSeconds.WithLabelValues(dbOpMerge).Observe(time.Since(t0).Seconds())
|
||||
|
||||
err := <-rc
|
||||
if err != nil {
|
||||
databaseOperations.WithLabelValues(dbOpMerge, dbResError).Inc()
|
||||
} else {
|
||||
databaseOperations.WithLabelValues(dbOpMerge, dbResSuccess).Inc()
|
||||
}
|
||||
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *levelDBStore) get(key string) (DatabaseRecord, error) {
|
||||
func (s *inMemoryStore) get(key *protocol.DeviceID) (DatabaseRecord, error) {
|
||||
t0 := time.Now()
|
||||
defer func() {
|
||||
databaseOperationSeconds.WithLabelValues(dbOpGet).Observe(time.Since(t0).Seconds())
|
||||
}()
|
||||
|
||||
keyBs := []byte(key)
|
||||
val, err := s.db.Get(keyBs, nil)
|
||||
if err == leveldb.ErrNotFound {
|
||||
rec, ok := s.m.Load(*key)
|
||||
if !ok {
|
||||
databaseOperations.WithLabelValues(dbOpGet, dbResNotFound).Inc()
|
||||
return DatabaseRecord{}, nil
|
||||
}
|
||||
if err != nil {
|
||||
databaseOperations.WithLabelValues(dbOpGet, dbResError).Inc()
|
||||
return DatabaseRecord{}, err
|
||||
}
|
||||
|
||||
var rec DatabaseRecord
|
||||
|
||||
if err := rec.Unmarshal(val); err != nil {
|
||||
databaseOperations.WithLabelValues(dbOpGet, dbResUnmarshalError).Inc()
|
||||
return DatabaseRecord{}, nil
|
||||
}
|
||||
|
||||
rec.Addresses = expire(rec.Addresses, s.clock.Now().UnixNano())
|
||||
rec.Addresses = expire(rec.Addresses, s.clock.Now())
|
||||
databaseOperations.WithLabelValues(dbOpGet, dbResSuccess).Inc()
|
||||
return rec, nil
|
||||
}
|
||||
|
||||
func (s *levelDBStore) Serve(ctx context.Context) error {
|
||||
t := time.NewTimer(0)
|
||||
defer t.Stop()
|
||||
defer s.db.Close()
|
||||
func (s *inMemoryStore) Serve(ctx context.Context) error {
|
||||
if s.flushInterval <= 0 {
|
||||
<-ctx.Done()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start the statistics serve routine. It will exit with us when
|
||||
// statisticsTrigger is closed.
|
||||
statisticsTrigger := make(chan struct{})
|
||||
statisticsDone := make(chan struct{})
|
||||
go s.statisticsServe(statisticsTrigger, statisticsDone)
|
||||
t := time.NewTimer(s.flushInterval)
|
||||
defer t.Stop()
|
||||
|
||||
loop:
|
||||
for {
|
||||
select {
|
||||
case fn := <-s.inbox:
|
||||
// Run function in serialized order.
|
||||
fn()
|
||||
|
||||
case <-t.C:
|
||||
// Trigger the statistics routine to do its thing in the
|
||||
// background.
|
||||
statisticsTrigger <- struct{}{}
|
||||
|
||||
case <-statisticsDone:
|
||||
// The statistics routine is done with one iteratation, schedule
|
||||
// the next.
|
||||
t.Reset(databaseStatisticsInterval)
|
||||
log.Println("Calculating statistics")
|
||||
s.expireAndCalculateStatistics()
|
||||
log.Println("Flushing database")
|
||||
if err := s.write(); err != nil {
|
||||
log.Println("Error writing database:", err)
|
||||
}
|
||||
log.Println("Finished flushing database")
|
||||
t.Reset(s.flushInterval)
|
||||
|
||||
case <-ctx.Done():
|
||||
// We're done.
|
||||
close(statisticsTrigger)
|
||||
break loop
|
||||
}
|
||||
}
|
||||
|
||||
// Also wait for statisticsServe to return
|
||||
<-statisticsDone
|
||||
return s.write()
|
||||
}
|
||||
|
||||
func (s *inMemoryStore) expireAndCalculateStatistics() {
|
||||
now := s.clock.Now()
|
||||
cutoff24h := now.Add(-24 * time.Hour).UnixNano()
|
||||
cutoff1w := now.Add(-7 * 24 * time.Hour).UnixNano()
|
||||
current, currentIPv4, currentIPv6, currentIPv6GUA, last24h, last1w := 0, 0, 0, 0, 0, 0
|
||||
|
||||
n := 0
|
||||
s.m.Range(func(key protocol.DeviceID, rec DatabaseRecord) bool {
|
||||
if n%1000 == 0 {
|
||||
runtime.Gosched()
|
||||
}
|
||||
n++
|
||||
|
||||
addresses := expire(rec.Addresses, now)
|
||||
if len(addresses) == 0 {
|
||||
rec.Addresses = nil
|
||||
s.m.Store(key, rec)
|
||||
} else if len(addresses) != len(rec.Addresses) {
|
||||
rec.Addresses = addresses
|
||||
s.m.Store(key, rec)
|
||||
}
|
||||
|
||||
switch {
|
||||
case len(rec.Addresses) > 0:
|
||||
current++
|
||||
seenIPv4, seenIPv6, seenIPv6GUA := false, false, false
|
||||
for _, addr := range rec.Addresses {
|
||||
// We do fast and loose matching on strings here instead of
|
||||
// parsing the address and the IP and doing "proper" checks,
|
||||
// to keep things fast and generate less garbage.
|
||||
if strings.Contains(addr.Address, "[") {
|
||||
seenIPv6 = true
|
||||
if strings.Contains(addr.Address, "[2") {
|
||||
seenIPv6GUA = true
|
||||
}
|
||||
} else {
|
||||
seenIPv4 = true
|
||||
}
|
||||
if seenIPv4 && seenIPv6 && seenIPv6GUA {
|
||||
break
|
||||
}
|
||||
}
|
||||
if seenIPv4 {
|
||||
currentIPv4++
|
||||
}
|
||||
if seenIPv6 {
|
||||
currentIPv6++
|
||||
}
|
||||
if seenIPv6GUA {
|
||||
currentIPv6GUA++
|
||||
}
|
||||
case rec.Seen > cutoff24h:
|
||||
last24h++
|
||||
case rec.Seen > cutoff1w:
|
||||
last1w++
|
||||
default:
|
||||
// drop the record if it's older than a week
|
||||
s.m.Delete(key)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
databaseKeys.WithLabelValues("current").Set(float64(current))
|
||||
databaseKeys.WithLabelValues("currentIPv4").Set(float64(currentIPv4))
|
||||
databaseKeys.WithLabelValues("currentIPv6").Set(float64(currentIPv6))
|
||||
databaseKeys.WithLabelValues("currentIPv6GUA").Set(float64(currentIPv6GUA))
|
||||
databaseKeys.WithLabelValues("last24h").Set(float64(last24h))
|
||||
databaseKeys.WithLabelValues("last1w").Set(float64(last1w))
|
||||
databaseStatisticsSeconds.Set(time.Since(now).Seconds())
|
||||
}
|
||||
|
||||
func (s *inMemoryStore) write() (err error) {
|
||||
t0 := time.Now()
|
||||
defer func() {
|
||||
if err == nil {
|
||||
databaseWriteSeconds.Set(time.Since(t0).Seconds())
|
||||
databaseLastWritten.Set(float64(t0.Unix()))
|
||||
}
|
||||
}()
|
||||
|
||||
dbf := path.Join(s.dir, "records.db")
|
||||
fd, err := os.Create(dbf + ".tmp")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bw := bufio.NewWriter(fd)
|
||||
|
||||
var buf []byte
|
||||
var rangeErr error
|
||||
now := s.clock.Now()
|
||||
cutoff1w := now.Add(-7 * 24 * time.Hour).UnixNano()
|
||||
n := 0
|
||||
s.m.Range(func(key protocol.DeviceID, value DatabaseRecord) bool {
|
||||
if n%1000 == 0 {
|
||||
runtime.Gosched()
|
||||
}
|
||||
n++
|
||||
|
||||
if value.Seen < cutoff1w {
|
||||
// drop the record if it's older than a week
|
||||
return true
|
||||
}
|
||||
rec := ReplicationRecord{
|
||||
Key: key[:],
|
||||
Addresses: value.Addresses,
|
||||
Seen: value.Seen,
|
||||
}
|
||||
s := rec.Size()
|
||||
if s+4 > len(buf) {
|
||||
buf = make([]byte, s+4)
|
||||
}
|
||||
n, err := rec.MarshalTo(buf[4:])
|
||||
if err != nil {
|
||||
rangeErr = err
|
||||
return false
|
||||
}
|
||||
binary.BigEndian.PutUint32(buf, uint32(n))
|
||||
if _, err := bw.Write(buf[:n+4]); err != nil {
|
||||
rangeErr = err
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
if rangeErr != nil {
|
||||
_ = fd.Close()
|
||||
return rangeErr
|
||||
}
|
||||
|
||||
if err := bw.Flush(); err != nil {
|
||||
_ = fd.Close
|
||||
return err
|
||||
}
|
||||
if err := fd.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Rename(dbf+".tmp", dbf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Upload to S3
|
||||
if s.s3 != nil {
|
||||
fd, err = os.Open(dbf)
|
||||
if err != nil {
|
||||
log.Printf("Error uploading database to S3: %v", err)
|
||||
return nil
|
||||
}
|
||||
defer fd.Close()
|
||||
if err := s.s3.upload(fd); err != nil {
|
||||
log.Printf("Error uploading database to S3: %v", err)
|
||||
}
|
||||
log.Println("Finished uploading database")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *levelDBStore) statisticsServe(trigger <-chan struct{}, done chan<- struct{}) {
|
||||
defer close(done)
|
||||
func (s *inMemoryStore) read() (int, error) {
|
||||
fd, err := os.Open(path.Join(s.dir, "records.db"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer fd.Close()
|
||||
|
||||
for range trigger {
|
||||
t0 := time.Now()
|
||||
nowNanos := t0.UnixNano()
|
||||
cutoff24h := t0.Add(-24 * time.Hour).UnixNano()
|
||||
cutoff1w := t0.Add(-7 * 24 * time.Hour).UnixNano()
|
||||
cutoff2Mon := t0.Add(-60 * 24 * time.Hour).UnixNano()
|
||||
current, currentIPv4, currentIPv6, last24h, last1w, inactive, errors := 0, 0, 0, 0, 0, 0, 0
|
||||
|
||||
iter := s.db.NewIterator(&util.Range{}, nil)
|
||||
for iter.Next() {
|
||||
// Attempt to unmarshal the record and count the
|
||||
// failure if there's something wrong with it.
|
||||
var rec DatabaseRecord
|
||||
if err := rec.Unmarshal(iter.Value()); err != nil {
|
||||
errors++
|
||||
continue
|
||||
}
|
||||
|
||||
// If there are addresses that have not expired it's a current
|
||||
// record, otherwise account it based on when it was last seen
|
||||
// (last 24 hours or last week) or finally as inactice.
|
||||
addrs := expire(rec.Addresses, nowNanos)
|
||||
switch {
|
||||
case len(addrs) > 0:
|
||||
current++
|
||||
seenIPv4, seenIPv6 := false, false
|
||||
for _, addr := range addrs {
|
||||
uri, err := url.Parse(addr.Address)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
host, _, err := net.SplitHostPort(uri.Host)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if ip := net.ParseIP(host); ip != nil && ip.To4() != nil {
|
||||
seenIPv4 = true
|
||||
} else if ip != nil {
|
||||
seenIPv6 = true
|
||||
}
|
||||
if seenIPv4 && seenIPv6 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if seenIPv4 {
|
||||
currentIPv4++
|
||||
}
|
||||
if seenIPv6 {
|
||||
currentIPv6++
|
||||
}
|
||||
case rec.Seen > cutoff24h:
|
||||
last24h++
|
||||
case rec.Seen > cutoff1w:
|
||||
last1w++
|
||||
case rec.Seen > cutoff2Mon:
|
||||
inactive++
|
||||
case rec.Missed < cutoff2Mon:
|
||||
// It hasn't been seen lately and we haven't recorded
|
||||
// someone asking for this device in a long time either;
|
||||
// delete the record.
|
||||
if err := s.db.Delete(iter.Key(), nil); err != nil {
|
||||
databaseOperations.WithLabelValues(dbOpDelete, dbResError).Inc()
|
||||
} else {
|
||||
databaseOperations.WithLabelValues(dbOpDelete, dbResSuccess).Inc()
|
||||
}
|
||||
default:
|
||||
inactive++
|
||||
br := bufio.NewReader(fd)
|
||||
var buf []byte
|
||||
nr := 0
|
||||
for {
|
||||
var n uint32
|
||||
if err := binary.Read(br, binary.BigEndian, &n); err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
return nr, err
|
||||
}
|
||||
if int(n) > len(buf) {
|
||||
buf = make([]byte, n)
|
||||
}
|
||||
if _, err := io.ReadFull(br, buf[:n]); err != nil {
|
||||
return nr, err
|
||||
}
|
||||
rec := ReplicationRecord{}
|
||||
if err := rec.Unmarshal(buf[:n]); err != nil {
|
||||
return nr, err
|
||||
}
|
||||
key, err := protocol.DeviceIDFromBytes(rec.Key)
|
||||
if err != nil {
|
||||
key, err = protocol.DeviceIDFromString(string(rec.Key))
|
||||
}
|
||||
if err != nil {
|
||||
log.Println("Bad device ID:", err)
|
||||
continue
|
||||
}
|
||||
|
||||
iter.Release()
|
||||
|
||||
databaseKeys.WithLabelValues("current").Set(float64(current))
|
||||
databaseKeys.WithLabelValues("currentIPv4").Set(float64(currentIPv4))
|
||||
databaseKeys.WithLabelValues("currentIPv6").Set(float64(currentIPv6))
|
||||
databaseKeys.WithLabelValues("last24h").Set(float64(last24h))
|
||||
databaseKeys.WithLabelValues("last1w").Set(float64(last1w))
|
||||
databaseKeys.WithLabelValues("inactive").Set(float64(inactive))
|
||||
databaseKeys.WithLabelValues("error").Set(float64(errors))
|
||||
databaseStatisticsSeconds.Set(time.Since(t0).Seconds())
|
||||
|
||||
// Signal that we are done and can be scheduled again.
|
||||
done <- struct{}{}
|
||||
slices.SortFunc(rec.Addresses, DatabaseAddress.Cmp)
|
||||
rec.Addresses = slices.CompactFunc(rec.Addresses, DatabaseAddress.Equal)
|
||||
s.m.Store(key, DatabaseRecord{
|
||||
Addresses: expire(rec.Addresses, s.clock.Now()),
|
||||
Seen: rec.Seen,
|
||||
})
|
||||
nr++
|
||||
}
|
||||
return nr, nil
|
||||
}
|
||||
|
||||
// merge returns the merged result of the two database records a and b. The
|
||||
// result is the union of the two address sets, with the newer expiry time
|
||||
// chosen for any duplicates.
|
||||
// chosen for any duplicates. The address list in a is overwritten and
|
||||
// reused for the result.
|
||||
func merge(a, b DatabaseRecord) DatabaseRecord {
|
||||
// Both lists must be sorted for this to work.
|
||||
if !sort.IsSorted(databaseAddressOrder(a.Addresses)) {
|
||||
log.Println("Warning: bug: addresses not correctly sorted in merge")
|
||||
a.Addresses = sortedAddressCopy(a.Addresses)
|
||||
}
|
||||
if !sort.IsSorted(databaseAddressOrder(b.Addresses)) {
|
||||
// no warning because this is the side we read from disk and it may
|
||||
// legitimately predate correct sorting.
|
||||
b.Addresses = sortedAddressCopy(b.Addresses)
|
||||
}
|
||||
|
||||
res := DatabaseRecord{
|
||||
Addresses: make([]DatabaseAddress, 0, len(a.Addresses)+len(b.Addresses)),
|
||||
Seen: a.Seen,
|
||||
}
|
||||
if b.Seen > a.Seen {
|
||||
res.Seen = b.Seen
|
||||
}
|
||||
a.Seen = max(a.Seen, b.Seen)
|
||||
|
||||
aIdx := 0
|
||||
bIdx := 0
|
||||
aAddrs := a.Addresses
|
||||
bAddrs := b.Addresses
|
||||
loop:
|
||||
for {
|
||||
switch {
|
||||
case aIdx == len(aAddrs) && bIdx == len(bAddrs):
|
||||
// both lists are exhausted, we are done
|
||||
break loop
|
||||
|
||||
case aIdx == len(aAddrs):
|
||||
// a is exhausted, pick from b and continue
|
||||
res.Addresses = append(res.Addresses, bAddrs[bIdx])
|
||||
bIdx++
|
||||
continue
|
||||
|
||||
case bIdx == len(bAddrs):
|
||||
// b is exhausted, pick from a and continue
|
||||
res.Addresses = append(res.Addresses, aAddrs[aIdx])
|
||||
aIdx++
|
||||
continue
|
||||
}
|
||||
|
||||
// We have values left on both sides.
|
||||
aVal := aAddrs[aIdx]
|
||||
bVal := bAddrs[bIdx]
|
||||
|
||||
switch {
|
||||
case aVal.Address == bVal.Address:
|
||||
// update for same address, pick newer
|
||||
if aVal.Expires > bVal.Expires {
|
||||
res.Addresses = append(res.Addresses, aVal)
|
||||
} else {
|
||||
res.Addresses = append(res.Addresses, bVal)
|
||||
}
|
||||
for aIdx < len(a.Addresses) && bIdx < len(b.Addresses) {
|
||||
switch cmp.Compare(a.Addresses[aIdx].Address, b.Addresses[bIdx].Address) {
|
||||
case 0:
|
||||
// a == b, choose the newer expiry time
|
||||
a.Addresses[aIdx].Expires = max(a.Addresses[aIdx].Expires, b.Addresses[bIdx].Expires)
|
||||
aIdx++
|
||||
bIdx++
|
||||
|
||||
case aVal.Address < bVal.Address:
|
||||
// a is smallest, pick it and continue
|
||||
res.Addresses = append(res.Addresses, aVal)
|
||||
case -1:
|
||||
// a < b, keep a and move on
|
||||
aIdx++
|
||||
|
||||
default:
|
||||
// b is smallest, pick it and continue
|
||||
res.Addresses = append(res.Addresses, bVal)
|
||||
case 1:
|
||||
// a > b, insert b before a
|
||||
a.Addresses = append(a.Addresses[:aIdx], append([]DatabaseAddress{b.Addresses[bIdx]}, a.Addresses[aIdx:]...)...)
|
||||
bIdx++
|
||||
}
|
||||
}
|
||||
return res
|
||||
if bIdx < len(b.Addresses) {
|
||||
a.Addresses = append(a.Addresses, b.Addresses[bIdx:]...)
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// expire returns the list of addresses after removing expired entries.
|
||||
// Expiration happen in place, so the slice given as the parameter is
|
||||
// destroyed. Internal order is not preserved.
|
||||
func expire(addrs []DatabaseAddress, now int64) []DatabaseAddress {
|
||||
i := 0
|
||||
for i < len(addrs) {
|
||||
if addrs[i].Expires < now {
|
||||
addrs = sliceutil.RemoveAndZero(addrs, i)
|
||||
// destroyed. Internal order is preserved.
|
||||
func expire(addrs []DatabaseAddress, now time.Time) []DatabaseAddress {
|
||||
cutoff := now.UnixNano()
|
||||
naddrs := addrs[:0]
|
||||
for i := range addrs {
|
||||
if i > 0 && addrs[i].Address == addrs[i-1].Address {
|
||||
// Skip duplicates
|
||||
continue
|
||||
}
|
||||
i++
|
||||
if addrs[i].Expires >= cutoff {
|
||||
naddrs = append(naddrs, addrs[i])
|
||||
}
|
||||
}
|
||||
return addrs
|
||||
if len(naddrs) == 0 {
|
||||
return nil
|
||||
}
|
||||
return naddrs
|
||||
}
|
||||
|
||||
func sortedAddressCopy(addrs []DatabaseAddress) []DatabaseAddress {
|
||||
sorted := make([]DatabaseAddress, len(addrs))
|
||||
copy(sorted, addrs)
|
||||
sort.Sort(databaseAddressOrder(sorted))
|
||||
return sorted
|
||||
func (d DatabaseAddress) Cmp(other DatabaseAddress) (n int) {
|
||||
if c := cmp.Compare(d.Address, other.Address); c != 0 {
|
||||
return c
|
||||
}
|
||||
return cmp.Compare(d.Expires, other.Expires)
|
||||
}
|
||||
|
||||
type databaseAddressOrder []DatabaseAddress
|
||||
|
||||
func (s databaseAddressOrder) Less(a, b int) bool {
|
||||
return s[a].Address < s[b].Address
|
||||
}
|
||||
|
||||
func (s databaseAddressOrder) Swap(a, b int) {
|
||||
s[a], s[b] = s[b], s[a]
|
||||
}
|
||||
|
||||
func (s databaseAddressOrder) Len() int {
|
||||
return len(s)
|
||||
func (d DatabaseAddress) Equal(other DatabaseAddress) bool {
|
||||
return d.Address == other.Address
|
||||
}
|
||||
|
||||
@@ -25,9 +25,7 @@ const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package
|
||||
|
||||
type DatabaseRecord struct {
|
||||
Addresses []DatabaseAddress `protobuf:"bytes,1,rep,name=addresses,proto3" json:"addresses"`
|
||||
Misses int32 `protobuf:"varint,2,opt,name=misses,proto3" json:"misses,omitempty"`
|
||||
Seen int64 `protobuf:"varint,3,opt,name=seen,proto3" json:"seen,omitempty"`
|
||||
Missed int64 `protobuf:"varint,4,opt,name=missed,proto3" json:"missed,omitempty"`
|
||||
}
|
||||
|
||||
func (m *DatabaseRecord) Reset() { *m = DatabaseRecord{} }
|
||||
@@ -64,7 +62,7 @@ func (m *DatabaseRecord) XXX_DiscardUnknown() {
|
||||
var xxx_messageInfo_DatabaseRecord proto.InternalMessageInfo
|
||||
|
||||
type ReplicationRecord struct {
|
||||
Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
|
||||
Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
|
||||
Addresses []DatabaseAddress `protobuf:"bytes,2,rep,name=addresses,proto3" json:"addresses"`
|
||||
Seen int64 `protobuf:"varint,3,opt,name=seen,proto3" json:"seen,omitempty"`
|
||||
}
|
||||
@@ -149,24 +147,23 @@ func init() {
|
||||
func init() { proto.RegisterFile("database.proto", fileDescriptor_b90fe3356ea5df07) }
|
||||
|
||||
var fileDescriptor_b90fe3356ea5df07 = []byte{
|
||||
// 270 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x90, 0x41, 0x4a, 0xc4, 0x30,
|
||||
0x18, 0x85, 0x9b, 0x49, 0x1d, 0x99, 0x08, 0xa3, 0x06, 0x94, 0x20, 0x12, 0x4b, 0xdd, 0x74, 0xd5,
|
||||
0x01, 0x5d, 0xb9, 0x74, 0xd0, 0x0b, 0xe4, 0x06, 0xe9, 0xe4, 0x77, 0x08, 0x3a, 0x4d, 0x49, 0x2a,
|
||||
0xe8, 0x29, 0xf4, 0x58, 0x5d, 0xce, 0xd2, 0x95, 0x68, 0x7b, 0x11, 0x69, 0x26, 0x55, 0x14, 0x37,
|
||||
0xb3, 0x7b, 0xdf, 0xff, 0xbf, 0x97, 0xbc, 0x84, 0x4c, 0x95, 0xac, 0x65, 0x21, 0x1d, 0xe4, 0x95,
|
||||
0x35, 0xb5, 0xa1, 0xf1, 0x4a, 0xea, 0xf2, 0xe4, 0xdc, 0x42, 0x65, 0xdc, 0xcc, 0x8f, 0x8a, 0xc7,
|
||||
0xbb, 0xd9, 0xd2, 0x2c, 0x8d, 0x07, 0xaf, 0x36, 0xd6, 0xf4, 0x05, 0x91, 0xe9, 0x4d, 0x48, 0x0b,
|
||||
0x58, 0x18, 0xab, 0xe8, 0x15, 0x99, 0x48, 0xa5, 0x2c, 0x38, 0x07, 0x8e, 0xa1, 0x04, 0x67, 0x7b,
|
||||
0x17, 0x47, 0x79, 0x7f, 0x62, 0x3e, 0x18, 0xaf, 0x37, 0xeb, 0x79, 0xdc, 0xbc, 0x9f, 0x45, 0xe2,
|
||||
0xc7, 0x4d, 0x8f, 0xc9, 0x78, 0xa5, 0x7d, 0x6e, 0x94, 0xa0, 0x6c, 0x47, 0x04, 0xa2, 0x94, 0xc4,
|
||||
0x0e, 0xa0, 0x64, 0x38, 0x41, 0x19, 0x16, 0x5e, 0x7f, 0x7b, 0x15, 0x8b, 0xfd, 0x34, 0x50, 0x5a,
|
||||
0x93, 0x43, 0x01, 0xd5, 0x83, 0x5e, 0xc8, 0x5a, 0x9b, 0x32, 0x74, 0x3a, 0x20, 0xf8, 0x1e, 0x9e,
|
||||
0x19, 0x4a, 0x50, 0x36, 0x11, 0xbd, 0xfc, 0xdd, 0x72, 0xb4, 0x55, 0xcb, 0x7f, 0xda, 0xa4, 0xb7,
|
||||
0x64, 0xff, 0x4f, 0x8e, 0x32, 0xb2, 0x1b, 0x32, 0xe1, 0xde, 0x01, 0xfb, 0x0d, 0x3c, 0x55, 0xda,
|
||||
0x86, 0x77, 0x62, 0x31, 0xe0, 0xfc, 0xb4, 0xf9, 0xe4, 0x51, 0xd3, 0x72, 0xb4, 0x6e, 0x39, 0xfa,
|
||||
0x68, 0x39, 0x7a, 0xed, 0x78, 0xb4, 0xee, 0x78, 0xf4, 0xd6, 0xf1, 0xa8, 0x18, 0xfb, 0x3f, 0xbf,
|
||||
0xfc, 0x0a, 0x00, 0x00, 0xff, 0xff, 0x7a, 0xa2, 0xf6, 0x1e, 0xb0, 0x01, 0x00, 0x00,
|
||||
// 243 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x4b, 0x49, 0x2c, 0x49,
|
||||
0x4c, 0x4a, 0x2c, 0x4e, 0xd5, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0xc9, 0x4d, 0xcc, 0xcc,
|
||||
0x93, 0x52, 0x2e, 0x4a, 0x2d, 0xc8, 0x2f, 0xd6, 0x07, 0x0b, 0x25, 0x95, 0xa6, 0xe9, 0xa7, 0xe7,
|
||||
0xa7, 0xe7, 0x83, 0x39, 0x60, 0x16, 0x44, 0xa9, 0x52, 0x3c, 0x17, 0x9f, 0x0b, 0x54, 0x73, 0x50,
|
||||
0x6a, 0x72, 0x7e, 0x51, 0x8a, 0x90, 0x25, 0x17, 0x67, 0x62, 0x4a, 0x4a, 0x51, 0x6a, 0x71, 0x71,
|
||||
0x6a, 0xb1, 0x04, 0xa3, 0x02, 0xb3, 0x06, 0xb7, 0x91, 0xa8, 0x1e, 0xc8, 0x40, 0x3d, 0x98, 0x42,
|
||||
0x47, 0x88, 0xb4, 0x13, 0xcb, 0x89, 0x7b, 0xf2, 0x0c, 0x41, 0x08, 0xd5, 0x42, 0x42, 0x5c, 0x2c,
|
||||
0xc5, 0xa9, 0xa9, 0x79, 0x12, 0xcc, 0x0a, 0x8c, 0x1a, 0xcc, 0x41, 0x60, 0xb6, 0x52, 0x09, 0x97,
|
||||
0x60, 0x50, 0x6a, 0x41, 0x4e, 0x66, 0x72, 0x62, 0x49, 0x66, 0x7e, 0x1e, 0xd4, 0x0e, 0x01, 0x2e,
|
||||
0xe6, 0xec, 0xd4, 0x4a, 0x09, 0x46, 0x05, 0x46, 0x0d, 0x9e, 0x20, 0x10, 0x13, 0xd5, 0x56, 0x26,
|
||||
0x8a, 0x6d, 0x75, 0xe5, 0xe2, 0x47, 0xd3, 0x27, 0x24, 0xc1, 0xc5, 0x0e, 0xd5, 0x03, 0xb6, 0x97,
|
||||
0x33, 0x08, 0xc6, 0x05, 0xc9, 0xa4, 0x56, 0x14, 0x64, 0x16, 0x81, 0x6d, 0x06, 0x99, 0x01, 0xe3,
|
||||
0x3a, 0xc9, 0x9c, 0x78, 0x28, 0xc7, 0x70, 0xe2, 0x91, 0x1c, 0xe3, 0x85, 0x47, 0x72, 0x8c, 0x0f,
|
||||
0x1e, 0xc9, 0x31, 0x4e, 0x78, 0x2c, 0xc7, 0x70, 0xe1, 0xb1, 0x1c, 0xc3, 0x8d, 0xc7, 0x72, 0x0c,
|
||||
0x49, 0x6c, 0xe0, 0x20, 0x34, 0x06, 0x04, 0x00, 0x00, 0xff, 0xff, 0xc6, 0x0b, 0x9b, 0x77, 0x7f,
|
||||
0x01, 0x00, 0x00,
|
||||
}
|
||||
|
||||
func (m *DatabaseRecord) Marshal() (dAtA []byte, err error) {
|
||||
@@ -189,21 +186,11 @@ func (m *DatabaseRecord) MarshalToSizedBuffer(dAtA []byte) (int, error) {
|
||||
_ = i
|
||||
var l int
|
||||
_ = l
|
||||
if m.Missed != 0 {
|
||||
i = encodeVarintDatabase(dAtA, i, uint64(m.Missed))
|
||||
i--
|
||||
dAtA[i] = 0x20
|
||||
}
|
||||
if m.Seen != 0 {
|
||||
i = encodeVarintDatabase(dAtA, i, uint64(m.Seen))
|
||||
i--
|
||||
dAtA[i] = 0x18
|
||||
}
|
||||
if m.Misses != 0 {
|
||||
i = encodeVarintDatabase(dAtA, i, uint64(m.Misses))
|
||||
i--
|
||||
dAtA[i] = 0x10
|
||||
}
|
||||
if len(m.Addresses) > 0 {
|
||||
for iNdEx := len(m.Addresses) - 1; iNdEx >= 0; iNdEx-- {
|
||||
{
|
||||
@@ -328,15 +315,9 @@ func (m *DatabaseRecord) Size() (n int) {
|
||||
n += 1 + l + sovDatabase(uint64(l))
|
||||
}
|
||||
}
|
||||
if m.Misses != 0 {
|
||||
n += 1 + sovDatabase(uint64(m.Misses))
|
||||
}
|
||||
if m.Seen != 0 {
|
||||
n += 1 + sovDatabase(uint64(m.Seen))
|
||||
}
|
||||
if m.Missed != 0 {
|
||||
n += 1 + sovDatabase(uint64(m.Missed))
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
@@ -447,25 +428,6 @@ func (m *DatabaseRecord) Unmarshal(dAtA []byte) error {
|
||||
return err
|
||||
}
|
||||
iNdEx = postIndex
|
||||
case 2:
|
||||
if wireType != 0 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field Misses", wireType)
|
||||
}
|
||||
m.Misses = 0
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowDatabase
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
m.Misses |= int32(b&0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
case 3:
|
||||
if wireType != 0 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field Seen", wireType)
|
||||
@@ -485,25 +447,6 @@ func (m *DatabaseRecord) Unmarshal(dAtA []byte) error {
|
||||
break
|
||||
}
|
||||
}
|
||||
case 4:
|
||||
if wireType != 0 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field Missed", wireType)
|
||||
}
|
||||
m.Missed = 0
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowDatabase
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
m.Missed |= int64(b&0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
default:
|
||||
iNdEx = preIndex
|
||||
skippy, err := skipDatabase(dAtA[iNdEx:])
|
||||
@@ -558,7 +501,7 @@ func (m *ReplicationRecord) Unmarshal(dAtA []byte) error {
|
||||
if wireType != 2 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field Key", wireType)
|
||||
}
|
||||
var stringLen uint64
|
||||
var byteLen int
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowDatabase
|
||||
@@ -568,23 +511,25 @@ func (m *ReplicationRecord) Unmarshal(dAtA []byte) error {
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
stringLen |= uint64(b&0x7F) << shift
|
||||
byteLen |= int(b&0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
intStringLen := int(stringLen)
|
||||
if intStringLen < 0 {
|
||||
if byteLen < 0 {
|
||||
return ErrInvalidLengthDatabase
|
||||
}
|
||||
postIndex := iNdEx + intStringLen
|
||||
postIndex := iNdEx + byteLen
|
||||
if postIndex < 0 {
|
||||
return ErrInvalidLengthDatabase
|
||||
}
|
||||
if postIndex > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
m.Key = string(dAtA[iNdEx:postIndex])
|
||||
m.Key = append(m.Key[:0], dAtA[iNdEx:postIndex]...)
|
||||
if m.Key == nil {
|
||||
m.Key = []byte{}
|
||||
}
|
||||
iNdEx = postIndex
|
||||
case 2:
|
||||
if wireType != 2 {
|
||||
|
||||
@@ -17,15 +17,11 @@ option (gogoproto.goproto_sizecache_all) = false;
|
||||
|
||||
message DatabaseRecord {
|
||||
repeated DatabaseAddress addresses = 1 [(gogoproto.nullable) = false];
|
||||
int32 misses = 2; // Number of lookups* without hits
|
||||
int64 seen = 3; // Unix nanos, last device announce
|
||||
int64 missed = 4; // Unix nanos, last* failed lookup
|
||||
}
|
||||
|
||||
// *) Not every lookup results in a write, so may not be completely accurate
|
||||
|
||||
message ReplicationRecord {
|
||||
string key = 1;
|
||||
bytes key = 1; // raw 32 byte device ID
|
||||
repeated DatabaseAddress addresses = 2 [(gogoproto.nullable) = false];
|
||||
int64 seen = 3; // Unix nanos, last device announce
|
||||
}
|
||||
|
||||
@@ -11,29 +11,25 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
)
|
||||
|
||||
func TestDatabaseGetSet(t *testing.T) {
|
||||
db, err := newMemoryLevelDBStore()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
db := newInMemoryStore(t.TempDir(), 0, nil)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go db.Serve(ctx)
|
||||
defer cancel()
|
||||
|
||||
// Check missing record
|
||||
|
||||
rec, err := db.get("abcd")
|
||||
rec, err := db.get(&protocol.EmptyDeviceID)
|
||||
if err != nil {
|
||||
t.Error("not found should not be an error")
|
||||
}
|
||||
if len(rec.Addresses) != 0 {
|
||||
t.Error("addresses should be empty")
|
||||
}
|
||||
if rec.Misses != 0 {
|
||||
t.Error("missing should be zero")
|
||||
}
|
||||
|
||||
// Set up a clock
|
||||
|
||||
@@ -46,13 +42,13 @@ func TestDatabaseGetSet(t *testing.T) {
|
||||
rec.Addresses = []DatabaseAddress{
|
||||
{Address: "tcp://1.2.3.4:5", Expires: tc.Now().Add(time.Minute).UnixNano()},
|
||||
}
|
||||
if err := db.put("abcd", rec); err != nil {
|
||||
if err := db.put(&protocol.EmptyDeviceID, rec); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Verify it
|
||||
|
||||
rec, err = db.get("abcd")
|
||||
rec, err = db.get(&protocol.EmptyDeviceID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -72,13 +68,13 @@ func TestDatabaseGetSet(t *testing.T) {
|
||||
addrs := []DatabaseAddress{
|
||||
{Address: "tcp://6.7.8.9:0", Expires: tc.Now().Add(time.Minute).UnixNano()},
|
||||
}
|
||||
if err := db.merge("abcd", addrs, tc.Now().UnixNano()); err != nil {
|
||||
if err := db.merge(&protocol.EmptyDeviceID, addrs, tc.Now().UnixNano()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Verify it
|
||||
|
||||
rec, err = db.get("abcd")
|
||||
rec, err = db.get(&protocol.EmptyDeviceID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -101,7 +97,7 @@ func TestDatabaseGetSet(t *testing.T) {
|
||||
|
||||
// Verify it
|
||||
|
||||
rec, err = db.get("abcd")
|
||||
rec, err = db.get(&protocol.EmptyDeviceID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -114,40 +110,18 @@ func TestDatabaseGetSet(t *testing.T) {
|
||||
t.Error("incorrect address")
|
||||
}
|
||||
|
||||
// Put a record with misses
|
||||
|
||||
rec = DatabaseRecord{Misses: 42, Missed: tc.Now().UnixNano()}
|
||||
if err := db.put("efgh", rec); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Verify it
|
||||
|
||||
rec, err = db.get("efgh")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(rec.Addresses) != 0 {
|
||||
t.Log(rec.Addresses)
|
||||
t.Fatal("should have no addresses")
|
||||
}
|
||||
if rec.Misses != 42 {
|
||||
t.Log(rec.Misses)
|
||||
t.Error("incorrect misses")
|
||||
}
|
||||
|
||||
// Set an address
|
||||
|
||||
addrs = []DatabaseAddress{
|
||||
{Address: "tcp://6.7.8.9:0", Expires: tc.Now().Add(time.Minute).UnixNano()},
|
||||
}
|
||||
if err := db.merge("efgh", addrs, tc.Now().UnixNano()); err != nil {
|
||||
if err := db.merge(&protocol.GlobalDeviceID, addrs, tc.Now().UnixNano()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Verify it
|
||||
|
||||
rec, err = db.get("efgh")
|
||||
rec, err = db.get(&protocol.GlobalDeviceID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -155,10 +129,6 @@ func TestDatabaseGetSet(t *testing.T) {
|
||||
t.Log(rec.Addresses)
|
||||
t.Fatal("should have one address")
|
||||
}
|
||||
if rec.Misses != 0 {
|
||||
t.Log(rec.Misses)
|
||||
t.Error("should have no misses")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilter(t *testing.T) {
|
||||
@@ -190,13 +160,95 @@ func TestFilter(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
res := expire(tc.a, 10)
|
||||
res := expire(tc.a, time.Unix(0, 10))
|
||||
if fmt.Sprint(res) != fmt.Sprint(tc.b) {
|
||||
t.Errorf("Incorrect result %v, expected %v", res, tc.b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMerge(t *testing.T) {
|
||||
cases := []struct {
|
||||
a, b, res []DatabaseAddress
|
||||
}{
|
||||
{nil, nil, nil},
|
||||
{
|
||||
nil,
|
||||
[]DatabaseAddress{{Address: "a", Expires: 10}},
|
||||
[]DatabaseAddress{{Address: "a", Expires: 10}},
|
||||
},
|
||||
{
|
||||
nil,
|
||||
[]DatabaseAddress{{Address: "a", Expires: 10}, {Address: "b", Expires: 10}, {Address: "c", Expires: 10}},
|
||||
[]DatabaseAddress{{Address: "a", Expires: 10}, {Address: "b", Expires: 10}, {Address: "c", Expires: 10}},
|
||||
},
|
||||
{
|
||||
[]DatabaseAddress{{Address: "a", Expires: 10}},
|
||||
[]DatabaseAddress{{Address: "a", Expires: 15}},
|
||||
[]DatabaseAddress{{Address: "a", Expires: 15}},
|
||||
},
|
||||
{
|
||||
[]DatabaseAddress{{Address: "a", Expires: 10}},
|
||||
[]DatabaseAddress{{Address: "b", Expires: 15}},
|
||||
[]DatabaseAddress{{Address: "a", Expires: 10}, {Address: "b", Expires: 15}},
|
||||
},
|
||||
{
|
||||
[]DatabaseAddress{{Address: "a", Expires: 10}, {Address: "b", Expires: 15}},
|
||||
[]DatabaseAddress{{Address: "a", Expires: 15}, {Address: "b", Expires: 15}},
|
||||
[]DatabaseAddress{{Address: "a", Expires: 15}, {Address: "b", Expires: 15}},
|
||||
},
|
||||
{
|
||||
[]DatabaseAddress{{Address: "a", Expires: 10}, {Address: "b", Expires: 15}},
|
||||
[]DatabaseAddress{{Address: "b", Expires: 15}, {Address: "c", Expires: 20}},
|
||||
[]DatabaseAddress{{Address: "a", Expires: 10}, {Address: "b", Expires: 15}, {Address: "c", Expires: 20}},
|
||||
},
|
||||
{
|
||||
[]DatabaseAddress{{Address: "a", Expires: 10}, {Address: "b", Expires: 15}},
|
||||
[]DatabaseAddress{{Address: "b", Expires: 5}, {Address: "c", Expires: 20}},
|
||||
[]DatabaseAddress{{Address: "a", Expires: 10}, {Address: "b", Expires: 15}, {Address: "c", Expires: 20}},
|
||||
},
|
||||
{
|
||||
[]DatabaseAddress{{Address: "y", Expires: 10}, {Address: "z", Expires: 10}},
|
||||
[]DatabaseAddress{{Address: "a", Expires: 5}, {Address: "b", Expires: 15}},
|
||||
[]DatabaseAddress{{Address: "a", Expires: 5}, {Address: "b", Expires: 15}, {Address: "y", Expires: 10}, {Address: "z", Expires: 10}},
|
||||
},
|
||||
{
|
||||
[]DatabaseAddress{{Address: "a", Expires: 10}, {Address: "b", Expires: 15}, {Address: "d", Expires: 10}},
|
||||
[]DatabaseAddress{{Address: "b", Expires: 5}, {Address: "c", Expires: 20}},
|
||||
[]DatabaseAddress{{Address: "a", Expires: 10}, {Address: "b", Expires: 15}, {Address: "c", Expires: 20}, {Address: "d", Expires: 10}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
rec := merge(DatabaseRecord{Addresses: tc.a}, DatabaseRecord{Addresses: tc.b})
|
||||
if fmt.Sprint(rec.Addresses) != fmt.Sprint(tc.res) {
|
||||
t.Errorf("Incorrect result %v, expected %v", rec.Addresses, tc.res)
|
||||
}
|
||||
rec = merge(DatabaseRecord{Addresses: tc.b}, DatabaseRecord{Addresses: tc.a})
|
||||
if fmt.Sprint(rec.Addresses) != fmt.Sprint(tc.res) {
|
||||
t.Errorf("Incorrect result %v, expected %v", rec.Addresses, tc.res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMergeEqual(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
ar := []DatabaseAddress{{Address: "a", Expires: 10}, {Address: "b", Expires: 15}}
|
||||
br := []DatabaseAddress{{Address: "a", Expires: 15}, {Address: "b", Expires: 10}}
|
||||
res := merge(DatabaseRecord{Addresses: ar}, DatabaseRecord{Addresses: br})
|
||||
if len(res.Addresses) != 2 {
|
||||
b.Fatal("wrong length")
|
||||
}
|
||||
if res.Addresses[0].Address != "a" || res.Addresses[1].Address != "b" {
|
||||
b.Fatal("wrong address")
|
||||
}
|
||||
if res.Addresses[0].Expires != 15 || res.Addresses[1].Expires != 15 {
|
||||
b.Fatal("wrong expiry")
|
||||
}
|
||||
}
|
||||
b.ReportAllocs() // should be zero per operation
|
||||
}
|
||||
|
||||
type testClock struct {
|
||||
now time.Time
|
||||
}
|
||||
|
||||
+83
-155
@@ -9,22 +9,22 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"flag"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "net/http/pprof"
|
||||
|
||||
"github.com/alecthomas/kong"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
_ "github.com/syncthing/syncthing/lib/automaxprocs"
|
||||
"github.com/syncthing/syncthing/lib/build"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/rand"
|
||||
"github.com/syncthing/syncthing/lib/tlsutil"
|
||||
"github.com/syndtr/goleveldb/leveldb/opt"
|
||||
"github.com/thejerf/suture/v4"
|
||||
)
|
||||
|
||||
@@ -39,17 +39,12 @@ const (
|
||||
errorRetryAfterSeconds = 1500
|
||||
errorRetryFuzzSeconds = 300
|
||||
|
||||
// Retry for not found is minSeconds + failures * incSeconds +
|
||||
// random(fuzz), where failures is the number of consecutive lookups
|
||||
// with no answer, up to maxSeconds. The fuzz is applied after capping
|
||||
// to maxSeconds.
|
||||
notFoundRetryMinSeconds = 60
|
||||
notFoundRetryMaxSeconds = 3540
|
||||
notFoundRetryIncSeconds = 10
|
||||
notFoundRetryFuzzSeconds = 60
|
||||
|
||||
// How often (in requests) we serialize the missed counter to database.
|
||||
notFoundMissesWriteInterval = 10
|
||||
// Retry for not found is notFoundRetrySeenSeconds for records we have
|
||||
// seen an announcement for (but it's not active right now) and
|
||||
// notFoundRetryUnknownSeconds for records we have never seen (or not
|
||||
// seen within the last week).
|
||||
notFoundRetryUnknownMinSeconds = 60
|
||||
notFoundRetryUnknownMaxSeconds = 3600
|
||||
|
||||
httpReadTimeout = 5 * time.Second
|
||||
httpWriteTimeout = 5 * time.Second
|
||||
@@ -59,184 +54,117 @@ const (
|
||||
replicationOutboxSize = 10000
|
||||
)
|
||||
|
||||
// These options make the database a little more optimized for writes, at
|
||||
// the expense of some memory usage and risk of losing writes in a (system)
|
||||
// crash.
|
||||
var levelDBOptions = &opt.Options{
|
||||
NoSync: true,
|
||||
WriteBuffer: 32 << 20, // default 4<<20
|
||||
}
|
||||
|
||||
var debug = false
|
||||
|
||||
type CLI struct {
|
||||
Cert string `group:"Listen" help:"Certificate file" default:"./cert.pem" env:"DISCOVERY_CERT_FILE"`
|
||||
Key string `group:"Listen" help:"Key file" default:"./key.pem" env:"DISCOVERY_KEY_FILE"`
|
||||
HTTP bool `group:"Listen" help:"Listen on HTTP (behind an HTTPS proxy)" env:"DISCOVERY_HTTP"`
|
||||
Compression bool `group:"Listen" help:"Enable GZIP compression of responses" env:"DISCOVERY_COMPRESSION"`
|
||||
Listen string `group:"Listen" help:"Listen address" default:":8443" env:"DISCOVERY_LISTEN"`
|
||||
MetricsListen string `group:"Listen" help:"Metrics listen address" env:"DISCOVERY_METRICS_LISTEN"`
|
||||
|
||||
DBDir string `group:"Database" help:"Database directory" default:"." env:"DISCOVERY_DB_DIR"`
|
||||
DBFlushInterval time.Duration `group:"Database" help:"Interval between database flushes" default:"5m" env:"DISCOVERY_DB_FLUSH_INTERVAL"`
|
||||
|
||||
DBS3Endpoint string `name:"db-s3-endpoint" group:"Database (S3 backup)" hidden:"true" help:"S3 endpoint for database" env:"DISCOVERY_DB_S3_ENDPOINT"`
|
||||
DBS3Region string `name:"db-s3-region" group:"Database (S3 backup)" hidden:"true" help:"S3 region for database" env:"DISCOVERY_DB_S3_REGION"`
|
||||
DBS3Bucket string `name:"db-s3-bucket" group:"Database (S3 backup)" hidden:"true" help:"S3 bucket for database" env:"DISCOVERY_DB_S3_BUCKET"`
|
||||
DBS3AccessKeyID string `name:"db-s3-access-key-id" group:"Database (S3 backup)" hidden:"true" help:"S3 access key ID for database" env:"DISCOVERY_DB_S3_ACCESS_KEY_ID"`
|
||||
DBS3SecretKey string `name:"db-s3-secret-key" group:"Database (S3 backup)" hidden:"true" help:"S3 secret key for database" env:"DISCOVERY_DB_S3_SECRET_KEY"`
|
||||
|
||||
AMQPAddress string `group:"AMQP replication" hidden:"true" help:"Address to AMQP broker" env:"DISCOVERY_AMQP_ADDRESS"`
|
||||
|
||||
Debug bool `short:"d" help:"Print debug output" env:"DISCOVERY_DEBUG"`
|
||||
Version bool `short:"v" help:"Print version and exit"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
var listen string
|
||||
var dir string
|
||||
var metricsListen string
|
||||
var replicationListen string
|
||||
var replicationPeers string
|
||||
var certFile string
|
||||
var keyFile string
|
||||
var replCertFile string
|
||||
var replKeyFile string
|
||||
var useHTTP bool
|
||||
var largeDB bool
|
||||
var amqpAddress string
|
||||
missesIncrease := 1
|
||||
|
||||
log.SetOutput(os.Stdout)
|
||||
log.SetFlags(0)
|
||||
|
||||
flag.StringVar(&certFile, "cert", "./cert.pem", "Certificate file")
|
||||
flag.StringVar(&keyFile, "key", "./key.pem", "Key file")
|
||||
flag.StringVar(&dir, "db-dir", "./discovery.db", "Database directory")
|
||||
flag.BoolVar(&debug, "debug", false, "Print debug output")
|
||||
flag.BoolVar(&useHTTP, "http", false, "Listen on HTTP (behind an HTTPS proxy)")
|
||||
flag.StringVar(&listen, "listen", ":8443", "Listen address")
|
||||
flag.StringVar(&metricsListen, "metrics-listen", "", "Metrics listen address")
|
||||
flag.StringVar(&replicationPeers, "replicate", "", "Replication peers, id@address, comma separated")
|
||||
flag.StringVar(&replicationListen, "replication-listen", ":19200", "Replication listen address")
|
||||
flag.StringVar(&replCertFile, "replication-cert", "", "Certificate file for replication")
|
||||
flag.StringVar(&replKeyFile, "replication-key", "", "Key file for replication")
|
||||
flag.BoolVar(&largeDB, "large-db", false, "Use larger database settings")
|
||||
flag.StringVar(&amqpAddress, "amqp-address", "", "Address to AMQP broker")
|
||||
flag.IntVar(&missesIncrease, "misses-increase", 1, "How many times to increase the misses counter on each miss")
|
||||
showVersion := flag.Bool("version", false, "Show version")
|
||||
flag.Parse()
|
||||
var cli CLI
|
||||
kong.Parse(&cli)
|
||||
debug = cli.Debug
|
||||
|
||||
log.Println(build.LongVersionFor("stdiscosrv"))
|
||||
if *showVersion {
|
||||
if cli.Version {
|
||||
return
|
||||
}
|
||||
|
||||
buildInfo.WithLabelValues(build.Version, runtime.Version(), build.User, build.Date.UTC().Format("2006-01-02T15:04:05Z")).Set(1)
|
||||
|
||||
if largeDB {
|
||||
levelDBOptions.BlockCacheCapacity = 64 << 20
|
||||
levelDBOptions.BlockSize = 64 << 10
|
||||
levelDBOptions.CompactionTableSize = 16 << 20
|
||||
levelDBOptions.CompactionTableSizeMultiplier = 2.0
|
||||
levelDBOptions.WriteBuffer = 64 << 20
|
||||
levelDBOptions.CompactionL0Trigger = 8
|
||||
}
|
||||
|
||||
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if os.IsNotExist(err) {
|
||||
log.Println("Failed to load keypair. Generating one, this might take a while...")
|
||||
cert, err = tlsutil.NewCertificate(certFile, keyFile, "stdiscosrv", 20*365)
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to generate X509 key pair:", err)
|
||||
}
|
||||
} else if err != nil {
|
||||
log.Fatalln("Failed to load keypair:", err)
|
||||
}
|
||||
devID := protocol.NewDeviceID(cert.Certificate[0])
|
||||
log.Println("Server device ID is", devID)
|
||||
|
||||
replCert := cert
|
||||
if replCertFile != "" && replKeyFile != "" {
|
||||
replCert, err = tls.LoadX509KeyPair(replCertFile, replKeyFile)
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to load replication keypair:", err)
|
||||
}
|
||||
}
|
||||
replDevID := protocol.NewDeviceID(replCert.Certificate[0])
|
||||
log.Println("Replication device ID is", replDevID)
|
||||
|
||||
// Parse the replication specs, if any.
|
||||
var allowedReplicationPeers []protocol.DeviceID
|
||||
var replicationDestinations []string
|
||||
parts := strings.Split(replicationPeers, ",")
|
||||
for _, part := range parts {
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
fields := strings.Split(part, "@")
|
||||
switch len(fields) {
|
||||
case 2:
|
||||
// This is an id@address specification. Grab the address for the
|
||||
// destination list. Try to resolve it once to catch obvious
|
||||
// syntax errors here rather than having the sender service fail
|
||||
// repeatedly later.
|
||||
_, err := net.ResolveTCPAddr("tcp", fields[1])
|
||||
var cert tls.Certificate
|
||||
if !cli.HTTP {
|
||||
var err error
|
||||
cert, err = tls.LoadX509KeyPair(cli.Cert, cli.Key)
|
||||
if os.IsNotExist(err) {
|
||||
log.Println("Failed to load keypair. Generating one, this might take a while...")
|
||||
cert, err = tlsutil.NewCertificate(cli.Cert, cli.Key, "stdiscosrv", 20*365)
|
||||
if err != nil {
|
||||
log.Fatalln("Resolving address:", err)
|
||||
log.Fatalln("Failed to generate X509 key pair:", err)
|
||||
}
|
||||
replicationDestinations = append(replicationDestinations, fields[1])
|
||||
fallthrough // N.B.
|
||||
|
||||
case 1:
|
||||
// The first part is always a device ID.
|
||||
id, err := protocol.DeviceIDFromString(fields[0])
|
||||
if err != nil {
|
||||
log.Fatalln("Parsing device ID:", err)
|
||||
}
|
||||
if id == protocol.EmptyDeviceID {
|
||||
log.Fatalf("Missing device ID for peer in %q", part)
|
||||
}
|
||||
allowedReplicationPeers = append(allowedReplicationPeers, id)
|
||||
|
||||
default:
|
||||
log.Fatalln("Unrecognized replication spec:", part)
|
||||
} else if err != nil {
|
||||
log.Fatalln("Failed to load keypair:", err)
|
||||
}
|
||||
devID := protocol.NewDeviceID(cert.Certificate[0])
|
||||
log.Println("Server device ID is", devID)
|
||||
}
|
||||
|
||||
// Root of the service tree.
|
||||
main := suture.New("main", suture.Spec{
|
||||
PassThroughPanics: true,
|
||||
Timeout: 2 * time.Minute,
|
||||
})
|
||||
|
||||
// Start the database.
|
||||
db, err := newLevelDBStore(dir)
|
||||
if err != nil {
|
||||
log.Fatalln("Open database:", err)
|
||||
// If configured, use S3 for database backups.
|
||||
var s3c *s3Copier
|
||||
if cli.DBS3Endpoint != "" {
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to get hostname: %v", err)
|
||||
}
|
||||
key := hostname + ".db"
|
||||
s3c = newS3Copier(cli.DBS3Endpoint, cli.DBS3Region, cli.DBS3Bucket, key, cli.DBS3AccessKeyID, cli.DBS3SecretKey)
|
||||
}
|
||||
|
||||
// Start the database.
|
||||
db := newInMemoryStore(cli.DBDir, cli.DBFlushInterval, s3c)
|
||||
main.Add(db)
|
||||
|
||||
// Start any replication senders.
|
||||
var repl replicationMultiplexer
|
||||
for _, dst := range replicationDestinations {
|
||||
rs := newReplicationSender(dst, replCert, allowedReplicationPeers)
|
||||
main.Add(rs)
|
||||
repl = append(repl, rs)
|
||||
}
|
||||
|
||||
// If we have replication configured, start the replication listener.
|
||||
if len(allowedReplicationPeers) > 0 {
|
||||
rl := newReplicationListener(replicationListen, replCert, allowedReplicationPeers, db)
|
||||
main.Add(rl)
|
||||
}
|
||||
|
||||
// If we have an AMQP broker, start that
|
||||
if amqpAddress != "" {
|
||||
// If we have an AMQP broker for replication, start that
|
||||
var repl replicator
|
||||
if cli.AMQPAddress != "" {
|
||||
clientID := rand.String(10)
|
||||
kr := newAMQPReplicator(amqpAddress, clientID, db)
|
||||
repl = append(repl, kr)
|
||||
kr := newAMQPReplicator(cli.AMQPAddress, clientID, db)
|
||||
main.Add(kr)
|
||||
repl = kr
|
||||
}
|
||||
|
||||
go func() {
|
||||
for range time.NewTicker(time.Second).C {
|
||||
for _, r := range repl {
|
||||
r.send("<heartbeat>", nil, time.Now().UnixNano())
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Start the main API server.
|
||||
qs := newAPISrv(listen, cert, db, repl, useHTTP, missesIncrease)
|
||||
qs := newAPISrv(cli.Listen, cert, db, repl, cli.HTTP, cli.Compression)
|
||||
main.Add(qs)
|
||||
|
||||
// If we have a metrics port configured, start a metrics handler.
|
||||
if metricsListen != "" {
|
||||
if cli.MetricsListen != "" {
|
||||
go func() {
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/metrics", promhttp.Handler())
|
||||
log.Fatal(http.ListenAndServe(metricsListen, mux))
|
||||
log.Fatal(http.ListenAndServe(cli.MetricsListen, mux))
|
||||
}()
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// Cancel on signal
|
||||
signalChan := make(chan os.Signal, 1)
|
||||
signal.Notify(signalChan, os.Interrupt)
|
||||
go func() {
|
||||
sig := <-signalChan
|
||||
log.Printf("Received signal %s; shutting down", sig)
|
||||
cancel()
|
||||
}()
|
||||
|
||||
// Engage!
|
||||
main.Serve(context.Background())
|
||||
main.Serve(ctx)
|
||||
}
|
||||
|
||||
@@ -1,325 +0,0 @@
|
||||
// Copyright (C) 2018 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
io "io"
|
||||
"log"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
)
|
||||
|
||||
const (
|
||||
replicationReadTimeout = time.Minute
|
||||
replicationWriteTimeout = 30 * time.Second
|
||||
replicationHeartbeatInterval = time.Second * 30
|
||||
)
|
||||
|
||||
type replicator interface {
|
||||
send(key string, addrs []DatabaseAddress, seen int64)
|
||||
}
|
||||
|
||||
// a replicationSender tries to connect to the remote address and provide
|
||||
// them with a feed of replication updates.
|
||||
type replicationSender struct {
|
||||
dst string
|
||||
cert tls.Certificate // our certificate
|
||||
allowedIDs []protocol.DeviceID
|
||||
outbox chan ReplicationRecord
|
||||
}
|
||||
|
||||
func newReplicationSender(dst string, cert tls.Certificate, allowedIDs []protocol.DeviceID) *replicationSender {
|
||||
return &replicationSender{
|
||||
dst: dst,
|
||||
cert: cert,
|
||||
allowedIDs: allowedIDs,
|
||||
outbox: make(chan ReplicationRecord, replicationOutboxSize),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *replicationSender) Serve(ctx context.Context) error {
|
||||
// Sleep a little at startup. Peers often restart at the same time, and
|
||||
// this avoid the service failing and entering backoff state
|
||||
// unnecessarily, while also reducing the reconnect rate to something
|
||||
// reasonable by default.
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
tlsCfg := &tls.Config{
|
||||
Certificates: []tls.Certificate{s.cert},
|
||||
MinVersion: tls.VersionTLS12,
|
||||
InsecureSkipVerify: true,
|
||||
}
|
||||
|
||||
// Dial the TLS connection.
|
||||
conn, err := tls.Dial("tcp", s.dst, tlsCfg)
|
||||
if err != nil {
|
||||
log.Println("Replication connect:", err)
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
conn.SetWriteDeadline(time.Now().Add(time.Second))
|
||||
conn.Close()
|
||||
}()
|
||||
|
||||
// The replication stream is not especially latency sensitive, but it is
|
||||
// quite a lot of data in small writes. Make it more efficient.
|
||||
if tcpc, ok := conn.NetConn().(*net.TCPConn); ok {
|
||||
_ = tcpc.SetNoDelay(false)
|
||||
}
|
||||
|
||||
// Get the other side device ID.
|
||||
remoteID, err := deviceID(conn)
|
||||
if err != nil {
|
||||
log.Println("Replication connect:", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Verify it's in the set of allowed device IDs.
|
||||
if !deviceIDIn(remoteID, s.allowedIDs) {
|
||||
log.Println("Replication connect: unexpected device ID:", remoteID)
|
||||
return err
|
||||
}
|
||||
|
||||
heartBeatTicker := time.NewTicker(replicationHeartbeatInterval)
|
||||
defer heartBeatTicker.Stop()
|
||||
|
||||
// Send records.
|
||||
buf := make([]byte, 1024)
|
||||
for {
|
||||
select {
|
||||
case <-heartBeatTicker.C:
|
||||
if len(s.outbox) > 0 {
|
||||
// No need to send heartbeats if there are events/prevrious
|
||||
// heartbeats to send, they will keep the connection alive.
|
||||
continue
|
||||
}
|
||||
// Empty replication message is the heartbeat:
|
||||
s.outbox <- ReplicationRecord{}
|
||||
|
||||
case rec := <-s.outbox:
|
||||
// Buffer must hold record plus four bytes for size
|
||||
size := rec.Size()
|
||||
if len(buf) < size+4 {
|
||||
buf = make([]byte, size+4)
|
||||
}
|
||||
|
||||
// Record comes after the four bytes size
|
||||
n, err := rec.MarshalTo(buf[4:])
|
||||
if err != nil {
|
||||
// odd to get an error here, but we haven't sent anything
|
||||
// yet so it's not fatal
|
||||
replicationSendsTotal.WithLabelValues("error").Inc()
|
||||
log.Println("Replication marshal:", err)
|
||||
continue
|
||||
}
|
||||
binary.BigEndian.PutUint32(buf, uint32(n))
|
||||
|
||||
// Send
|
||||
conn.SetWriteDeadline(time.Now().Add(replicationWriteTimeout))
|
||||
if _, err := conn.Write(buf[:4+n]); err != nil {
|
||||
replicationSendsTotal.WithLabelValues("error").Inc()
|
||||
log.Println("Replication write:", err)
|
||||
// Yes, we are losing the replication event here.
|
||||
return err
|
||||
}
|
||||
replicationSendsTotal.WithLabelValues("success").Inc()
|
||||
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *replicationSender) String() string {
|
||||
return fmt.Sprintf("replicationSender(%q)", s.dst)
|
||||
}
|
||||
|
||||
func (s *replicationSender) send(key string, ps []DatabaseAddress, seen int64) {
|
||||
item := ReplicationRecord{
|
||||
Key: key,
|
||||
Addresses: ps,
|
||||
Seen: seen,
|
||||
}
|
||||
|
||||
// The send should never block. The inbox is suitably buffered for at
|
||||
// least a few seconds of stalls, which shouldn't happen in practice.
|
||||
select {
|
||||
case s.outbox <- item:
|
||||
default:
|
||||
replicationSendsTotal.WithLabelValues("drop").Inc()
|
||||
}
|
||||
}
|
||||
|
||||
// a replicationMultiplexer sends to multiple replicators
|
||||
type replicationMultiplexer []replicator
|
||||
|
||||
func (m replicationMultiplexer) send(key string, ps []DatabaseAddress, seen int64) {
|
||||
for _, s := range m {
|
||||
// each send is nonblocking
|
||||
s.send(key, ps, seen)
|
||||
}
|
||||
}
|
||||
|
||||
// replicationListener accepts incoming connections and reads replication
|
||||
// items from them. Incoming items are applied to the KV store.
|
||||
type replicationListener struct {
|
||||
addr string
|
||||
cert tls.Certificate
|
||||
allowedIDs []protocol.DeviceID
|
||||
db database
|
||||
}
|
||||
|
||||
func newReplicationListener(addr string, cert tls.Certificate, allowedIDs []protocol.DeviceID, db database) *replicationListener {
|
||||
return &replicationListener{
|
||||
addr: addr,
|
||||
cert: cert,
|
||||
allowedIDs: allowedIDs,
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *replicationListener) Serve(ctx context.Context) error {
|
||||
tlsCfg := &tls.Config{
|
||||
Certificates: []tls.Certificate{l.cert},
|
||||
ClientAuth: tls.RequestClientCert,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
InsecureSkipVerify: true,
|
||||
}
|
||||
|
||||
lst, err := tls.Listen("tcp", l.addr, tlsCfg)
|
||||
if err != nil {
|
||||
log.Println("Replication listen:", err)
|
||||
return err
|
||||
}
|
||||
defer lst.Close()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
|
||||
// Accept a connection
|
||||
conn, err := lst.Accept()
|
||||
if err != nil {
|
||||
log.Println("Replication accept:", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Figure out the other side device ID
|
||||
remoteID, err := deviceID(conn.(*tls.Conn))
|
||||
if err != nil {
|
||||
log.Println("Replication accept:", err)
|
||||
conn.SetWriteDeadline(time.Now().Add(time.Second))
|
||||
conn.Close()
|
||||
continue
|
||||
}
|
||||
|
||||
// Verify it is in the set of allowed device IDs
|
||||
if !deviceIDIn(remoteID, l.allowedIDs) {
|
||||
log.Println("Replication accept: unexpected device ID:", remoteID)
|
||||
conn.SetWriteDeadline(time.Now().Add(time.Second))
|
||||
conn.Close()
|
||||
continue
|
||||
}
|
||||
|
||||
go l.handle(ctx, conn)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *replicationListener) String() string {
|
||||
return fmt.Sprintf("replicationListener(%q)", l.addr)
|
||||
}
|
||||
|
||||
func (l *replicationListener) handle(ctx context.Context, conn net.Conn) {
|
||||
defer func() {
|
||||
conn.SetWriteDeadline(time.Now().Add(time.Second))
|
||||
conn.Close()
|
||||
}()
|
||||
|
||||
buf := make([]byte, 1024)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
conn.SetReadDeadline(time.Now().Add(replicationReadTimeout))
|
||||
|
||||
// First four bytes are the size
|
||||
if _, err := io.ReadFull(conn, buf[:4]); err != nil {
|
||||
log.Println("Replication read size:", err)
|
||||
replicationRecvsTotal.WithLabelValues("error").Inc()
|
||||
return
|
||||
}
|
||||
|
||||
// Read the rest of the record
|
||||
size := int(binary.BigEndian.Uint32(buf[:4]))
|
||||
if len(buf) < size {
|
||||
buf = make([]byte, size)
|
||||
}
|
||||
|
||||
if size == 0 {
|
||||
// Heartbeat, ignore
|
||||
continue
|
||||
}
|
||||
|
||||
if _, err := io.ReadFull(conn, buf[:size]); err != nil {
|
||||
log.Println("Replication read record:", err)
|
||||
replicationRecvsTotal.WithLabelValues("error").Inc()
|
||||
return
|
||||
}
|
||||
|
||||
// Unmarshal
|
||||
var rec ReplicationRecord
|
||||
if err := rec.Unmarshal(buf[:size]); err != nil {
|
||||
log.Println("Replication unmarshal:", err)
|
||||
replicationRecvsTotal.WithLabelValues("error").Inc()
|
||||
continue
|
||||
}
|
||||
|
||||
// Store
|
||||
l.db.merge(rec.Key, rec.Addresses, rec.Seen)
|
||||
replicationRecvsTotal.WithLabelValues("success").Inc()
|
||||
}
|
||||
}
|
||||
|
||||
func deviceID(conn *tls.Conn) (protocol.DeviceID, error) {
|
||||
// Handshake may not be complete on the server side yet, which we need
|
||||
// to get the client certificate.
|
||||
if !conn.ConnectionState().HandshakeComplete {
|
||||
if err := conn.Handshake(); err != nil {
|
||||
return protocol.DeviceID{}, err
|
||||
}
|
||||
}
|
||||
|
||||
// We expect exactly one certificate.
|
||||
certs := conn.ConnectionState().PeerCertificates
|
||||
if len(certs) != 1 {
|
||||
return protocol.DeviceID{}, fmt.Errorf("unexpected number of certificates (%d != 1)", len(certs))
|
||||
}
|
||||
|
||||
return protocol.NewDeviceID(certs[0].Raw), nil
|
||||
}
|
||||
|
||||
func deviceIDIn(id protocol.DeviceID, ids []protocol.DeviceID) bool {
|
||||
for _, candidate := range ids {
|
||||
if id == candidate {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// Copyright (C) 2024 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
||||
)
|
||||
|
||||
type s3Copier struct {
|
||||
endpoint string
|
||||
region string
|
||||
bucket string
|
||||
key string
|
||||
accessKeyID string
|
||||
secretKey string
|
||||
}
|
||||
|
||||
func newS3Copier(endpoint, region, bucket, key, accessKeyID, secretKey string) *s3Copier {
|
||||
return &s3Copier{
|
||||
endpoint: endpoint,
|
||||
region: region,
|
||||
bucket: bucket,
|
||||
key: key,
|
||||
accessKeyID: accessKeyID,
|
||||
secretKey: secretKey,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *s3Copier) upload(r io.Reader) error {
|
||||
sess, err := session.NewSession(&aws.Config{
|
||||
Region: aws.String(s.region),
|
||||
Endpoint: aws.String(s.endpoint),
|
||||
Credentials: credentials.NewStaticCredentials(s.accessKeyID, s.secretKey, ""),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uploader := s3manager.NewUploader(sess)
|
||||
_, err = uploader.Upload(&s3manager.UploadInput{
|
||||
Bucket: aws.String(s.bucket),
|
||||
Key: aws.String(s.key),
|
||||
Body: r,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *s3Copier) downloadLatest(w io.WriterAt) error {
|
||||
sess, err := session.NewSession(&aws.Config{
|
||||
Region: aws.String(s.region),
|
||||
Endpoint: aws.String(s.endpoint),
|
||||
Credentials: credentials.NewStaticCredentials(s.accessKeyID, s.secretKey, ""),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
svc := s3.New(sess)
|
||||
resp, err := svc.ListObjectsV2(&s3.ListObjectsV2Input{Bucket: aws.String(s.bucket)})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var lastKey string
|
||||
var lastModified time.Time
|
||||
var lastSize int64
|
||||
for _, item := range resp.Contents {
|
||||
if item.LastModified.After(lastModified) && *item.Size > lastSize {
|
||||
lastKey = *item.Key
|
||||
lastModified = *item.LastModified
|
||||
lastSize = *item.Size
|
||||
} else if lastModified.Sub(*item.LastModified) < 5*time.Minute && *item.Size > lastSize {
|
||||
lastKey = *item.Key
|
||||
lastSize = *item.Size
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("Downloading database from", lastKey)
|
||||
downloader := s3manager.NewDownloader(sess)
|
||||
_, err = downloader.Download(w, &s3.GetObjectInput{
|
||||
Bucket: aws.String(s.bucket),
|
||||
Key: aws.String(lastKey),
|
||||
})
|
||||
return err
|
||||
}
|
||||
+24
-8
@@ -96,13 +96,28 @@ var (
|
||||
Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
|
||||
}, []string{"operation"})
|
||||
|
||||
retryAfterHistogram = prometheus.NewHistogram(prometheus.HistogramOpts{
|
||||
Namespace: "syncthing",
|
||||
Subsystem: "discovery",
|
||||
Name: "retry_after_seconds",
|
||||
Help: "Retry-After header value in seconds.",
|
||||
Buckets: prometheus.ExponentialBuckets(60, 2, 7), // 60, 120, 240, 480, 960, 1920, 3840
|
||||
})
|
||||
databaseWriteSeconds = prometheus.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: "syncthing",
|
||||
Subsystem: "discovery",
|
||||
Name: "database_write_seconds",
|
||||
Help: "Time spent writing the database.",
|
||||
})
|
||||
databaseLastWritten = prometheus.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: "syncthing",
|
||||
Subsystem: "discovery",
|
||||
Name: "database_last_written",
|
||||
Help: "Timestamp of the last successful database write.",
|
||||
})
|
||||
|
||||
retryAfterLevel = prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: "syncthing",
|
||||
Subsystem: "discovery",
|
||||
Name: "retry_after_seconds",
|
||||
Help: "Retry-After header value in seconds.",
|
||||
}, []string{"name"})
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -123,5 +138,6 @@ func init() {
|
||||
replicationSendsTotal, replicationRecvsTotal,
|
||||
databaseKeys, databaseStatisticsSeconds,
|
||||
databaseOperations, databaseOperationSeconds,
|
||||
retryAfterHistogram)
|
||||
databaseWriteSeconds, databaseLastWritten,
|
||||
retryAfterLevel)
|
||||
}
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
- runtime: go1.21
|
||||
requirements:
|
||||
# See https://en.wikipedia.org/wiki/MacOS_version_history#Releases
|
||||
#
|
||||
# macOS 10.15 (Catalina) per https://go.dev/doc/go1.22#darwin
|
||||
darwin: "19"
|
||||
# Per https://go.dev/doc/go1.23#linux
|
||||
linux: "2.6.32"
|
||||
# Windows 10's initial release was 10.0.10240.16405, per
|
||||
# https://learn.microsoft.com/en-us/windows/release-health/release-information
|
||||
# and Windows 11's initial release was 10.0.22000.194 per
|
||||
# https://learn.microsoft.com/en-us/windows/release-health/windows11-release-information
|
||||
#
|
||||
# Windows 10/Windows Server 2016 per https://go.dev/doc/go1.21#windows
|
||||
windows: "10.0"
|
||||
|
||||
- runtime: go1.22
|
||||
requirements:
|
||||
darwin: "19"
|
||||
linux: "2.6.32"
|
||||
windows: "10.0"
|
||||
|
||||
- runtime: go1.23
|
||||
requirements:
|
||||
# macOS 11 (Big Sur) per https://tip.golang.org/doc/go1.23#darwin
|
||||
darwin: "20"
|
||||
linux: "2.6.32"
|
||||
windows: "10.0"
|
||||
@@ -1,10 +1,11 @@
|
||||
module github.com/syncthing/syncthing
|
||||
|
||||
go 1.21.0
|
||||
go 1.22.0
|
||||
|
||||
require (
|
||||
github.com/AudriusButkevicius/recli v0.0.7-0.20220911121932-d000ce8fbf0f
|
||||
github.com/alecthomas/kong v0.9.0
|
||||
github.com/alecthomas/kong v1.2.1
|
||||
github.com/aws/aws-sdk-go v1.55.5
|
||||
github.com/calmh/incontainer v1.0.0
|
||||
github.com/calmh/xdr v1.1.0
|
||||
github.com/ccding/go-stun v0.1.5
|
||||
@@ -27,11 +28,12 @@ require (
|
||||
github.com/miscreant/miscreant.go v0.0.0-20200214223636-26d376326b75
|
||||
github.com/oschwald/geoip2-golang v1.11.0
|
||||
github.com/pierrec/lz4/v4 v4.1.21
|
||||
github.com/prometheus/client_golang v1.19.1
|
||||
github.com/quic-go/quic-go v0.46.0
|
||||
github.com/prometheus/client_golang v1.20.4
|
||||
github.com/puzpuzpuz/xsync/v3 v3.4.0
|
||||
github.com/quic-go/quic-go v0.47.0
|
||||
github.com/rabbitmq/amqp091-go v1.10.0
|
||||
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475
|
||||
github.com/shirou/gopsutil/v4 v4.24.7
|
||||
github.com/shirou/gopsutil/v4 v4.24.8
|
||||
github.com/syncthing/notify v0.0.0-20210616190510-c6b7342338d2
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d
|
||||
github.com/thejerf/suture/v4 v4.0.5
|
||||
@@ -39,13 +41,14 @@ require (
|
||||
github.com/vitrun/qart v0.0.0-20160531060029-bf64b92db6b0
|
||||
github.com/willabides/kongplete v0.4.0
|
||||
go.uber.org/automaxprocs v1.5.3
|
||||
golang.org/x/crypto v0.26.0
|
||||
golang.org/x/net v0.28.0
|
||||
golang.org/x/sys v0.24.0
|
||||
golang.org/x/text v0.17.0
|
||||
golang.org/x/crypto v0.27.0
|
||||
golang.org/x/net v0.29.0
|
||||
golang.org/x/sys v0.25.0
|
||||
golang.org/x/text v0.18.0
|
||||
golang.org/x/time v0.6.0
|
||||
golang.org/x/tools v0.24.0
|
||||
golang.org/x/tools v0.25.0
|
||||
google.golang.org/protobuf v1.34.2
|
||||
sigs.k8s.io/yaml v1.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -54,7 +57,7 @@ require (
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
|
||||
@@ -62,33 +65,35 @@ require (
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/gofrs/flock v0.12.1 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect
|
||||
github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/nxadm/tail v1.4.11 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.20.0 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.20.2 // indirect
|
||||
github.com/oschwald/maxminddb-golang v1.13.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/posener/complete v1.2.3 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.55.0 // indirect
|
||||
github.com/prometheus/common v0.59.1 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/riywo/loginshell v0.0.0-20200815045211-7d26008be1ab // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/stretchr/testify v1.9.0 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.14 // indirect
|
||||
github.com/tklauser/numcpus v0.8.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.uber.org/mock v0.4.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa // indirect
|
||||
golang.org/x/mod v0.20.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect
|
||||
golang.org/x/mod v0.21.0 // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -3,14 +3,16 @@ github.com/AudriusButkevicius/recli v0.0.7-0.20220911121932-d000ce8fbf0f/go.mod
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
||||
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU=
|
||||
github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/kong v0.9.0 h1:G5diXxc85KvoV2f0ZRVuMsi45IrBgx9zDNGNj165aPA=
|
||||
github.com/alecthomas/kong v0.9.0/go.mod h1:Y47y5gKfHp1hDc7CH7OeXgLIpp+Q2m1Ni0L5s3bI8Os=
|
||||
github.com/alecthomas/assert/v2 v2.10.0 h1:jjRCHsj6hBJhkmhznrCzoNpbA3zqy0fYiUcYZP/GkPY=
|
||||
github.com/alecthomas/assert/v2 v2.10.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/kong v1.2.1 h1:E8jH4Tsgv6wCRX2nGrdPyHDUCSG83WH2qE4XLACD33Q=
|
||||
github.com/alecthomas/kong v1.2.1/go.mod h1:rKTSFhbdp3Ryefn8x5MOEprnRFQ7nlmMC01GKhehhBM=
|
||||
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
||||
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU=
|
||||
github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
|
||||
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=
|
||||
@@ -32,8 +34,9 @@ github.com/chmduquesne/rollinghash v4.0.0+incompatible/go.mod h1:Uc2I36RRfTAf7Dg
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/d4l3k/messagediff v1.2.1 h1:ZcAIMYsUg0EAp9X+tt8/enBE/Q8Yd5kzPynLyKptt9U=
|
||||
github.com/d4l3k/messagediff v1.2.1/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -79,13 +82,13 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k=
|
||||
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
|
||||
github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 h1:c5FlPPgxOn7kJz3VoPLkQYQXGBS3EklQ4Zfi57uOuqQ=
|
||||
github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
@@ -123,20 +126,28 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
|
||||
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
|
||||
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0=
|
||||
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
|
||||
github.com/maruel/panicparse/v2 v2.3.1 h1:NtJavmbMn0DyzmmSStE8yUsmPZrZmudPH7kplxBinOA=
|
||||
github.com/maruel/panicparse/v2 v2.3.1/go.mod h1:s3UmQB9Fm/n7n/prcD2xBGDkwXD6y2LeZnhbEXvs9Dg=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
@@ -160,8 +171,8 @@ github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vv
|
||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
|
||||
github.com/onsi/ginkgo/v2 v2.20.0 h1:PE84V2mHqoT1sglvHc8ZdQtPcwmvvt29WLEEO3xmdZw=
|
||||
github.com/onsi/ginkgo/v2 v2.20.0/go.mod h1:lG9ey2Z29hR41WMVthyJBGUBcBhGOtoPF2VFMvBXFCI=
|
||||
github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4=
|
||||
github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
@@ -185,16 +196,18 @@ github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
|
||||
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
|
||||
github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI=
|
||||
github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
|
||||
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
|
||||
github.com/prometheus/common v0.59.1 h1:LXb1quJHWm1P6wq/U824uxYi4Sg0oGvNeUm1z5dJoX0=
|
||||
github.com/prometheus/common v0.59.1/go.mod h1:GpWM7dewqmVYcd7SmRaiWVe9SSqjf0UrwnYnpEZNuT0=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/quic-go/quic-go v0.46.0 h1:uuwLClEEyk1DNvchH8uCByQVjo3yKL9opKulExNDs7Y=
|
||||
github.com/quic-go/quic-go v0.46.0/go.mod h1:1dLehS7TIR64+vxGR70GDcatWTOtMX2PUtnKsjbTurI=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/quic-go/quic-go v0.47.0 h1:yXs3v7r2bm1wmPTYNLKAAJTHMYkPEsfYJmTazXrCZ7Y=
|
||||
github.com/quic-go/quic-go v0.47.0/go.mod h1:3bCapYsJvXGZcipOHuu7plYtaV6tnF+z7wIFsU0WK9E=
|
||||
github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=
|
||||
github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM=
|
||||
@@ -207,8 +220,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.24.7 h1:V9UGTK4gQ8HvcnPKf6Zt3XHyQq/peaekfxpJ2HSocJk=
|
||||
github.com/shirou/gopsutil/v4 v4.24.7/go.mod h1:0uW/073rP7FYLOkvxolUQM5rMOLTNmRXnFKafpb71rw=
|
||||
github.com/shirou/gopsutil/v4 v4.24.8 h1:pVQjIenQkIhqO81mwTaXjTzOMT7d3TZkf43PlVFHENI=
|
||||
github.com/shirou/gopsutil/v4 v4.24.8/go.mod h1:wE0OrJtj4dG+hYkxqDH3QiBICdKSf04/npcvLLc/oRg=
|
||||
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
||||
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
|
||||
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
|
||||
@@ -233,10 +246,10 @@ github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDd
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48=
|
||||
github.com/thejerf/suture/v4 v4.0.5 h1:F1E/4FZwXWqvlWDKEUo6/ndLtxGAUzMmNqkrMknZbAA=
|
||||
github.com/thejerf/suture/v4 v4.0.5/go.mod h1:gu9Y4dXNUWFrByqRt30Rm9/UZ0wzRSt9AJS6xu/ZGxU=
|
||||
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||
github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
|
||||
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
|
||||
github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY=
|
||||
github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE=
|
||||
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
github.com/urfave/cli v1.22.15 h1:nuqt+pdC/KqswQKhETJjo7pvn/k4xMUxgW6liI7XpnM=
|
||||
github.com/urfave/cli v1.22.15/go.mod h1:wSan1hmo5zeyLGBjRJbzRTNk8gwoYa2B9n4q9dmRIc0=
|
||||
@@ -262,16 +275,16 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
|
||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
|
||||
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
||||
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI=
|
||||
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
|
||||
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
||||
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
|
||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
|
||||
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
|
||||
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
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=
|
||||
@@ -289,8 +302,8 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
|
||||
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
|
||||
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
|
||||
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
|
||||
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-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -326,11 +339,10 @@ golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
|
||||
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
@@ -344,8 +356,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
|
||||
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
||||
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -355,8 +367,8 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
|
||||
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
|
||||
golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE=
|
||||
golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg=
|
||||
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=
|
||||
@@ -380,8 +392,12 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkep
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
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=
|
||||
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
||||
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
|
||||
|
||||
@@ -304,11 +304,7 @@ a.toggler:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Panel padding decrease
|
||||
*/
|
||||
|
||||
.panel-collapse .panel-body {
|
||||
.panel-body.less-padding {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "Perintah eksternal menangani pemversian. Ia harus menghapus berkas dari folder yang dibagi. Jika lokasi aplikasi terdapat spasi, itu harus dikutip.",
|
||||
"Anonymous Usage Reporting": "Pelaporan Penggunaan Anonim",
|
||||
"Anonymous usage report format has changed. Would you like to move to the new format?": "Format pelaporan penggunaan anonim telah berubah. Maukah anda pindah menggunakan format yang baru?",
|
||||
"Applied to LAN": "Digunakan di LAN",
|
||||
"Apply": "Terapkan",
|
||||
"Are you sure you want to override all remote changes?": "Apakah anda yakin ingin menimpa semua perubahan jarak jauh?",
|
||||
"Are you sure you want to permanently delete all these files?": "Apakah anda yakin ingin menghapus semua berkas berikut secara permanen?",
|
||||
@@ -38,6 +39,7 @@
|
||||
"Are you sure you want to restore {%count%} files?": "Apakah anda yakin ingin memulihkan {{count}} berkas?",
|
||||
"Are you sure you want to revert all local changes?": "Apakah anda yakin ingin mengembalikan semua perubahan lokal?",
|
||||
"Are you sure you want to upgrade?": "Apakah anda yakin ingin meningkatkan?",
|
||||
"Authentication Required": "Otentikasi diperlukan",
|
||||
"Authors": "Penulis",
|
||||
"Auto Accept": "Terima Otomatis",
|
||||
"Automatic Crash Reporting": "Pelaporan Crash Otomatis",
|
||||
@@ -48,6 +50,7 @@
|
||||
"Available debug logging facilities:": "Fasilitas log debug yang ada:",
|
||||
"Be careful!": "Harap hati-hati!",
|
||||
"Body:": "Badan:",
|
||||
"Bugs": "Bugs",
|
||||
"Cancel": "Batal",
|
||||
"Changelog": "Log Perubahan",
|
||||
"Clean out after": "Bersihkan setelah",
|
||||
@@ -63,6 +66,7 @@
|
||||
"Configured": "Terkonfigurasi",
|
||||
"Connected (Unused)": "Terkoneksi (Tidak Digunakan)",
|
||||
"Connection Error": "Koneksi Galat",
|
||||
"Connection Management": "Pengaturan Koneksi",
|
||||
"Connection Type": "Tipe Koneksi",
|
||||
"Connections": "Koneksi",
|
||||
"Connections via relays might be rate limited by the relay": "Koneksi melalui relai mungkin dibatasi oleh relai",
|
||||
@@ -77,6 +81,7 @@
|
||||
"Danger!": "Bahaya!",
|
||||
"Database Location": "Lokasi Database",
|
||||
"Debugging Facilities": "Fasilitas Debug",
|
||||
"Default": "Default",
|
||||
"Default Configuration": "Konfigurasi Bawaan",
|
||||
"Default Device": "Perangkat Bawaan",
|
||||
"Default Folder": "Folder Bawaan",
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"Advanced": "Avancerat",
|
||||
"Advanced Configuration": "Avancerad konfiguration",
|
||||
"All Data": "Alla data",
|
||||
"All Time": "All tid",
|
||||
"All Time": "Någonsin",
|
||||
"All folders shared with this device must be protected by a password, such that all sent data is unreadable without the given password.": "Alla mappar som delas med denna enhet måste skyddas av ett lösenord, så att alla skickade data är oläsliga utan det angivna lösenordet.",
|
||||
"Allow Anonymous Usage Reporting?": "Tillåt anonym användarstatistiksrapportering?",
|
||||
"Allowed Networks": "Tillåtna nätverk",
|
||||
|
||||
@@ -275,8 +275,8 @@
|
||||
"Oldest First": "最旧优先",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "文件夹的可选描述性标签。每个设备上可能不同。",
|
||||
"Options": "选项",
|
||||
"Out of Sync": "不同步",
|
||||
"Out of Sync Items": "不同步的项目",
|
||||
"Out of Sync": "未同步",
|
||||
"Out of Sync Items": "未同步的项目",
|
||||
"Outgoing Rate Limit (KiB/s)": "传出速率限制(KiB/s)",
|
||||
"Override": "覆盖",
|
||||
"Override Changes": "覆盖更改",
|
||||
|
||||
@@ -1 +1 @@
|
||||
var langPrettyprint = {"ar":"Arabic","bg":"Bulgarian","ca":"Catalan","ca@valencia":"Valencian","cs":"Czech","da":"Danish","de":"German","el":"Greek","en":"English","en-GB":"English (United Kingdom)","es":"Spanish","eu":"Basque","fil":"Filipino","fr":"French","fy":"Frisian","ga":"Irish","hi":"Hindi","hu":"Hungarian","id":"Indonesian","it":"Italian","ja":"Japanese","ko-KR":"Korean","lt":"Lithuanian","nl":"Dutch","pl":"Polish","pt-BR":"Portuguese (Brazil)","pt-PT":"Portuguese (Portugal)","ro-RO":"Romanian","ru":"Russian","sk":"Slovak","sl":"Slovenian","sv":"Swedish","tr":"Turkish","uk":"Ukrainian","zh-CN":"Chinese (Simplified)","zh-HK":"Chinese (Traditional, Hong Kong)","zh-TW":"Chinese (Traditional)"}
|
||||
var langPrettyprint = {"ar":"Arabic","bg":"Bulgarian","ca":"Catalan","ca@valencia":"Valencian","cs":"Czech","da":"Danish","de":"German","el":"Greek","en":"English","en-GB":"English (United Kingdom)","es":"Spanish","eu":"Basque","fil":"Filipino","fr":"French","fy":"Frisian","ga":"Irish","hi":"Hindi","hu":"Hungarian","id":"Indonesian","it":"Italian","ja":"Japanese","ko-KR":"Korean","lt":"Lithuanian","nl":"Dutch","pl":"Polish","pt-BR":"Portuguese (Brazil)","pt-PT":"Portuguese (Portugal)","ro-RO":"Romanian","ru":"Russian","sk":"Slovak","sl":"Slovenian","sv":"Swedish","tr":"Turkish","uk":"Ukrainian","zh-CN":"Chinese (Simplified Han script)","zh-HK":"Chinese (Traditional Han script, Hong Kong)","zh-TW":"Chinese (Traditional Han script)"}
|
||||
|
||||
@@ -413,7 +413,7 @@
|
||||
</h4>
|
||||
</button>
|
||||
<div id="folder-{{$index}}" class="panel-collapse collapse">
|
||||
<div class="panel-body">
|
||||
<div class="panel-body less-padding">
|
||||
<table class="table table-condensed table-striped table-auto">
|
||||
<tbody>
|
||||
<tr class="visible-xs">
|
||||
@@ -681,7 +681,7 @@
|
||||
</h4>
|
||||
</button>
|
||||
<div id="device-this" class="panel-collapse collapse in">
|
||||
<div class="panel-body">
|
||||
<div class="panel-body less-padding">
|
||||
<table class="table table-condensed table-striped table-auto">
|
||||
<tbody>
|
||||
<tr>
|
||||
@@ -796,7 +796,7 @@
|
||||
</h4>
|
||||
</button>
|
||||
<div id="device-{{$index}}" class="panel-collapse collapse">
|
||||
<div class="panel-body">
|
||||
<div class="panel-body less-padding">
|
||||
<table class="table table-condensed table-striped table-auto">
|
||||
<tbody>
|
||||
<tr class="visible-xs">
|
||||
|
||||
@@ -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, Jesse Lucas, Simon Frei, Tomasz Wilczyński, Alexander Graf, Alexandre Viau, Anderson Mesquita, André Colomb, Antony Male, Ben Schulz, Caleb Callaway, Daniel Harte, Eric P, Evgeny Kuznetsov, Lars K.W. Gohlke, Lode Hoste, Michael Ploujnikov, Nate Morrison, Philippe Schommers, Ryan Sullivan, Sergey Mishin, Stefan Tatschner, Wulf Weich, bt90, greatroar, Aaron Bieber, Adam Piggott, Adel Qalieh, Alan Pope, Alberto Donato, Aleksey Vasenev, Alessandro G., Alex Lindeman, Alex Xu, Alexander Seiler, Alexandre Alves, Aman Gupta, Anatoli Babenia, Andreas Sommer, Andrew Dunham, Andrew Meyer, Andrew Rabert, Andrey D, Anjan Momi, Anthony Goeckner, Antoine Lamielle, Anur, Aranjedeath, Arkadiusz Tymiński, Aroun, Arthur Axel fREW Schmidt, Artur Zubilewicz, Aurélien Rainone, BAHADIR YILMAZ, Bart De Vries, Beat Reichenbach, Ben Curthoys, Ben Shepherd, Ben Sidhom, Benedikt Heine, Benedikt Morbach, Benjamin Nater, Benno Fünfstück, Benny Ng, Boqin Qin, Boris Rybalkin, Brandon Philips, Brendan Long, Brian R. Becker, Carsten Hagemann, Catfriend1, Cathryne Linenweaver, Cedric Staniewski, Chih-Hsuan Yen, Choongkyu, Chris Howie, Chris Joel, Chris Tonkinson, Christian Kujau, Christian Prescott, Colin Kennedy, Cromefire_, Cyprien Devillez, Dale Visser, Dan, Daniel Barczyk, Daniel Bergmann, Daniel Martí, Daniel Padrta, Darshil Chanpura, David Rimmer, DeflateAwning, Denis A., Dennis Wilson, DerRockWolf, Devon G. Redekopp, Dimitri Papadopoulos Orfanos, Dmitry Saveliev, Domenic Horner, Dominik Heidler, Elias Jarlebring, Elliot Huffman, Emil Hessman, Emil Lundberg, Eng Zer Jun, Eric Lesiuta, Erik Meitner, Evan Spensley, Federico Castagnini, Felix, Felix Ableitner, Felix Lampe, Felix Unterpaintner, Francois-Xavier Gsell, Frank Isemann, Gahl Saraf, Gilli Sigurdsson, Gleb Sinyavskiy, Graham Miln, Greg, Gusted, Han Boetes, HansK-p, Harrison Jones, Heiko Zuerker, Hugo Locurcio, Iain Barnett, Ian Johnson, Ikko Ashimine, Ilya Brin, Iskander Sharipov, Jaakko Hannikainen, Jacek Szafarkiewicz, Jack Croft, Jacob, Jake Peterson, James O'Beirne, James Patterson, Jaroslav Lichtblau, Jaroslav Malec, Jaspitta, Jauder Ho, Jaya Chithra, Jaya Kumar, Jeffery To, Jens Diemer, Jerry Jacobs, Jochen Voss, Johan Andersson, Johan Vromans, John Rinehart, Jonas Thelemann, Jonathan, Jonathan Cross, Jonta, Jose Manuel Delicado, Julian Lehrhuber, Jörg Thalheim, Jędrzej Kula, K.B.Dharun Krishna, Kalle Laine, Karol Różycki, Kebin Liu, Keith Harrison, Keith Turner, Kelong Cong, Ken'ichi Kamada, Kevin Allen, Kevin Bushiri, Kevin White, Jr., Kurt Fitzner, LSmithx2, Lars Lehtonen, Laurent Arnoud, Laurent Etiemble, Leo Arias, Liu Siyuan, Lord Landon Agahnim, Lukas Lihotzki, Luke Hamburg, Majed Abdulaziz, Marc Laporte, Marc Pujol, Marcin Dziadus, Marcus Legendre, Mario Majila, Mark Pulford, Martchus, Martin Polehla, Mateusz Naściszewski, Mateusz Ż, Matic Potočnik, Matt Burke, Matt Robenolt, Matteo Ruina, Maurizio Tomasi, Max, Max Schulze, MaximAL, Maxime Thirouin, Maximilian, MichaIng, Michael Jephcote, Michael Rienstra, Michael Tilli, Migelo, Mike Boone, MikeLund, MikolajTwarog, Mingxuan Lin, Naveen, Nicholas Rishel, Nick Busey, Nico Stapelbroek, Nicolas Braud-Santoni, Nicolas Perraut, Niels Peter Roest, Nils Jakobi, NinoM4ster, Nitroretro, NoLooseEnds, Oliver Freyermuth, Otiel, Oyebanji Jacob Mayowa, Pablo, Pascal Jungblut, Paul Brit, Pawel Palenica, Paweł Rozlach, Peter Badida, Peter Dave Hello, Peter Hoeg, Peter Marquardt, Phani Rithvij, Phil Davis, Phill Luby, Pier Paolo Ramon, Piotr Bejda, Pramodh KP, Quentin Hibon, Rahmi Pruitt, Richard Hartmann, Robert Carosi, Roberto Santalla, Robin Schoonover, Roman Zaynetdinov, Ross Smith II, Ruslan Yevdokymov, Ryan Qian, Sacheendra Talluri, Scott Klupfel, Sertonix, Severin von Wnuck-Lipinski, Shaarad Dalvi, Simon Mwepu, Simon Pickup, Sly_tom_cat, Stefan Kuntz, Steven Eckhoff, Suhas Gundimeda, Sven Bachmann, Taylor Khan, Thomas, Thomas Hipp, Tim Abell, Tim Howes, Tim Nordenfur, Tobias Klauser, Tobias Nygren, Tobias Tom, Tom Jakubowski, Tommy Thorn, Tommy van der Vorst, Tully Robinson, Tyler Brazier, Tyler Kropp, Unrud, Veeti Paananen, Victor Buinsky, Vik, Vil Brekin, Vladimir Rusinov, WangXi, Will Rouesnel, William A. Kennington III, Xavier O., Yannic A., andresvia, andyleap, boomsquared, chenrui, chucic, cjc7373, cui fliter, d-volution, derekriemer, desbma, diemade, digital, entity0xfe, georgespatton, ghjklw, guangwu, gudvinr, ignacy123, janost, jaseg, jelle van der Waa, jtagcat, klemens, kylosus, luchenhan, luzpaz, marco-m, mclang, mv1005, nf, orangekame3, otbutz, overkill, perewa, red_led, rubenbe, sec65, vapatel2, villekalliomaki, wangguoliang, wouter bolsterlee, xarx00, xjtdy888, 佛跳墙, 落心
|
||||
Jakob Borg, Audrius Butkevicius, Jesse Lucas, Simon Frei, Tomasz Wilczyński, Alexander Graf, Alexandre Viau, Anderson Mesquita, André Colomb, Antony Male, Ben Schulz, Caleb Callaway, Daniel Harte, Emil Lundberg, Eric P, Evgeny Kuznetsov, Lars K.W. Gohlke, Lode Hoste, Michael Ploujnikov, Nate Morrison, Philippe Schommers, Ryan Sullivan, Sergey Mishin, Stefan Tatschner, Wulf Weich, bt90, greatroar, Aaron Bieber, Adam Piggott, Adel Qalieh, Alan Pope, Alberto Donato, Aleksey Vasenev, Alessandro G., Alex Lindeman, Alex Xu, Alexander Seiler, Alexandre Alves, Aman Gupta, Anatoli Babenia, Andreas Sommer, Andrew Dunham, Andrew Meyer, Andrew Rabert, Andrey D, Anjan Momi, Anthony Goeckner, Antoine Lamielle, Anur, Aranjedeath, Arkadiusz Tymiński, Aroun, Arthur Axel fREW Schmidt, Artur Zubilewicz, Aurélien Rainone, BAHADIR YILMAZ, Bart De Vries, Beat Reichenbach, Ben Curthoys, Ben Shepherd, Ben Sidhom, Benedikt Heine, Benedikt Morbach, Benjamin Nater, Benno Fünfstück, Benny Ng, Boqin Qin, Boris Rybalkin, Brandon Philips, Brendan Long, Brian R. Becker, Carsten Hagemann, Catfriend1, Cathryne Linenweaver, Cedric Staniewski, Chih-Hsuan Yen, Choongkyu, Chris Howie, Chris Joel, Chris Tonkinson, Christian Kujau, Christian Prescott, Colin Kennedy, Cromefire_, Cyprien Devillez, Dale Visser, Dan, Daniel Barczyk, Daniel Bergmann, Daniel Martí, Daniel Padrta, Darshil Chanpura, David Rimmer, DeflateAwning, Denis A., Dennis Wilson, DerRockWolf, Devon G. Redekopp, Dimitri Papadopoulos Orfanos, Dmitry Saveliev, Domenic Horner, Dominik Heidler, Elias Jarlebring, Elliot Huffman, Emil Hessman, Eng Zer Jun, Eric Lesiuta, Erik Meitner, Evan Spensley, Federico Castagnini, Felix, Felix Ableitner, Felix Lampe, Felix Unterpaintner, Francois-Xavier Gsell, Frank Isemann, Gahl Saraf, Gilli Sigurdsson, Gleb Sinyavskiy, Graham Miln, Greg, Gusted, Han Boetes, HansK-p, Harrison Jones, Heiko Zuerker, Hugo Locurcio, Iain Barnett, Ian Johnson, Ikko Ashimine, Ilya Brin, Iskander Sharipov, Jaakko Hannikainen, Jacek Szafarkiewicz, Jack Croft, Jacob, Jake Peterson, James O'Beirne, James Patterson, Jaroslav Lichtblau, Jaroslav Malec, Jaspitta, Jauder Ho, Jaya Chithra, Jaya Kumar, Jeffery To, Jens Diemer, Jerry Jacobs, Jochen Voss, Johan Andersson, Johan Vromans, John Rinehart, Jonas Thelemann, Jonathan, Jonathan Cross, Jonta, Jose Manuel Delicado, Julian Lehrhuber, Jörg Thalheim, Jędrzej Kula, K.B.Dharun Krishna, Kalle Laine, Karol Różycki, Kebin Liu, Keith Harrison, Keith Turner, Kelong Cong, Ken'ichi Kamada, Kevin Allen, Kevin Bushiri, Kevin White, Jr., Kurt Fitzner, LSmithx2, Lars Lehtonen, Laurent Arnoud, Laurent Etiemble, Leo Arias, Liu Siyuan, Lord Landon Agahnim, Lukas Lihotzki, Luke Hamburg, Majed Abdulaziz, Marc Laporte, Marc Pujol, Marcin Dziadus, Marcus Legendre, Mario Majila, Mark Pulford, Martchus, Martin Polehla, Mateusz Naściszewski, Mateusz Ż, Matic Potočnik, Matt Burke, Matt Robenolt, Matteo Ruina, Maurizio Tomasi, Max, Max Schulze, MaximAL, Maxime Thirouin, Maximilian, MichaIng, Michael Jephcote, Michael Rienstra, Michael Tilli, Migelo, Mike Boone, MikeLund, MikolajTwarog, Mingxuan Lin, Naveen, Nicholas Rishel, Nick Busey, Nico Stapelbroek, Nicolas Braud-Santoni, Nicolas Perraut, Niels Peter Roest, Nils Jakobi, NinoM4ster, Nitroretro, NoLooseEnds, Oliver Freyermuth, Otiel, Oyebanji Jacob Mayowa, Pablo, Pascal Jungblut, Paul Brit, Pawel Palenica, Paweł Rozlach, Peter Badida, Peter Dave Hello, Peter Hoeg, Peter Marquardt, Phani Rithvij, Phil Davis, Phill Luby, Pier Paolo Ramon, Piotr Bejda, Pramodh KP, Quentin Hibon, Rahmi Pruitt, Richard Hartmann, Robert Carosi, Roberto Santalla, Robin Schoonover, Roman Zaynetdinov, Ross Smith II, Ruslan Yevdokymov, Ryan Qian, Sacheendra Talluri, Scott Klupfel, Sertonix, Severin von Wnuck-Lipinski, Shaarad Dalvi, Simon Mwepu, Simon Pickup, Sly_tom_cat, Sonu Kumar Saw, Stefan Kuntz, Steven Eckhoff, Suhas Gundimeda, Sven Bachmann, Taylor Khan, Thomas, Thomas Hipp, Tim Abell, Tim Howes, Tim Nordenfur, Tobias Klauser, Tobias Nygren, Tobias Tom, Tom Jakubowski, Tommy Thorn, Tommy van der Vorst, Tully Robinson, Tyler Brazier, Tyler Kropp, Unrud, Veeti Paananen, Victor Buinsky, Vik, Vil Brekin, Vladimir Rusinov, WangXi, Will Rouesnel, William A. Kennington III, Xavier O., Yannic A., andresvia, andyleap, boomsquared, chenrui, chucic, cjc7373, cui fliter, d-volution, derekriemer, desbma, diemade, digital, entity0xfe, georgespatton, ghjklw, guangwu, gudvinr, ignacy123, janost, jaseg, jelle van der Waa, jtagcat, klemens, kylosus, luchenhan, luzpaz, marco-m, mclang, mv1005, nf, orangekame3, otbutz, overkill, perewa, red_led, rubenbe, sec65, vapatel2, villekalliomaki, wangguoliang, wouter bolsterlee, xarx00, xjtdy888, 佛跳墙, 落心
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -59,30 +59,35 @@ angular.module('syncthing.core')
|
||||
// Find the first language in the list provided by the user's browser
|
||||
// that is a prefix of a language we have available. That is, "en"
|
||||
// sent by the browser will match "en" or "en-US", while "zh-TW" will
|
||||
// match only "zh-TW" and not "zh-CN".
|
||||
// match only "zh-TW" and not "zh" or "zh-CN".
|
||||
|
||||
var i,
|
||||
lang,
|
||||
browserLang,
|
||||
matching,
|
||||
locale = _defaultLocale;
|
||||
locale = _defaultLocale; // Fallback if nothing matched
|
||||
|
||||
for (i = 0; i < langs.length; i++) {
|
||||
lang = langs[i];
|
||||
browserLang = langs[i];
|
||||
|
||||
if (lang.length < 2) {
|
||||
if (browserLang.length < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
matching = _availableLocales.filter(function (possibleLang) {
|
||||
// The langs returned by the /rest/langs call will be in lower
|
||||
// case. We compare to the lowercase version of the language
|
||||
// code we have as well.
|
||||
// The langs returned by the /rest/svc/langs call will be in
|
||||
// lower case. We compare to the lowercase version of the
|
||||
// language code we have as well.
|
||||
possibleLang = possibleLang.toLowerCase();
|
||||
if (possibleLang.length > lang.length) {
|
||||
return possibleLang.indexOf(lang) === 0;
|
||||
} else {
|
||||
return lang.indexOf(possibleLang) === 0;
|
||||
if (possibleLang.indexOf(browserLang) !== 0) {
|
||||
// Prefix does not match
|
||||
return false;
|
||||
}
|
||||
if (possibleLang.length > browserLang.length) {
|
||||
// Must match up to the next hyphen separator
|
||||
return possibleLang[browserLang.length] === '-';
|
||||
}
|
||||
// Same length, exact match
|
||||
return true;
|
||||
});
|
||||
|
||||
if (matching.length >= 1) {
|
||||
@@ -90,7 +95,6 @@ angular.module('syncthing.core')
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Fallback if nothing matched
|
||||
useLocale(locale);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -16,6 +16,15 @@ angular.module('syncthing.core')
|
||||
LocaleService.autoConfigLocale();
|
||||
|
||||
if (!$scope.authenticated) {
|
||||
function setVersionFromHeader(_data, _status, headers) {
|
||||
var version = headers('X-Syncthing-Version');
|
||||
if (version) {
|
||||
$scope.version = { version: version };
|
||||
}
|
||||
}
|
||||
// Get index.html again (likely cached) to retrieve the version header
|
||||
$http.get('').success(setVersionFromHeader).error(setVersionFromHeader);
|
||||
|
||||
// Can't proceed yet - wait for the page reload after successful login.
|
||||
return;
|
||||
}
|
||||
@@ -1447,7 +1456,7 @@ angular.module('syncthing.core')
|
||||
// Assume hasRemoteGUIAddress is true or we would not be here
|
||||
var conn = $scope.connections[deviceCfg.deviceID];
|
||||
// Use regex to filter out scope ID from IPv6 addresses.
|
||||
return 'http://' + replaceAddressPort(conn.address, deviceCfg.remoteGUIPort).replace('%.*?\]:', ']:');
|
||||
return 'http://' + replaceAddressPort(conn.address, deviceCfg.remoteGUIPort).replace(/%.*?\]:/, ']:');
|
||||
};
|
||||
|
||||
function replaceAddressPort(address, newPort) {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<h4 class="panel-title" translate tabindex="0">GUI</h4>
|
||||
</div>
|
||||
<div id="guiConfig" class="panel-collapse collapse" role="tabpanel" aria-labelledby="guiHeading">
|
||||
<div class="panel-body">
|
||||
<div class="panel-body less-padding">
|
||||
<form class="form-horizontal" role="form">
|
||||
<div ng-repeat="(key, value) in advancedConfig.gui" ng-init="type = inputTypeFor(key, value)" ng-if="type != 'skip'" class="form-group">
|
||||
<label for="guiInput{{$index}}" class="col-sm-4 control-label">{{key | uncamel}} <a href="{{docsURL('users/config#config-option-gui.')}}{{key | lowercase}}" target="_blank"><span class="fas fa-question-circle"></span></a></label>
|
||||
@@ -32,7 +32,7 @@
|
||||
<h4 class="panel-title" tabindex="0" translate>Options</h4>
|
||||
</div>
|
||||
<div id="optionsConfig" class="panel-collapse collapse" role="tabpanel" aria-labelledby="optionsHeading">
|
||||
<div class="panel-body">
|
||||
<div class="panel-body less-padding">
|
||||
<form class="form-horizontal" role="form">
|
||||
<div ng-repeat="(key, value) in advancedConfig.options" ng-if="inputTypeFor(key, value) != 'skip'" class="form-group">
|
||||
<label for="optionsInput{{$index}}" class="col-sm-4 control-label">{{key | uncamel}} <a href="{{docsURL('users/config#config-option-options.')}}{{key | lowercase}}" target="_blank"><span class="fas fa-question-circle"></span></a></label>
|
||||
@@ -51,7 +51,7 @@
|
||||
<h4 class="panel-title" tabindex="0" translate>LDAP</h4>
|
||||
</div>
|
||||
<div id="ldapConfig" class="panel-collapse collapse" role="tabpanel" aria-labelledby="ldapHeading">
|
||||
<div class="panel-body">
|
||||
<div class="panel-body less-padding">
|
||||
<form class="form-horizontal" role="form">
|
||||
<div ng-repeat="(key, value) in advancedConfig.ldap" ng-if="inputTypeFor(key, value) != 'skip'" class="form-group">
|
||||
<label for="ldapInput{{$index}}" class="col-sm-4 control-label">{{key | uncamel}} <a href="{{docsURL('users/config#config-option-ldap.')}}{{key | lowercase}}" target="_blank"><span class="fas fa-question-circle"></span></a></label>
|
||||
@@ -70,7 +70,7 @@
|
||||
<h4 class="panel-title" translate>Folders</h4>
|
||||
</div>
|
||||
<div id="advancedFolders" class="panel-collapse collapse" role="tabpanel" aria-labelledby="advancedFoldersHeading">
|
||||
<div class="panel-body">
|
||||
<div class="panel-body less-padding">
|
||||
<div class="panel panel-default" ng-repeat="folder in advancedConfig.folders" ng-init="folderIndex = $index">
|
||||
<div class="panel-heading" role="tab" id="folder{{folderIndex}}Heading" data-toggle="collapse" data-parent="#advancedFolders" href="#folder{{folderIndex}}Config" aria-expanded="false" aria-controls="folder{{folderIndex}}Config" style="cursor: pointer;">
|
||||
<h4 ng-if="folder.label.length == 0" class="panel-title" tabindex="0">
|
||||
@@ -81,7 +81,7 @@
|
||||
</h4>
|
||||
</div>
|
||||
<div id="folder{{folderIndex}}Config" class="panel-collapse collapse" role="tabpanel" aria-labelledby="folder{{folderIndex}}Heading">
|
||||
<div class="panel-body">
|
||||
<div class="panel-body less-padding">
|
||||
<form class="form-horizontal" role="form">
|
||||
<div ng-repeat="(key, value) in folder" ng-if="inputTypeFor(key, value) != 'skip'" class="form-group">
|
||||
<label for="folder{{folderIndex}}Input{{$index}}" class="col-sm-4 control-label">{{key | uncamel}} <a href="{{docsURL('users/config#config-option-folder.')}}{{key | lowercase}}" target="_blank"><span class="fas fa-question-circle"></span></a></label>
|
||||
@@ -103,7 +103,7 @@
|
||||
<h4 class="panel-title" tabindex="0" translate>Devices</h4>
|
||||
</div>
|
||||
<div id="advancedDevices" class="panel-collapse collapse" role="tabpanel" aria-labelledby="advancedDevicesHeading">
|
||||
<div class="panel-body">
|
||||
<div class="panel-body less-padding">
|
||||
<div class="panel panel-default" ng-repeat="device in advancedConfig.devices" ng-init="deviceIndex = $index">
|
||||
<div class="panel-heading" role="tab" id="device{{deviceIndex}}Heading" data-toggle="collapse" data-parent="#advancedDevices" href="#device{{deviceIndex}}Config" aria-expanded="false" aria-controls="device{{deviceIndex}}Config" style="cursor: pointer;">
|
||||
<h4 class="panel-title" tabindex="0">
|
||||
@@ -111,7 +111,7 @@
|
||||
</h4>
|
||||
</div>
|
||||
<div id="device{{deviceIndex}}Config" class="panel-collapse collapse" role="tabpanel" aria-labelledby="device{{deviceIndex}}Heading">
|
||||
<div class="panel-body">
|
||||
<div class="panel-body less-padding">
|
||||
<form class="form-horizontal" role="form">
|
||||
<div ng-repeat="(key, value) in device" ng-if="inputTypeFor(key, value) != 'skip'" class="form-group">
|
||||
<label for="device{{deviceIndex}}Input{{$index}}" class="col-sm-4 control-label">{{key | uncamel}} <a href="{{docsURL('users/config#config-option-device.')}}{{key | lowercase}}" target="_blank"><span class="fas fa-question-circle"></span></a></label>
|
||||
@@ -133,7 +133,7 @@
|
||||
<h4 class="panel-title" tabindex="0" translate>Defaults</h4>
|
||||
</div>
|
||||
<div id="advancedDefaults" class="panel-collapse collapse" role="tabpanel" aria-labelledby="advancedDefaultsHeading">
|
||||
<div class="panel-body">
|
||||
<div class="panel-body less-padding">
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading" role="tab" id="advancedDefaultFolderHeading" data-toggle="collapse" data-parent="#advancedDefaults" href="#advancedDefaultFolder" aria-expanded="false" aria-controls="advancedDefaultFolder" style="cursor: pointer;">
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</h4>
|
||||
</button>
|
||||
<div id="remoteNeed-{{$index}}" class="panel-collapse" ng-class="{collapse: sizeOf(remoteNeedFolders) > 1}">
|
||||
<div class="panel-body">
|
||||
<div class="panel-body less-padding">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
+20
-12
@@ -41,6 +41,7 @@
|
||||
* @prop {string} [delimiter]
|
||||
* @prop {DigitReplacements} [_digitReplacements]
|
||||
* @prop {boolean} [_numberFirst]
|
||||
* @prop {boolean} [_hideCountIf2]
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -116,6 +117,7 @@
|
||||
// minute -> د stands for "دقيقة"
|
||||
// second -> ث stands for "ثانية"
|
||||
ar: assign(language("س", "ش", "أ", "ي", "س", "د", "ث", "م ث", ","), {
|
||||
_hideCountIf2: true,
|
||||
_digitReplacements: ["۰", "١", "٢", "٣", "٤", "٥", "٦", "٧", "٨", "٩"]
|
||||
}),
|
||||
// български (Bulgarian)
|
||||
@@ -177,7 +179,7 @@
|
||||
// ಕನ್ನಡ (Kannada)
|
||||
kn: language("ವ", "ತ", "ವ", "ದ", "ಗಂ", "ನಿ", "ಸೆ", "ಮಿಸೆ"),
|
||||
// 한국어 (Korean)
|
||||
ko: language("년", "월", "주", "일", "시", "분", "초", "밀리초"),
|
||||
ko: language("년", "달", "주", "일", "시간", "분", "초", "밀리초"),
|
||||
// Kurdî (Kurdish)
|
||||
ku: language("sal", "m", "h", "r", "s", "d", "ç", "ms", ","),
|
||||
// ລາວ (Lao)
|
||||
@@ -464,19 +466,25 @@
|
||||
: Math.floor(unitCount * Math.pow(10, maxDecimalPoints)) /
|
||||
Math.pow(10, maxDecimalPoints);
|
||||
var countStr = normalizedUnitCount.toString();
|
||||
if (digitReplacements) {
|
||||
|
||||
if (language._hideCountIf2 && unitCount === 2) {
|
||||
formattedCount = "";
|
||||
for (var i = 0; i < countStr.length; i++) {
|
||||
var char = countStr[i];
|
||||
if (char === ".") {
|
||||
formattedCount += decimal;
|
||||
} else {
|
||||
// @ts-ignore because `char` should always be 0-9 at this point.
|
||||
formattedCount += digitReplacements[char];
|
||||
}
|
||||
}
|
||||
spacer = "";
|
||||
} else {
|
||||
formattedCount = countStr.replace(".", decimal);
|
||||
if (digitReplacements) {
|
||||
formattedCount = "";
|
||||
for (var i = 0; i < countStr.length; i++) {
|
||||
var char = countStr[i];
|
||||
if (char === ".") {
|
||||
formattedCount += decimal;
|
||||
} else {
|
||||
// @ts-ignore because `char` should always be 0-9 at this point.
|
||||
formattedCount += digitReplacements[char];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
formattedCount = countStr.replace(".", decimal);
|
||||
}
|
||||
}
|
||||
|
||||
var languageWord = language[unitName];
|
||||
|
||||
+24
-2
@@ -1483,11 +1483,33 @@ func (*service) getDeviceID(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (*service) getLang(w http.ResponseWriter, r *http.Request) {
|
||||
lang := r.Header.Get("Accept-Language")
|
||||
var langs []string
|
||||
var weights = make(map[string]float64)
|
||||
for _, l := range strings.Split(lang, ",") {
|
||||
parts := strings.SplitN(l, ";", 2)
|
||||
langs = append(langs, strings.ToLower(strings.TrimSpace(parts[0])))
|
||||
code := strings.ToLower(strings.TrimSpace(parts[0]))
|
||||
weights[code] = 1.0
|
||||
if len(parts) < 2 {
|
||||
continue
|
||||
}
|
||||
weight := strings.ToLower(strings.TrimSpace(parts[1]))
|
||||
if !strings.HasPrefix(weight, "q=") {
|
||||
continue
|
||||
}
|
||||
if q, err := strconv.ParseFloat(weight[2:], 32); err != nil {
|
||||
// Completely dismiss entries with invalid weight
|
||||
delete(weights, code)
|
||||
} else {
|
||||
weights[code] = q
|
||||
}
|
||||
}
|
||||
var langs = make([]string, 0, len(weights))
|
||||
for code := range weights {
|
||||
langs = append(langs, code)
|
||||
}
|
||||
// Reorder by descending q value
|
||||
sort.SliceStable(langs, func(i, j int) bool {
|
||||
return weights[langs[i]] > weights[langs[j]]
|
||||
})
|
||||
sendJSON(w, langs)
|
||||
}
|
||||
|
||||
|
||||
@@ -53,14 +53,14 @@ var (
|
||||
// DefaultDiscoveryServersV4 should be substituted when the configuration
|
||||
// contains <globalAnnounceServer>default-v4</globalAnnounceServer>.
|
||||
DefaultDiscoveryServersV4 = []string{
|
||||
"https://discovery.syncthing.net/v2/?noannounce&id=LYXKCHX-VI3NYZR-ALCJBHF-WMZYSPK-QG6QJA3-MPFYMSO-U56GTUK-NA2MIAW",
|
||||
"https://discovery-v4.syncthing.net/v2/?nolookup&id=LYXKCHX-VI3NYZR-ALCJBHF-WMZYSPK-QG6QJA3-MPFYMSO-U56GTUK-NA2MIAW",
|
||||
"https://discovery-lookup.syncthing.net/v2/?noannounce",
|
||||
"https://discovery-announce-v4.syncthing.net/v2/?nolookup",
|
||||
}
|
||||
// DefaultDiscoveryServersV6 should be substituted when the configuration
|
||||
// contains <globalAnnounceServer>default-v6</globalAnnounceServer>.
|
||||
DefaultDiscoveryServersV6 = []string{
|
||||
"https://discovery.syncthing.net/v2/?noannounce&id=LYXKCHX-VI3NYZR-ALCJBHF-WMZYSPK-QG6QJA3-MPFYMSO-U56GTUK-NA2MIAW",
|
||||
"https://discovery-v6.syncthing.net/v2/?nolookup&id=LYXKCHX-VI3NYZR-ALCJBHF-WMZYSPK-QG6QJA3-MPFYMSO-U56GTUK-NA2MIAW",
|
||||
"https://discovery-lookup.syncthing.net/v2/?noannounce",
|
||||
"https://discovery-announce-v6.syncthing.net/v2/?nolookup",
|
||||
}
|
||||
// DefaultDiscoveryServers should be substituted when the configuration
|
||||
// contains <globalAnnounceServer>default</globalAnnounceServer>.
|
||||
|
||||
@@ -85,7 +85,7 @@ func (t *writeTrackingTracer) loggingTracer() *logging.Tracer {
|
||||
SentPacket: func(net.Addr, *logging.Header, logging.ByteCount, []logging.Frame) {
|
||||
t.lastWrite.Store(time.Now().UnixNano())
|
||||
},
|
||||
SentVersionNegotiationPacket: func(net.Addr, logging.ArbitraryLenConnectionID, logging.ArbitraryLenConnectionID, []logging.VersionNumber) {
|
||||
SentVersionNegotiationPacket: func(net.Addr, logging.ArbitraryLenConnectionID, logging.ArbitraryLenConnectionID, []logging.Version) {
|
||||
t.lastWrite.Store(time.Now().UnixNano())
|
||||
},
|
||||
}
|
||||
|
||||
@@ -283,7 +283,11 @@ func (s *service) handleConns(ctx context.Context) error {
|
||||
}
|
||||
|
||||
if err := s.connectionCheckEarly(remoteID, c); err != nil {
|
||||
l.Infof("Connection from %s at %s (%s) rejected: %v", remoteID, c.RemoteAddr(), c.Type(), err)
|
||||
if errors.Is(err, errDeviceAlreadyConnected) {
|
||||
l.Debugf("Connection from %s at %s (%s) rejected: %v", remoteID, c.RemoteAddr(), c.Type(), err)
|
||||
} else {
|
||||
l.Infof("Connection from %s at %s (%s) rejected: %v", remoteID, c.RemoteAddr(), c.Type(), err)
|
||||
}
|
||||
c.Close()
|
||||
continue
|
||||
}
|
||||
@@ -795,7 +799,7 @@ func (s *lanChecker) isLAN(addr net.Addr) bool {
|
||||
}
|
||||
}
|
||||
|
||||
lans, err := osutil.GetLans()
|
||||
lans, err := osutil.GetInterfaceAddrs(false)
|
||||
if err != nil {
|
||||
l.Debugln("Failed to retrieve interface IPs:", err)
|
||||
priv := ip.IsPrivate()
|
||||
|
||||
@@ -60,7 +60,7 @@ func getURLsForAllAdaptersIfUnspecified(network string, uri *url.URL) []*url.URL
|
||||
}
|
||||
|
||||
func getHostPortsForAllAdapters(port int) []string {
|
||||
nets, err := osutil.GetLans()
|
||||
nets, err := osutil.GetInterfaceAddrs(true)
|
||||
if err != nil {
|
||||
// Ignore failure.
|
||||
return nil
|
||||
|
||||
+16
-1
@@ -20,10 +20,14 @@ type FileInfoBatch struct {
|
||||
infos []protocol.FileInfo
|
||||
size int
|
||||
flushFn func([]protocol.FileInfo) error
|
||||
error error
|
||||
}
|
||||
|
||||
// NewFileInfoBatch returns a new FileInfoBatch that calls fn when it's time
|
||||
// to flush.
|
||||
// to flush. Errors from the flush function are considered non-recoverable;
|
||||
// once an error is returned the flush function wil not be called again, and
|
||||
// any further calls to Flush will return the same error (unless Reset is
|
||||
// called).
|
||||
func NewFileInfoBatch(fn func([]protocol.FileInfo) error) *FileInfoBatch {
|
||||
return &FileInfoBatch{flushFn: fn}
|
||||
}
|
||||
@@ -33,6 +37,9 @@ func (b *FileInfoBatch) SetFlushFunc(fn func([]protocol.FileInfo) error) {
|
||||
}
|
||||
|
||||
func (b *FileInfoBatch) Append(f protocol.FileInfo) {
|
||||
if b.error != nil {
|
||||
panic("bug: calling append on a failed batch")
|
||||
}
|
||||
if b.infos == nil {
|
||||
b.infos = make([]protocol.FileInfo, 0, MaxBatchSizeFiles)
|
||||
}
|
||||
@@ -45,6 +52,9 @@ func (b *FileInfoBatch) Full() bool {
|
||||
}
|
||||
|
||||
func (b *FileInfoBatch) FlushIfFull() error {
|
||||
if b.error != nil {
|
||||
return b.error
|
||||
}
|
||||
if b.Full() {
|
||||
return b.Flush()
|
||||
}
|
||||
@@ -52,10 +62,14 @@ func (b *FileInfoBatch) FlushIfFull() error {
|
||||
}
|
||||
|
||||
func (b *FileInfoBatch) Flush() error {
|
||||
if b.error != nil {
|
||||
return b.error
|
||||
}
|
||||
if len(b.infos) == 0 {
|
||||
return nil
|
||||
}
|
||||
if err := b.flushFn(b.infos); err != nil {
|
||||
b.error = err
|
||||
return err
|
||||
}
|
||||
b.Reset()
|
||||
@@ -64,6 +78,7 @@ func (b *FileInfoBatch) Flush() error {
|
||||
|
||||
func (b *FileInfoBatch) Reset() {
|
||||
b.infos = nil
|
||||
b.error = nil
|
||||
b.size = 0
|
||||
}
|
||||
|
||||
|
||||
@@ -8,12 +8,14 @@ package db
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/db/backend"
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
)
|
||||
|
||||
// writeJSONS serializes the database to a JSON stream that can be checked
|
||||
@@ -178,3 +180,53 @@ func snapshot(t testing.TB, fset *FileSet) *Snapshot {
|
||||
// defer fd.Close()
|
||||
// writeJSONS(fd, db)
|
||||
// }
|
||||
|
||||
func TestFileInfoBatchError(t *testing.T) {
|
||||
// Verify behaviour of the flush function returning an error.
|
||||
|
||||
var errReturn error
|
||||
var called int
|
||||
b := NewFileInfoBatch(func([]protocol.FileInfo) error {
|
||||
called += 1
|
||||
return errReturn
|
||||
})
|
||||
|
||||
// Flush should work when the flush function error is nil
|
||||
b.Append(protocol.FileInfo{Name: "test"})
|
||||
if err := b.Flush(); err != nil {
|
||||
t.Fatalf("expected nil, got %v", err)
|
||||
}
|
||||
if called != 1 {
|
||||
t.Fatalf("expected 1, got %d", called)
|
||||
}
|
||||
|
||||
// Flush should fail with an error retur
|
||||
errReturn = errors.New("problem")
|
||||
b.Append(protocol.FileInfo{Name: "test"})
|
||||
if err := b.Flush(); err != errReturn {
|
||||
t.Fatalf("expected %v, got %v", errReturn, err)
|
||||
}
|
||||
if called != 2 {
|
||||
t.Fatalf("expected 2, got %d", called)
|
||||
}
|
||||
|
||||
// Flush function should not be called again when it's already errored,
|
||||
// same error should be returned by Flush()
|
||||
if err := b.Flush(); err != errReturn {
|
||||
t.Fatalf("expected %v, got %v", errReturn, err)
|
||||
}
|
||||
if called != 2 {
|
||||
t.Fatalf("expected 2, got %d", called)
|
||||
}
|
||||
|
||||
// Reset should clear the error (and the file list)
|
||||
errReturn = nil
|
||||
b.Reset()
|
||||
b.Append(protocol.FileInfo{Name: "test"})
|
||||
if err := b.Flush(); err != nil {
|
||||
t.Fatalf("expected nil, got %v", err)
|
||||
}
|
||||
if called != 3 {
|
||||
t.Fatalf("expected 3, got %d", called)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +116,7 @@ func NewGlobal(server string, cert tls.Certificate, addrList AddressLister, evLo
|
||||
InsecureSkipVerify: opts.insecure,
|
||||
Certificates: []tls.Certificate{cert},
|
||||
MinVersion: tls.VersionTLS12,
|
||||
ClientSessionCache: tls.NewLRUClientSessionCache(0),
|
||||
},
|
||||
}),
|
||||
}}
|
||||
@@ -134,6 +135,7 @@ func NewGlobal(server string, cert tls.Certificate, addrList AddressLister, evLo
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: opts.insecure,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
ClientSessionCache: tls.NewLRUClientSessionCache(0),
|
||||
},
|
||||
}),
|
||||
}}
|
||||
|
||||
@@ -11,15 +11,15 @@ package fs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/version"
|
||||
"os"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
const IO_REPARSE_TAG_DEDUP = 0x80000013
|
||||
|
||||
func readReparseTag(path string) (uint32, error) {
|
||||
namep, err := syscall.UTF16PtrFromString(path)
|
||||
if err != nil {
|
||||
@@ -60,10 +60,6 @@ func isDirectoryJunction(reparseTag uint32) bool {
|
||||
return reparseTag == windows.IO_REPARSE_TAG_MOUNT_POINT
|
||||
}
|
||||
|
||||
func isDeduplicatedFile(reparseTag uint32) bool {
|
||||
return reparseTag == IO_REPARSE_TAG_DEDUP
|
||||
}
|
||||
|
||||
type dirJunctFileInfo struct {
|
||||
os.FileInfo
|
||||
}
|
||||
@@ -71,21 +67,22 @@ type dirJunctFileInfo struct {
|
||||
func (fi *dirJunctFileInfo) Mode() os.FileMode {
|
||||
// Simulate a directory and not a symlink; also set the execute
|
||||
// bits so the directory can be traversed Unix-side.
|
||||
return fi.FileInfo.Mode() ^ os.ModeSymlink | os.ModeDir | 0111
|
||||
return fi.FileInfo.Mode() ^ junctionPointModeMask | os.ModeDir | 0111
|
||||
}
|
||||
|
||||
func (fi *dirJunctFileInfo) IsDir() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type dedupFileInfo struct {
|
||||
os.FileInfo
|
||||
}
|
||||
var junctionPointModeMask os.FileMode
|
||||
|
||||
func (fi *dedupFileInfo) Mode() os.FileMode {
|
||||
// A deduplicated file should be treated as a regular file and not an
|
||||
// irregular file.
|
||||
return fi.FileInfo.Mode() &^ os.ModeIrregular
|
||||
func init() {
|
||||
junctionPointModeMask = os.ModeSymlink
|
||||
if version.Compare(runtime.Version(), "go1.23") >= 0 {
|
||||
// if GODEBUG=winsymlink=0, then we are in g1.22 mode so let's check for
|
||||
// os.ModeSymlink as well, as it can't hurt.
|
||||
junctionPointModeMask |= os.ModeIrregular
|
||||
}
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) underlyingLstat(name string) (os.FileInfo, error) {
|
||||
@@ -96,19 +93,11 @@ func (f *BasicFilesystem) underlyingLstat(name string) (os.FileInfo, error) {
|
||||
if err == nil {
|
||||
// NTFS directory junctions can be treated as ordinary directories,
|
||||
// see https://forum.syncthing.net/t/option-to-follow-directory-junctions-symbolic-links/14750
|
||||
if fi.Mode()&os.ModeSymlink != 0 && f.junctionsAsDirs {
|
||||
if fi.Mode()&junctionPointModeMask != 0 && f.junctionsAsDirs {
|
||||
if reparseTag, reparseErr := readReparseTag(name); reparseErr == nil && isDirectoryJunction(reparseTag) {
|
||||
return &dirJunctFileInfo{fi}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Workaround for #9120 till golang properly handles deduplicated files by
|
||||
// considering them regular files.
|
||||
if fi.Mode()&os.ModeIrregular != 0 {
|
||||
if reparseTag, reparseErr := readReparseTag(name); reparseErr == nil && isDeduplicatedFile(reparseTag) {
|
||||
return &dedupFileInfo{fi}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fi, err
|
||||
|
||||
+34
-38
@@ -50,7 +50,7 @@ type fskey struct {
|
||||
// caseFilesystemRegistry caches caseFilesystems and runs a routine to drop
|
||||
// their cache every now and then.
|
||||
type caseFilesystemRegistry struct {
|
||||
fss map[fskey]*caseFilesystem
|
||||
caseCaches map[fskey]*caseCache
|
||||
mut sync.RWMutex
|
||||
startCleaner sync.Once
|
||||
}
|
||||
@@ -77,18 +77,15 @@ func (r *caseFilesystemRegistry) get(fs Filesystem) Filesystem {
|
||||
// take a write lock and try again.
|
||||
|
||||
r.mut.RLock()
|
||||
caseFs, ok := r.fss[k]
|
||||
cache, ok := r.caseCaches[k]
|
||||
r.mut.RUnlock()
|
||||
|
||||
if !ok {
|
||||
r.mut.Lock()
|
||||
caseFs, ok = r.fss[k]
|
||||
cache, ok = r.caseCaches[k]
|
||||
if !ok {
|
||||
caseFs = &caseFilesystem{
|
||||
Filesystem: fs,
|
||||
realCaser: newDefaultRealCaser(fs),
|
||||
}
|
||||
r.fss[k] = caseFs
|
||||
cache = newCaseCache()
|
||||
r.caseCaches[k] = cache
|
||||
r.startCleaner.Do(func() {
|
||||
go r.cleaner()
|
||||
})
|
||||
@@ -96,7 +93,13 @@ func (r *caseFilesystemRegistry) get(fs Filesystem) Filesystem {
|
||||
r.mut.Unlock()
|
||||
}
|
||||
|
||||
return caseFs
|
||||
return &caseFilesystem{
|
||||
Filesystem: fs,
|
||||
realCaser: &defaultRealCaser{
|
||||
fs: fs,
|
||||
cache: cache,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *caseFilesystemRegistry) cleaner() {
|
||||
@@ -109,19 +112,19 @@ func (r *caseFilesystemRegistry) cleaner() {
|
||||
// within the loop.
|
||||
|
||||
r.mut.RLock()
|
||||
toProcess := make([]*caseFilesystem, 0, len(r.fss))
|
||||
for _, caseFs := range r.fss {
|
||||
toProcess = append(toProcess, caseFs)
|
||||
toProcess := make([]*caseCache, 0, len(r.caseCaches))
|
||||
for _, cache := range r.caseCaches {
|
||||
toProcess = append(toProcess, cache)
|
||||
}
|
||||
r.mut.RUnlock()
|
||||
|
||||
for _, caseFs := range toProcess {
|
||||
caseFs.dropCache()
|
||||
for _, cache := range toProcess {
|
||||
cache.Purge()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var globalCaseFilesystemRegistry = caseFilesystemRegistry{fss: make(map[fskey]*caseFilesystem)}
|
||||
var globalCaseFilesystemRegistry = caseFilesystemRegistry{caseCaches: make(map[fskey]*caseCache)}
|
||||
|
||||
// OptionDetectCaseConflicts ensures that the potentially case-insensitive filesystem
|
||||
// behaves like a case-sensitive filesystem. Meaning that it takes into account
|
||||
@@ -392,21 +395,20 @@ func (f *caseFilesystem) checkCaseExisting(name string) error {
|
||||
}
|
||||
|
||||
type defaultRealCaser struct {
|
||||
cache caseCache
|
||||
cache *caseCache
|
||||
fs Filesystem
|
||||
mut sync.Mutex
|
||||
}
|
||||
|
||||
func newDefaultRealCaser(fs Filesystem) *defaultRealCaser {
|
||||
type caseCache = lru.TwoQueueCache[string, *caseNode]
|
||||
|
||||
func newCaseCache() *caseCache {
|
||||
cache, err := lru.New2Q[string, *caseNode](caseCacheItemLimit)
|
||||
// New2Q only errors if given invalid parameters, which we don't.
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return &defaultRealCaser{
|
||||
cache: caseCache{
|
||||
fs: fs,
|
||||
TwoQueueCache: cache,
|
||||
},
|
||||
}
|
||||
return cache
|
||||
}
|
||||
|
||||
func (r *defaultRealCaser) realCase(name string) (string, error) {
|
||||
@@ -416,7 +418,7 @@ func (r *defaultRealCaser) realCase(name string) (string, error) {
|
||||
}
|
||||
|
||||
for _, comp := range PathComponents(name) {
|
||||
node := r.cache.getExpireAdd(realName)
|
||||
node := r.getExpireAdd(realName)
|
||||
|
||||
if node.err != nil {
|
||||
return "", node.err
|
||||
@@ -440,26 +442,20 @@ func (r *defaultRealCaser) dropCache() {
|
||||
r.cache.Purge()
|
||||
}
|
||||
|
||||
type caseCache struct {
|
||||
*lru.TwoQueueCache[string, *caseNode]
|
||||
fs Filesystem
|
||||
mut sync.Mutex
|
||||
}
|
||||
|
||||
// getExpireAdd gets an entry for the given key. If no entry exists, or it is
|
||||
// expired a new one is created and added to the cache.
|
||||
func (c *caseCache) getExpireAdd(key string) *caseNode {
|
||||
c.mut.Lock()
|
||||
defer c.mut.Unlock()
|
||||
node, ok := c.Get(key)
|
||||
func (r *defaultRealCaser) getExpireAdd(key string) *caseNode {
|
||||
r.mut.Lock()
|
||||
defer r.mut.Unlock()
|
||||
node, ok := r.cache.Get(key)
|
||||
if !ok {
|
||||
node := newCaseNode(key, c.fs)
|
||||
c.Add(key, node)
|
||||
node := newCaseNode(key, r.fs)
|
||||
r.cache.Add(key, node)
|
||||
return node
|
||||
}
|
||||
if node.expires.Before(time.Now()) {
|
||||
node = newCaseNode(key, c.fs)
|
||||
c.Add(key, node)
|
||||
node = newCaseNode(key, r.fs)
|
||||
r.cache.Add(key, node)
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
@@ -175,7 +175,10 @@ func BenchmarkWalkCaseFakeFS100k(b *testing.B) {
|
||||
// Construct the casefs manually or it will get cached and the benchmark is invalid.
|
||||
casefs := &caseFilesystem{
|
||||
Filesystem: fsys,
|
||||
realCaser: newDefaultRealCaser(fsys),
|
||||
realCaser: &defaultRealCaser{
|
||||
fs: fsys,
|
||||
cache: newCaseCache(),
|
||||
},
|
||||
}
|
||||
var fakefs *fakeFS
|
||||
if ffs, ok := unwrapFilesystem(fsys, filesystemWrapperTypeNone); ok {
|
||||
@@ -201,7 +204,10 @@ func BenchmarkWalkCaseFakeFS100k(b *testing.B) {
|
||||
// Construct the casefs manually or it will get cached and the benchmark is invalid.
|
||||
casefs := &caseFilesystem{
|
||||
Filesystem: fsys,
|
||||
realCaser: newDefaultRealCaser(fsys),
|
||||
realCaser: &defaultRealCaser{
|
||||
fs: fsys,
|
||||
cache: newCaseCache(),
|
||||
},
|
||||
}
|
||||
var fakefs *fakeFS
|
||||
if ffs, ok := unwrapFilesystem(fsys, filesystemWrapperTypeNone); ok {
|
||||
|
||||
+15
-9
@@ -54,18 +54,20 @@ const randomBlockShift = 14 // 128k
|
||||
// latency=d to set the amount of time each "disk" operation takes, where d is time.ParseDuration format
|
||||
// content=true to save actual file contents instead of generating pseudorandomly; n.b. memory usage
|
||||
// nostfolder=true skip the creation of .stfolder
|
||||
// timeprecisionsecond=true Modification times are stored with only second precision
|
||||
//
|
||||
// - Two fakeFS:s pointing at the same root path see the same files.
|
||||
type fakeFS struct {
|
||||
counters fakeFSCounters
|
||||
uri string
|
||||
mut sync.Mutex
|
||||
root *fakeEntry
|
||||
insens bool
|
||||
withContent bool
|
||||
latency time.Duration
|
||||
userCache *userCache
|
||||
groupCache *groupCache
|
||||
counters fakeFSCounters
|
||||
uri string
|
||||
mut sync.Mutex
|
||||
root *fakeEntry
|
||||
insens bool
|
||||
withContent bool
|
||||
timePrecisionSecond bool
|
||||
latency time.Duration
|
||||
userCache *userCache
|
||||
groupCache *groupCache
|
||||
}
|
||||
|
||||
type fakeFSCounters struct {
|
||||
@@ -126,6 +128,7 @@ func newFakeFilesystem(rootURI string, _ ...Option) *fakeFS {
|
||||
fs.insens = params.Get("insens") == "true"
|
||||
fs.withContent = params.Get("content") == "true"
|
||||
nostfolder := params.Get("nostfolder") == "true"
|
||||
fs.timePrecisionSecond = params.Get("timeprecisionsecond") == "true"
|
||||
|
||||
if sizeavg == 0 {
|
||||
sizeavg = 1 << 20
|
||||
@@ -259,6 +262,9 @@ func (fs *fakeFS) Chtimes(name string, _ time.Time, mtime time.Time) error {
|
||||
if entry == nil {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
if fs.timePrecisionSecond {
|
||||
mtime = mtime.Truncate(time.Second)
|
||||
}
|
||||
entry.mtime = mtime
|
||||
return nil
|
||||
}
|
||||
|
||||
+24
-9
@@ -261,11 +261,6 @@ func NewFilesystem(fsType FilesystemType, uri string, opts ...Option) Filesystem
|
||||
}
|
||||
}
|
||||
|
||||
// Case handling is the innermost, as any filesystem calls by wrappers should be case-resolved
|
||||
if caseOpt != nil {
|
||||
fs = caseOpt.apply(fs)
|
||||
}
|
||||
|
||||
// mtime handling should happen inside walking, as filesystem calls while
|
||||
// walking should be mtime-resolved too
|
||||
if mtimeOpt != nil {
|
||||
@@ -274,15 +269,35 @@ func NewFilesystem(fsType FilesystemType, uri string, opts ...Option) Filesystem
|
||||
|
||||
fs = &metricsFS{next: fs}
|
||||
|
||||
layersAboveWalkFilesystem := 0
|
||||
if caseOpt != nil {
|
||||
// DirNames calls made to check the case of a name will also be
|
||||
// attributed to the calling function.
|
||||
layersAboveWalkFilesystem++
|
||||
}
|
||||
if l.ShouldDebug("walkfs") {
|
||||
return NewWalkFilesystem(&logFilesystem{fs})
|
||||
// A walkFilesystem is not a layer to skip, it embeds the underlying
|
||||
// filesystem, passing calls directly trough. Except for calls made
|
||||
// during walking, however those are truly originating in the walk
|
||||
// filesystem.
|
||||
fs = NewWalkFilesystem(newLogFilesystem(fs, layersAboveWalkFilesystem))
|
||||
} else if l.ShouldDebug("fs") {
|
||||
fs = newLogFilesystem(NewWalkFilesystem(fs), layersAboveWalkFilesystem)
|
||||
} else {
|
||||
fs = NewWalkFilesystem(fs)
|
||||
}
|
||||
|
||||
if l.ShouldDebug("fs") {
|
||||
return &logFilesystem{NewWalkFilesystem(fs)}
|
||||
// Case handling is at the outermost layer to resolve all input names.
|
||||
// Reason being is that the only names/paths that are potentially "wrong"
|
||||
// come from outside the fs package. Any paths that result from filesystem
|
||||
// operations itself already have the correct case. Thus there's e.g. no
|
||||
// point to check the case on all the stating the walk filesystem does, it
|
||||
// just adds overhead.
|
||||
if caseOpt != nil {
|
||||
fs = caseOpt.apply(fs)
|
||||
}
|
||||
|
||||
return NewWalkFilesystem(fs)
|
||||
return fs
|
||||
}
|
||||
|
||||
// IsInternal returns true if the file, as a path relative to the folder
|
||||
|
||||
@@ -7,8 +7,10 @@
|
||||
package fs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/build"
|
||||
)
|
||||
@@ -171,3 +173,64 @@ func TestIsParent(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reproduces issue 9677:
|
||||
// The combination of caching the entire case FS and moving the case FS to be
|
||||
// the outmost layer of the FS lead to the mtime FS disappearing. This is
|
||||
// because in many places we intentionally create the filesystem without access
|
||||
// to the DB and thus without the mtime FS layer. With the case FS layer
|
||||
// outside, all the inner layers are also cached - notable without an mtime FS
|
||||
// layer. Later when we do try to create an FS with DB/mtime FS, we still get
|
||||
// the cached FS without mtime FS.
|
||||
func TestRepro9677MissingMtimeFS(t *testing.T) {
|
||||
mtimeDB := make(mapStore)
|
||||
name := "Testfile"
|
||||
nameLower := UnicodeLowercaseNormalized(name)
|
||||
testTime := time.Unix(1723491493, 123456789)
|
||||
|
||||
// Create a file with an mtime FS entry
|
||||
firstFS := NewFilesystem(FilesystemTypeFake, fmt.Sprintf("%v?insens=true&timeprecisionsecond=true", t.Name()), &OptionDetectCaseConflicts{}, NewMtimeOption(mtimeDB))
|
||||
|
||||
// Create a file, set its mtime and check that we get the expected mtime when stat-ing.
|
||||
file, err := firstFS.Create(name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
file.Close()
|
||||
err = firstFS.Chtimes(name, testTime, testTime)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
checkMtime := func(fs Filesystem) {
|
||||
t.Helper()
|
||||
info, err := fs.Lstat(name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !info.ModTime().Equal(testTime) {
|
||||
t.Errorf("Expected mtime %v for %v, got %v", testTime, name, info.ModTime())
|
||||
}
|
||||
info, err = fs.Lstat(nameLower)
|
||||
if !IsErrCaseConflict(err) {
|
||||
t.Errorf("Expected case-conflict error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
checkMtime(firstFS)
|
||||
|
||||
// Now syncthing gets upgraded (or even just restarted), which resets the
|
||||
// case FS registry as it lives in memory.
|
||||
globalCaseFilesystemRegistry = caseFilesystemRegistry{caseCaches: make(map[fskey]*caseCache)}
|
||||
|
||||
// This time we first create some filesystem without a database and thus no
|
||||
// mtime-FS, which is used in various places outside of the folder code. We
|
||||
// aren't actually going to do anything, this just adds an entry to the
|
||||
// caseFS cache. And that's the crucial bit: In the broken case this test is
|
||||
// reproducing, it will add the FS without mtime-FS, so all future FSes will
|
||||
// be without mtime, even if requested:
|
||||
NewFilesystem(FilesystemTypeFake, fmt.Sprintf("%v?insens=true&timeprecisionsecond=true", t.Name()), &OptionDetectCaseConflicts{})
|
||||
|
||||
newFS := NewFilesystem(FilesystemTypeFake, fmt.Sprintf("%v?insens=true&timeprecisionsecond=true", t.Name()), &OptionDetectCaseConflicts{}, NewMtimeOption(mtimeDB))
|
||||
checkMtime(newFS)
|
||||
}
|
||||
|
||||
+35
-25
@@ -16,10 +16,20 @@ import (
|
||||
|
||||
type logFilesystem struct {
|
||||
Filesystem
|
||||
// Number of filesystem layers on top of logFilesystem to skip when looking
|
||||
// for the true caller of the filesystem
|
||||
layers int
|
||||
}
|
||||
|
||||
func getCaller() string {
|
||||
_, file, line, ok := runtime.Caller(2)
|
||||
func newLogFilesystem(fs Filesystem, layers int) *logFilesystem {
|
||||
return &logFilesystem{
|
||||
Filesystem: fs,
|
||||
layers: layers,
|
||||
}
|
||||
}
|
||||
|
||||
func (fs *logFilesystem) getCaller() string {
|
||||
_, file, line, ok := runtime.Caller(fs.layers + 1)
|
||||
if !ok {
|
||||
return "unknown"
|
||||
}
|
||||
@@ -28,139 +38,139 @@ func getCaller() string {
|
||||
|
||||
func (fs *logFilesystem) Chmod(name string, mode FileMode) error {
|
||||
err := fs.Filesystem.Chmod(name, mode)
|
||||
l.Debugln(getCaller(), fs.Type(), fs.URI(), "Chmod", name, mode, err)
|
||||
l.Debugln(fs.getCaller(), fs.Type(), fs.URI(), "Chmod", name, mode, err)
|
||||
return err
|
||||
}
|
||||
|
||||
func (fs *logFilesystem) Chtimes(name string, atime time.Time, mtime time.Time) error {
|
||||
err := fs.Filesystem.Chtimes(name, atime, mtime)
|
||||
l.Debugln(getCaller(), fs.Type(), fs.URI(), "Chtimes", name, atime, mtime, err)
|
||||
l.Debugln(fs.getCaller(), fs.Type(), fs.URI(), "Chtimes", name, atime, mtime, err)
|
||||
return err
|
||||
}
|
||||
|
||||
func (fs *logFilesystem) Create(name string) (File, error) {
|
||||
file, err := fs.Filesystem.Create(name)
|
||||
l.Debugln(getCaller(), fs.Type(), fs.URI(), "Create", name, file, err)
|
||||
l.Debugln(fs.getCaller(), fs.Type(), fs.URI(), "Create", name, file, err)
|
||||
return file, err
|
||||
}
|
||||
|
||||
func (fs *logFilesystem) CreateSymlink(target, name string) error {
|
||||
err := fs.Filesystem.CreateSymlink(target, name)
|
||||
l.Debugln(getCaller(), fs.Type(), fs.URI(), "CreateSymlink", target, name, err)
|
||||
l.Debugln(fs.getCaller(), fs.Type(), fs.URI(), "CreateSymlink", target, name, err)
|
||||
return err
|
||||
}
|
||||
|
||||
func (fs *logFilesystem) DirNames(name string) ([]string, error) {
|
||||
names, err := fs.Filesystem.DirNames(name)
|
||||
l.Debugln(getCaller(), fs.Type(), fs.URI(), "DirNames", name, names, err)
|
||||
l.Debugln(fs.getCaller(), fs.Type(), fs.URI(), "DirNames", name, names, err)
|
||||
return names, err
|
||||
}
|
||||
|
||||
func (fs *logFilesystem) Lstat(name string) (FileInfo, error) {
|
||||
info, err := fs.Filesystem.Lstat(name)
|
||||
l.Debugln(getCaller(), fs.Type(), fs.URI(), "Lstat", name, info, err)
|
||||
l.Debugln(fs.getCaller(), fs.Type(), fs.URI(), "Lstat", name, info, err)
|
||||
return info, err
|
||||
}
|
||||
|
||||
func (fs *logFilesystem) Mkdir(name string, perm FileMode) error {
|
||||
err := fs.Filesystem.Mkdir(name, perm)
|
||||
l.Debugln(getCaller(), fs.Type(), fs.URI(), "Mkdir", name, perm, err)
|
||||
l.Debugln(fs.getCaller(), fs.Type(), fs.URI(), "Mkdir", name, perm, err)
|
||||
return err
|
||||
}
|
||||
|
||||
func (fs *logFilesystem) MkdirAll(name string, perm FileMode) error {
|
||||
err := fs.Filesystem.MkdirAll(name, perm)
|
||||
l.Debugln(getCaller(), fs.Type(), fs.URI(), "MkdirAll", name, perm, err)
|
||||
l.Debugln(fs.getCaller(), fs.Type(), fs.URI(), "MkdirAll", name, perm, err)
|
||||
return err
|
||||
}
|
||||
|
||||
func (fs *logFilesystem) Open(name string) (File, error) {
|
||||
file, err := fs.Filesystem.Open(name)
|
||||
l.Debugln(getCaller(), fs.Type(), fs.URI(), "Open", name, file, err)
|
||||
l.Debugln(fs.getCaller(), fs.Type(), fs.URI(), "Open", name, file, err)
|
||||
return file, err
|
||||
}
|
||||
|
||||
func (fs *logFilesystem) OpenFile(name string, flags int, mode FileMode) (File, error) {
|
||||
file, err := fs.Filesystem.OpenFile(name, flags, mode)
|
||||
l.Debugln(getCaller(), fs.Type(), fs.URI(), "OpenFile", name, flags, mode, file, err)
|
||||
l.Debugln(fs.getCaller(), fs.Type(), fs.URI(), "OpenFile", name, flags, mode, file, err)
|
||||
return file, err
|
||||
}
|
||||
|
||||
func (fs *logFilesystem) ReadSymlink(name string) (string, error) {
|
||||
target, err := fs.Filesystem.ReadSymlink(name)
|
||||
l.Debugln(getCaller(), fs.Type(), fs.URI(), "ReadSymlink", name, target, err)
|
||||
l.Debugln(fs.getCaller(), fs.Type(), fs.URI(), "ReadSymlink", name, target, err)
|
||||
return target, err
|
||||
}
|
||||
|
||||
func (fs *logFilesystem) Remove(name string) error {
|
||||
err := fs.Filesystem.Remove(name)
|
||||
l.Debugln(getCaller(), fs.Type(), fs.URI(), "Remove", name, err)
|
||||
l.Debugln(fs.getCaller(), fs.Type(), fs.URI(), "Remove", name, err)
|
||||
return err
|
||||
}
|
||||
|
||||
func (fs *logFilesystem) RemoveAll(name string) error {
|
||||
err := fs.Filesystem.RemoveAll(name)
|
||||
l.Debugln(getCaller(), fs.Type(), fs.URI(), "RemoveAll", name, err)
|
||||
l.Debugln(fs.getCaller(), fs.Type(), fs.URI(), "RemoveAll", name, err)
|
||||
return err
|
||||
}
|
||||
|
||||
func (fs *logFilesystem) Rename(oldname, newname string) error {
|
||||
err := fs.Filesystem.Rename(oldname, newname)
|
||||
l.Debugln(getCaller(), fs.Type(), fs.URI(), "Rename", oldname, newname, err)
|
||||
l.Debugln(fs.getCaller(), fs.Type(), fs.URI(), "Rename", oldname, newname, err)
|
||||
return err
|
||||
}
|
||||
|
||||
func (fs *logFilesystem) Stat(name string) (FileInfo, error) {
|
||||
info, err := fs.Filesystem.Stat(name)
|
||||
l.Debugln(getCaller(), fs.Type(), fs.URI(), "Stat", name, info, err)
|
||||
l.Debugln(fs.getCaller(), fs.Type(), fs.URI(), "Stat", name, info, err)
|
||||
return info, err
|
||||
}
|
||||
|
||||
func (fs *logFilesystem) SymlinksSupported() bool {
|
||||
supported := fs.Filesystem.SymlinksSupported()
|
||||
l.Debugln(getCaller(), fs.Type(), fs.URI(), "SymlinksSupported", supported)
|
||||
l.Debugln(fs.getCaller(), fs.Type(), fs.URI(), "SymlinksSupported", supported)
|
||||
return supported
|
||||
}
|
||||
|
||||
func (fs *logFilesystem) Walk(root string, walkFn WalkFunc) error {
|
||||
err := fs.Filesystem.Walk(root, walkFn)
|
||||
l.Debugln(getCaller(), fs.Type(), fs.URI(), "Walk", root, walkFn, err)
|
||||
l.Debugln(fs.getCaller(), fs.Type(), fs.URI(), "Walk", root, walkFn, err)
|
||||
return err
|
||||
}
|
||||
|
||||
func (fs *logFilesystem) Watch(path string, ignore Matcher, ctx context.Context, ignorePerms bool) (<-chan Event, <-chan error, error) {
|
||||
evChan, errChan, err := fs.Filesystem.Watch(path, ignore, ctx, ignorePerms)
|
||||
l.Debugln(getCaller(), fs.Type(), fs.URI(), "Watch", path, ignore, ignorePerms, err)
|
||||
l.Debugln(fs.getCaller(), fs.Type(), fs.URI(), "Watch", path, ignore, ignorePerms, err)
|
||||
return evChan, errChan, err
|
||||
}
|
||||
|
||||
func (fs *logFilesystem) Unhide(name string) error {
|
||||
err := fs.Filesystem.Unhide(name)
|
||||
l.Debugln(getCaller(), fs.Type(), fs.URI(), "Unhide", name, err)
|
||||
l.Debugln(fs.getCaller(), fs.Type(), fs.URI(), "Unhide", name, err)
|
||||
return err
|
||||
}
|
||||
|
||||
func (fs *logFilesystem) Hide(name string) error {
|
||||
err := fs.Filesystem.Hide(name)
|
||||
l.Debugln(getCaller(), fs.Type(), fs.URI(), "Hide", name, err)
|
||||
l.Debugln(fs.getCaller(), fs.Type(), fs.URI(), "Hide", name, err)
|
||||
return err
|
||||
}
|
||||
|
||||
func (fs *logFilesystem) Glob(name string) ([]string, error) {
|
||||
names, err := fs.Filesystem.Glob(name)
|
||||
l.Debugln(getCaller(), fs.Type(), fs.URI(), "Glob", name, names, err)
|
||||
l.Debugln(fs.getCaller(), fs.Type(), fs.URI(), "Glob", name, names, err)
|
||||
return names, err
|
||||
}
|
||||
|
||||
func (fs *logFilesystem) Roots() ([]string, error) {
|
||||
roots, err := fs.Filesystem.Roots()
|
||||
l.Debugln(getCaller(), fs.Type(), fs.URI(), "Roots", roots, err)
|
||||
l.Debugln(fs.getCaller(), fs.Type(), fs.URI(), "Roots", roots, err)
|
||||
return roots, err
|
||||
}
|
||||
|
||||
func (fs *logFilesystem) Usage(name string) (Usage, error) {
|
||||
usage, err := fs.Filesystem.Usage(name)
|
||||
l.Debugln(getCaller(), fs.Type(), fs.URI(), "Usage", name, usage, err)
|
||||
l.Debugln(fs.getCaller(), fs.Type(), fs.URI(), "Usage", name, usage, err)
|
||||
return usage, err
|
||||
}
|
||||
|
||||
|
||||
@@ -30,8 +30,8 @@ func newFakeConnection(id protocol.DeviceID, model Model) *fakeConnection {
|
||||
model: model,
|
||||
closed: make(chan struct{}),
|
||||
}
|
||||
f.RequestCalls(func(ctx context.Context, folder, name string, blockNo int, offset int64, size int, hash []byte, weakHash uint32, fromTemporary bool) ([]byte, error) {
|
||||
return f.fileData[name], nil
|
||||
f.RequestCalls(func(ctx context.Context, req *protocol.Request) ([]byte, error) {
|
||||
return f.fileData[req.Name], nil
|
||||
})
|
||||
f.DeviceIDReturns(id)
|
||||
f.ConnectionIDReturns(rand.String(16))
|
||||
@@ -60,14 +60,16 @@ type fakeConnection struct {
|
||||
}
|
||||
|
||||
func (f *fakeConnection) setIndexFn(fn func(_ context.Context, folder string, fs []protocol.FileInfo) error) {
|
||||
f.IndexCalls(fn)
|
||||
f.IndexUpdateCalls(fn)
|
||||
f.IndexCalls(func(ctx context.Context, idx *protocol.Index) error { return fn(ctx, idx.Folder, idx.Files) })
|
||||
f.IndexUpdateCalls(func(ctx context.Context, idxUp *protocol.IndexUpdate) error {
|
||||
return fn(ctx, idxUp.Folder, idxUp.Files)
|
||||
})
|
||||
}
|
||||
|
||||
func (f *fakeConnection) DownloadProgress(_ context.Context, folder string, updates []protocol.FileDownloadProgressUpdate) {
|
||||
func (f *fakeConnection) DownloadProgress(_ context.Context, dp *protocol.DownloadProgress) {
|
||||
f.downloadProgressMessages = append(f.downloadProgressMessages, downloadProgressMessage{
|
||||
folder: folder,
|
||||
updates: updates,
|
||||
folder: dp.Folder,
|
||||
updates: dp.Updates,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+128
-10
@@ -17,6 +17,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"
|
||||
)
|
||||
|
||||
type indexHandler struct {
|
||||
@@ -222,15 +223,55 @@ func (s *indexHandler) pause() {
|
||||
// sendIndexTo sends file infos with a sequence number higher than prevSequence and
|
||||
// returns the highest sent sequence number.
|
||||
func (s *indexHandler) sendIndexTo(ctx context.Context, fset *db.FileSet) error {
|
||||
// Keep track of the previous sequence we sent. This is separate from
|
||||
// s.prevSequence because the latter will skip over holes in the
|
||||
// sequence numberings, while sentPrevSequence should always be
|
||||
// precisely the highest previously sent sequence.
|
||||
sentPrevSequence := s.prevSequence
|
||||
|
||||
initial := s.prevSequence == 0
|
||||
batch := db.NewFileInfoBatch(nil)
|
||||
var batchError error
|
||||
batch.SetFlushFunc(func(fs []protocol.FileInfo) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
if len(fs) == 0 {
|
||||
// can't happen, flush is not called with an empty batch
|
||||
panic("bug: flush called with empty batch (race condition?)")
|
||||
}
|
||||
if batchError != nil {
|
||||
// can't happen, once an error is returned the index sender exits
|
||||
panic(fmt.Sprintf("bug: once failed it should stay failed (%v)", batchError))
|
||||
}
|
||||
l.Debugf("%v: Sending %d files (<%d bytes)", s, len(fs), batch.Size())
|
||||
|
||||
lastSequence := fs[len(fs)-1].Sequence
|
||||
var err error
|
||||
if initial {
|
||||
initial = false
|
||||
return s.conn.Index(ctx, s.folder, fs)
|
||||
err = s.conn.Index(ctx, &protocol.Index{
|
||||
Folder: s.folder,
|
||||
Files: fs,
|
||||
LastSequence: lastSequence,
|
||||
})
|
||||
} else {
|
||||
err = s.conn.IndexUpdate(ctx, &protocol.IndexUpdate{
|
||||
Folder: s.folder,
|
||||
Files: fs,
|
||||
PrevSequence: sentPrevSequence,
|
||||
LastSequence: lastSequence,
|
||||
})
|
||||
}
|
||||
return s.conn.IndexUpdate(ctx, s.folder, fs)
|
||||
if err != nil {
|
||||
batchError = err
|
||||
return err
|
||||
}
|
||||
sentPrevSequence = lastSequence
|
||||
return nil
|
||||
})
|
||||
|
||||
var err error
|
||||
@@ -251,14 +292,19 @@ func (s *indexHandler) sendIndexTo(ctx context.Context, fset *db.FileSet) error
|
||||
}
|
||||
}
|
||||
|
||||
if shouldDebug() {
|
||||
if fi.SequenceNo() < s.prevSequence+1 {
|
||||
panic(fmt.Sprintln("sequence lower than requested, got:", fi.SequenceNo(), ", asked to start at:", s.prevSequence+1))
|
||||
}
|
||||
if fi.SequenceNo() < s.prevSequence+1 {
|
||||
s.logSequenceAnomaly("database returned sequence lower than requested", map[string]any{
|
||||
"sequence": fi.SequenceNo(),
|
||||
"start": s.prevSequence + 1,
|
||||
})
|
||||
}
|
||||
|
||||
if f.Sequence > 0 && fi.SequenceNo() <= f.Sequence {
|
||||
l.Warnln("Non-increasing sequence detected: Checking and repairing the db...")
|
||||
s.logSequenceAnomaly("database returned non-increasing sequence", map[string]any{
|
||||
"sequence": fi.SequenceNo(),
|
||||
"start": s.prevSequence + 1,
|
||||
"previous": f.Sequence,
|
||||
})
|
||||
// Abort this round of index sending - the next one will pick
|
||||
// up from the last successful one with the repeaired db.
|
||||
defer func() {
|
||||
@@ -307,7 +353,7 @@ func (s *indexHandler) sendIndexTo(ctx context.Context, fset *db.FileSet) error
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *indexHandler) receive(fs []protocol.FileInfo, update bool, op string) error {
|
||||
func (s *indexHandler) receive(fs []protocol.FileInfo, update bool, op string, prevSequence, lastSequence int64) error {
|
||||
deviceID := s.conn.DeviceID()
|
||||
|
||||
s.cond.L.Lock()
|
||||
@@ -328,12 +374,66 @@ func (s *indexHandler) receive(fs []protocol.FileInfo, update bool, op string) e
|
||||
if !update {
|
||||
fset.Drop(deviceID)
|
||||
}
|
||||
|
||||
l.Debugf("Received %d files for %s from %s, prevSeq=%d, lastSeq=%d", len(fs), s.folder, deviceID.Short(), prevSequence, lastSequence)
|
||||
|
||||
// Verify that the previous sequence number matches what we expected
|
||||
if exp := fset.Sequence(deviceID); prevSequence > 0 && prevSequence != exp {
|
||||
s.logSequenceAnomaly("index update with unexpected sequence", map[string]any{
|
||||
"prevSeq": prevSequence,
|
||||
"lastSeq": lastSequence,
|
||||
"batch": len(fs),
|
||||
"expectedPrev": exp,
|
||||
})
|
||||
}
|
||||
|
||||
for i := range fs {
|
||||
// Verify index in relation to the claimed sequence boundaries
|
||||
if fs[i].Sequence < prevSequence {
|
||||
s.logSequenceAnomaly("file with sequence before prevSequence", map[string]any{
|
||||
"prevSeq": prevSequence,
|
||||
"lastSeq": lastSequence,
|
||||
"batch": len(fs),
|
||||
"seenSeq": fs[i].Sequence,
|
||||
"atIndex": i,
|
||||
})
|
||||
}
|
||||
if lastSequence > 0 && fs[i].Sequence > lastSequence {
|
||||
s.logSequenceAnomaly("file with sequence after lastSequence", map[string]any{
|
||||
"prevSeq": prevSequence,
|
||||
"lastSeq": lastSequence,
|
||||
"batch": len(fs),
|
||||
"seenSeq": fs[i].Sequence,
|
||||
"atIndex": i,
|
||||
})
|
||||
}
|
||||
if i > 0 && fs[i].Sequence <= fs[i-1].Sequence {
|
||||
s.logSequenceAnomaly("index update with non-increasing sequence", map[string]any{
|
||||
"prevSeq": prevSequence,
|
||||
"lastSeq": lastSequence,
|
||||
"batch": len(fs),
|
||||
"seenSeq": fs[i].Sequence,
|
||||
"atIndex": i,
|
||||
"precedingSeq": fs[i-1].Sequence,
|
||||
})
|
||||
}
|
||||
|
||||
// The local attributes should never be transmitted over the wire.
|
||||
// Make sure they look like they weren't.
|
||||
fs[i].LocalFlags = 0
|
||||
fs[i].VersionHash = nil
|
||||
}
|
||||
|
||||
// Verify the claimed last sequence number
|
||||
if lastSequence > 0 && len(fs) > 0 && lastSequence != fs[len(fs)-1].Sequence {
|
||||
s.logSequenceAnomaly("index update with unexpected last sequence", map[string]any{
|
||||
"prevSeq": prevSequence,
|
||||
"lastSeq": lastSequence,
|
||||
"batch": len(fs),
|
||||
"seenSeq": fs[len(fs)-1].Sequence,
|
||||
})
|
||||
}
|
||||
|
||||
fset.Update(deviceID, fs)
|
||||
|
||||
seq := fset.Sequence(deviceID)
|
||||
@@ -348,6 +448,24 @@ func (s *indexHandler) receive(fs []protocol.FileInfo, update bool, op string) e
|
||||
return nil
|
||||
}
|
||||
|
||||
var warnSequenceAnomalyOnce sync.Once
|
||||
|
||||
func (s *indexHandler) logSequenceAnomaly(msg string, extra map[string]any) {
|
||||
warnSequenceAnomalyOnce.Do(func() {
|
||||
l.Warnf("Index sequence anomaly detected (please report at https://forum.syncthing.net/t/22660): %s (%v)", msg, extra)
|
||||
})
|
||||
|
||||
extraStrs := make(map[string]string, len(extra))
|
||||
for k, v := range extra {
|
||||
extraStrs[k] = fmt.Sprint(v)
|
||||
}
|
||||
|
||||
s.evLogger.Log(events.Failure, ur.FailureData{
|
||||
Description: msg,
|
||||
Extra: extraStrs,
|
||||
})
|
||||
}
|
||||
|
||||
func prepareFileInfoForIndex(f protocol.FileInfo) protocol.FileInfo {
|
||||
// Mark the file as invalid if any of the local bad stuff flags are set.
|
||||
f.RawInvalid = f.IsInvalid()
|
||||
@@ -534,7 +652,7 @@ func (r *indexHandlerRegistry) folderRunningLocked(folder config.FolderConfigura
|
||||
}
|
||||
}
|
||||
|
||||
func (r *indexHandlerRegistry) ReceiveIndex(folder string, fs []protocol.FileInfo, update bool, op string) error {
|
||||
func (r *indexHandlerRegistry) ReceiveIndex(folder string, fs []protocol.FileInfo, update bool, op string, prevSequence, lastSequence int64) error {
|
||||
r.mut.Lock()
|
||||
defer r.mut.Unlock()
|
||||
is, isOk := r.indexHandlers.Get(folder)
|
||||
@@ -542,7 +660,7 @@ func (r *indexHandlerRegistry) ReceiveIndex(folder string, fs []protocol.FileInf
|
||||
l.Infof("%v for nonexistent or paused folder %q", op, folder)
|
||||
return fmt.Errorf("%s: %w", folder, ErrFolderMissing)
|
||||
}
|
||||
return is.receive(fs, update, op)
|
||||
return is.receive(fs, update, op, prevSequence, lastSequence)
|
||||
}
|
||||
|
||||
// makeForgetUpdate takes an index update and constructs a download progress update
|
||||
|
||||
@@ -40,10 +40,10 @@ func TestIndexhandlerConcurrency(t *testing.T) {
|
||||
c2.Start()
|
||||
defer c2.Close(io.EOF)
|
||||
|
||||
c1.ClusterConfig(protocol.ClusterConfig{})
|
||||
c2.ClusterConfig(protocol.ClusterConfig{})
|
||||
c1.Index(ctx, "foo", nil)
|
||||
c2.Index(ctx, "foo", nil)
|
||||
c1.ClusterConfig(&protocol.ClusterConfig{})
|
||||
c2.ClusterConfig(&protocol.ClusterConfig{})
|
||||
c1.Index(ctx, &protocol.Index{Folder: "foo"})
|
||||
c2.Index(ctx, &protocol.Index{Folder: "foo"})
|
||||
|
||||
const msgs = 5e2
|
||||
const files = 1e3
|
||||
@@ -64,7 +64,7 @@ func TestIndexhandlerConcurrency(t *testing.T) {
|
||||
})
|
||||
|
||||
b1 := db.NewFileInfoBatch(func(fs []protocol.FileInfo) error {
|
||||
return c1.IndexUpdate(ctx, "foo", fs)
|
||||
return c1.IndexUpdate(ctx, &protocol.IndexUpdate{Folder: "foo", Files: fs})
|
||||
})
|
||||
sentEntries := 0
|
||||
for i := 0; i < msgs; i++ {
|
||||
|
||||
+10
-9
@@ -1136,16 +1136,16 @@ func (p *pager) done() bool {
|
||||
// Index is called when a new device is connected and we receive their full index.
|
||||
// Implements the protocol.Model interface.
|
||||
func (m *model) Index(conn protocol.Connection, idx *protocol.Index) error {
|
||||
return m.handleIndex(conn, idx.Folder, idx.Files, false)
|
||||
return m.handleIndex(conn, idx.Folder, idx.Files, false, 0, idx.LastSequence)
|
||||
}
|
||||
|
||||
// IndexUpdate is called for incremental updates to connected devices' indexes.
|
||||
// Implements the protocol.Model interface.
|
||||
func (m *model) IndexUpdate(conn protocol.Connection, idxUp *protocol.IndexUpdate) error {
|
||||
return m.handleIndex(conn, idxUp.Folder, idxUp.Files, true)
|
||||
return m.handleIndex(conn, idxUp.Folder, idxUp.Files, true, idxUp.PrevSequence, idxUp.LastSequence)
|
||||
}
|
||||
|
||||
func (m *model) handleIndex(conn protocol.Connection, folder string, fs []protocol.FileInfo, update bool) error {
|
||||
func (m *model) handleIndex(conn protocol.Connection, folder string, fs []protocol.FileInfo, update bool, prevSequence, lastSequence int64) error {
|
||||
op := "Index"
|
||||
if update {
|
||||
op += " update"
|
||||
@@ -1173,7 +1173,8 @@ func (m *model) handleIndex(conn protocol.Connection, folder string, fs []protoc
|
||||
l.Debugf("%v for folder (ID %q) sent from device %q: missing index handler", op, folder, deviceID)
|
||||
return fmt.Errorf("%s: %w", folder, ErrFolderNotRunning)
|
||||
}
|
||||
return indexHandler.ReceiveIndex(folder, fs, update, op)
|
||||
|
||||
return indexHandler.ReceiveIndex(folder, fs, update, op, prevSequence, lastSequence)
|
||||
}
|
||||
|
||||
type clusterConfigDeviceInfo struct {
|
||||
@@ -2414,7 +2415,7 @@ func (m *model) promoteConnections() {
|
||||
if conn.Statistics().StartedAt.IsZero() {
|
||||
conn.SetFolderPasswords(passwords)
|
||||
conn.Start()
|
||||
conn.ClusterConfig(protocol.ClusterConfig{Secondary: true})
|
||||
conn.ClusterConfig(&protocol.ClusterConfig{Secondary: true})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2469,7 +2470,7 @@ func (m *model) RequestGlobal(ctx context.Context, deviceID protocol.DeviceID, f
|
||||
}
|
||||
|
||||
l.Debugf("%v REQ(out): %s (%s): %q / %q b=%d o=%d s=%d h=%x wh=%x ft=%t", m, deviceID.Short(), conn, folder, name, blockNo, offset, size, hash, weakHash, fromTemporary)
|
||||
return conn.Request(ctx, folder, name, blockNo, offset, size, hash, weakHash, fromTemporary)
|
||||
return conn.Request(ctx, &protocol.Request{Folder: folder, Name: name, BlockNo: blockNo, Offset: offset, Size: size, Hash: hash, WeakHash: weakHash, FromTemporary: fromTemporary})
|
||||
}
|
||||
|
||||
// requestConnectionForDevice returns a connection to the given device, to
|
||||
@@ -2586,14 +2587,14 @@ func (m *model) numHashers(folder string) int {
|
||||
|
||||
// generateClusterConfig returns a ClusterConfigMessage that is correct and the
|
||||
// set of folder passwords for the given peer device
|
||||
func (m *model) generateClusterConfig(device protocol.DeviceID) (protocol.ClusterConfig, map[string]string) {
|
||||
func (m *model) generateClusterConfig(device protocol.DeviceID) (*protocol.ClusterConfig, map[string]string) {
|
||||
m.mut.RLock()
|
||||
defer m.mut.RUnlock()
|
||||
return m.generateClusterConfigRLocked(device)
|
||||
}
|
||||
|
||||
func (m *model) generateClusterConfigRLocked(device protocol.DeviceID) (protocol.ClusterConfig, map[string]string) {
|
||||
var message protocol.ClusterConfig
|
||||
func (m *model) generateClusterConfigRLocked(device protocol.DeviceID) (*protocol.ClusterConfig, map[string]string) {
|
||||
message := &protocol.ClusterConfig{}
|
||||
folders := m.cfg.FolderList()
|
||||
passwords := make(map[string]string, len(folders))
|
||||
for _, folderCfg := range folders {
|
||||
|
||||
@@ -3631,11 +3631,11 @@ func testConfigChangeTriggersClusterConfigs(t *testing.T, expectFirst, expectSec
|
||||
cc1 := make(chan struct{}, 1)
|
||||
cc2 := make(chan struct{}, 1)
|
||||
fc1 := newFakeConnection(device1, m)
|
||||
fc1.ClusterConfigCalls(func(_ protocol.ClusterConfig) {
|
||||
fc1.ClusterConfigCalls(func(_ *protocol.ClusterConfig) {
|
||||
cc1 <- struct{}{}
|
||||
})
|
||||
fc2 := newFakeConnection(device2, m)
|
||||
fc2.ClusterConfigCalls(func(_ protocol.ClusterConfig) {
|
||||
fc2.ClusterConfigCalls(func(_ *protocol.ClusterConfig) {
|
||||
cc2 <- struct{}{}
|
||||
})
|
||||
m.AddConnection(fc1, protocol.Hello{})
|
||||
|
||||
@@ -39,7 +39,7 @@ type progressUpdate struct {
|
||||
}
|
||||
|
||||
func (p progressUpdate) send(ctx context.Context) {
|
||||
p.conn.DownloadProgress(ctx, p.folder, p.updates)
|
||||
p.conn.DownloadProgress(ctx, &protocol.DownloadProgress{Folder: p.folder, Updates: p.updates})
|
||||
}
|
||||
|
||||
// NewProgressEmitter creates a new progress emitter which emits
|
||||
@@ -334,7 +334,7 @@ func (t *ProgressEmitter) clearLocked() {
|
||||
}
|
||||
for _, folder := range state.folders() {
|
||||
if updates := state.cleanup(folder); len(updates) > 0 {
|
||||
conn.DownloadProgress(context.Background(), folder, updates)
|
||||
conn.DownloadProgress(context.Background(), &protocol.DownloadProgress{Folder: folder, Updates: updates})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-11
@@ -143,11 +143,11 @@ func TestSymlinkTraversalWrite(t *testing.T) {
|
||||
}
|
||||
return nil
|
||||
})
|
||||
fc.RequestCalls(func(ctx context.Context, folder, name string, blockNo int, offset int64, size int, hash []byte, weakHash uint32, fromTemporary bool) ([]byte, error) {
|
||||
if name != "symlink" && strings.HasPrefix(name, "symlink") {
|
||||
badReq <- name
|
||||
fc.RequestCalls(func(ctx context.Context, req *protocol.Request) ([]byte, error) {
|
||||
if req.Name != "symlink" && strings.HasPrefix(req.Name, "symlink") {
|
||||
badReq <- req.Name
|
||||
}
|
||||
return fc.fileData[name], nil
|
||||
return fc.fileData[req.Name], nil
|
||||
})
|
||||
|
||||
// Send an update for the symlink, wait for it to sync and be reported back.
|
||||
@@ -338,7 +338,7 @@ func pullInvalidIgnored(t *testing.T, ft config.FolderType) {
|
||||
})
|
||||
// Make sure pulling doesn't interfere, as index updates are racy and
|
||||
// thus we cannot distinguish between scan and pull results.
|
||||
fc.RequestCalls(func(ctx context.Context, folder, name string, blockNo int, offset int64, size int, hash []byte, weakHash uint32, fromTemporary bool) ([]byte, error) {
|
||||
fc.RequestCalls(func(_ context.Context, _ *protocol.Request) ([]byte, error) {
|
||||
return nil, nil
|
||||
})
|
||||
|
||||
@@ -926,7 +926,7 @@ func TestNeedFolderFiles(t *testing.T) {
|
||||
defer sub.Unsubscribe()
|
||||
|
||||
errPreventSync := errors.New("you aren't getting any of this")
|
||||
fc.RequestCalls(func(ctx context.Context, folder, name string, blockNo int, offset int64, size int, hash []byte, weakHash uint32, fromTemporary bool) ([]byte, error) {
|
||||
fc.RequestCalls(func(_ context.Context, _ *protocol.Request) ([]byte, error) {
|
||||
return nil, errPreventSync
|
||||
})
|
||||
|
||||
@@ -1065,9 +1065,9 @@ func TestRequestLastFileProgress(t *testing.T) {
|
||||
|
||||
done := make(chan struct{})
|
||||
|
||||
fc.RequestCalls(func(ctx context.Context, folder, name string, blockNo int, offset int64, size int, hash []byte, weakHash uint32, fromTemporary bool) ([]byte, error) {
|
||||
fc.RequestCalls(func(_ context.Context, req *protocol.Request) ([]byte, error) {
|
||||
defer close(done)
|
||||
progress, queued, rest, err := m.NeedFolderFiles(folder, 1, 10)
|
||||
progress, queued, rest, err := m.NeedFolderFiles(req.Folder, 1, 10)
|
||||
must(t, err)
|
||||
if len(queued)+len(rest) != 0 {
|
||||
t.Error(`There should not be any queued or "rest" items`)
|
||||
@@ -1075,7 +1075,7 @@ func TestRequestLastFileProgress(t *testing.T) {
|
||||
if len(progress) != 1 {
|
||||
t.Error("Expected exactly one item in progress.")
|
||||
}
|
||||
return fc.fileData[name], nil
|
||||
return fc.fileData[req.Name], nil
|
||||
})
|
||||
|
||||
contents := []byte("test file contents\n")
|
||||
@@ -1232,7 +1232,7 @@ func TestRequestIndexSenderClusterConfigBeforeStart(t *testing.T) {
|
||||
done := make(chan struct{})
|
||||
defer close(done) // Must be the last thing to be deferred, thus first to run.
|
||||
indexChan := make(chan []protocol.FileInfo, 1)
|
||||
ccChan := make(chan protocol.ClusterConfig, 1)
|
||||
ccChan := make(chan *protocol.ClusterConfig, 1)
|
||||
fc.setIndexFn(func(_ context.Context, folder string, fs []protocol.FileInfo) error {
|
||||
select {
|
||||
case indexChan <- fs:
|
||||
@@ -1240,7 +1240,7 @@ func TestRequestIndexSenderClusterConfigBeforeStart(t *testing.T) {
|
||||
}
|
||||
return nil
|
||||
})
|
||||
fc.ClusterConfigCalls(func(cc protocol.ClusterConfig) {
|
||||
fc.ClusterConfigCalls(func(cc *protocol.ClusterConfig) {
|
||||
select {
|
||||
case ccChan <- cc:
|
||||
case <-done:
|
||||
|
||||
+13
-6
@@ -10,22 +10,29 @@ import (
|
||||
"net"
|
||||
)
|
||||
|
||||
func GetLans() ([]*net.IPNet, error) {
|
||||
ifs, err := net.Interfaces()
|
||||
// GetInterfaceAddrs returns the IP networks of all interfaces that are up.
|
||||
// Point-to-point interfaces are exluded unless includePtP is true.
|
||||
func GetInterfaceAddrs(includePtP bool) ([]*net.IPNet, error) {
|
||||
intfs, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var addrs []net.Addr
|
||||
|
||||
for _, currentIf := range ifs {
|
||||
if currentIf.Flags&net.FlagRunning == 0 {
|
||||
for _, intf := range intfs {
|
||||
if intf.Flags&net.FlagRunning == 0 {
|
||||
continue
|
||||
}
|
||||
currentAddrs, err := currentIf.Addrs()
|
||||
if !includePtP && intf.Flags&net.FlagPointToPoint != 0 {
|
||||
// Point-to-point interfaces are typically VPNs and similar
|
||||
// which, for our purposes, do not qualify as LANs.
|
||||
continue
|
||||
}
|
||||
intfAddrs, err := intf.Addrs()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addrs = append(addrs, currentAddrs...)
|
||||
addrs = append(addrs, intfAddrs...)
|
||||
}
|
||||
|
||||
nets := make([]*net.IPNet, 0, len(addrs))
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user