mirror of
https://github.com/syncthing/syncthing.git
synced 2026-01-20 11:48:06 -05:00
Compare commits
27 Commits
v2.0.0-rc.
...
v2.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d8033343f | ||
|
|
c74d2a9872 | ||
|
|
3da84804b6 | ||
|
|
5b75c6ddcb | ||
|
|
ae03854575 | ||
|
|
ad196173d0 | ||
|
|
d682220305 | ||
|
|
29e10e00d2 | ||
|
|
34f61ce464 | ||
|
|
adcbd31e62 | ||
|
|
431da839cf | ||
|
|
836045ee87 | ||
|
|
49462448d0 | ||
|
|
e3424ad503 | ||
|
|
5703423c00 | ||
|
|
356ec26c87 | ||
|
|
d37cb02e40 | ||
|
|
953944e54e | ||
|
|
6e26fab3a0 | ||
|
|
532e30eb6b | ||
|
|
54bb987fae | ||
|
|
74367d2f66 | ||
|
|
0f6750c8f5 | ||
|
|
c8c38f735f | ||
|
|
fa4bd5c057 | ||
|
|
36fb5425a5 | ||
|
|
32a913c0ff |
11
.github/workflows/build-syncthing.yaml
vendored
11
.github/workflows/build-syncthing.yaml
vendored
@@ -47,6 +47,7 @@ jobs:
|
||||
outputs:
|
||||
version: ${{ steps.get-version.outputs.version }}
|
||||
release-kind: ${{ steps.get-version.outputs.release-kind }}
|
||||
release-generation: ${{ steps.get-version.outputs.release-generation }}
|
||||
go-version: ${{ steps.get-go.outputs.go-version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -76,6 +77,12 @@ jobs:
|
||||
echo "release-kind=$kind" >> "$GITHUB_OUTPUT"
|
||||
echo "Release kind: $kind"
|
||||
|
||||
generation=v1
|
||||
if [[ $version == v2.* ]] ; then
|
||||
generation=v2
|
||||
fi
|
||||
echo "release-generation=$generation" >> "$GITHUB_OUTPUT"
|
||||
echo "Release generation: $generation"
|
||||
- name: Get Go version
|
||||
id: get-go
|
||||
run: |
|
||||
@@ -879,6 +886,7 @@ jobs:
|
||||
env:
|
||||
VERSION: ${{ needs.facts.outputs.version }}
|
||||
RELEASE_KIND: ${{ needs.facts.outputs.release-kind }}
|
||||
RELEASE_GENERATION: ${{ needs.facts.outputs.release-generation }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -892,6 +900,9 @@ jobs:
|
||||
# Decide whether packages should go to stable, candidate or nightly
|
||||
- name: Prepare packages
|
||||
run: |
|
||||
if [[ $RELEASE_KIND == stable && $RELEASE_GENERATION == v2 ]] ; then
|
||||
RELEASE_KIND=stable-v2
|
||||
fi
|
||||
mkdir -p packages/syncthing/$RELEASE_KIND
|
||||
mv packages/*.deb packages/syncthing/$RELEASE_KIND
|
||||
|
||||
|
||||
18
.github/workflows/mirrors.yaml
vendored
Normal file
18
.github/workflows/mirrors.yaml
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
name: Mirrors
|
||||
|
||||
on: [push, delete]
|
||||
|
||||
jobs:
|
||||
codeberg:
|
||||
name: Mirror to Codeberg
|
||||
if: github.repository_owner == 'syncthing'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: yesolutions/mirror-action@master
|
||||
with:
|
||||
REMOTE: ssh://git@codeberg.org/${{ github.repository }}.git
|
||||
GIT_SSH_PRIVATE_KEY: ${{ secrets.CODEBERG_PUSH_KEY }}
|
||||
GIT_SSH_NO_VERIFY_HOST: "true"
|
||||
@@ -60,6 +60,16 @@ linters:
|
||||
- builtin$
|
||||
- examples$
|
||||
- _test\.go$
|
||||
rules:
|
||||
# relax the slog rules for debug lines, for now
|
||||
- linters: [sloglint]
|
||||
source: Debug
|
||||
settings:
|
||||
sloglint:
|
||||
context: "scope"
|
||||
static-msg: true
|
||||
msg-style: capitalized
|
||||
key-naming-case: camel
|
||||
formatters:
|
||||
enable:
|
||||
- gofumpt
|
||||
|
||||
1
AUTHORS
1
AUTHORS
@@ -95,6 +95,7 @@ Daniel Barczyk <46358936+DanielBarczyk@users.noreply.github.com>
|
||||
Daniel Bergmann (brgmnn) <dan.arne.bergmann@gmail.com> <brgmnn@users.noreply.github.com>
|
||||
Daniel Martí (mvdan) <mvdan@mvdan.cc>
|
||||
Daniel Padrta <64928366+danpadcz@users.noreply.github.com>
|
||||
Daniil Gentili <daniil@daniil.it>
|
||||
Darshil Chanpura (dtchanpura) <dtchanpura@gmail.com> <dcprime314@gmail.com>
|
||||
dashangcun <907225865@qq.com>
|
||||
David Rimmer (dinosore) <dinosore@dbrsoftware.co.uk>
|
||||
|
||||
1
build.go
1
build.go
@@ -870,7 +870,6 @@ func testmocks() {
|
||||
"github.com/syncthing/syncthing/lib/connections",
|
||||
"github.com/syncthing/syncthing/lib/discover",
|
||||
"github.com/syncthing/syncthing/lib/events",
|
||||
"github.com/syncthing/syncthing/lib/logger",
|
||||
"github.com/syncthing/syncthing/lib/model",
|
||||
"github.com/syncthing/syncthing/lib/protocol",
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
@@ -31,7 +32,6 @@ import (
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/rand"
|
||||
"github.com/syncthing/syncthing/lib/relay/client"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
"github.com/syncthing/syncthing/lib/tlsutil"
|
||||
)
|
||||
|
||||
@@ -115,7 +115,7 @@ var (
|
||||
|
||||
requests chan request
|
||||
|
||||
mut = sync.NewRWMutex()
|
||||
mut sync.RWMutex
|
||||
knownRelays = make([]*relay, 0)
|
||||
permanentRelays = make([]*relay, 0)
|
||||
evictionTimers = make(map[string]*time.Timer)
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -28,8 +27,6 @@ func init() {
|
||||
{URL: "known2"},
|
||||
{URL: "known3"},
|
||||
}
|
||||
|
||||
mut = new(sync.RWMutex)
|
||||
}
|
||||
|
||||
// Regression test: handleGetRequest should not modify permanentRelays.
|
||||
|
||||
@@ -6,10 +6,10 @@ import (
|
||||
"encoding/json"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -104,7 +104,7 @@ func refreshStats() {
|
||||
mut.RUnlock()
|
||||
|
||||
now := time.Now()
|
||||
wg := sync.NewWaitGroup()
|
||||
var wg sync.WaitGroup
|
||||
|
||||
results := make(chan statsFetchResult, len(relays))
|
||||
for _, rel := range relays {
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
|
||||
"github.com/alecthomas/kong"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/syncthing/syncthing/internal/slogutil"
|
||||
_ "github.com/syncthing/syncthing/lib/automaxprocs"
|
||||
"github.com/syncthing/syncthing/lib/httpcache"
|
||||
"github.com/syncthing/syncthing/lib/upgrade"
|
||||
@@ -58,10 +59,10 @@ func server(params *cli) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("metrics: %w", err)
|
||||
}
|
||||
slog.Info("Metrics listener started", "addr", params.MetricsListen)
|
||||
slog.Info("Metrics listener started", slogutil.Address(params.MetricsListen))
|
||||
go func() {
|
||||
if err := http.Serve(metricsListen, mux); err != nil {
|
||||
slog.Warn("Metrics server returned", "error", err)
|
||||
slog.Warn("Metrics server returned", slogutil.Error(err))
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -75,9 +76,9 @@ func server(params *cli) error {
|
||||
|
||||
go func() {
|
||||
for range time.NewTicker(params.CacheTime).C {
|
||||
slog.Info("Refreshing cached releases", "url", params.URL)
|
||||
slog.Info("Refreshing cached releases", slogutil.URI(params.URL))
|
||||
if err := cache.Update(context.Background()); err != nil {
|
||||
slog.Error("Failed to refresh cached releases", "url", params.URL, "error", err)
|
||||
slog.Error("Failed to refresh cached releases", slogutil.URI(params.URL), slogutil.Error(err))
|
||||
}
|
||||
}
|
||||
}()
|
||||
@@ -109,7 +110,7 @@ func server(params *cli) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("listen: %w", err)
|
||||
}
|
||||
slog.Info("Main listener started", "addr", params.Listen)
|
||||
slog.Info("Main listener started", slogutil.Address(params.Listen))
|
||||
|
||||
return srv.Serve(srvListener)
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"github.com/syncthing/syncthing/internal/blob"
|
||||
"github.com/syncthing/syncthing/internal/blob/azureblob"
|
||||
"github.com/syncthing/syncthing/internal/blob/s3"
|
||||
"github.com/syncthing/syncthing/internal/slogutil"
|
||||
"github.com/syncthing/syncthing/lib/build"
|
||||
"github.com/syncthing/syncthing/lib/geoip"
|
||||
"github.com/syncthing/syncthing/lib/ur/contract"
|
||||
@@ -104,23 +105,23 @@ func (cli *CLI) Run() error {
|
||||
|
||||
urListener, err := net.Listen("tcp", cli.Listen)
|
||||
if err != nil {
|
||||
slog.Error("Failed to listen (usage reports)", "error", err)
|
||||
slog.Error("Failed to listen (usage reports)", slogutil.Error(err))
|
||||
return err
|
||||
}
|
||||
slog.Info("Listening (usage reports)", "address", urListener.Addr())
|
||||
slog.Info("Listening (usage reports)", slogutil.Address(urListener.Addr()))
|
||||
|
||||
internalListener, err := net.Listen("tcp", cli.ListenInternal)
|
||||
if err != nil {
|
||||
slog.Error("Failed to listen (internal)", "error", err)
|
||||
slog.Error("Failed to listen (internal)", slogutil.Error(err))
|
||||
return err
|
||||
}
|
||||
slog.Info("Listening (internal)", "address", internalListener.Addr())
|
||||
slog.Info("Listening (internal)", slogutil.Address(internalListener.Addr()))
|
||||
|
||||
var geo *geoip.Provider
|
||||
if cli.GeoIPAccountID != 0 && cli.GeoIPLicenseKey != "" {
|
||||
geo, err = geoip.NewGeoLite2CityProvider(context.Background(), cli.GeoIPAccountID, cli.GeoIPLicenseKey, os.TempDir())
|
||||
if err != nil {
|
||||
slog.Error("Failed to load GeoIP", "error", err)
|
||||
slog.Error("Failed to load GeoIP", slogutil.Error(err))
|
||||
return err
|
||||
}
|
||||
go geo.Serve(context.TODO())
|
||||
@@ -132,20 +133,20 @@ func (cli *CLI) Run() error {
|
||||
if cli.S3Endpoint != "" {
|
||||
blobs, err = s3.NewSession(cli.S3Endpoint, cli.S3Region, cli.S3Bucket, cli.S3AccessKeyID, cli.S3SecretKey)
|
||||
if err != nil {
|
||||
slog.Error("Failed to create S3 session", "error", err)
|
||||
slog.Error("Failed to create S3 session", slogutil.Error(err))
|
||||
return err
|
||||
}
|
||||
} else if cli.AzureBlobAccount != "" {
|
||||
blobs, err = azureblob.NewBlobStore(cli.AzureBlobAccount, cli.AzureBlobKey, cli.AzureBlobContainer)
|
||||
if err != nil {
|
||||
slog.Error("Failed to create Azure blob store", "error", err)
|
||||
slog.Error("Failed to create Azure blob store", slogutil.Error(err))
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := os.Stat(cli.DumpFile); err != nil && blobs != nil {
|
||||
if err := cli.downloadDumpFile(blobs); err != nil {
|
||||
slog.Error("Failed to download dump file", "error", err)
|
||||
slog.Error("Failed to download dump file", slogutil.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,7 +168,7 @@ func (cli *CLI) Run() error {
|
||||
go func() {
|
||||
for range time.Tick(cli.DumpInterval) {
|
||||
if err := cli.saveDumpFile(srv, blobs); err != nil {
|
||||
slog.Error("Failed to write dump file", "error", err)
|
||||
slog.Error("Failed to write dump file", slogutil.Error(err))
|
||||
}
|
||||
}
|
||||
}()
|
||||
@@ -307,7 +308,7 @@ func (s *server) handleNewData(w http.ResponseWriter, r *http.Request) {
|
||||
lr := &io.LimitedReader{R: r.Body, N: 40 * 1024}
|
||||
bs, _ := io.ReadAll(lr)
|
||||
if err := json.Unmarshal(bs, &rep); err != nil {
|
||||
log.Error("Failed to decode JSON", "error", err)
|
||||
log.Error("Failed to decode JSON", slogutil.Error(err))
|
||||
http.Error(w, "JSON Decode Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
@@ -317,7 +318,7 @@ func (s *server) handleNewData(w http.ResponseWriter, r *http.Request) {
|
||||
rep.Address = addr
|
||||
|
||||
if err := rep.Validate(); err != nil {
|
||||
log.Error("Failed to validate report", "error", err)
|
||||
log.Error("Failed to validate report", slogutil.Error(err))
|
||||
http.Error(w, "Validation Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
@@ -394,7 +395,7 @@ func (s *server) load(r io.Reader) {
|
||||
if err := dec.Decode(&rep); errors.Is(err, io.EOF) {
|
||||
break
|
||||
} else if err != nil {
|
||||
slog.Error("Failed to load record", "error", err)
|
||||
slog.Error("Failed to load record", slogutil.Error(err))
|
||||
break
|
||||
}
|
||||
s.addReport(&rep)
|
||||
|
||||
@@ -8,11 +8,14 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/internal/slogutil"
|
||||
)
|
||||
|
||||
func startBlockProfiler() {
|
||||
@@ -20,10 +23,10 @@ func startBlockProfiler() {
|
||||
if profiler == nil {
|
||||
panic("Couldn't find block profiler")
|
||||
}
|
||||
l.Debugln("Starting block profiling")
|
||||
slog.Debug("Starting block profiling")
|
||||
go func() {
|
||||
err := saveBlockingProfiles(profiler) // Only returns on error
|
||||
l.Warnln("Block profiler failed:", err)
|
||||
slog.Error("Block profiler failed", slogutil.Error(err))
|
||||
panic("Block profiler failed")
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -131,15 +131,6 @@ func prettyPrintResponse(response *http.Response) error {
|
||||
return prettyPrintJSON(data)
|
||||
}
|
||||
|
||||
func nulString(bs []byte) string {
|
||||
for i := range bs {
|
||||
if bs[i] == 0 {
|
||||
return string(bs[:i])
|
||||
}
|
||||
}
|
||||
return string(bs)
|
||||
}
|
||||
|
||||
func normalizePath(path string) string {
|
||||
return filepath.ToSlash(filepath.Clean(path))
|
||||
}
|
||||
|
||||
@@ -11,12 +11,15 @@ import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/internal/slogutil"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -33,7 +36,7 @@ const (
|
||||
func uploadPanicLogs(ctx context.Context, urlBase, dir string) {
|
||||
files, err := filepath.Glob(filepath.Join(dir, "panic-*.log"))
|
||||
if err != nil {
|
||||
l.Warnln("Failed to list panic logs:", err)
|
||||
slog.ErrorContext(ctx, "Failed to list panic logs", slogutil.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -48,7 +51,7 @@ func uploadPanicLogs(ctx context.Context, urlBase, dir string) {
|
||||
}
|
||||
|
||||
if err := uploadPanicLog(ctx, urlBase, file); err != nil {
|
||||
l.Warnln("Reporting crash:", err)
|
||||
slog.ErrorContext(ctx, "Reporting crash", slogutil.Error(err))
|
||||
} else {
|
||||
// Rename the log so we don't have to try to report it again. This
|
||||
// succeeds, or it does not. There is no point complaining about it.
|
||||
@@ -71,7 +74,7 @@ func uploadPanicLog(ctx context.Context, urlBase, file string) error {
|
||||
data = filterLogLines(data)
|
||||
|
||||
hash := fmt.Sprintf("%x", sha256.Sum256(data))
|
||||
l.Infof("Reporting crash found in %s (report ID %s) ...\n", filepath.Base(file), hash[:8])
|
||||
slog.InfoContext(ctx, "Reporting crash", slogutil.FilePath(filepath.Base(file)), slog.String("id", hash[:8]))
|
||||
|
||||
url := fmt.Sprintf("%s/%s", urlBase, hash)
|
||||
headReq, err := http.NewRequest(http.MethodHead, url, nil)
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/syncthing/syncthing/lib/logger"
|
||||
)
|
||||
import "github.com/syncthing/syncthing/internal/slogutil"
|
||||
|
||||
var l = logger.DefaultLogger.NewFacility("main", "Main package")
|
||||
func init() { slogutil.RegisterPackage("Main package") }
|
||||
|
||||
@@ -12,13 +12,13 @@ import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
"github.com/syncthing/syncthing/lib/fs"
|
||||
"github.com/syncthing/syncthing/lib/locations"
|
||||
"github.com/syncthing/syncthing/lib/logger"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/syncthing"
|
||||
)
|
||||
@@ -29,7 +29,7 @@ type CLI struct {
|
||||
NoPortProbing bool `help:"Don't try to find free ports for GUI and listen addresses on first startup" env:"STNOPORTPROBING"`
|
||||
}
|
||||
|
||||
func (c *CLI) Run(l logger.Logger) error {
|
||||
func (c *CLI) Run() error {
|
||||
// Support reading the password from a pipe or similar
|
||||
if c.GUIPassword == "-" {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
@@ -40,13 +40,13 @@ func (c *CLI) Run(l logger.Logger) error {
|
||||
c.GUIPassword = string(password)
|
||||
}
|
||||
|
||||
if err := Generate(l, locations.GetBaseDir(locations.ConfigBaseDir), c.GUIUser, c.GUIPassword, c.NoPortProbing); err != nil {
|
||||
if err := Generate(locations.GetBaseDir(locations.ConfigBaseDir), c.GUIUser, c.GUIPassword, c.NoPortProbing); err != nil {
|
||||
return fmt.Errorf("failed to generate config and keys: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Generate(l logger.Logger, confDir, guiUser, guiPassword string, skipPortProbing bool) error {
|
||||
func Generate(confDir, guiUser, guiPassword string, skipPortProbing bool) error {
|
||||
dir, err := fs.ExpandTilde(confDir)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -61,15 +61,16 @@ func Generate(l logger.Logger, confDir, guiUser, guiPassword string, skipPortPro
|
||||
certFile, keyFile := locations.Get(locations.CertFile), locations.Get(locations.KeyFile)
|
||||
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if err == nil {
|
||||
l.Warnln("Key exists; will not overwrite.")
|
||||
slog.Warn("Key exists; will not overwrite")
|
||||
} else {
|
||||
cert, err = syncthing.GenerateCertificate(certFile, keyFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create certificate: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
myID = protocol.NewDeviceID(cert.Certificate[0])
|
||||
l.Infoln("Device ID:", myID)
|
||||
slog.Info("Calculated device ID", slog.String("device", myID.String()))
|
||||
|
||||
cfgFile := locations.Get(locations.ConfigFile)
|
||||
cfg, _, err := config.Load(cfgFile, myID, events.NoopLogger)
|
||||
@@ -87,7 +88,7 @@ func Generate(l logger.Logger, confDir, guiUser, guiPassword string, skipPortPro
|
||||
|
||||
var updateErr error
|
||||
waiter, err := cfg.Modify(func(cfg *config.Configuration) {
|
||||
updateErr = updateGUIAuthentication(l, &cfg.GUI, guiUser, guiPassword)
|
||||
updateErr = updateGUIAuthentication(&cfg.GUI, guiUser, guiPassword)
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("modify config: %w", err)
|
||||
@@ -103,17 +104,17 @@ func Generate(l logger.Logger, confDir, guiUser, guiPassword string, skipPortPro
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateGUIAuthentication(l logger.Logger, guiCfg *config.GUIConfiguration, guiUser, guiPassword string) error {
|
||||
func updateGUIAuthentication(guiCfg *config.GUIConfiguration, guiUser, guiPassword string) error {
|
||||
if guiUser != "" && guiCfg.User != guiUser {
|
||||
guiCfg.User = guiUser
|
||||
l.Infoln("Updated GUI authentication user name:", guiUser)
|
||||
slog.Info("Updated GUI authentication user", "name", guiUser)
|
||||
}
|
||||
|
||||
if guiPassword != "" && guiCfg.Password != guiPassword {
|
||||
if err := guiCfg.SetPassword(guiPassword); err != nil {
|
||||
return fmt.Errorf("failed to set GUI authentication password: %w", err)
|
||||
}
|
||||
l.Infoln("Updated GUI authentication password.")
|
||||
slog.Info("Updated GUI authentication password")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -8,18 +8,21 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/internal/slogutil"
|
||||
)
|
||||
|
||||
func startHeapProfiler() {
|
||||
l.Debugln("Starting heap profiling")
|
||||
slog.Debug("Starting heap profiling")
|
||||
go func() {
|
||||
err := saveHeapProfiles(1) // Only returns on error
|
||||
l.Warnln("Heap profiler failed:", err)
|
||||
slog.Error("Heap profiler failed", slogutil.Error(err))
|
||||
panic("Heap profiler failed")
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"log/slog"
|
||||
"maps"
|
||||
"net/http"
|
||||
_ "net/http/pprof" // Need to import this to support STPROFILER.
|
||||
"net/url"
|
||||
@@ -40,6 +41,7 @@ import (
|
||||
"github.com/syncthing/syncthing/cmd/syncthing/generate"
|
||||
"github.com/syncthing/syncthing/internal/db"
|
||||
"github.com/syncthing/syncthing/internal/db/sqlite"
|
||||
"github.com/syncthing/syncthing/internal/slogutil"
|
||||
_ "github.com/syncthing/syncthing/lib/automaxprocs"
|
||||
"github.com/syncthing/syncthing/lib/build"
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
@@ -47,7 +49,6 @@ import (
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
"github.com/syncthing/syncthing/lib/fs"
|
||||
"github.com/syncthing/syncthing/lib/locations"
|
||||
"github.com/syncthing/syncthing/lib/logger"
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/svcutil"
|
||||
@@ -61,20 +62,8 @@ const (
|
||||
|
||||
const (
|
||||
extraUsage = `
|
||||
The --logflags value is a sum of the following:
|
||||
|
||||
1 Date
|
||||
2 Time
|
||||
4 Microsecond time
|
||||
8 Long filename
|
||||
16 Short filename
|
||||
|
||||
I.e. to prefix each log line with time and filename, set --logflags=18 (2 + 16
|
||||
from above). The value 0 is used to disable all of the above. The default is
|
||||
to show date and time (3).
|
||||
|
||||
Logging always happens to the command line (stdout) and optionally to the
|
||||
file at the path specified by --logfile=path. In addition to an path, the special
|
||||
file at the path specified by --log-file=path. In addition to an path, the special
|
||||
values "default" and "-" may be used. The former logs to DATADIR/syncthing.log
|
||||
(see --data), which is the default on Windows, and the latter only to stdout,
|
||||
no file, which is the default anywhere else.
|
||||
@@ -87,11 +76,10 @@ The following environment variables modify Syncthing's behavior in ways that
|
||||
are mostly useful for developers. Use with care. See also the --debug-* options
|
||||
above.
|
||||
|
||||
STTRACE A comma separated string of facilities to trace. The valid
|
||||
facility strings are listed below.
|
||||
|
||||
STLOCKTHRESHOLD Used for debugging internal deadlocks; sets debug
|
||||
sensitivity. Use only under direction of a developer.
|
||||
STTRACE A comma separated string of packages to trace or change log
|
||||
level for. The valid package strings are listed below. A log
|
||||
level (DEBUG, INFO, WARN or ERROR) can be added after each
|
||||
package, separated by a colon. Ex: "model:WARN,nat:DEBUG".
|
||||
|
||||
STVERSIONEXTRA Add extra information to the version string in logs and the
|
||||
version line in the GUI. Can be set to the name of a wrapper
|
||||
@@ -106,8 +94,8 @@ above.
|
||||
of CPU usage (i.e. performance).
|
||||
|
||||
|
||||
Debugging Facilities
|
||||
--------------------
|
||||
Logging Facilities
|
||||
------------------
|
||||
|
||||
The following are valid values for the STTRACE variable:
|
||||
|
||||
@@ -170,8 +158,9 @@ type serveCmd struct {
|
||||
DBDeleteRetentionInterval time.Duration `help:"Database deleted item retention interval" default:"4320h" env:"STDBDELETERETENTIONINTERVAL"`
|
||||
GUIAddress string `name:"gui-address" help:"Override GUI address (e.g. \"http://192.0.2.42:8443\")" placeholder:"URL" env:"STGUIADDRESS"`
|
||||
GUIAPIKey string `name:"gui-apikey" help:"Override GUI API key" placeholder:"API-KEY" env:"STGUIAPIKEY"`
|
||||
LogFile string `name:"logfile" help:"Log file name (see below)" default:"${logFile}" placeholder:"PATH" env:"STLOGFILE"`
|
||||
LogFlags int `name:"logflags" help:"Select information in log line prefix (see below)" default:"${logFlags}" placeholder:"BITS" env:"STLOGFLAGS"`
|
||||
LogFile string `name:"log-file" aliases:"logfile" help:"Log file name (see below)" default:"${logFile}" placeholder:"PATH" env:"STLOGFILE"`
|
||||
LogFlags int `name:"logflags" help:"Deprecated option that does nothing, kept for compatibility" hidden:""`
|
||||
LogLevel slog.Level `help:"Log level for all packages (DEBUG,INFO,WARN,ERROR)" env:"STLOGLEVEL" default:"INFO"`
|
||||
LogMaxFiles int `name:"log-max-old-files" help:"Number of old files to keep (zero to keep only current)" default:"${logMaxFiles}" placeholder:"N" env:"STLOGMAXOLDFILES"`
|
||||
LogMaxSize int `help:"Maximum size of any file (zero to disable log rotation)" default:"${logMaxSize}" placeholder:"BYTES" env:"STLOGMAXSIZE"`
|
||||
NoBrowser bool `help:"Do not start browser" env:"STNOBROWSER"`
|
||||
@@ -180,7 +169,6 @@ type serveCmd struct {
|
||||
NoUpgrade bool `help:"Disable automatic upgrades" env:"STNOUPGRADE"`
|
||||
Paused bool `help:"Start with all devices and folders paused" env:"STPAUSED"`
|
||||
Unpaused bool `help:"Start with all devices and folders unpaused" env:"STUNPAUSED"`
|
||||
Verbose bool `help:"Print verbose log output" env:"STVERBOSE"`
|
||||
|
||||
// Debug options below
|
||||
DebugGUIAssetsDir string `help:"Directory to load GUI assets from" placeholder:"PATH" env:"STGUIASSETS"`
|
||||
@@ -199,14 +187,9 @@ type serveCmd struct {
|
||||
func defaultVars() kong.Vars {
|
||||
vars := kong.Vars{}
|
||||
|
||||
vars["logFlags"] = strconv.Itoa(logger.DefaultFlags)
|
||||
vars["logMaxSize"] = strconv.Itoa(10 << 20) // 10 MiB
|
||||
vars["logMaxFiles"] = "3" // plus the current one
|
||||
|
||||
if os.Getenv("STTRACE") != "" {
|
||||
vars["logFlags"] = strconv.Itoa(logger.DebugFlags)
|
||||
}
|
||||
|
||||
// On non-Windows, we explicitly default to "-" which means stdout. On
|
||||
// Windows, the "default" options.logFile will later be replaced with the
|
||||
// default path, unless the user has manually specified "-" or
|
||||
@@ -234,13 +217,13 @@ func main() {
|
||||
defaultVars(),
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
slog.Error("Parsing startup", slogutil.Error(err))
|
||||
os.Exit(svcutil.ExitError.AsInt())
|
||||
}
|
||||
|
||||
kongplete.Complete(parser)
|
||||
ctx, err := parser.Parse(os.Args[1:])
|
||||
parser.FatalIfErrorf(err)
|
||||
ctx.BindTo(l, (*logger.Logger)(nil)) // main logger available to subcommands
|
||||
err = ctx.Run()
|
||||
parser.FatalIfErrorf(err)
|
||||
}
|
||||
@@ -252,15 +235,13 @@ func helpHandler(options kong.HelpOptions, ctx *kong.Context) error {
|
||||
if ctx.Command() == "serve" {
|
||||
// Help was requested for `syncthing serve`, so we add our extra
|
||||
// usage info afte the normal options output.
|
||||
fmt.Printf(extraUsage, debugFacilities())
|
||||
fmt.Printf(extraUsage, logPackages())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// serveCmd.Run() is the entrypoint for `syncthing serve`
|
||||
func (c *serveCmd) Run() error {
|
||||
l.SetFlags(c.LogFlags)
|
||||
|
||||
if c.GUIAddress != "" {
|
||||
// The config picks this up from the environment.
|
||||
os.Setenv("STGUIADDRESS", c.GUIAddress)
|
||||
@@ -274,6 +255,9 @@ func (c *serveCmd) Run() error {
|
||||
osutil.HideConsole()
|
||||
}
|
||||
|
||||
// The default log level for all packages
|
||||
slogutil.SetDefaultLevel(c.LogLevel)
|
||||
|
||||
// Treat an explicitly empty log file name as no log file
|
||||
if c.LogFile == "" {
|
||||
c.LogFile = "-"
|
||||
@@ -281,7 +265,7 @@ func (c *serveCmd) Run() error {
|
||||
if c.LogFile != "default" {
|
||||
// We must set this *after* expandLocations above.
|
||||
if err := locations.Set(locations.LogFile, c.LogFile); err != nil {
|
||||
l.Warnln("Setting log file path:", err)
|
||||
slog.Error("Failed to set log file path", slogutil.Error(err))
|
||||
os.Exit(svcutil.ExitError.AsInt())
|
||||
}
|
||||
}
|
||||
@@ -290,7 +274,7 @@ func (c *serveCmd) Run() error {
|
||||
// The asset dir is blank if STGUIASSETS wasn't set, in which case we
|
||||
// should look for extra assets in the default place.
|
||||
if err := locations.Set(locations.GUIAssets, c.DebugGUIAssetsDir); err != nil {
|
||||
l.Warnln("Setting GUI assets path:", err)
|
||||
slog.Error("Failed to set GUI assets path", slogutil.Error(err))
|
||||
os.Exit(svcutil.ExitError.AsInt())
|
||||
}
|
||||
}
|
||||
@@ -298,7 +282,7 @@ func (c *serveCmd) Run() error {
|
||||
// Ensure that our config and data directories exist.
|
||||
for _, loc := range []locations.BaseDirEnum{locations.ConfigBaseDir, locations.DataBaseDir} {
|
||||
if err := syncthing.EnsureDir(locations.GetBaseDir(loc), 0o700); err != nil {
|
||||
l.Warnln("Failed to ensure directory exists:", err)
|
||||
slog.Error("Failed to ensure directory exists", slogutil.Error(err))
|
||||
os.Exit(svcutil.ExitError.AsInt())
|
||||
}
|
||||
}
|
||||
@@ -321,29 +305,27 @@ func openGUI() error {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
l.Warnln("Browser: GUI is currently disabled")
|
||||
slog.Error("Browser: GUI is currently disabled")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func debugFacilities() string {
|
||||
facilities := l.Facilities()
|
||||
func logPackages() string {
|
||||
packages := slogutil.PackageDescrs()
|
||||
|
||||
// Get a sorted list of names
|
||||
var names []string
|
||||
names := slices.Sorted(maps.Keys(packages))
|
||||
maxLen := 0
|
||||
for name := range facilities {
|
||||
names = append(names, name)
|
||||
for _, name := range names {
|
||||
if len(name) > maxLen {
|
||||
maxLen = len(name)
|
||||
}
|
||||
}
|
||||
slices.Sort(names)
|
||||
|
||||
// Format the choices
|
||||
b := new(bytes.Buffer)
|
||||
for _, name := range names {
|
||||
fmt.Fprintf(b, " %-*s - %s\n", maxLen, name, facilities[name])
|
||||
fmt.Fprintf(b, " %-*s - %s\n", maxLen, name, packages[name])
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
@@ -371,7 +353,7 @@ func checkUpgrade() (upgrade.Release, error) {
|
||||
return upgrade.Release{}, &errNoUpgrade{build.Version, release.Tag}
|
||||
}
|
||||
|
||||
l.Infof("Upgrade available (current %q < latest %q)", build.Version, release.Tag)
|
||||
slog.Info("Upgrade available", "current", build.Version, "latest", release.Tag)
|
||||
return release, nil
|
||||
}
|
||||
|
||||
@@ -428,13 +410,9 @@ func (c *serveCmd) syncthingMain() {
|
||||
startPerfStats()
|
||||
}
|
||||
|
||||
// Set a log prefix similar to the ID we will have later on, or early log
|
||||
// lines look ugly.
|
||||
l.SetPrefix("[start] ")
|
||||
|
||||
// Print our version information up front, so any crash that happens
|
||||
// early etc. will have it available.
|
||||
l.Infoln(build.LongVersion)
|
||||
slog.Info(build.LongVersion) //nolint:sloglint
|
||||
|
||||
// Ensure that we have a certificate and key.
|
||||
cert, err := syncthing.LoadOrGenerateCertificate(
|
||||
@@ -442,7 +420,7 @@ func (c *serveCmd) syncthingMain() {
|
||||
locations.Get(locations.KeyFile),
|
||||
)
|
||||
if err != nil {
|
||||
l.Warnln("Failed to load/generate certificate:", err)
|
||||
slog.Error("Failed to load/generate certificate", slogutil.Error(err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -450,10 +428,10 @@ func (c *serveCmd) syncthingMain() {
|
||||
lf := flock.New(locations.Get(locations.LockFile))
|
||||
locked, err := lf.TryLock()
|
||||
if err != nil {
|
||||
l.Warnln("Failed to acquire lock:", err)
|
||||
slog.Error("Failed to acquire lock", slogutil.Error(err))
|
||||
os.Exit(1)
|
||||
} else if !locked {
|
||||
l.Warnln("Failed to acquire lock: is another Syncthing instance already running?")
|
||||
slog.Error("Failed to acquire lock: is another Syncthing instance already running?")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -462,7 +440,7 @@ func (c *serveCmd) syncthingMain() {
|
||||
|
||||
// earlyService is a supervisor that runs the services needed for or
|
||||
// before app startup; the event logger, and the config service.
|
||||
spec := svcutil.SpecWithDebugLogger(l)
|
||||
spec := svcutil.SpecWithDebugLogger()
|
||||
earlyService := suture.New("early", spec)
|
||||
earlyService.ServeBackground(ctx)
|
||||
|
||||
@@ -471,7 +449,7 @@ func (c *serveCmd) syncthingMain() {
|
||||
|
||||
cfgWrapper, err := syncthing.LoadConfigAtStartup(locations.Get(locations.ConfigFile), cert, evLogger, c.AllowNewerConfig, c.NoPortProbing)
|
||||
if err != nil {
|
||||
l.Warnln("Failed to initialize config:", err)
|
||||
slog.Error("Failed to initialize config", slogutil.Error(err))
|
||||
os.Exit(svcutil.ExitError.AsInt())
|
||||
}
|
||||
earlyService.Add(cfgWrapper)
|
||||
@@ -483,7 +461,7 @@ func (c *serveCmd) syncthingMain() {
|
||||
|
||||
if build.IsCandidate && !upgrade.DisabledByCompilation && !c.NoUpgrade {
|
||||
cfgWrapper.Modify(func(cfg *config.Configuration) {
|
||||
l.Infoln("Automatic upgrade is always enabled for candidate releases.")
|
||||
slog.Info("Automatic upgrade is always enabled for candidate releases")
|
||||
if cfg.Options.AutoUpgradeIntervalH == 0 || cfg.Options.AutoUpgradeIntervalH > 24 {
|
||||
cfg.Options.AutoUpgradeIntervalH = 12
|
||||
// Set the option into the config as well, as the auto upgrade
|
||||
@@ -495,13 +473,13 @@ func (c *serveCmd) syncthingMain() {
|
||||
}
|
||||
|
||||
if err := syncthing.TryMigrateDatabase(c.DBDeleteRetentionInterval); err != nil {
|
||||
l.Warnln("Failed to migrate old-style database:", err)
|
||||
slog.Error("Failed to migrate old-style database", slogutil.Error(err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
sdb, err := syncthing.OpenDatabase(locations.Get(locations.Database), c.DBDeleteRetentionInterval)
|
||||
if err != nil {
|
||||
l.Warnln("Error opening database:", err)
|
||||
slog.Error("Error opening database", slogutil.Error(err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -518,12 +496,12 @@ func (c *serveCmd) syncthingMain() {
|
||||
}
|
||||
if err != nil {
|
||||
if _, ok := err.(*errNoUpgrade); ok || err == errTooEarlyUpgradeCheck || err == errTooEarlyUpgrade {
|
||||
l.Debugln("Initial automatic upgrade:", err)
|
||||
slog.Debug("Initial automatic upgrade", slogutil.Error(err))
|
||||
} else {
|
||||
l.Infoln("Initial automatic upgrade:", err)
|
||||
slog.Info("Initial automatic upgrade", slogutil.Error(err))
|
||||
}
|
||||
} else {
|
||||
l.Infof("Upgraded to %q, should exit now.", release.Tag)
|
||||
slog.Info("Upgraded, should exit now", "newVersion", release.Tag)
|
||||
os.Exit(svcutil.ExitUpgrade.AsInt())
|
||||
}
|
||||
}
|
||||
@@ -538,18 +516,17 @@ func (c *serveCmd) syncthingMain() {
|
||||
NoUpgrade: c.NoUpgrade,
|
||||
ProfilerAddr: c.DebugProfilerListen,
|
||||
ResetDeltaIdxs: c.DebugResetDeltaIdxs,
|
||||
Verbose: c.Verbose,
|
||||
DBMaintenanceInterval: c.DBMaintenanceInterval,
|
||||
}
|
||||
|
||||
if c.Audit || cfgWrapper.Options().AuditEnabled {
|
||||
l.Infoln("Auditing is enabled.")
|
||||
slog.Info("Auditing is enabled")
|
||||
|
||||
auditFile := cfgWrapper.Options().AuditFile
|
||||
|
||||
// Ignore config option if command-line option is set
|
||||
if c.AuditFile != "" {
|
||||
l.Debugln("Using the audit file from the command-line parameter.")
|
||||
slog.Debug("Using the audit file from the command-line parameter", slogutil.FilePath(c.AuditFile))
|
||||
auditFile = c.AuditFile
|
||||
}
|
||||
|
||||
@@ -558,7 +535,7 @@ func (c *serveCmd) syncthingMain() {
|
||||
|
||||
app, err := syncthing.New(cfgWrapper, sdb, evLogger, cert, appOpts)
|
||||
if err != nil {
|
||||
l.Warnln("Failed to start Syncthing:", err)
|
||||
slog.Error("Failed to start Syncthing", slogutil.Error(err))
|
||||
os.Exit(svcutil.ExitError.AsInt())
|
||||
}
|
||||
|
||||
@@ -571,11 +548,11 @@ func (c *serveCmd) syncthingMain() {
|
||||
if c.DebugProfileCPU {
|
||||
f, err := os.Create(fmt.Sprintf("cpu-%d.pprof", os.Getpid()))
|
||||
if err != nil {
|
||||
l.Warnln("Creating profile:", err)
|
||||
slog.Error("Failed to create profile", slogutil.Error(err))
|
||||
os.Exit(svcutil.ExitError.AsInt())
|
||||
}
|
||||
if err := pprof.StartCPUProfile(f); err != nil {
|
||||
l.Warnln("Starting profile:", err)
|
||||
slog.Error("Failed to start profile", slogutil.Error(err))
|
||||
os.Exit(svcutil.ExitError.AsInt())
|
||||
}
|
||||
}
|
||||
@@ -595,7 +572,7 @@ func (c *serveCmd) syncthingMain() {
|
||||
status := app.Wait()
|
||||
|
||||
if status == svcutil.ExitError {
|
||||
l.Warnln("Syncthing stopped with error:", app.Error())
|
||||
slog.Error("Syncthing stopped with error", slogutil.Error(app.Error()))
|
||||
}
|
||||
|
||||
if c.DebugProfileCPU {
|
||||
@@ -663,13 +640,13 @@ func auditWriter(auditFile string) io.Writer {
|
||||
}
|
||||
fd, err = os.OpenFile(auditFile, auditFlags, 0o600)
|
||||
if err != nil {
|
||||
l.Warnln("Audit:", err)
|
||||
slog.Error("Failed to open audit file", slogutil.Error(err))
|
||||
os.Exit(svcutil.ExitError.AsInt())
|
||||
}
|
||||
auditDest = auditFile
|
||||
}
|
||||
|
||||
l.Infoln("Audit log in", auditDest)
|
||||
slog.Info("Writing audit log", slogutil.FilePath(auditDest))
|
||||
|
||||
return fd
|
||||
}
|
||||
@@ -679,7 +656,7 @@ func (c *serveCmd) autoUpgradePossible() bool {
|
||||
return false
|
||||
}
|
||||
if c.NoUpgrade {
|
||||
l.Infof("No automatic upgrades; STNOUPGRADE environment variable defined.")
|
||||
slog.Info("No automatic upgrades; STNOUPGRADE environment variable defined")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@@ -696,7 +673,7 @@ func autoUpgrade(cfg config.Wrapper, app *syncthing.App, evLogger events.Logger)
|
||||
continue
|
||||
}
|
||||
if cfg.Options().AutoUpgradeEnabled() {
|
||||
l.Infof("Connected to device %s with a newer version (current %q < remote %q). Checking for upgrades.", data["id"], build.Version, data["clientVersion"])
|
||||
slog.Info("Connected to device with a newer version; checking for upgrades", slog.String("device", data["id"]), slog.String("ourVersion", build.Version), slog.String("theirVersion", data["clientVersion"]))
|
||||
}
|
||||
case <-timer.C:
|
||||
}
|
||||
@@ -716,7 +693,7 @@ func autoUpgrade(cfg config.Wrapper, app *syncthing.App, evLogger events.Logger)
|
||||
if err != nil {
|
||||
// Don't complain too loudly here; we might simply not have
|
||||
// internet connectivity, or the upgrade server might be down.
|
||||
l.Infoln("Automatic upgrade:", err)
|
||||
slog.Info("Automatic upgrade", slogutil.Error(err))
|
||||
timer.Reset(checkInterval)
|
||||
continue
|
||||
}
|
||||
@@ -727,15 +704,15 @@ func autoUpgrade(cfg config.Wrapper, app *syncthing.App, evLogger events.Logger)
|
||||
continue
|
||||
}
|
||||
|
||||
l.Infof("Automatic upgrade (current %q < latest %q)", build.Version, rel.Tag)
|
||||
slog.Info("Automatic upgrade", "current", build.Version, "latest", rel.Tag)
|
||||
err = upgrade.To(rel)
|
||||
if err != nil {
|
||||
l.Warnln("Automatic upgrade:", err)
|
||||
slog.Error("Automatic upgrade failed", slogutil.Error(err))
|
||||
timer.Reset(checkInterval)
|
||||
continue
|
||||
}
|
||||
sub.Unsubscribe()
|
||||
l.Warnf("Automatically upgraded to version %q. Restarting in 1 minute.", rel.Tag)
|
||||
slog.Error("Automatically upgraded, restarting in 1 minute", slog.String("newVersion", rel.Tag))
|
||||
time.Sleep(time.Minute)
|
||||
app.Stop(svcutil.ExitUpgrade)
|
||||
return
|
||||
@@ -788,22 +765,22 @@ func cleanConfigDirectory() {
|
||||
fs := fs.NewFilesystem(fs.FilesystemTypeBasic, locations.GetBaseDir(locations.ConfigBaseDir))
|
||||
files, err := fs.Glob(pat)
|
||||
if err != nil {
|
||||
l.Infoln("Cleaning:", err)
|
||||
slog.Warn("Failed to clean config directory", slogutil.Error(err))
|
||||
continue
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
info, err := fs.Lstat(file)
|
||||
if err != nil {
|
||||
l.Infoln("Cleaning:", err)
|
||||
slog.Warn("Failed to clean config directory", slogutil.Error(err))
|
||||
continue
|
||||
}
|
||||
|
||||
if time.Since(info.ModTime()) > dur {
|
||||
if err = fs.RemoveAll(file); err != nil {
|
||||
l.Infoln("Cleaning:", err)
|
||||
slog.Warn("Failed to clean config directory", slogutil.Error(err))
|
||||
} else {
|
||||
l.Infoln("Cleaned away old file", filepath.Base(file))
|
||||
slog.Warn("Cleaned away old file", slogutil.FilePath(filepath.Base(file)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -820,7 +797,7 @@ func setPauseState(cfgWrapper config.Wrapper, paused bool) {
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
l.Warnln("Cannot adjust paused state:", err)
|
||||
slog.Error("Cannot adjust paused state", slogutil.Error(err))
|
||||
os.Exit(svcutil.ExitError.AsInt())
|
||||
}
|
||||
}
|
||||
@@ -847,7 +824,7 @@ func (deviceIDCmd) Run() error {
|
||||
locations.Get(locations.KeyFile),
|
||||
)
|
||||
if err != nil {
|
||||
l.Warnln("Error reading device ID:", err)
|
||||
slog.Error("Failed to read device ID", slogutil.Error(err))
|
||||
os.Exit(svcutil.ExitError.AsInt())
|
||||
}
|
||||
|
||||
@@ -870,7 +847,7 @@ type upgradeCmd struct {
|
||||
func (u upgradeCmd) Run() error {
|
||||
if u.CheckOnly {
|
||||
if _, err := checkUpgrade(); err != nil {
|
||||
l.Warnln("Checking for upgrade:", err)
|
||||
slog.Error("Failed to check for upgrade", slogutil.Error(err))
|
||||
os.Exit(exitCodeForUpgrade(err))
|
||||
}
|
||||
return nil
|
||||
@@ -879,10 +856,10 @@ func (u upgradeCmd) Run() error {
|
||||
if u.From != "" {
|
||||
err := upgrade.ToURL(u.From)
|
||||
if err != nil {
|
||||
l.Warnln("Error while Upgrading:", err)
|
||||
slog.Error("Failed to upgrade", slogutil.Error(err))
|
||||
os.Exit(svcutil.ExitError.AsInt())
|
||||
}
|
||||
l.Infoln("Upgraded from", u.From)
|
||||
slog.Info("Upgraded", "from", u.From)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -892,7 +869,7 @@ func (u upgradeCmd) Run() error {
|
||||
var locked bool
|
||||
locked, err = lf.TryLock()
|
||||
if err != nil {
|
||||
l.Warnln("Upgrade:", err)
|
||||
slog.Error("Failed to lock for upgrade", slogutil.Error(err))
|
||||
os.Exit(1)
|
||||
} else if locked {
|
||||
err = upgradeViaRest()
|
||||
@@ -901,10 +878,10 @@ func (u upgradeCmd) Run() error {
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
l.Warnln("Upgrade:", err)
|
||||
slog.Error("Failed to check for upgrade", slogutil.Error(err))
|
||||
os.Exit(exitCodeForUpgrade(err))
|
||||
}
|
||||
l.Infof("Upgraded to %q", release.Tag)
|
||||
slog.Info("Upgraded", "to", release.Tag)
|
||||
os.Exit(svcutil.ExitUpgrade.AsInt())
|
||||
return nil
|
||||
}
|
||||
@@ -913,7 +890,7 @@ type browserCmd struct{}
|
||||
|
||||
func (browserCmd) Run() error {
|
||||
if err := openGUI(); err != nil {
|
||||
l.Warnln("Failed to open web UI:", err)
|
||||
slog.Error("Failed to open web UI", slogutil.Error(err))
|
||||
os.Exit(svcutil.ExitError.AsInt())
|
||||
}
|
||||
return nil
|
||||
@@ -929,12 +906,12 @@ type debugCmd struct {
|
||||
type resetDatabaseCmd struct{}
|
||||
|
||||
func (resetDatabaseCmd) Run() error {
|
||||
l.Infoln("Removing database in", locations.Get(locations.Database))
|
||||
slog.Info("Removing database", slogutil.FilePath(locations.Get(locations.Database)))
|
||||
if err := os.RemoveAll(locations.Get(locations.Database)); err != nil {
|
||||
l.Warnln("Resetting database:", err)
|
||||
slog.Error("Failed to reset database", slogutil.Error(err))
|
||||
os.Exit(svcutil.ExitError.AsInt())
|
||||
}
|
||||
l.Infoln("Successfully reset database - it will be rebuilt after next start.")
|
||||
slog.Info("Reset database - it will be rebuilt after next start")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -11,26 +11,28 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/internal/slogutil"
|
||||
"github.com/syncthing/syncthing/lib/build"
|
||||
"github.com/syncthing/syncthing/lib/fs"
|
||||
"github.com/syncthing/syncthing/lib/locations"
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
"github.com/syncthing/syncthing/lib/svcutil"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
)
|
||||
|
||||
var (
|
||||
stdoutFirstLines []string // The first 10 lines of stdout
|
||||
stdoutLastLines []string // The last 50 lines of stdout
|
||||
stdoutMut = sync.NewMutex()
|
||||
stdoutMut sync.Mutex
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -44,8 +46,6 @@ const (
|
||||
)
|
||||
|
||||
func (c *serveCmd) monitorMain() {
|
||||
l.SetPrefix("[monitor] ")
|
||||
|
||||
var dst io.Writer = os.Stdout
|
||||
|
||||
logFile := locations.Get(locations.LogFile)
|
||||
@@ -64,7 +64,7 @@ func (c *serveCmd) monitorMain() {
|
||||
fileDst, err = open(logFile)
|
||||
}
|
||||
if err != nil {
|
||||
l.Warnln("Failed to set up logging to file, proceeding with logging to stdout only:", err)
|
||||
slog.Error("Failed to set up logging to file, proceeding with logging to stdout only", slogutil.Error(err))
|
||||
} else {
|
||||
if build.IsWindows {
|
||||
// Translate line breaks to Windows standard
|
||||
@@ -78,14 +78,14 @@ func (c *serveCmd) monitorMain() {
|
||||
// Log to both stdout and file.
|
||||
dst = io.MultiWriter(dst, fileDst)
|
||||
|
||||
l.Infof(`Log output saved to file "%s"`, logFile)
|
||||
slog.Info("Saved log output", slogutil.FilePath(logFile))
|
||||
}
|
||||
}
|
||||
|
||||
args := os.Args
|
||||
binary, err := getBinary(args[0])
|
||||
if err != nil {
|
||||
l.Warnln("Error starting the main Syncthing process:", err)
|
||||
slog.Error("Failed to start the main Syncthing process", slogutil.Error(err))
|
||||
panic("Error starting the main Syncthing process")
|
||||
}
|
||||
var restarts [restartCounts]time.Time
|
||||
@@ -102,7 +102,7 @@ func (c *serveCmd) monitorMain() {
|
||||
maybeReportPanics()
|
||||
|
||||
if t := time.Since(restarts[0]); t < restartLoopThreshold {
|
||||
l.Warnf("%d restarts in %v; not retrying further", restartCounts, t)
|
||||
slog.Error("Too many restarts; not retrying further", slog.Int("count", restartCounts), slog.Any("interval", t))
|
||||
os.Exit(svcutil.ExitError.AsInt())
|
||||
}
|
||||
|
||||
@@ -122,10 +122,10 @@ func (c *serveCmd) monitorMain() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
l.Debugln("Starting syncthing")
|
||||
slog.Debug("Starting syncthing")
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
l.Warnln("Error starting the main Syncthing process:", err)
|
||||
slog.Error("Failed to start the main Syncthing process", slogutil.Error(err))
|
||||
panic("Error starting the main Syncthing process")
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ func (c *serveCmd) monitorMain() {
|
||||
stdoutLastLines = make([]string, 0, 50)
|
||||
stdoutMut.Unlock()
|
||||
|
||||
wg := sync.NewWaitGroup()
|
||||
var wg sync.WaitGroup
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
@@ -158,13 +158,13 @@ func (c *serveCmd) monitorMain() {
|
||||
stopped := false
|
||||
select {
|
||||
case s := <-stopSign:
|
||||
l.Infof("Signal %d received; exiting", s)
|
||||
slog.Info("Received signal; exiting", "signal", s)
|
||||
cmd.Process.Signal(sigTerm)
|
||||
err = <-exit
|
||||
stopped = true
|
||||
|
||||
case s := <-restartSign:
|
||||
l.Infof("Signal %d received; restarting", s)
|
||||
slog.Info("Received signal; restarting", "signal", s)
|
||||
cmd.Process.Signal(sigHup)
|
||||
err = <-exit
|
||||
|
||||
@@ -184,9 +184,9 @@ func (c *serveCmd) monitorMain() {
|
||||
if exitCode == svcutil.ExitUpgrade.AsInt() {
|
||||
// Restart the monitor process to release the .old
|
||||
// binary as part of the upgrade process.
|
||||
l.Infoln("Restarting monitor...")
|
||||
slog.Info("Restarting monitor...")
|
||||
if err = restartMonitor(binary, args); err != nil {
|
||||
l.Warnln("Restart:", err)
|
||||
slog.Error("Failed to restart monitor", slogutil.Error(err))
|
||||
}
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
@@ -196,7 +196,7 @@ func (c *serveCmd) monitorMain() {
|
||||
os.Exit(svcutil.ExitError.AsInt())
|
||||
}
|
||||
|
||||
l.Infoln("Syncthing exited:", err)
|
||||
slog.Info("Syncthing exited", slogutil.Error(err))
|
||||
time.Sleep(restartPause)
|
||||
|
||||
if first {
|
||||
@@ -243,29 +243,13 @@ func copyStderr(stderr io.Reader, dst io.Writer) {
|
||||
if panicFd == nil && (strings.HasPrefix(line, "panic:") || strings.HasPrefix(line, "fatal error:")) {
|
||||
panicFd, err = os.Create(locations.GetTimestamped(locations.PanicLog))
|
||||
if err != nil {
|
||||
l.Warnln("Create panic log:", err)
|
||||
slog.Error("Failed to create panic log", slogutil.Error(err))
|
||||
continue
|
||||
}
|
||||
|
||||
l.Warnf("Panic detected, writing to \"%s\"", panicFd.Name())
|
||||
if strings.Contains(line, "leveldb") && strings.Contains(line, "corrupt") {
|
||||
l.Warnln(`
|
||||
*********************************************************************************
|
||||
* Crash due to corrupt database. *
|
||||
* *
|
||||
* This crash usually occurs due to one of the following reasons: *
|
||||
* - Syncthing being stopped abruptly (killed/loss of power) *
|
||||
* - Bad hardware (memory/disk issues) *
|
||||
* - Software that affects disk writes (SSD caching software and similar) *
|
||||
* *
|
||||
* Please see the following URL for instructions on how to recover: *
|
||||
* https://docs.syncthing.net/users/faq.html#my-syncthing-database-is-corrupt *
|
||||
*********************************************************************************
|
||||
`)
|
||||
} else {
|
||||
l.Warnln("Please check for existing issues with similar panic message at https://github.com/syncthing/syncthing/issues/")
|
||||
l.Warnln("If no issue with similar panic message exists, please create a new issue with the panic log attached")
|
||||
}
|
||||
slog.Error("Panic detected, writing to file", slogutil.FilePath(panicFd.Name()))
|
||||
slog.Info("Please check for existing issues with similar panic message at https://github.com/syncthing/syncthing/issues/")
|
||||
slog.Info("If no issue with similar panic message exists, please create a new issue with the panic log attached")
|
||||
|
||||
stdoutMut.Lock()
|
||||
for _, line := range stdoutFirstLines {
|
||||
@@ -446,7 +430,6 @@ func newAutoclosedFile(name string, closeDelay, maxOpenTime time.Duration) (*aut
|
||||
name: name,
|
||||
closeDelay: closeDelay,
|
||||
maxOpenTime: maxOpenTime,
|
||||
mut: sync.NewMutex(),
|
||||
closed: make(chan struct{}),
|
||||
closeTimer: time.NewTimer(time.Minute),
|
||||
}
|
||||
@@ -554,7 +537,7 @@ func maybeReportPanics() {
|
||||
// Try to get a config to see if/where panics should be reported.
|
||||
cfg, err := loadOrDefaultConfig()
|
||||
if err != nil {
|
||||
l.Warnln("Couldn't load config; not reporting crash")
|
||||
slog.Error("Couldn't load config; not reporting crash")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -574,7 +557,7 @@ func maybeReportPanics() {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(panicUploadNoticeWait):
|
||||
l.Warnln("Uploading crash reports is taking a while, please wait...")
|
||||
slog.Warn("Uploading crash reports is taking a while, please wait")
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ StartLimitBurst=4
|
||||
|
||||
[Service]
|
||||
User=%i
|
||||
ExecStart=/usr/bin/syncthing serve --no-browser --no-restart --logflags=0
|
||||
ExecStart=/usr/bin/syncthing serve --no-browser --no-restart
|
||||
Restart=on-failure
|
||||
RestartSec=1
|
||||
SuccessExitStatus=3 4
|
||||
|
||||
@@ -370,7 +370,7 @@
|
||||
"Show diff with previous version": "أظهر الفرق مقارنةً بالنسخة السابقة",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "يُعرَض بدلا من المُعرِّف ضمن العناقيد. سيُروَّج للأجهزة الأخرى على أنه اسم أساسي محتمل.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "يُعرَض بدلا من المُعرِّف ضمن العناقيد. إذا تُرك فارغا، سيُحدَّث إلى الاسم المختار من قِبَل الجهاز.",
|
||||
"Shutdown": "إغلاق",
|
||||
"Shut Down": "إغلاق",
|
||||
"Shutdown Complete": "أُغلِق",
|
||||
"Simple": "بسيط",
|
||||
"Simple File Versioning": "التقسيم البسيط لإصدارات الملفات",
|
||||
|
||||
@@ -152,7 +152,7 @@
|
||||
"Show ID": "Паказаць ID",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.",
|
||||
"Shutdown": "Выключыць",
|
||||
"Shut Down": "Выключыць",
|
||||
"Shutdown Complete": "Выключэньне завершанае",
|
||||
"Simple File Versioning": "Простае захоўваньне вэрсій",
|
||||
"Single level wildcard (matches within a directory only)": "Single level wildcard (matches within a directory only)",
|
||||
|
||||
@@ -371,7 +371,7 @@
|
||||
"Show diff with previous version": "Показване на разликите с предходната версия",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "В списъка на устройствата се показва вместо идентификатор. Ще бъде предложено на другите устройства като име по подразбиране.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "В списъка на устройствата се показва вместо идентификатор. Ако бъде оставено празно ще бъде променено на името, което носи устройството.",
|
||||
"Shutdown": "Изключване",
|
||||
"Shut Down": "Изключване",
|
||||
"Shutdown Complete": "Спирането завършено",
|
||||
"Simple": "Обикновени",
|
||||
"Simple File Versioning": "Обикновени версии",
|
||||
|
||||
@@ -370,7 +370,7 @@
|
||||
"Show diff with previous version": "Mostra la diferència amb la versió anterior",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Mostrat en comptes del ID del Node en l'estat del cluster. Serà advertit als altres dispositius com un nom opcional per defecte.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Mostrat en comptes del ID del Node en l'estat del cluster. S'actualitzarà al nom del dispositiu si es deixa buit.",
|
||||
"Shutdown": "Apaga",
|
||||
"Shut Down": "Apaga",
|
||||
"Shutdown Complete": "Apagat complet",
|
||||
"Simple": "Simple",
|
||||
"Simple File Versioning": "Versionat de Fitxers Senzill",
|
||||
|
||||
@@ -353,7 +353,7 @@
|
||||
"Show diff with previous version": "Mostrar les diferències amb la versió prèvia",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Mostrat en lloc de l'ID del dispositiu en l'estat del grup (cluster). S'anunciarà als altres dispositius com el nom opcional per defecte.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Mostrat en lloc de l'ID del dispositiu en l'estat del grup (cluster). S'actualitzarà al nom que el dispositiu anuncia si es deixa buit.",
|
||||
"Shutdown": "Apagar",
|
||||
"Shut Down": "Apagar",
|
||||
"Shutdown Complete": "Apagar completament",
|
||||
"Simple": "Senzill",
|
||||
"Simple File Versioning": "Versionat de fitxers senzill",
|
||||
|
||||
@@ -360,7 +360,7 @@
|
||||
"Show diff with previous version": "Ukázat rozdíl oproti předchozí verzi",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Zobrazeno místo identifikátoru zařízení na náhledu stavu clusteru. Bude odesíláno ostatním zařízením jako výchozí název zařízení.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Zobrazeno místo identifikátoru zařízení na náhledu stavu clusteru. Pokud nebude vyplněno, bude nastaveno na název, který zařízení odesílá.",
|
||||
"Shutdown": "Vypnout",
|
||||
"Shut Down": "Vypnout",
|
||||
"Shutdown Complete": "Vypnutí dokončeno",
|
||||
"Simple": "Jednoduché",
|
||||
"Simple File Versioning": "Jednoduchá správa verzí souborů",
|
||||
|
||||
@@ -368,7 +368,7 @@
|
||||
"Show diff with previous version": "Vis forskelle fra tidligere version",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Vises i stedet for enheds-ID i klyngestatus. Vil blive sendt til andre enheder som valgfrit standardnavn.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Vises i stedet for enheds-ID i klyngestatus. Vil blive opdateret til det navn, som enheden sender, hvis det ikke er udfyldt.",
|
||||
"Shutdown": "Luk ned",
|
||||
"Shut Down": "Luk ned",
|
||||
"Shutdown Complete": "Nedlukning fuldført",
|
||||
"Simple": "Enkel",
|
||||
"Simple File Versioning": "Simpel filversionering",
|
||||
|
||||
@@ -371,7 +371,7 @@
|
||||
"Show diff with previous version": "Unterschied zur vorherigen Version anzeigen",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Wird anstelle der Gerätekennung im Verbundstatus angezeigt. Wird anderen Geräten als optionaler Standardname bekannt gegeben.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Wird anstelle der Gerätekennung im Verbundstatus angezeigt. Wird auf den Namen aktualisiert, den das Gerät anzeigt, wenn er leer bleibt.",
|
||||
"Shutdown": "Herunterfahren",
|
||||
"Shut Down": "Herunterfahren",
|
||||
"Shutdown Complete": "Vollständig heruntergefahren",
|
||||
"Simple": "Einfach",
|
||||
"Simple File Versioning": "Einfache Dateiversionierung",
|
||||
|
||||
@@ -368,7 +368,7 @@
|
||||
"Show diff with previous version": "Εμφάνιση διαφορών με προηγούμενη έκδοση",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Θα φαίνεται αντί για την ταυτότητα της συσκευής στην προβολή της κατάστασης ολόκληρης της συστάδας. Θα γνωστοποιείται σαν το προαιρετικό όνομα της συσκευής.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Θα φαίνεται αντί για την ταυτότητα της συσκευής στην προβολή της κατάστασης ολόκληρης της συστάδας. Θα ενημερώνεται αυτόματα αν αλλάξει το όνομα της συσκευής.",
|
||||
"Shutdown": "Απενεργοποίηση",
|
||||
"Shut Down": "Απενεργοποίηση",
|
||||
"Shutdown Complete": "Πλήρης απενεργοποίηση",
|
||||
"Simple": "Απλό",
|
||||
"Simple File Versioning": "Απλή τήρηση εκδόσεων",
|
||||
|
||||
@@ -353,7 +353,7 @@
|
||||
"Show diff with previous version": "Show diff with previous version",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.",
|
||||
"Shutdown": "Shutdown",
|
||||
"Shut Down": "Shut Down",
|
||||
"Shutdown Complete": "Shutdown Complete",
|
||||
"Simple": "Simple",
|
||||
"Simple File Versioning": "Simple File Versioning",
|
||||
|
||||
@@ -371,7 +371,7 @@
|
||||
"Show diff with previous version": "Show diff with previous version",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.",
|
||||
"Shutdown": "Shutdown",
|
||||
"Shut Down": "Shut Down",
|
||||
"Shutdown Complete": "Shutdown Complete",
|
||||
"Simple": "Simple",
|
||||
"Simple File Versioning": "Simple File Versioning",
|
||||
|
||||
@@ -82,6 +82,7 @@
|
||||
"Custom Range": "Custom Range",
|
||||
"Danger!": "Danger!",
|
||||
"Database Location": "Database Location",
|
||||
"Debug": "Debug",
|
||||
"Debugging Facilities": "Debugging Facilities",
|
||||
"Default": "Default",
|
||||
"Default Configuration": "Default Configuration",
|
||||
@@ -210,6 +211,7 @@
|
||||
"Incoming Rate Limit (KiB/s)": "Incoming Rate Limit (KiB/s)",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Incorrect configuration may damage your folder contents and render Syncthing inoperable.",
|
||||
"Incorrect user name or password.": "Incorrect user name or password.",
|
||||
"Info": "Info",
|
||||
"Internally used paths:": "Internally used paths:",
|
||||
"Introduced By": "Introduced By",
|
||||
"Introducer": "Introducer",
|
||||
@@ -371,7 +373,7 @@
|
||||
"Show diff with previous version": "Show diff with previous version",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.",
|
||||
"Shutdown": "Shutdown",
|
||||
"Shut Down": "Shut Down",
|
||||
"Shutdown Complete": "Shutdown Complete",
|
||||
"Simple": "Simple",
|
||||
"Simple File Versioning": "Simple File Versioning",
|
||||
|
||||
@@ -258,7 +258,7 @@
|
||||
"Show diff with previous version": "Montri diferenco kun antaŭa versio",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Montrita anstataŭ ID de Aparato en la statuso de la grupo. Estos anoncita al aliaj aparatoj kiel laŭvola defaŭlta nomo.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Montri anstataŭ ID de Aparato en la statuso de la grupo. Estos ĝisdatigita al la nomo de la aparato sciigante se ĝi estas lasita malplena.",
|
||||
"Shutdown": "Sistemfermo",
|
||||
"Shut Down": "Sistemfermo",
|
||||
"Shutdown Complete": "Sistemfermo Tuta",
|
||||
"Simple File Versioning": "Simpla Versionado de Dosieroj",
|
||||
"Single level wildcard (matches within a directory only)": "Ununivela ĵokero (egalas nur ene de dosierujo)",
|
||||
|
||||
@@ -368,7 +368,7 @@
|
||||
"Show diff with previous version": "Mostrar la diferencia con la versión anterior",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Se muestra en lugar del ID del dispositivo en el estado del grupo (cluster). Se notificará a los otros dispositivos como nombre opcional por defecto.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Se muestra en lugar del ID del dispositivo en el estado del grupo (cluster). Se actualizará al nombre que el dispositivo anuncia si se deja vacío.",
|
||||
"Shutdown": "Apagar",
|
||||
"Shut Down": "Apagar",
|
||||
"Shutdown Complete": "Apagar completamente",
|
||||
"Simple": "Sencillo",
|
||||
"Simple File Versioning": "Versionado simple de fichero",
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
{
|
||||
"A device with that ID is already added.": "Sellise seadme ID'ga seade on juba lisatud.",
|
||||
"A device with that ID is already added.": "Sellise tunnusega seade on juba lisatud.",
|
||||
"A negative number of days doesn't make sense.": "Negatiivne päevade arv ei ole loogiline.",
|
||||
"API Key": "API Võti",
|
||||
"API Key": "API võti",
|
||||
"About": "Rakenduse teave",
|
||||
"Action": "Tegevus",
|
||||
"Actions": "Tegevused",
|
||||
"Add": "Lisa",
|
||||
"Add Device": "Lisa seade",
|
||||
"Add Folder": "Lisa kaust",
|
||||
"Add new folder?": "Lisa uus kaust?",
|
||||
"Add Remote Device": "Lisa kaugseade",
|
||||
"Add new folder?": "Kas lisad uue kausta?",
|
||||
"Address": "Aadress",
|
||||
"Addresses": "Aadressid",
|
||||
"All Data": "Kõik andmed",
|
||||
"All Time": "Kõik ajad",
|
||||
"Allowed Networks": "Lubatud võrgud",
|
||||
"Alphabetic": "Tähestikuline",
|
||||
"Apply": "Rakenda",
|
||||
"Automatic upgrades": "Automaatsed uuendused",
|
||||
"Be careful!": "Ettevaatust!",
|
||||
"Cancel": "Loobu",
|
||||
@@ -47,7 +50,7 @@
|
||||
"Folder Label": "Kausta Silt",
|
||||
"Folder Type": "Kausta Tüüp",
|
||||
"Folders": "Kaustad",
|
||||
"GUI": "GUI",
|
||||
"GUI": "Kasutajaliides",
|
||||
"GUI Authentication Password": "GUI Autentimise Salasõna",
|
||||
"GUI Authentication User": "GUI Autentimise Kasutajatunnus",
|
||||
"GUI Theme": "GUI Teema",
|
||||
|
||||
@@ -309,7 +309,7 @@
|
||||
"Show diff with previous version": "Erakutsi aurreko bertsioarekiko aldeak",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Tresnaren ID-aren ordez erakutsia, taldearen egoeran. Beste tresneri erakutsia izanen da, izen erabilgarri bat bezala",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Tresnaren ID-aren ordez erakutsia, taldearen egoeran. Hutsa utzia balin bada, urrun den tresnak proposatu izenarekin aktualizatua izanen da",
|
||||
"Shutdown": "Geldi",
|
||||
"Shut Down": "Geldi",
|
||||
"Shutdown Complete": "Gelditua!",
|
||||
"Simple File Versioning": "Bertsioen segitze sinplifikatua",
|
||||
"Single level wildcard (matches within a directory only)": "Hein bakar bateko jokerra (karpetaren barnean bakarrik dagokiona)",
|
||||
|
||||
@@ -288,7 +288,7 @@
|
||||
"Show diff with previous version": "Näytä muutokset edelliseen versioon",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Näytetään ryhmän tiedoissa laitteen ID:n sijaan. Ilmoitetaan muille laitteille vaihtoehtoisena oletusnimenä.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Näytetään ryhmän tiedoissa laitteen ID:n sijaan. Tyhjä nimi päivitetään laitteen ilmoittamaksi nimeksi.",
|
||||
"Shutdown": "Sammuta",
|
||||
"Shut Down": "Sammuta",
|
||||
"Shutdown Complete": "Sammutus valmis",
|
||||
"Simple File Versioning": "Yksinkertainen tiedostoversiointi",
|
||||
"Single level wildcard (matches within a directory only)": "Yksitasoinen jokerimerkki (vaikuttaa vain kyseisen kansion sisällä)",
|
||||
|
||||
@@ -370,7 +370,7 @@
|
||||
"Show diff with previous version": "Ipakita ang diff sa nakaraang bersyon",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Ipinapakita sa halip na Device ID sa status ng cluster. Ia-advertise sa iba pang mga device bilang opsyonal na default na pangalan.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Ipinapakita sa halip na Device ID sa status ng cluster. Ia-update sa pangalan na ina-advertise ng device kung iiwanang walang laman.",
|
||||
"Shutdown": "I-shutdown",
|
||||
"Shut Down": "I-shutdown",
|
||||
"Shutdown Complete": "Tapos na ang Shutdown",
|
||||
"Simple": "Simple",
|
||||
"Simple File Versioning": "Simpleng File Versioning",
|
||||
|
||||
@@ -160,7 +160,7 @@
|
||||
"Show QR": "Afficher l'image QR",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Affiché à la place de l'ID de l'appareil dans l'état du groupe. Sera diffusé aux autres appareils comme nom convivial optionnel par défaut.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Affiché à la place de l'ID de l'appareil dans l'état du groupe. Si laissé vide, il sera renseigné par le nom convivial proposé par l'appareil distant.",
|
||||
"Shutdown": "Arrêter",
|
||||
"Shut Down": "Arrêter",
|
||||
"Shutdown Complete": "Arrêté !",
|
||||
"Simple File Versioning": "Suivi simplifié des versions",
|
||||
"Single level wildcard (matches within a directory only)": "Joker à un seul niveau (correspond uniquement à l’intérieur du répertoire)",
|
||||
|
||||
@@ -371,7 +371,7 @@
|
||||
"Show diff with previous version": "Afficher les différences avec la version précédente",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Affiché à la place de l'ID de l'appareil dans l'état du groupe. Sera diffusé aux autres appareils comme nom convivial optionnel par défaut.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Nom convivial local affiché à la place de l'ID de l'appareil dans la plupart des écrans. Si laissé vide, c'est le nom convivial local de l'appareil distant qui sera utilisé. (Modifiable ultérieurement).",
|
||||
"Shutdown": "Arrêter",
|
||||
"Shut Down": "Arrêter",
|
||||
"Shutdown Complete": "Arrêt complet",
|
||||
"Simple": "Suivi simplifié",
|
||||
"Simple File Versioning": "Suivi simplifié des versions",
|
||||
|
||||
@@ -312,7 +312,7 @@
|
||||
"Show diff with previous version": "Ferskil (diff) mei de foarige ferzje sjen litte",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Wurd ynstee fan apparaat-ID sjen litten by de bondeltastân. Wurd nei oare apparaten advertearre as in mooglike standertnamme.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Wurd yn de bondeltastân sjen litten ynstee fan apparaat-ID. Wannear't leech litten wurd, wurd it fernijt nei de namme die it apparaat útstjoert.",
|
||||
"Shutdown": "Ofslute",
|
||||
"Shut Down": "Ofslute",
|
||||
"Shutdown Complete": "Ofsluten klear",
|
||||
"Simple File Versioning": "Ienfâldich triemferzjebehear",
|
||||
"Single level wildcard (matches within a directory only)": "Inkel-nivo jokerteken (wildcard) (fergeliket allinnich binnen in map)",
|
||||
|
||||
@@ -371,7 +371,7 @@
|
||||
"Show diff with previous version": "Taispeáin diff leis an leagan roimhe seo",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Taispeántar é in ionad ID gléis i stádas na braisle. Fógrófar é do ghléasanna eile mar ainm réamhshocraithe roghnach.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Taispeántar é in ionad ID gléis i stádas na braisle. Déanfar é a nuashonrú go dtí an t-ainm a fhógraíonn an gléas má fhágtar folamh é.",
|
||||
"Shutdown": "Múchadh",
|
||||
"Shut Down": "Múchadh",
|
||||
"Shutdown Complete": "Múchadh Críochnaithe",
|
||||
"Simple": "Simplí",
|
||||
"Simple File Versioning": "Leagan Simplí Comhad",
|
||||
|
||||
@@ -323,7 +323,7 @@
|
||||
"Show diff with previous version": "Mostrar a diferencia coa versión anterior",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Mostrado en lugar do ID de Dispositivo no estado do clúster. Anunciarase a outros dispositivos como nome por defecto opcional.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Mostrado en lugar do ID de Dispositivo no estado do clúster. Actualizarase ao nome que anuncia o dispositivo de se deixar baleiro.",
|
||||
"Shutdown": "Apagar",
|
||||
"Shut Down": "Apagar",
|
||||
"Shutdown Complete": "Apagado Completado",
|
||||
"Simple": "Simple",
|
||||
"Simple File Versioning": "Versionado de Ficheiros Sinxelo",
|
||||
|
||||
@@ -371,7 +371,7 @@
|
||||
"Show diff with previous version": "הצג הבדל עם הגרסה הקודמת",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "מוצג במקום מזהה ההתקן במצב האשכול. יפורסם להתקנים אחרים כשם ברירת מחדל אופציונלי.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "מוצג במקום מזהה ההתקן במצב האשכול. יעודכן לשם שההתקן מפרסם אם נותר ריק.",
|
||||
"Shutdown": "כיבוי",
|
||||
"Shut Down": "כיבוי",
|
||||
"Shutdown Complete": "הכיבוי הושלם",
|
||||
"Simple": "פשוט",
|
||||
"Simple File Versioning": "ניהול גרסאות קבצים פשוט",
|
||||
|
||||
@@ -368,7 +368,7 @@
|
||||
"Show diff with previous version": "पिछले संस्करण के साथ अंतर दिखाएं",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "समूह स्थिति में उपकरण ID के बजाय दिखाया गया। वैकल्पिक तयशुदा नाम के रूप में अन्य उपकरणों पर विज्ञापित किया जाएगा।",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "समूह स्थिति में उपकरण ID के बजाय दिखाया गया। खाली छोड़े जाने पर उपकरण द्वारा विज्ञापित नाम में अद्यतित कर दिया जाएगा।",
|
||||
"Shutdown": "शटडाउन",
|
||||
"Shut Down": "शटडाउन",
|
||||
"Shutdown Complete": "शटडाउन पूर्ण",
|
||||
"Simple": "सरल",
|
||||
"Simple File Versioning": "सरल फाइल संस्करण",
|
||||
|
||||
@@ -345,7 +345,7 @@
|
||||
"Show diff with previous version": "Előző verzió eltérésének megjelenítése",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Az eszközazonosító helyett jelenik meg. A többi eszközön alapértelmezett névként használható.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Az eszközazonosító helyett jelenik meg. Üresen hagyva az eszköz saját neve lesz alkalmazva.",
|
||||
"Shutdown": "Leállítás",
|
||||
"Shut Down": "Leállítás",
|
||||
"Shutdown Complete": "Leállítás kész",
|
||||
"Simple": "Egyszerű",
|
||||
"Simple File Versioning": "Egyszerű fájlverzió-követés",
|
||||
|
||||
@@ -322,7 +322,7 @@
|
||||
"Show diff with previous version": "Tampilkan perbedaan dengan versi sebelumnya",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Ditampilkan sebagai ganti ID Perangkat dalam status gugus. Akan diumumkan ke perangkat lain sebagai nama bawaan opsional.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Ditampilkan sebagai ganti ID Perangkat dalam status gugus. Akan diubah menjadi nama yang perangkat umumkan jika tidak diisi.",
|
||||
"Shutdown": "Matikan",
|
||||
"Shut Down": "Matikan",
|
||||
"Shutdown Complete": "Pematian Selesai",
|
||||
"Simple File Versioning": "Pemversian Berkas Sederhana",
|
||||
"Single level wildcard (matches within a directory only)": "Wildcard tingkat tunggal (cocok hanya dalam satu direktori saja)",
|
||||
|
||||
@@ -370,7 +370,7 @@
|
||||
"Show diff with previous version": "Mostra le differenze con la versione precedente",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Visibile al posto dell'ID Dispositivo nello stato del cluster. Negli altri dispositivi verrà presentato come nome predefinito opzionale.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Visibile al posto dell'ID Dispositivo nello stato del cluster. Se viene lasciato vuoto, verrà utilizzato il nome proposto dal dispositivo.",
|
||||
"Shutdown": "Arresta",
|
||||
"Shut Down": "Arresta",
|
||||
"Shutdown Complete": "Arresto Eseguito",
|
||||
"Simple": "Semplice",
|
||||
"Simple File Versioning": "Controllo Versione Semplice",
|
||||
|
||||
@@ -311,7 +311,7 @@
|
||||
"Show diff with previous version": "前バージョンとの差分を表示",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "ステータス画面でデバイスIDの代わりに表示されます。他のデバイスに対してもデフォルトの名前として通知されます。",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "ステータス画面でデバイスIDの代わりに表示されます。空欄にすると相手側デバイスが通知してきた名前で更新されます。",
|
||||
"Shutdown": "シャットダウン",
|
||||
"Shut Down": "シャットダウン",
|
||||
"Shutdown Complete": "シャットダウン完了",
|
||||
"Simple File Versioning": "単純バージョン管理",
|
||||
"Single level wildcard (matches within a directory only)": "ワイルドカード (単一のディレクトリ内だけでマッチします)",
|
||||
|
||||
@@ -371,7 +371,7 @@
|
||||
"Show diff with previous version": "이전 버전과의 diff 보기",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "기기 아이디를 대신해 기기 목록에서 나타납니다. 다른 기기에 선택적 기본값 이름으로 통보됩니다.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "기기 아이디를 대신해 기기 목록에서 나타납니다. 비워 둘 경우 다른 기기에서 통보받은 이름으로 갱신됩니다.",
|
||||
"Shutdown": "종료",
|
||||
"Shut Down": "종료",
|
||||
"Shutdown Complete": "종료 완료",
|
||||
"Simple": "간단",
|
||||
"Simple File Versioning": "간단한 파일 버전 관리",
|
||||
|
||||
@@ -337,7 +337,7 @@
|
||||
"Show diff with previous version": "Rodyti skirtumus su ankstesne versija",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Grupės būsenoje rodomas vietoje įrenginio vardo. Kiti įrenginiai matys kaip pasirinktinį vardą.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Grupės būsenoje rodomas vietoje įrenginio vardo. Bus atnaujintas į įrenginio vardą jei nieko neįrašysite.",
|
||||
"Shutdown": "Išjungti",
|
||||
"Shut Down": "Išjungti",
|
||||
"Shutdown Complete": "Sėkmingai išjungta",
|
||||
"Simple File Versioning": "Supaprastintas versijų valdymas",
|
||||
"Single level wildcard (matches within a directory only)": "Vieno lygio pakaitos simbolis (atitinka tik vieną katalogo lygį)",
|
||||
|
||||
@@ -339,7 +339,7 @@
|
||||
"Show diff with previous version": "Vis diff med forrige version",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Vis i stedet for enhets-ID i gruppestatus. Vil bli kringkastet til andre enheter som et valgfritt forvalgsnavn.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Vist i stedet for mappe-ID i gruppestatus. Vil bli oppdatert til navnet enheten kringkaster dersom tomt.",
|
||||
"Shutdown": "Avslutt",
|
||||
"Shut Down": "Avslutt",
|
||||
"Shutdown Complete": "Avslutning fullført",
|
||||
"Simple": "Enkel",
|
||||
"Simple File Versioning": "Enkel versjonskontroll",
|
||||
|
||||
@@ -371,7 +371,7 @@
|
||||
"Show diff with previous version": "Verschil met vorige versie weergeven",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Weergegeven in plaats van de apparaat-ID in de cluster-status. Zal aan andere apparaten voorgesteld worden als een optionele standaardnaam.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Weergegeven in plaats van de apparaat-ID in de clusterstatus. Zal bijgewerkt worden met de naam die het apparaat voorstelt wanneer leeg gelaten.",
|
||||
"Shutdown": "Afsluiten",
|
||||
"Shut Down": "Afsluiten",
|
||||
"Shutdown Complete": "Afsluiten voltooid",
|
||||
"Simple": "Eenvoudig",
|
||||
"Simple File Versioning": "Eenvoudig versiebeheer",
|
||||
|
||||
@@ -187,7 +187,7 @@
|
||||
"Show QR": "Vis QR",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Vist i staden for einings-ID-en i klyngjestatusen. Vil verta kringkasta til dei andre einingane som eit valfritt standardnamn.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Vist i staden for mappe-ID-en i klyngjestatuses. Vil verta oppdatert til namnet eininga kringkastar dersom tomt.",
|
||||
"Shutdown": "Slå Av",
|
||||
"Shut Down": "Slå Av",
|
||||
"Shutdown Complete": "Slått av",
|
||||
"Simple File Versioning": "Enkel filutgåvehandtering",
|
||||
"Single level wildcard (matches within a directory only)": "Enkeltnivå-jokerteikn (søkjer berre i éi mappe)",
|
||||
|
||||
@@ -371,7 +371,7 @@
|
||||
"Show diff with previous version": "Pokaż diff z poprzednią wersją",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Widoczna na liście urządzeń zamiast identyfikatora urządzenia. Będzie anonsowana do innych urządzeń jako opcjonalna nazwa domyślna.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Widoczna na liście urządzeń zamiast identyfikatora urządzenia. Zostanie zaktualizowana do nazwy anonsowanej przez urządzenie, jeżeli pozostanie pusta.",
|
||||
"Shutdown": "Wyłącz",
|
||||
"Shut Down": "Wyłącz",
|
||||
"Shutdown Complete": "Wyłączanie ukończone",
|
||||
"Simple": "Proste",
|
||||
"Simple File Versioning": "Proste wersjonowanie plików",
|
||||
|
||||
@@ -370,7 +370,7 @@
|
||||
"Show diff with previous version": "Mostrar diferenças da versão anterior",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Mostrado no lugar do ID do dispositivo no indicador de estado do grupo. Será divulgado aos outros dispositivos como um nome opcional e pré-definido.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Mostrado no lugar do ID do dispositivo no indicador de estado do grupo. Será atualizado para o nome que o dispositivo divulga, caso seja deixado em branco.",
|
||||
"Shutdown": "Desligar",
|
||||
"Shut Down": "Desligar",
|
||||
"Shutdown Complete": "Desligamento completado",
|
||||
"Simple": "Simples",
|
||||
"Simple File Versioning": "Simples",
|
||||
|
||||
@@ -370,7 +370,7 @@
|
||||
"Show diff with previous version": "Mostrar diferenças em relação à versão anterior",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Apresentado ao invés do ID do dispositivo no indicador de estado do grupo. Será divulgado aos outros dispositivos como um nome predefinido opcional.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Apresentado ao invés do ID do dispositivo no indicador de estado do grupo. Será actualizado para o nome que o dispositivo divulga, se for deixado em branco.",
|
||||
"Shutdown": "Desligar",
|
||||
"Shut Down": "Desligar",
|
||||
"Shutdown Complete": "Encerramento completado",
|
||||
"Simple": "Simples",
|
||||
"Simple File Versioning": "Simples",
|
||||
|
||||
@@ -312,7 +312,7 @@
|
||||
"Show diff with previous version": "Show diff with previous version",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Vizibil în locul ID-ului dispozitivului într-un grup. Va fi sugerat celorlalte dispozitive ca nume opţional. ",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Vizibil în locul ID-ului dispozitivului într-un grup. Va fi înlocuit de numele sugerat de dispozitiv daca nu este completat. ",
|
||||
"Shutdown": "Opreşte",
|
||||
"Shut Down": "Opreşte",
|
||||
"Shutdown Complete": "Oprește Complet",
|
||||
"Simple File Versioning": "Versiuni simple ale documentelor",
|
||||
"Single level wildcard (matches within a directory only)": "Asterisc de nivel simplu (corespunde doar unui fişier)",
|
||||
|
||||
@@ -371,7 +371,7 @@
|
||||
"Show diff with previous version": "Показать различия с предыдущей версией",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Отображается вместо ID устройства в статусе группы. Будет разослан другим устройствам в качестве имени по умолчанию.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Отображается вместо ID устройства в статусе группы. Если поле не заполнено, то будет установлено имя, передаваемое этим устройством.",
|
||||
"Shutdown": "Выключить",
|
||||
"Shut Down": "Выключить",
|
||||
"Shutdown Complete": "Выключение",
|
||||
"Simple": "Просто",
|
||||
"Simple File Versioning": "Простое управление версиями файлов",
|
||||
|
||||
@@ -340,7 +340,7 @@
|
||||
"Show diff with previous version": "පෙර අනුවාදය සමඟ වෙනස පෙන්වන්න",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "පොකුරු තත්ත්වයේ උපාංග හැඳුනුම්පත වෙනුවට පෙන්වා ඇත. විකල්ප පෙරනිමි නාමයක් ලෙස වෙනත් උපාංග වෙත ප්රචාරණය කරනු ඇත.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "පොකුරු තත්ත්වයේ උපාංග හැඳුනුම්පත වෙනුවට පෙන්වා ඇත. හිස්ව තැබුවහොත් උපාංගය ප්රචාරණය කරන නමට යාවත්කාලීන වේ.",
|
||||
"Shutdown": "වසා දමන්න",
|
||||
"Shut Down": "වසා දමන්න",
|
||||
"Shutdown Complete": "වසා දැමීම සම්පූර්ණයි",
|
||||
"Simple": "සරල",
|
||||
"Simple File Versioning": "සරල ගොනු අනුවාදය",
|
||||
|
||||
@@ -366,7 +366,7 @@
|
||||
"Show diff with previous version": "Ukázať rozdiely s predchádzajúcou verziou",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Zobrazované namiesto ID zariadenia v štatúte klastra. Toto pomenovanie bude oznamovať ostatným zariadeniam ako voliteľné predvolené pomenovanie.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Zobrazované namiesto ID zariadenia v klastri. Ak je ponechané prázdne, bude nahradené pomenovaním, ktoré oznamuje zariadenie.",
|
||||
"Shutdown": "Vypnutie",
|
||||
"Shut Down": "Vypnutie",
|
||||
"Shutdown Complete": "Vypnutie ukončené",
|
||||
"Simple": "Jednoduché",
|
||||
"Simple File Versioning": "Jednoduché verzie súborov",
|
||||
|
||||
@@ -315,7 +315,7 @@
|
||||
"Show diff with previous version": "Pokaži razliko s prejšnjo različico",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Prikazano namesto ID-ja naprave v stanju gruče. Oglašuje se drugim napravam kot neobvezno privzeto ime.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Prikazano namesto ID-ja naprave v stanju gruče. Če ostane prazno, bo posodobljeno na ime, ki ga oglašuje naprava.",
|
||||
"Shutdown": "Izklopi",
|
||||
"Shut Down": "Izklopi",
|
||||
"Shutdown Complete": "Izklop končan",
|
||||
"Simple File Versioning": "Enostvno beleženje različic datotek",
|
||||
"Single level wildcard (matches within a directory only)": "Enostopenjski nadomestni znak (ujema se samo v imeniku)",
|
||||
|
||||
@@ -316,8 +316,8 @@
|
||||
"Received data is already encrypted": "Mottagna data är redan krypterade",
|
||||
"Recent Changes": "Senaste ändringar",
|
||||
"Reduced by ignore patterns": "Minskas med ignoreringsmönster",
|
||||
"Relay LAN": "Relä LAN",
|
||||
"Relay WAN": "Relä WAN",
|
||||
"Relay LAN": "LAN-relä",
|
||||
"Relay WAN": "WAN-relä",
|
||||
"Release Notes": "Versionsanteckningar",
|
||||
"Release candidates contain the latest features and fixes. They are similar to the traditional bi-weekly Syncthing releases.": "Utgåvskandidater innehåller de senaste funktionerna och korrigeringarna. De liknar de traditionella Syncthing-utgåvorna som kommer ut varannan vecka.",
|
||||
"Remote Devices": "Fjärrenheter",
|
||||
@@ -371,7 +371,7 @@
|
||||
"Show diff with previous version": "Visa skillnad med tidigare version",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Visas istället för enhets-ID i klusterstatus. Kommer att annonseras på andra enheter som ett valfritt standardnamn.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Visas istället för enhets-ID i klusterstatusen. Kommer att uppdateras till det namn som enheten annonserar om det lämnas tomt.",
|
||||
"Shutdown": "Stäng av",
|
||||
"Shut Down": "Stäng av",
|
||||
"Shutdown Complete": "Avstängning slutförd",
|
||||
"Simple": "Enkel",
|
||||
"Simple File Versioning": "Enkel filversionshantering",
|
||||
|
||||
@@ -371,7 +371,7 @@
|
||||
"Show diff with previous version": "Önceki sürüm ile farklılıkları göster",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Küme durumunda Cihaz Kimliği yerine gösterilir. İsteğe bağlı varsayılan ad olarak diğer cihazlara duyurulacaktır.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Küme durumunda Cihaz Kimliği yerine gösterilir. Boş bırakılırsa duyurulan cihaz adına güncellenecektir.",
|
||||
"Shutdown": "Kapat",
|
||||
"Shut Down": "Kapat",
|
||||
"Shutdown Complete": "Kapatma İşlemi Tamamlandı",
|
||||
"Simple": "Basit",
|
||||
"Simple File Versioning": "Basit Dosya Sürümlendirme",
|
||||
|
||||
@@ -227,6 +227,7 @@
|
||||
"Learn more": "Дізнатися більше",
|
||||
"Learn more at {%url%}": "Дізнайтесь більше за посиланням {{url}}",
|
||||
"Limit": "Ліміт",
|
||||
"Limit Bandwidth in LAN": "Обмеження пропускної здатності в локальній мережі",
|
||||
"Listener Failures": "Помилки приймачів",
|
||||
"Listener Status": "Стан приймача",
|
||||
"Listeners": "Приймачі",
|
||||
@@ -370,7 +371,7 @@
|
||||
"Show diff with previous version": "Показати відмінності від попередньої версії",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Показується замість ID пристрою в статусі кластера. Буде розголошено іншим вузлам як опціональне типове ім’я.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Показується замість ID пристрою в статусі кластера. Буде оновлено ім’ям, яке розголошене пристроєм, якщо залишити порожнім.",
|
||||
"Shutdown": "Вимкнути",
|
||||
"Shut Down": "Вимкнути",
|
||||
"Shutdown Complete": "Вимикання завершене",
|
||||
"Simple": "Просте",
|
||||
"Simple File Versioning": "Просте версіювання",
|
||||
|
||||
@@ -161,7 +161,7 @@
|
||||
"Show QR": "Hiển thị QR",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Hiển thị thay cho ID th.bị trong trạng thái cụm. Sẽ được giới thiệu đến các th.bị khác như tên mặc định tuỳ chọn.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Hiển thị thay cho ID thiết bị trong trạng thái cụm. Nếu để trống sẽ được cập nhật thành tên mà thiết bị giới thiệu.",
|
||||
"Shutdown": "Tắt",
|
||||
"Shut Down": "Tắt",
|
||||
"Shutdown Complete": "Tắt hoàn tất",
|
||||
"Simple File Versioning": "Kiểu đơn giản",
|
||||
"Single level wildcard (matches within a directory only)": "Ký tự thay thế đơn cấp (phù hợp với chỉ một thư mục)",
|
||||
|
||||
@@ -371,7 +371,7 @@
|
||||
"Show diff with previous version": "显示与以前版本的差异",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "在集群状态中显示该名称,而不是设备 ID。将作为可选的默认名称向其他设备通告。",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "在集群状态中显示该名称,而不是设备 ID。如果留空,将更新为设备通告的名称。",
|
||||
"Shutdown": "关闭",
|
||||
"Shut Down": "关闭",
|
||||
"Shutdown Complete": "关闭完成",
|
||||
"Simple": "简单",
|
||||
"Simple File Versioning": "简单文件版本控制",
|
||||
|
||||
@@ -340,7 +340,7 @@
|
||||
"Show diff with previous version": "顯示與先前版本的差異",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "在集群狀態中顯示該名稱,而不是設備 ID。將會作為當前設備的可選的默認名稱,報告給所有其他設備。",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "在集群狀態中顯示該名稱,而不是設備 ID。如果設置為空,則會使用目標設備自報的默認名稱。",
|
||||
"Shutdown": "關閉",
|
||||
"Shut Down": "關閉",
|
||||
"Shutdown Complete": "關閉完成",
|
||||
"Simple": "簡易",
|
||||
"Simple File Versioning": "簡易版本控制",
|
||||
|
||||
@@ -368,7 +368,7 @@
|
||||
"Show diff with previous version": "顯示與前一個版本的差異",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "代替裝置識別碼顯示在叢集狀態中。這段文字將會廣播到其他的裝置作為一個可選的預設名稱。",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "代替裝置識別碼顯示在叢集狀態中。本欄若未填寫則將被更新為此裝置所廣播的名稱。",
|
||||
"Shutdown": "關閉",
|
||||
"Shut Down": "關閉",
|
||||
"Shutdown Complete": "關閉完成",
|
||||
"Simple": "簡單",
|
||||
"Simple File Versioning": "簡單檔案版本控制",
|
||||
|
||||
@@ -108,27 +108,23 @@
|
||||
<li><a href="" ng-click="about.show()"><span class="fa fa-fw fa-heart"></span> <span translate>About</span></a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li ng-if="authenticated || config.gui.debugging" class="dropdown action-menu">
|
||||
<li ng-if="authenticated" class="dropdown action-menu">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
|
||||
<span class="fa fa-cog"></span>
|
||||
<span class="hidden-xs" translate>Actions</span>
|
||||
<span class="caret"></span>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-if="authenticated"><a href="" ng-click="showSettings()"><span class="fa fa-fw fa-cog"></span> <span translate>Settings</span></a></li>
|
||||
<li ng-if="authenticated"><a href="" ng-click="showDeviceIdentification(thisDevice())"><span class="fa fa-fw fa-qrcode"></span> <span translate>Show ID</span></a></li>
|
||||
|
||||
<li ng-if="authenticated" class="divider" aria-hidden="true"></li>
|
||||
<li ng-if="authenticated"><a href="" ng-click="advanced()"><span class="fa fa-fw fa-cogs"></span> <span translate>Advanced</span></a></li>
|
||||
<li ng-if="authenticated"><a href="" ng-click="logging.show()"><span class="fa fa-fw fa-wrench"></span> <span translate>Logs</span></a></li>
|
||||
|
||||
<li class="divider" aria-hidden="true" ng-if="config.gui.debugging"></li>
|
||||
<li><a href="/rest/debug/support" target="_blank" ng-if="config.gui.debugging"><span class="fa fa-fw fa-user-md"></span> <span translate>Support Bundle</span></a></li>
|
||||
|
||||
<li ng-if="authenticated" class="divider" aria-hidden="true"></li>
|
||||
<li ng-if="authenticated && isAuthEnabled()"><a href="" ng-click="logout()"><span class="far fa-fw fa-sign-out"></span> <span translate>Log Out</span></a></li>
|
||||
<li ng-if="authenticated"><a href="" ng-click="restart()"><span class="fa fa-fw fa-refresh"></span> <span translate>Restart</span></a></li>
|
||||
<li ng-if="authenticated"><a href="" ng-click="shutdown()"><span class="fa fa-fw fa-power-off"></span> <span translate>Shutdown</span></a></li>
|
||||
<li><a href="" ng-click="showSettings()"><span class="fa fa-fw fa-cog"></span> <span translate>Settings</span></a></li>
|
||||
<li><a href="" ng-click="advanced()"><span class="fa fa-fw fa-cogs"></span> <span translate>Advanced</span></a></li>
|
||||
<li class="divider" aria-hidden="true"></li>
|
||||
<li><a href="" ng-click="showDeviceIdentification(thisDevice())"><span class="fa fa-fw fa-qrcode"></span> <span translate>Show ID</span></a></li>
|
||||
<li><a href="" ng-click="logging.show()"><span class="fa fa-fw fa-wrench"></span> <span translate>Logs</span></a></li>
|
||||
<li><a href="/rest/debug/support" target="_blank"><span class="fa fa-fw fa-user-md"></span> <span translate>Support Bundle</span></a></li>
|
||||
<li class="divider" aria-hidden="true"></li>
|
||||
<li ng-if="isAuthEnabled()"><a href="" ng-click="logout()"><span class="far fa-fw fa-sign-out"></span> <span translate>Log Out</span></a></li>
|
||||
<li><a href="" ng-click="restart()"><span class="fa fa-fw fa-refresh"></span> <span translate>Restart</span></a></li>
|
||||
<li><a href="" ng-click="shutdown()"><span class="fa fa-fw fa-power-off"></span> <span translate>Shut Down</span></a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -619,10 +615,10 @@
|
||||
<button type="button" class="btn btn-sm btn-danger pull-left" ng-click="revertOverrideConfirmationModal('override', folder.id)" ng-if="folderStatus(folder) == 'outofsync' && folder.type == 'sendonly'">
|
||||
<span class="fas fa-arrow-circle-up"></span> <span translate>Override Changes</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-danger pull-left" ng-click="revertOverrideConfirmationModal('revert', folder.id)" ng-if="hasReceiveOnlyChanged(folder)">
|
||||
<button type="button" class="btn btn-sm btn-danger pull-left" ng-click="revertOverrideConfirmationModal('revert', folder.id)" ng-if="hasReceiveOnlyChanged(folder) && ['outofsync', 'faileditems', 'localadditions'].indexOf(folderStatus(folder)) >= 0">
|
||||
<span class="fa fa-arrow-circle-down"></span> <span translate>Revert Local Changes</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-danger pull-left" ng-click="revertOverrideConfirmationModal('deleteEnc', folder.id)" ng-if="hasReceiveEncryptedItems(folder)">
|
||||
<button type="button" class="btn btn-sm btn-danger pull-left" ng-click="revertOverrideConfirmationModal('deleteEnc', folder.id)" ng-if="hasReceiveEncryptedItems(folder) && ['outofsync', 'faileditems', 'localunencrypted'].indexOf(folderStatus(folder)) >= 0">
|
||||
<span class="fa fa-minus-circle"></span> <span translate>Delete Unexpected Items</span>
|
||||
</button>
|
||||
<span class="pull-right">
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<h4 class="text-center" translate>The Syncthing Authors</h4>
|
||||
<div class="row">
|
||||
<div class="col-md-12" id="contributor-list">
|
||||
Jakob Borg, Audrius Butkevicius, Simon Frei, Tomasz Wilczyński, Alexander Graf, Alexandre Viau, Anderson Mesquita, André Colomb, Antony Male, Ben Schulz, bt90, Caleb Callaway, Daniel Harte, Emil Lundberg, Eric P, Evgeny Kuznetsov, greatroar, Lars K.W. Gohlke, Lode Hoste, Michael Ploujnikov, Ross Smith II, Stefan Tatschner, Wulf Weich, Adam Piggott, Adel Qalieh, Aleksey Vasenev, Alessandro G., Alex Ionescu, Alex Lindeman, Alex Xu, Alexander Seiler, Alexandre Alves, Aman Gupta, Andreas Sommer, andresvia, Andrew Rabert, Andrey D, andyleap, Anjan Momi, Anthony Goeckner, Antoine Lamielle, Anur, Aranjedeath, ardevd, Arkadiusz Tymiński, Aroun, Arthur Axel fREW Schmidt, Artur Zubilewicz, Ashish Bhate, Aurélien Rainone, BAHADIR YILMAZ, Bart De Vries, Beat Reichenbach, Ben Shepherd, Ben Sidhom, Benedikt Heine, Benno Fünfstück, Benny Ng, boomsquared, Boqin Qin, Boris Rybalkin, Brendan Long, Catfriend1, Cathryne Linenweaver, Cedric Staniewski, Chih-Hsuan Yen, Choongkyu, Chris Howie, Chris Joel, Christian Kujau, Christian Prescott, chucic, cjc7373, Colin Kennedy, Cromefire_, Cyprien Devillez, d-volution, Dan, Daniel Barczyk, Daniel Bergmann, Daniel Martí, Daniel Padrta, Darshil Chanpura, dashangcun, David Rimmer, DeflateAwning, Denis A., Dennis Wilson, derekriemer, DerRockWolf, desbma, Devon G. Redekopp, digital, Dimitri Papadopoulos Orfanos, Dmitry Saveliev, domain, Domenic Horner, Dominik Heidler, Elias Jarlebring, Elliot Huffman, Emil Hessman, Eng Zer Jun, entity0xfe, Eric Lesiuta, Erik Meitner, Evan Spensley, Federico Castagnini, Felix, Felix Ableitner, Felix Lampe, Felix Unterpaintner, Francois-Xavier Gsell, Frank Isemann, Gahl Saraf, georgespatton, ghjklw, Gilli Sigurdsson, Gleb Sinyavskiy, Graham Miln, Greg, guangwu, gudvinr, Gusted, Han Boetes, HansK-p, Harrison Jones, Hazem Krimi, Heiko Zuerker, Hireworks, Hugo Locurcio, Iain Barnett, Ian Johnson, ignacy123, Iskander Sharipov, Jaakko Hannikainen, Jack Croft, Jacob, Jake Peterson, James O'Beirne, James Patterson, Jaroslav Lichtblau, Jaroslav Malec, Jaspitta, Jaya Chithra, Jaya Kumar, Jeffery To, jelle van der Waa, Jens Diemer, Jochen Voss, Johan Vromans, John Rinehart, Jonas Thelemann, Jonathan, Jose Manuel Delicado, jtagcat, Julian Lehrhuber, Jörg Thalheim, Jędrzej Kula, Kapil Sareen, Karol Różycki, Kebin Liu, Keith Harrison, Kelong Cong, Ken'ichi Kamada, Kevin Allen, Kevin Bushiri, Kevin White, Jr., klemens, Kurt Fitzner, kylosus, Lars Lehtonen, Laurent Etiemble, Leo Arias, Liu Siyuan, Lord Landon Agahnim, LSmithx2, Lukas Lihotzki, Luke Hamburg, luzpaz, Majed Abdulaziz, Marc Laporte, Marcel Meyer, Marcin Dziadus, Marcus B Spencer, Marcus Legendre, Mario Majila, Mark Pulford, Martchus, Mateusz Naściszewski, Mateusz Ż, mathias4833, Matic Potočnik, Matt Burke, Matt Robenolt, Matteo Ruina, Maurizio Tomasi, Max, Max Schulze, MaximAL, Maximilian, Michael Jephcote, Michael Rienstra, MichaIng, Migelo, Mike Boone, MikeLund, MikolajTwarog, Mingxuan Lin, mv1005, Nate Morrison, nf, Nicholas Rishel, Nick Busey, Nico Stapelbroek, Nicolas Braud-Santoni, Nicolas Perraut, Niels Peter Roest, Nils Jakobi, NinoM4ster, Nitroretro, NoLooseEnds, Oliver Freyermuth, orangekame3, otbutz, overkill, Oyebanji Jacob Mayowa, Pablo, Pascal Jungblut, Paul Brit, Paul Donald, Pawel Palenica, perewa, Peter Badida, Peter Dave Hello, Peter Hoeg, Peter Marquardt, Phani Rithvij, Phil Davis, Philippe Schommers, Phill Luby, Piotr Bejda, polyfloyd, pullmerge, Quentin Hibon, Rahmi Pruitt, red_led, Robert Carosi, Roberto Santalla, Robin Schoonover, Roman Zaynetdinov, rubenbe, Ruslan Yevdokymov, Ryan Qian, Ryan Sullivan, Sacheendra Talluri, Scott Klupfel, sec65, Sergey Mishin, Sertonix, Severin von Wnuck-Lipinski, Shaarad Dalvi, Simon Mwepu, Simon Pickup, Sly_tom_cat, Sonu Kumar Saw, Stefan Kuntz, Steven Eckhoff, Suhas Gundimeda, Sven Bachmann, Sébastien WENSKE, Taylor Khan, Terrance, TheCreeper, Thomas, Thomas Hipp, Tim Abell, Tim Howes, Tobias Frölich, Tobias Klauser, Tobias Nygren, Tobias Tom, Tom Jakubowski, Tommy van der Vorst, Tully Robinson, Tyler Brazier, Tyler Kropp, Unrud, vapatel2, Veeti Paananen, Victor Buinsky, Vik, Vil Brekin, villekalliomaki, Vladimir Rusinov, wangguoliang, WangXi, Will Rouesnel, William A. Kennington III, wouter bolsterlee, xarx00, Xavier O., xjtdy888, Yannic A., yparitcher, 佛跳墙, 落心
|
||||
Jakob Borg, Audrius Butkevicius, Simon Frei, Tomasz Wilczyński, Alexander Graf, Alexandre Viau, Anderson Mesquita, André Colomb, Antony Male, Ben Schulz, bt90, Caleb Callaway, Daniel Harte, Emil Lundberg, Eric P, Evgeny Kuznetsov, greatroar, Lars K.W. Gohlke, Lode Hoste, Michael Ploujnikov, Ross Smith II, Stefan Tatschner, Wulf Weich, Adam Piggott, Adel Qalieh, Aleksey Vasenev, Alessandro G., Alex Ionescu, Alex Lindeman, Alex Xu, Alexander Seiler, Alexandre Alves, Aman Gupta, Andreas Sommer, andresvia, Andrew Rabert, Andrey D, andyleap, Anjan Momi, Anthony Goeckner, Antoine Lamielle, Anur, Aranjedeath, ardevd, Arkadiusz Tymiński, Aroun, Arthur Axel fREW Schmidt, Artur Zubilewicz, Ashish Bhate, Aurélien Rainone, BAHADIR YILMAZ, Bart De Vries, Beat Reichenbach, Ben Shepherd, Ben Sidhom, Benedikt Heine, Benno Fünfstück, Benny Ng, boomsquared, Boqin Qin, Boris Rybalkin, Brendan Long, Catfriend1, Cathryne Linenweaver, Cedric Staniewski, Chih-Hsuan Yen, Choongkyu, Chris Howie, Chris Joel, Christian Kujau, Christian Prescott, chucic, cjc7373, Colin Kennedy, Cromefire_, Cyprien Devillez, d-volution, Dan, Daniel Barczyk, Daniel Bergmann, Daniel Martí, Daniel Padrta, Daniil Gentili, Darshil Chanpura, dashangcun, David Rimmer, DeflateAwning, Denis A., Dennis Wilson, derekriemer, DerRockWolf, desbma, Devon G. Redekopp, digital, Dimitri Papadopoulos Orfanos, Dmitry Saveliev, domain, Domenic Horner, Dominik Heidler, Elias Jarlebring, Elliot Huffman, Emil Hessman, Eng Zer Jun, entity0xfe, Eric Lesiuta, Erik Meitner, Evan Spensley, Federico Castagnini, Felix, Felix Ableitner, Felix Lampe, Felix Unterpaintner, Francois-Xavier Gsell, Frank Isemann, Gahl Saraf, georgespatton, ghjklw, Gilli Sigurdsson, Gleb Sinyavskiy, Graham Miln, Greg, guangwu, gudvinr, Gusted, Han Boetes, HansK-p, Harrison Jones, Hazem Krimi, Heiko Zuerker, Hireworks, Hugo Locurcio, Iain Barnett, Ian Johnson, ignacy123, Iskander Sharipov, Jaakko Hannikainen, Jack Croft, Jacob, Jake Peterson, James O'Beirne, James Patterson, Jaroslav Lichtblau, Jaroslav Malec, Jaspitta, Jaya Chithra, Jaya Kumar, Jeffery To, jelle van der Waa, Jens Diemer, Jochen Voss, Johan Vromans, John Rinehart, Jonas Thelemann, Jonathan, Jose Manuel Delicado, jtagcat, Julian Lehrhuber, Jörg Thalheim, Jędrzej Kula, Kapil Sareen, Karol Różycki, Kebin Liu, Keith Harrison, Kelong Cong, Ken'ichi Kamada, Kevin Allen, Kevin Bushiri, Kevin White, Jr., klemens, Kurt Fitzner, kylosus, Lars Lehtonen, Laurent Etiemble, Leo Arias, Liu Siyuan, Lord Landon Agahnim, LSmithx2, Lukas Lihotzki, Luke Hamburg, luzpaz, Majed Abdulaziz, Marc Laporte, Marcel Meyer, Marcin Dziadus, Marcus B Spencer, Marcus Legendre, Mario Majila, Mark Pulford, Martchus, Mateusz Naściszewski, Mateusz Ż, mathias4833, Matic Potočnik, Matt Burke, Matt Robenolt, Matteo Ruina, Maurizio Tomasi, Max, Max Schulze, MaximAL, Maximilian, Michael Jephcote, Michael Rienstra, MichaIng, Migelo, Mike Boone, MikeLund, MikolajTwarog, Mingxuan Lin, mv1005, Nate Morrison, nf, Nicholas Rishel, Nick Busey, Nico Stapelbroek, Nicolas Braud-Santoni, Nicolas Perraut, Niels Peter Roest, Nils Jakobi, NinoM4ster, Nitroretro, NoLooseEnds, Oliver Freyermuth, orangekame3, otbutz, overkill, Oyebanji Jacob Mayowa, Pablo, Pascal Jungblut, Paul Brit, Paul Donald, Pawel Palenica, perewa, Peter Badida, Peter Dave Hello, Peter Hoeg, Peter Marquardt, Phani Rithvij, Phil Davis, Philippe Schommers, Phill Luby, Piotr Bejda, polyfloyd, pullmerge, Quentin Hibon, Rahmi Pruitt, red_led, Robert Carosi, Roberto Santalla, Robin Schoonover, Roman Zaynetdinov, rubenbe, Ruslan Yevdokymov, Ryan Qian, Ryan Sullivan, Sacheendra Talluri, Scott Klupfel, sec65, Sergey Mishin, Sertonix, Severin von Wnuck-Lipinski, Shaarad Dalvi, Simon Mwepu, Simon Pickup, Sly_tom_cat, Sonu Kumar Saw, Stefan Kuntz, Steven Eckhoff, Suhas Gundimeda, Sven Bachmann, Sébastien WENSKE, Taylor Khan, Terrance, TheCreeper, Thomas, Thomas Hipp, Tim Abell, Tim Howes, Tobias Frölich, Tobias Klauser, Tobias Nygren, Tobias Tom, Tom Jakubowski, Tommy van der Vorst, Tully Robinson, Tyler Brazier, Tyler Kropp, Unrud, vapatel2, Veeti Paananen, Victor Buinsky, Vik, Vil Brekin, villekalliomaki, Vladimir Rusinov, wangguoliang, WangXi, Will Rouesnel, William A. Kennington III, wouter bolsterlee, xarx00, Xavier O., xjtdy888, Yannic A., yparitcher, 佛跳墙, 落心
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -102,7 +102,6 @@ Jakob Borg, Audrius Butkevicius, Simon Frei, Tomasz Wilczyński, Alexander Graf,
|
||||
<li><a href="https://github.com/urfave/cli">urfave/cli</a>, Copyright © 2016 Jeremy Saenz & Contributors.</li>
|
||||
<li><a href="https://github.com/vitrun/qart">vitrun/qart</a>, Copyright © 2010-2011 The Go Authors.</li>
|
||||
<li><a href="https://github.com/willabides/kongplete">willabides/kongplete</a>, Copyright © 2020 WillAbides.</li>
|
||||
<li><a href="https://github.com/wlynxg/anet">wlynxg/anet</a>, Copyright © 2023, wlynxg.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ angular.module('syncthing.core')
|
||||
middleCol = Math.ceil(size / 2) - 1;
|
||||
|
||||
if (value) {
|
||||
value = value.toString().replace(/[\W_]/i, '');
|
||||
value = value.toString().replace(/[\W_]/g, '');
|
||||
|
||||
for (row = 0; row < size; ++row) {
|
||||
for (col = middleCol; col > -1; --col) {
|
||||
|
||||
@@ -16,11 +16,16 @@
|
||||
<label translate>Available debug logging facilities:</label>
|
||||
<table class="table table-condensed table-striped">
|
||||
<tbody>
|
||||
<tr ng-repeat="(name, data) in logging.facilities">
|
||||
<td>
|
||||
<input type="checkbox" ng-model="data.enabled" ng-change="logging.onFacilityChange(name)" ng-disabled="data.enabled == null"> <span>{{ name }}</span>
|
||||
<tr ng-repeat="(key, level) in logging.facilities.levels">
|
||||
<td>{{ logging.facilities.packages[key] }} (<code>{{ key }}</code>)</td>
|
||||
<td class="form-group">
|
||||
<select class="form-control" ng-model="logging.facilities.levels[key]" ng-change="logging.onFacilityChange()" ng-disabled="logging.facilities.updating">
|
||||
<option value="DEBUG" translate>Debug</option>
|
||||
<option value="INFO" translate>Info</option>
|
||||
<option value="WARN" translate>Warning</option>
|
||||
<option value="ERROR" translate>Error</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>{{ data.description }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -1568,16 +1568,8 @@ angular.module('syncthing.core')
|
||||
$scope.logging = {
|
||||
facilities: {},
|
||||
refreshFacilities: function () {
|
||||
$http.get(urlbase + '/system/debug').success(function (data) {
|
||||
var facilities = {};
|
||||
data.enabled = data.enabled || [];
|
||||
$.each(data.facilities, function (key, value) {
|
||||
facilities[key] = {
|
||||
description: value,
|
||||
enabled: data.enabled.indexOf(key) > -1
|
||||
}
|
||||
})
|
||||
$scope.logging.facilities = facilities;
|
||||
$http.get(urlbase + '/system/loglevels').success(function (data) {
|
||||
$scope.logging.facilities = data;
|
||||
}).error($scope.emitHTTPError);
|
||||
},
|
||||
show: function () {
|
||||
@@ -1597,13 +1589,10 @@ angular.module('syncthing.core')
|
||||
});
|
||||
showModal('#logViewer');
|
||||
},
|
||||
onFacilityChange: function (facility) {
|
||||
var enabled = $scope.logging.facilities[facility].enabled;
|
||||
// Disable checkboxes while we're in flight.
|
||||
$.each($scope.logging.facilities, function (key) {
|
||||
$scope.logging.facilities[key].enabled = null;
|
||||
})
|
||||
$http.post(urlbase + '/system/debug?' + (enabled ? 'enable=' : 'disable=') + facility)
|
||||
onFacilityChange: function () {
|
||||
// Disable editing while we're in flight.
|
||||
$scope.logging.facilities.updating = true;
|
||||
$http.post(urlbase + '/system/loglevels', $scope.logging.facilities.levels)
|
||||
.success($scope.logging.refreshFacilities)
|
||||
.error($scope.emitHTTPError);
|
||||
},
|
||||
@@ -1626,7 +1615,7 @@ angular.module('syncthing.core')
|
||||
content: function () {
|
||||
var content = "";
|
||||
$.each($scope.logging.entries, function (idx, entry) {
|
||||
content += entry.when.split('.')[0].replace('T', ' ') + ' ' + entry.message + "\n";
|
||||
content += entry.when.split('.')[0].replace('T', ' ') + ' ' + entry.level + ' ' + entry.message + "\n";
|
||||
});
|
||||
return content;
|
||||
},
|
||||
|
||||
2
gui/default/vendor/fancytree/LICENSE.txt
vendored
2
gui/default/vendor/fancytree/LICENSE.txt
vendored
@@ -1,4 +1,4 @@
|
||||
Copyright 2008-2021 Martin Wendt,
|
||||
Copyright 2008-2023 Martin Wendt,
|
||||
https://wwWendt.de/
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,9 +9,9 @@ package olddb
|
||||
import (
|
||||
"encoding/binary"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"github.com/syncthing/syncthing/internal/db/olddb/backend"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
)
|
||||
|
||||
// A smallIndex is an in memory bidirectional []byte to uint32 map. It gives
|
||||
@@ -32,7 +32,6 @@ func newSmallIndex(db backend.Backend, prefix []byte) *smallIndex {
|
||||
prefix: prefix,
|
||||
id2val: make(map[uint32]string),
|
||||
val2id: make(map[string]uint32),
|
||||
mut: sync.NewMutex(),
|
||||
}
|
||||
idx.load()
|
||||
return idx
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"iter"
|
||||
"log/slog"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -78,7 +79,7 @@ func (s *DB) getFolderDB(folder string, create bool) (*folderDB, error) {
|
||||
}
|
||||
}
|
||||
|
||||
l.Debugf("Folder %s in database %s", folder, dbName)
|
||||
slog.Debug("Folder database opened", "folder", folder, "db", dbName)
|
||||
path := dbName
|
||||
if !filepath.IsAbs(path) {
|
||||
path = filepath.Join(s.pathBase, dbName)
|
||||
|
||||
@@ -7,12 +7,14 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/internal/db"
|
||||
"github.com/syncthing/syncthing/internal/slogutil"
|
||||
)
|
||||
|
||||
const maxDBConns = 16
|
||||
@@ -128,7 +130,7 @@ func OpenTemp() (*DB, error) {
|
||||
return nil, wrap(err)
|
||||
}
|
||||
path := filepath.Join(dir, "db")
|
||||
l.Debugln("Test DB in", path)
|
||||
slog.Debug("Test DB", slogutil.FilePath(path))
|
||||
return Open(path)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,10 +9,12 @@ package sqlite
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/syncthing/syncthing/internal/db"
|
||||
"github.com/syncthing/syncthing/internal/slogutil"
|
||||
"github.com/thejerf/suture/v4"
|
||||
)
|
||||
|
||||
@@ -56,7 +58,7 @@ func (s *Service) Serve(ctx context.Context) error {
|
||||
if wait < 0 {
|
||||
wait = time.Minute
|
||||
}
|
||||
l.Debugln("Next periodic run in", wait)
|
||||
slog.DebugContext(ctx, "Next periodic run due", "after", wait)
|
||||
|
||||
timer := time.NewTimer(wait)
|
||||
for {
|
||||
@@ -71,17 +73,17 @@ func (s *Service) Serve(ctx context.Context) error {
|
||||
}
|
||||
|
||||
timer.Reset(s.maintenanceInterval)
|
||||
l.Debugln("Next periodic run in", s.maintenanceInterval)
|
||||
slog.DebugContext(ctx, "Next periodic run due", "after", s.maintenanceInterval)
|
||||
_ = s.internalMeta.PutTime(lastMaintKey, time.Now())
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) periodic(ctx context.Context) error {
|
||||
t0 := time.Now()
|
||||
l.Debugln("Periodic start")
|
||||
slog.DebugContext(ctx, "Periodic start")
|
||||
|
||||
t1 := time.Now()
|
||||
defer func() { l.Debugln("Periodic done in", time.Since(t1), "+", t1.Sub(t0)) }()
|
||||
defer func() { slog.DebugContext(ctx, "Periodic done in", "t1", time.Since(t1), "t0t1", t1.Sub(t0)) }()
|
||||
|
||||
s.sdb.updateLock.Lock()
|
||||
err := tidy(ctx, s.sdb.sql)
|
||||
@@ -94,7 +96,7 @@ func (s *Service) periodic(ctx context.Context) error {
|
||||
fdb.updateLock.Lock()
|
||||
defer fdb.updateLock.Unlock()
|
||||
|
||||
if err := garbageCollectOldDeletedLocked(fdb); err != nil {
|
||||
if err := garbageCollectOldDeletedLocked(ctx, fdb); err != nil {
|
||||
return wrap(err)
|
||||
}
|
||||
if err := garbageCollectBlocklistsAndBlocksLocked(ctx, fdb); err != nil {
|
||||
@@ -118,15 +120,16 @@ func tidy(ctx context.Context, db *sqlx.DB) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func garbageCollectOldDeletedLocked(fdb *folderDB) error {
|
||||
func garbageCollectOldDeletedLocked(ctx context.Context, fdb *folderDB) error {
|
||||
l := slog.With("fdb", fdb.baseDB)
|
||||
if fdb.deleteRetention <= 0 {
|
||||
l.Debugln(fdb.baseName, "delete retention is infinite, skipping cleanup")
|
||||
slog.DebugContext(ctx, "Delete retention is infinite, skipping cleanup")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove deleted files that are marked as not needed (we have processed
|
||||
// them) and they were deleted more than MaxDeletedFileAge ago.
|
||||
l.Debugln(fdb.baseName, "forgetting deleted files older than", fdb.deleteRetention)
|
||||
l.DebugContext(ctx, "Forgetting deleted files", "retention", fdb.deleteRetention)
|
||||
res, err := fdb.stmt(`
|
||||
DELETE FROM files
|
||||
WHERE deleted AND modified < ? AND local_flags & {{.FlagLocalNeeded}} == 0
|
||||
@@ -135,7 +138,7 @@ func garbageCollectOldDeletedLocked(fdb *folderDB) error {
|
||||
return wrap(err)
|
||||
}
|
||||
if aff, err := res.RowsAffected(); err == nil {
|
||||
l.Debugln(fdb.baseName, "removed old deleted file records:", aff)
|
||||
l.DebugContext(ctx, "Removed old deleted file records", "affected", aff)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -176,9 +179,14 @@ func garbageCollectBlocklistsAndBlocksLocked(ctx context.Context, fdb *folderDB)
|
||||
SELECT 1 FROM files WHERE files.blocklist_hash = blocklists.blocklist_hash
|
||||
)`); err != nil {
|
||||
return wrap(err, "delete blocklists")
|
||||
} else if shouldDebug() {
|
||||
rows, err := res.RowsAffected()
|
||||
l.Debugln(fdb.baseName, "blocklist GC:", rows, err)
|
||||
} else {
|
||||
slog.DebugContext(ctx, "Blocklist GC", "fdb", fdb.baseName, "result", slogutil.Expensive(func() any {
|
||||
rows, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return slogutil.Error(err)
|
||||
}
|
||||
return slog.Int64("rows", rows)
|
||||
}))
|
||||
}
|
||||
|
||||
if res, err := tx.ExecContext(ctx, `
|
||||
@@ -187,9 +195,14 @@ func garbageCollectBlocklistsAndBlocksLocked(ctx context.Context, fdb *folderDB)
|
||||
SELECT 1 FROM blocklists WHERE blocklists.blocklist_hash = blocks.blocklist_hash
|
||||
)`); err != nil {
|
||||
return wrap(err, "delete blocks")
|
||||
} else if shouldDebug() {
|
||||
rows, err := res.RowsAffected()
|
||||
l.Debugln(fdb.baseName, "blocks GC:", rows, err)
|
||||
} else {
|
||||
slog.DebugContext(ctx, "Blocks GC", "fdb", fdb.baseName, "result", slogutil.Expensive(func() any {
|
||||
rows, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return slogutil.Error(err)
|
||||
}
|
||||
return slog.Int64("rows", rows)
|
||||
}))
|
||||
}
|
||||
|
||||
return wrap(tx.Commit())
|
||||
|
||||
@@ -6,10 +6,6 @@
|
||||
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"github.com/syncthing/syncthing/lib/logger"
|
||||
)
|
||||
import "github.com/syncthing/syncthing/internal/slogutil"
|
||||
|
||||
var l = logger.DefaultLogger.NewFacility("sqlite", "SQLite database")
|
||||
|
||||
func shouldDebug() bool { return l.ShouldDebug("sqlite") }
|
||||
func init() { slogutil.RegisterPackage("SQLite database") }
|
||||
|
||||
@@ -10,11 +10,13 @@ import (
|
||||
"cmp"
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"slices"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/syncthing/syncthing/internal/gen/dbproto"
|
||||
"github.com/syncthing/syncthing/internal/itererr"
|
||||
"github.com/syncthing/syncthing/internal/slogutil"
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/sliceutil"
|
||||
@@ -486,12 +488,12 @@ func (s *folderDB) periodicCheckpointLocked(fs []protocol.FileInfo) {
|
||||
if s.updatePoints > updatePointsThreshold {
|
||||
conn, err := s.sql.Conn(context.Background())
|
||||
if err != nil {
|
||||
l.Debugln(s.baseName, "conn:", err)
|
||||
slog.Debug("Connection error", slog.String("db", s.baseName), slogutil.Error(err))
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
if _, err := conn.ExecContext(context.Background(), `PRAGMA journal_size_limit = 8388608`); err != nil {
|
||||
l.Debugln(s.baseName, "PRAGMA journal_size_limit:", err)
|
||||
slog.Debug("PRAGMA journal_size_limit error", slog.String("db", s.baseName), slogutil.Error(err))
|
||||
}
|
||||
|
||||
// Every 50th checkpoint becomes a truncate, in an effort to bring
|
||||
@@ -505,11 +507,11 @@ func (s *folderDB) periodicCheckpointLocked(fs []protocol.FileInfo) {
|
||||
|
||||
var res, modified, moved int
|
||||
if row.Err() != nil {
|
||||
l.Debugln(s.baseName, cmd+":", err)
|
||||
slog.Debug("Command error", slog.String("db", s.baseName), slog.String("cmd", cmd), slogutil.Error(err))
|
||||
} else if err := row.Scan(&res, &modified, &moved); err != nil {
|
||||
l.Debugln(s.baseName, cmd+" (scan):", err)
|
||||
slog.Debug("Command scan error", slog.String("db", s.baseName), slog.String("cmd", cmd), slogutil.Error(err))
|
||||
} else {
|
||||
l.Debugln(s.baseName, cmd, s.checkpointsCount, "at", s.updatePoints, "returned", res, modified, moved)
|
||||
slog.Debug("Checkpoint result", "db", s.baseName, "checkpointscount", s.checkpointsCount, "updatepoints", s.updatePoints, "res", res, "modified", modified, "moved", moved)
|
||||
}
|
||||
|
||||
// Reset the truncate counter when a truncate succeeded. If it
|
||||
|
||||
25
internal/slogutil/expensive.go
Normal file
25
internal/slogutil/expensive.go
Normal file
@@ -0,0 +1,25 @@
|
||||
// Copyright (C) 2025 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package slogutil
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
// Expensive wraps a log value that is expensive to compute and should only
|
||||
// be called if the log line is actually emitted.
|
||||
func Expensive(fn func() any) expensive {
|
||||
return expensive{fn}
|
||||
}
|
||||
|
||||
type expensive struct {
|
||||
fn func() any
|
||||
}
|
||||
|
||||
func (e expensive) LogValue() slog.Value {
|
||||
return slog.AnyValue(e.fn())
|
||||
}
|
||||
188
internal/slogutil/formatting.go
Normal file
188
internal/slogutil/formatting.go
Normal file
@@ -0,0 +1,188 @@
|
||||
// Copyright (C) 2025 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package slogutil
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"io"
|
||||
"log/slog"
|
||||
"path"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type formattingHandler struct {
|
||||
attrs []slog.Attr
|
||||
groups []string
|
||||
out io.Writer
|
||||
recs []*lineRecorder
|
||||
timeOverride time.Time
|
||||
}
|
||||
|
||||
var _ slog.Handler = (*formattingHandler)(nil)
|
||||
|
||||
func (h *formattingHandler) Enabled(context.Context, slog.Level) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (h *formattingHandler) Handle(_ context.Context, rec slog.Record) error {
|
||||
fr := runtime.CallersFrames([]uintptr{rec.PC})
|
||||
var logAttrs []any
|
||||
if fram, _ := fr.Next(); fram.Function != "" {
|
||||
pkgName, typeName := funcNameToPkg(fram.Function)
|
||||
lvl := globalLevels.Get(pkgName)
|
||||
if lvl > rec.Level {
|
||||
// Logging not enabled at the record's level
|
||||
return nil
|
||||
}
|
||||
logAttrs = append(logAttrs, slog.String("pkg", pkgName))
|
||||
if lvl <= slog.LevelDebug {
|
||||
// We are debugging, add additional source line data
|
||||
if typeName != "" {
|
||||
logAttrs = append(logAttrs, slog.String("type", typeName))
|
||||
}
|
||||
logAttrs = append(logAttrs, slog.Group("src", slog.String("file", path.Base(fram.File)), slog.Int("line", fram.Line)))
|
||||
}
|
||||
}
|
||||
|
||||
var prefix string
|
||||
if len(h.groups) > 0 {
|
||||
prefix = strings.Join(h.groups, ".") + "."
|
||||
}
|
||||
|
||||
// Build the message string.
|
||||
var sb strings.Builder
|
||||
sb.WriteString(rec.Message)
|
||||
|
||||
// Collect all the attributes, adding the handler prefix.
|
||||
attrs := make([]slog.Attr, 0, rec.NumAttrs()+len(h.attrs)+1)
|
||||
rec.Attrs(func(attr slog.Attr) bool {
|
||||
attr.Key = prefix + attr.Key
|
||||
attrs = append(attrs, attr)
|
||||
return true
|
||||
})
|
||||
attrs = append(attrs, h.attrs...)
|
||||
attrs = append(attrs, slog.Group("log", logAttrs...))
|
||||
|
||||
// Expand and format attributes
|
||||
var attrCount int
|
||||
for _, attr := range attrs {
|
||||
for _, attr := range expandAttrs("", attr) {
|
||||
appendAttr(&sb, "", attr, &attrCount)
|
||||
}
|
||||
}
|
||||
if attrCount > 0 {
|
||||
sb.WriteRune(')')
|
||||
}
|
||||
|
||||
line := Line{
|
||||
When: cmp.Or(h.timeOverride, rec.Time),
|
||||
Message: sb.String(),
|
||||
Level: rec.Level,
|
||||
}
|
||||
|
||||
// If there is a recorder, record the line.
|
||||
for _, rec := range h.recs {
|
||||
rec.record(line)
|
||||
}
|
||||
|
||||
// If there's an output, print the line.
|
||||
if h.out != nil {
|
||||
_, _ = line.WriteTo(h.out)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func expandAttrs(prefix string, a slog.Attr) []slog.Attr {
|
||||
if prefix != "" {
|
||||
a.Key = prefix + "." + a.Key
|
||||
}
|
||||
val := a.Value.Resolve()
|
||||
if val.Kind() != slog.KindGroup {
|
||||
return []slog.Attr{a}
|
||||
}
|
||||
var attrs []slog.Attr
|
||||
for _, attr := range val.Group() {
|
||||
attrs = append(attrs, expandAttrs(a.Key, attr)...)
|
||||
}
|
||||
return attrs
|
||||
}
|
||||
|
||||
func appendAttr(sb *strings.Builder, prefix string, a slog.Attr, attrCount *int) {
|
||||
const confusables = ` "()[]{},`
|
||||
if a.Key == "" {
|
||||
return
|
||||
}
|
||||
sb.WriteRune(' ')
|
||||
if *attrCount == 0 {
|
||||
sb.WriteRune('(')
|
||||
}
|
||||
sb.WriteString(prefix)
|
||||
sb.WriteString(a.Key)
|
||||
sb.WriteRune('=')
|
||||
v := a.Value.Resolve().String()
|
||||
if v == "" || strings.ContainsAny(v, confusables) {
|
||||
v = strconv.Quote(v)
|
||||
}
|
||||
sb.WriteString(v)
|
||||
*attrCount++
|
||||
}
|
||||
|
||||
func (h *formattingHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||
if len(h.groups) > 0 {
|
||||
prefix := strings.Join(h.groups, ".") + "."
|
||||
for i := range attrs {
|
||||
attrs[i].Key = prefix + attrs[i].Key
|
||||
}
|
||||
}
|
||||
return &formattingHandler{
|
||||
attrs: append(h.attrs, attrs...),
|
||||
groups: h.groups,
|
||||
recs: h.recs,
|
||||
out: h.out,
|
||||
timeOverride: h.timeOverride,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *formattingHandler) WithGroup(name string) slog.Handler {
|
||||
if name == "" {
|
||||
return h
|
||||
}
|
||||
return &formattingHandler{
|
||||
attrs: h.attrs,
|
||||
groups: append([]string{name}, h.groups...),
|
||||
recs: h.recs,
|
||||
out: h.out,
|
||||
timeOverride: h.timeOverride,
|
||||
}
|
||||
}
|
||||
|
||||
func funcNameToPkg(fn string) (string, string) {
|
||||
fn = strings.ToLower(fn)
|
||||
fn = strings.TrimPrefix(fn, "github.com/syncthing/syncthing/lib/")
|
||||
fn = strings.TrimPrefix(fn, "github.com/syncthing/syncthing/internal/")
|
||||
|
||||
pkgTypFn := strings.Split(fn, ".") // [package, type, method] or [package, function]
|
||||
if len(pkgTypFn) <= 2 {
|
||||
return pkgTypFn[0], ""
|
||||
}
|
||||
|
||||
pkg := pkgTypFn[0]
|
||||
// Remove parenthesis and asterisk from the type name
|
||||
typ := strings.TrimLeft(strings.TrimRight(pkgTypFn[1], ")"), "(*")
|
||||
// Skip certain type names that add no value
|
||||
typ = strings.TrimSuffix(typ, "service")
|
||||
switch typ {
|
||||
case pkg, "", "serveparams":
|
||||
return pkg, ""
|
||||
default:
|
||||
return pkg, typ
|
||||
}
|
||||
}
|
||||
55
internal/slogutil/formatting_test.go
Normal file
55
internal/slogutil/formatting_test.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright (C) 2025 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package slogutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestFormattingHandler(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
h := &formattingHandler{
|
||||
out: buf,
|
||||
timeOverride: time.Unix(1234567890, 0).In(time.UTC),
|
||||
}
|
||||
|
||||
l := slog.New(h).With("a", "a")
|
||||
l.Info("A basic info line", "attr1", "val with spaces", "attr2", 2, "attr3", `val"quote`)
|
||||
l.Info("A basic info line", "attr1", "paren)thesis")
|
||||
l.Info("An info line with an empty value", "attr1", "")
|
||||
l.Info("An info line with grouped values", "attr1", "val1", slog.Group("foo", "attr2", 2, slog.Group("bar", "attr3", "3")))
|
||||
|
||||
l2 := l.WithGroup("foo")
|
||||
l2.Info("An info line with grouped values via logger", "attr1", "val1", "attr2", 2)
|
||||
|
||||
l3 := l2.WithGroup("bar")
|
||||
l3.Info("An info line with nested grouped values via logger", "attr1", "val1", "attr2", 2)
|
||||
|
||||
l3.Debug("A debug entry")
|
||||
l3.Warn("A warning entry")
|
||||
l3.Error("An error")
|
||||
|
||||
exp := `
|
||||
2009-02-13 23:31:30 INF A basic info line (attr1="val with spaces" attr2=2 attr3="val\"quote" a=a log.pkg=slogutil)
|
||||
2009-02-13 23:31:30 INF A basic info line (attr1="paren)thesis" a=a log.pkg=slogutil)
|
||||
2009-02-13 23:31:30 INF An info line with an empty value (attr1="" a=a log.pkg=slogutil)
|
||||
2009-02-13 23:31:30 INF An info line with grouped values (attr1=val1 foo.attr2=2 foo.bar.attr3=3 a=a log.pkg=slogutil)
|
||||
2009-02-13 23:31:30 INF An info line with grouped values via logger (foo.attr1=val1 foo.attr2=2 a=a log.pkg=slogutil)
|
||||
2009-02-13 23:31:30 INF An info line with nested grouped values via logger (bar.foo.attr1=val1 bar.foo.attr2=2 a=a log.pkg=slogutil)
|
||||
2009-02-13 23:31:30 WRN A warning entry (a=a log.pkg=slogutil)
|
||||
2009-02-13 23:31:30 ERR An error (a=a log.pkg=slogutil)`
|
||||
|
||||
if strings.TrimSpace(buf.String()) != strings.TrimSpace(exp) {
|
||||
t.Log(buf.String())
|
||||
t.Log(exp)
|
||||
t.Error("mismatch")
|
||||
}
|
||||
}
|
||||
104
internal/slogutil/leveler.go
Normal file
104
internal/slogutil/leveler.go
Normal file
@@ -0,0 +1,104 @@
|
||||
// Copyright (C) 2025 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package slogutil
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"maps"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// A levelTracker keeps track of log level per package. This enables the
|
||||
// traditional STTRACE variable to set certain packages to debug level, but
|
||||
// also allows setting packages to other levels such as WARN to silence
|
||||
// INFO-level messages.
|
||||
//
|
||||
// The STTRACE environment variable is one way of controlling this, where
|
||||
// mentioning a package makes it DEBUG level:
|
||||
// STTRACE="model,protocol" # model and protocol are at DEBUG level
|
||||
// however you can also give specific levels after a colon:
|
||||
// STTRACE="model:WARNING,protocol:DEBUG"
|
||||
|
||||
func PackageDescrs() map[string]string {
|
||||
return globalLevels.Descrs()
|
||||
}
|
||||
|
||||
func PackageLevels() map[string]slog.Level {
|
||||
return globalLevels.Levels()
|
||||
}
|
||||
|
||||
func SetPackageLevel(pkg string, level slog.Level) {
|
||||
globalLevels.Set(pkg, level)
|
||||
}
|
||||
|
||||
func SetDefaultLevel(level slog.Level) {
|
||||
globalLevels.SetDefault(level)
|
||||
}
|
||||
|
||||
type levelTracker struct {
|
||||
mut sync.RWMutex
|
||||
defLevel slog.Level
|
||||
descrs map[string]string // package name to description
|
||||
levels map[string]slog.Level // package name to level
|
||||
}
|
||||
|
||||
func (t *levelTracker) Get(pkg string) slog.Level {
|
||||
t.mut.RLock()
|
||||
defer t.mut.RUnlock()
|
||||
if level, ok := t.levels[pkg]; ok {
|
||||
return level
|
||||
}
|
||||
return t.defLevel
|
||||
}
|
||||
|
||||
func (t *levelTracker) Set(pkg string, level slog.Level) {
|
||||
t.mut.Lock()
|
||||
changed := t.levels[pkg] != level
|
||||
t.levels[pkg] = level
|
||||
t.mut.Unlock()
|
||||
if changed {
|
||||
slog.Info("Changed package log level", "package", pkg, "level", level)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *levelTracker) SetDefault(level slog.Level) {
|
||||
t.mut.Lock()
|
||||
changed := t.defLevel != level
|
||||
t.defLevel = level
|
||||
t.mut.Unlock()
|
||||
if changed {
|
||||
slog.Info("Changed default log level", "level", level)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *levelTracker) SetDescr(pkg, descr string) {
|
||||
t.mut.Lock()
|
||||
t.descrs[pkg] = descr
|
||||
t.mut.Unlock()
|
||||
}
|
||||
|
||||
func (t *levelTracker) Descrs() map[string]string {
|
||||
t.mut.RLock()
|
||||
defer t.mut.RUnlock()
|
||||
m := make(map[string]string, len(t.descrs))
|
||||
maps.Copy(m, t.descrs)
|
||||
return m
|
||||
}
|
||||
|
||||
func (t *levelTracker) Levels() map[string]slog.Level {
|
||||
t.mut.RLock()
|
||||
defer t.mut.RUnlock()
|
||||
m := make(map[string]slog.Level, len(t.descrs))
|
||||
for pkg := range t.descrs {
|
||||
if level, ok := t.levels[pkg]; ok {
|
||||
m[pkg] = level
|
||||
} else {
|
||||
m[pkg] = t.defLevel
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
61
internal/slogutil/line.go
Normal file
61
internal/slogutil/line.go
Normal file
@@ -0,0 +1,61 @@
|
||||
// Copyright (C) 2025 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package slogutil
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"time"
|
||||
)
|
||||
|
||||
// A Line is our internal representation of a formatted log line. This is
|
||||
// what we present in the API and what we buffer internally.
|
||||
type Line struct {
|
||||
When time.Time `json:"when"`
|
||||
Message string `json:"message"`
|
||||
Level slog.Level `json:"level"`
|
||||
}
|
||||
|
||||
func (l *Line) WriteTo(w io.Writer) (int64, error) {
|
||||
n, err := fmt.Fprintf(w, "%s %s %s\n", l.timeStr(), l.levelStr(), l.Message)
|
||||
return int64(n), err
|
||||
}
|
||||
|
||||
func (l *Line) timeStr() string {
|
||||
return l.When.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
func (l *Line) levelStr() string {
|
||||
str := func(base string, val slog.Level) string {
|
||||
if val == 0 {
|
||||
return base
|
||||
}
|
||||
return fmt.Sprintf("%s%+d", base, val)
|
||||
}
|
||||
|
||||
switch {
|
||||
case l.Level < slog.LevelInfo:
|
||||
return str("DBG", l.Level-slog.LevelDebug)
|
||||
case l.Level < slog.LevelWarn:
|
||||
return str("INF", l.Level-slog.LevelInfo)
|
||||
case l.Level < slog.LevelError:
|
||||
return str("WRN", l.Level-slog.LevelWarn)
|
||||
default:
|
||||
return str("ERR", l.Level-slog.LevelError)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Line) MarshalJSON() ([]byte, error) {
|
||||
// Custom marshal to get short level strings instead of default JSON serialisation
|
||||
return json.Marshal(map[string]any{
|
||||
"when": l.When,
|
||||
"message": l.Message,
|
||||
"level": l.levelStr(),
|
||||
})
|
||||
}
|
||||
59
internal/slogutil/recorder.go
Normal file
59
internal/slogutil/recorder.go
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright (C) 2025 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package slogutil
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const maxLogLines = 1000
|
||||
|
||||
type Recorder interface {
|
||||
Since(t time.Time) []Line
|
||||
Clear()
|
||||
}
|
||||
|
||||
func NewRecorder(level slog.Level) Recorder {
|
||||
return &lineRecorder{level: level}
|
||||
}
|
||||
|
||||
type lineRecorder struct {
|
||||
level slog.Level
|
||||
mut sync.Mutex
|
||||
lines []Line
|
||||
}
|
||||
|
||||
func (r *lineRecorder) record(line Line) {
|
||||
if line.Level < r.level {
|
||||
return
|
||||
}
|
||||
r.mut.Lock()
|
||||
r.lines = append(r.lines, line)
|
||||
if len(r.lines) > maxLogLines {
|
||||
r.lines = r.lines[len(r.lines)-maxLogLines:]
|
||||
}
|
||||
r.mut.Unlock()
|
||||
}
|
||||
|
||||
func (r *lineRecorder) Clear() {
|
||||
r.mut.Lock()
|
||||
r.lines = nil
|
||||
r.mut.Unlock()
|
||||
}
|
||||
|
||||
func (r *lineRecorder) Since(t time.Time) []Line {
|
||||
r.mut.Lock()
|
||||
defer r.mut.Unlock()
|
||||
for i := range r.lines {
|
||||
if r.lines[i].When.After(t) {
|
||||
return r.lines[i:]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
71
internal/slogutil/slogadapter.go
Normal file
71
internal/slogutil/slogadapter.go
Normal file
@@ -0,0 +1,71 @@
|
||||
// Copyright (C) 2025 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package slogutil
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Log levels:
|
||||
// - DEBUG: programmers only (not user troubleshooting)
|
||||
// - INFO: most stuff, files syncing properly
|
||||
// - WARN: errors that can be ignored or will be retried (e.g., sync failures)
|
||||
// - ERROR: errors that need handling, shown in the GUI
|
||||
|
||||
func RegisterPackage(descr string) {
|
||||
registerPackage(descr, 2)
|
||||
}
|
||||
|
||||
func NewAdapter(descr string) *adapter {
|
||||
registerPackage(descr, 2)
|
||||
return &adapter{slogDef}
|
||||
}
|
||||
|
||||
func registerPackage(descr string, frames int) {
|
||||
var pcs [1]uintptr
|
||||
runtime.Callers(1+frames, pcs[:])
|
||||
pc := pcs[0]
|
||||
fr := runtime.CallersFrames([]uintptr{pc})
|
||||
if fram, _ := fr.Next(); fram.Function != "" {
|
||||
pkgName, _ := funcNameToPkg(fram.Function)
|
||||
globalLevels.SetDescr(pkgName, descr)
|
||||
}
|
||||
}
|
||||
|
||||
type adapter struct {
|
||||
l *slog.Logger
|
||||
}
|
||||
|
||||
func (a adapter) Debugln(vals ...interface{}) {
|
||||
a.log(strings.TrimSpace(fmt.Sprintln(vals...)), slog.LevelDebug)
|
||||
}
|
||||
|
||||
func (a adapter) Debugf(format string, vals ...interface{}) {
|
||||
a.log(fmt.Sprintf(format, vals...), slog.LevelDebug)
|
||||
}
|
||||
|
||||
func (a adapter) log(msg string, level slog.Level) {
|
||||
h := a.l.Handler()
|
||||
if !h.Enabled(context.Background(), level) {
|
||||
return
|
||||
}
|
||||
var pcs [1]uintptr
|
||||
// skip [runtime.Callers, this function, this function's caller]
|
||||
runtime.Callers(3, pcs[:])
|
||||
pc := pcs[0]
|
||||
r := slog.NewRecord(time.Now(), level, msg, pc)
|
||||
_ = h.Handle(context.Background(), r)
|
||||
}
|
||||
|
||||
func (a adapter) ShouldDebug(facility string) bool {
|
||||
return globalLevels.Get(facility) <= slog.LevelDebug
|
||||
}
|
||||
47
internal/slogutil/sloginit.go
Normal file
47
internal/slogutil/sloginit.go
Normal file
@@ -0,0 +1,47 @@
|
||||
// Copyright (C) 2025 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package slogutil
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
GlobalRecorder = &lineRecorder{level: -1000}
|
||||
ErrorRecorder = &lineRecorder{level: slog.LevelError}
|
||||
globalLevels = &levelTracker{
|
||||
levels: make(map[string]slog.Level),
|
||||
descrs: make(map[string]string),
|
||||
}
|
||||
slogDef = slog.New(&formattingHandler{
|
||||
recs: []*lineRecorder{GlobalRecorder, ErrorRecorder},
|
||||
out: os.Stdout,
|
||||
})
|
||||
)
|
||||
|
||||
func init() {
|
||||
slog.SetDefault(slogDef)
|
||||
|
||||
// Handle legacy STTRACE var
|
||||
pkgs := strings.Split(os.Getenv("STTRACE"), ",")
|
||||
for _, pkg := range pkgs {
|
||||
pkg = strings.TrimSpace(pkg)
|
||||
if pkg == "" {
|
||||
continue
|
||||
}
|
||||
level := slog.LevelDebug
|
||||
if cutPkg, levelStr, ok := strings.Cut(pkg, ":"); ok {
|
||||
pkg = cutPkg
|
||||
if err := level.UnmarshalText([]byte(levelStr)); err != nil {
|
||||
slog.Warn("Bad log level requested in STTRACE", slog.String("pkg", pkg), slog.String("level", levelStr), Error(err))
|
||||
}
|
||||
}
|
||||
globalLevels.Set(pkg, level)
|
||||
}
|
||||
}
|
||||
40
internal/slogutil/slogvalues.go
Normal file
40
internal/slogutil/slogvalues.go
Normal file
@@ -0,0 +1,40 @@
|
||||
// Copyright (C) 2025 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package slogutil
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"maps"
|
||||
"slices"
|
||||
)
|
||||
|
||||
func Address(v any) slog.Attr {
|
||||
return slog.Any("address", v)
|
||||
}
|
||||
|
||||
func Error(err any) slog.Attr {
|
||||
if err == nil {
|
||||
return slog.Attr{}
|
||||
}
|
||||
return slog.Any("error", err)
|
||||
}
|
||||
|
||||
func FilePath(path string) slog.Attr {
|
||||
return slog.String("path", path)
|
||||
}
|
||||
|
||||
func URI(v any) slog.Attr {
|
||||
return slog.Any("uri", v)
|
||||
}
|
||||
|
||||
func Map[T any](m map[string]T) []any {
|
||||
var attrs []any
|
||||
for _, key := range slices.Sorted(maps.Keys(m)) {
|
||||
attrs = append(attrs, slog.Any(key, m[key]))
|
||||
}
|
||||
return attrs
|
||||
}
|
||||
168
lib/api/api.go
168
lib/api/api.go
@@ -17,6 +17,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -28,13 +29,13 @@ import (
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/calmh/incontainer"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/rcrowley/go-metrics"
|
||||
"github.com/thejerf/suture/v4"
|
||||
"github.com/vitrun/qart/qr"
|
||||
"golang.org/x/text/runes"
|
||||
@@ -42,6 +43,7 @@ import (
|
||||
"golang.org/x/text/unicode/norm"
|
||||
|
||||
"github.com/syncthing/syncthing/internal/db"
|
||||
"github.com/syncthing/syncthing/internal/slogutil"
|
||||
"github.com/syncthing/syncthing/lib/build"
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/connections"
|
||||
@@ -49,12 +51,10 @@ import (
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
"github.com/syncthing/syncthing/lib/fs"
|
||||
"github.com/syncthing/syncthing/lib/locations"
|
||||
"github.com/syncthing/syncthing/lib/logger"
|
||||
"github.com/syncthing/syncthing/lib/model"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/rand"
|
||||
"github.com/syncthing/syncthing/lib/svcutil"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
"github.com/syncthing/syncthing/lib/tlsutil"
|
||||
"github.com/syncthing/syncthing/lib/upgrade"
|
||||
"github.com/syncthing/syncthing/lib/ur"
|
||||
@@ -95,8 +95,8 @@ type service struct {
|
||||
miscDB *db.Typed
|
||||
shutdownTimeout time.Duration
|
||||
|
||||
guiErrors logger.Recorder
|
||||
systemLog logger.Recorder
|
||||
guiErrors slogutil.Recorder
|
||||
systemLog slogutil.Recorder
|
||||
}
|
||||
|
||||
var _ config.Verifier = &service{}
|
||||
@@ -107,7 +107,7 @@ type Service interface {
|
||||
WaitForStart() error
|
||||
}
|
||||
|
||||
func New(id protocol.DeviceID, cfg config.Wrapper, assetDir, tlsDefaultCommonName string, m model.Model, defaultSub, diskSub events.BufferedSubscription, evLogger events.Logger, discoverer discover.Manager, connectionsService connections.Service, urService *ur.Service, fss model.FolderSummaryService, errors, systemLog logger.Recorder, noUpgrade bool, miscDB *db.Typed) Service {
|
||||
func New(id protocol.DeviceID, cfg config.Wrapper, assetDir, tlsDefaultCommonName string, m model.Model, defaultSub, diskSub events.BufferedSubscription, evLogger events.Logger, discoverer discover.Manager, connectionsService connections.Service, urService *ur.Service, fss model.FolderSummaryService, errors, systemLog slogutil.Recorder, noUpgrade bool, miscDB *db.Typed) Service {
|
||||
return &service{
|
||||
id: id,
|
||||
cfg: cfg,
|
||||
@@ -117,7 +117,6 @@ func New(id protocol.DeviceID, cfg config.Wrapper, assetDir, tlsDefaultCommonNam
|
||||
DefaultEventMask: defaultSub,
|
||||
DiskEventMask: diskSub,
|
||||
},
|
||||
eventSubsMut: sync.NewMutex(),
|
||||
evLogger: evLogger,
|
||||
discoverer: discoverer,
|
||||
connectionsService: connectionsService,
|
||||
@@ -151,8 +150,10 @@ func (s *service) getListener(guiCfg config.GUIConfiguration) (net.Listener, err
|
||||
err = shouldRegenerateCertificate(cert)
|
||||
}
|
||||
if err != nil {
|
||||
l.Infoln("Loading HTTPS certificate:", err)
|
||||
l.Infoln("Creating new HTTPS certificate")
|
||||
if !os.IsNotExist(err) {
|
||||
slog.Warn("Failed to load HTTPS certificate", slogutil.Error(err))
|
||||
}
|
||||
slog.Info("Creating new HTTPS certificate")
|
||||
|
||||
// When generating the HTTPS certificate, use the system host name per
|
||||
// default. If that isn't available, use the "syncthing" default.
|
||||
@@ -222,7 +223,7 @@ func (s *service) Serve(ctx context.Context) error {
|
||||
case <-s.startedOnce:
|
||||
// We let this be a loud user-visible warning as it may be the only
|
||||
// indication they get that the GUI won't be available.
|
||||
l.Warnln("Starting API/GUI:", err)
|
||||
slog.ErrorContext(ctx, "Failed to start API/GUI", slogutil.Error(err))
|
||||
|
||||
default:
|
||||
// This is during initialization. A failure here should be fatal
|
||||
@@ -280,7 +281,7 @@ func (s *service) Serve(ctx context.Context) error {
|
||||
restMux.HandlerFunc(http.MethodGet, "/rest/system/status", s.getSystemStatus) // -
|
||||
restMux.HandlerFunc(http.MethodGet, "/rest/system/upgrade", s.getSystemUpgrade) // -
|
||||
restMux.HandlerFunc(http.MethodGet, "/rest/system/version", s.getSystemVersion) // -
|
||||
restMux.HandlerFunc(http.MethodGet, "/rest/system/debug", s.getSystemDebug) // -
|
||||
restMux.HandlerFunc(http.MethodGet, "/rest/system/loglevels", s.getSystemDebug) // -
|
||||
restMux.HandlerFunc(http.MethodGet, "/rest/system/log", s.getSystemLog) // [since]
|
||||
restMux.HandlerFunc(http.MethodGet, "/rest/system/log.txt", s.getSystemLogTxt) // [since]
|
||||
|
||||
@@ -300,7 +301,7 @@ func (s *service) Serve(ctx context.Context) error {
|
||||
restMux.HandlerFunc(http.MethodPost, "/rest/system/upgrade", s.postSystemUpgrade) // -
|
||||
restMux.HandlerFunc(http.MethodPost, "/rest/system/pause", s.makeDevicePauseHandler(true)) // [device]
|
||||
restMux.HandlerFunc(http.MethodPost, "/rest/system/resume", s.makeDevicePauseHandler(false)) // [device]
|
||||
restMux.HandlerFunc(http.MethodPost, "/rest/system/debug", s.postSystemDebug) // [enable] [disable]
|
||||
restMux.HandlerFunc(http.MethodPost, "/rest/system/loglevels", s.postSystemDebug) // [enable] [disable]
|
||||
|
||||
// The DELETE handlers
|
||||
restMux.HandlerFunc(http.MethodDelete, "/rest/cluster/pending/devices", s.deletePendingDevices) // device
|
||||
@@ -334,16 +335,14 @@ func (s *service) Serve(ctx context.Context) error {
|
||||
|
||||
// Debug endpoints, not for general use
|
||||
debugMux := http.NewServeMux()
|
||||
debugMux.HandleFunc("/rest/debug/peerCompletion", s.getPeerCompletion)
|
||||
debugMux.HandleFunc("/rest/debug/httpmetrics", s.getSystemHTTPMetrics)
|
||||
debugMux.HandleFunc("/rest/debug/cpuprof", s.getCPUProf) // duration
|
||||
debugMux.HandleFunc("/rest/debug/heapprof", s.getHeapProf)
|
||||
debugMux.HandleFunc("/rest/debug/support", s.getSupportBundle)
|
||||
debugMux.HandleFunc("/rest/debug/file", s.getDebugFile)
|
||||
restMux.Handler(http.MethodGet, "/rest/debug/*method", s.whenDebugging(debugMux))
|
||||
restMux.Handler(http.MethodGet, "/rest/debug/*method", debugMux)
|
||||
|
||||
// A handler that disables caching
|
||||
noCacheRestMux := noCacheMiddleware(metricsMiddleware(restMux))
|
||||
noCacheRestMux := noCacheMiddleware(restMux)
|
||||
|
||||
// The main routing handler
|
||||
mux := http.NewServeMux()
|
||||
@@ -409,8 +408,8 @@ func (s *service) Serve(ctx context.Context) error {
|
||||
srv.ErrorLog = log.Default()
|
||||
}
|
||||
|
||||
l.Infoln("GUI and API listening on", listener.Addr())
|
||||
l.Infoln("Access the GUI via the following URL:", guiCfg.URL())
|
||||
slog.InfoContext(ctx, "GUI and API listening", slogutil.Address(listener.Addr()))
|
||||
slog.InfoContext(ctx, "Access the GUI via the following URL: "+guiCfg.URL()) //nolint:sloglint
|
||||
if s.started != nil {
|
||||
// only set when run by the tests
|
||||
select {
|
||||
@@ -443,14 +442,14 @@ func (s *service) Serve(ctx context.Context) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// Shutting down permanently
|
||||
l.Debugln("shutting down (stop)")
|
||||
slog.DebugContext(ctx, "Shutting down (stop)")
|
||||
case <-s.configChanged:
|
||||
// Soft restart due to configuration change
|
||||
l.Debugln("restarting (config changed)")
|
||||
slog.DebugContext(ctx, "Restarting (config changed)")
|
||||
case err = <-s.exitChan:
|
||||
case err = <-serveError:
|
||||
// Restart due to listen/serve failure
|
||||
l.Warnln("GUI/API:", err, "(restarting)")
|
||||
slog.ErrorContext(ctx, "GUI/API error (restarting)", slogutil.Error(err))
|
||||
}
|
||||
// Give it a moment to shut down gracefully, e.g. if we are restarting
|
||||
// due to a config change through the API, let that finish successfully.
|
||||
@@ -487,9 +486,6 @@ func (*service) VerifyConfiguration(_, to config.Configuration) error {
|
||||
}
|
||||
|
||||
func (s *service) CommitConfiguration(from, to config.Configuration) bool {
|
||||
// No action required when this changes, so mask the fact that it changed at all.
|
||||
from.GUI.Debugging = to.GUI.Debugging
|
||||
|
||||
if to.GUI == from.GUI {
|
||||
// No GUI changes, we're done here.
|
||||
return true
|
||||
@@ -592,15 +588,6 @@ func corsMiddleware(next http.Handler, allowFrameLoading bool) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
func metricsMiddleware(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t := metrics.GetOrRegisterTimer(r.URL.Path, nil)
|
||||
t0 := time.Now()
|
||||
h.ServeHTTP(w, r)
|
||||
t.UpdateSince(t0)
|
||||
})
|
||||
}
|
||||
|
||||
func redirectToHTTPSMiddleware(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.TLS == nil {
|
||||
@@ -642,17 +629,6 @@ func localhostMiddleware(h http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
func (s *service) whenDebugging(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if s.cfg.GUI().Debugging {
|
||||
h.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
http.Error(w, "Debugging disabled", http.StatusForbidden)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *service) getPendingDevices(w http.ResponseWriter, _ *http.Request) {
|
||||
devices, err := s.model.PendingDevices()
|
||||
if err != nil {
|
||||
@@ -749,31 +725,20 @@ func (*service) getSystemVersion(w http.ResponseWriter, _ *http.Request) {
|
||||
}
|
||||
|
||||
func (*service) getSystemDebug(w http.ResponseWriter, _ *http.Request) {
|
||||
names := l.Facilities()
|
||||
enabled := l.FacilityDebugging()
|
||||
slices.Sort(enabled)
|
||||
sendJSON(w, map[string]interface{}{
|
||||
"facilities": names,
|
||||
"enabled": enabled,
|
||||
sendJSON(w, map[string]any{
|
||||
"packages": slogutil.PackageDescrs(),
|
||||
"levels": slogutil.PackageLevels(),
|
||||
})
|
||||
}
|
||||
|
||||
func (*service) postSystemDebug(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
q := r.URL.Query()
|
||||
for _, f := range strings.Split(q.Get("enable"), ",") {
|
||||
if f == "" || l.ShouldDebug(f) {
|
||||
continue
|
||||
}
|
||||
l.SetDebug(f, true)
|
||||
l.Infof("Enabled debug data for %q", f)
|
||||
var levelRequest map[string]slog.Level
|
||||
if err := json.NewDecoder(r.Body).Decode(&levelRequest); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
for _, f := range strings.Split(q.Get("disable"), ",") {
|
||||
if f == "" || !l.ShouldDebug(f) {
|
||||
continue
|
||||
}
|
||||
l.SetDebug(f, false)
|
||||
l.Infof("Disabled debug data for %q", f)
|
||||
for pkg, level := range levelRequest {
|
||||
slogutil.SetPackageLevel(pkg, level)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1106,7 +1071,7 @@ func (s *service) getSystemStatus(w http.ResponseWriter, _ *http.Request) {
|
||||
}
|
||||
|
||||
func (s *service) getSystemError(w http.ResponseWriter, _ *http.Request) {
|
||||
sendJSON(w, map[string][]logger.Line{
|
||||
sendJSON(w, map[string][]slogutil.Line{
|
||||
"errors": s.guiErrors.Since(time.Time{}),
|
||||
})
|
||||
}
|
||||
@@ -1114,7 +1079,7 @@ func (s *service) getSystemError(w http.ResponseWriter, _ *http.Request) {
|
||||
func (*service) postSystemError(_ http.ResponseWriter, r *http.Request) {
|
||||
bs, _ := io.ReadAll(r.Body)
|
||||
r.Body.Close()
|
||||
l.Warnln(string(bs))
|
||||
slog.Error("External error report", slogutil.Error(string(bs)))
|
||||
}
|
||||
|
||||
func (s *service) postSystemErrorClear(_ http.ResponseWriter, _ *http.Request) {
|
||||
@@ -1127,7 +1092,7 @@ func (s *service) getSystemLog(w http.ResponseWriter, r *http.Request) {
|
||||
if err != nil {
|
||||
l.Debugln(err)
|
||||
}
|
||||
sendJSON(w, map[string][]logger.Line{
|
||||
sendJSON(w, map[string][]slogutil.Line{
|
||||
"messages": s.systemLog.Since(since),
|
||||
})
|
||||
}
|
||||
@@ -1156,7 +1121,7 @@ func (s *service) getSupportBundle(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Redacted configuration as a JSON
|
||||
if jsonConfig, err := json.MarshalIndent(getRedactedConfig(s), "", " "); err != nil {
|
||||
l.Warnln("Support bundle: failed to create config.json:", err)
|
||||
slog.Warn("Failed to create config.json in support bundle", slogutil.Error(err))
|
||||
} else {
|
||||
files = append(files, fileEntry{name: "config.json.txt", data: jsonConfig})
|
||||
}
|
||||
@@ -1171,7 +1136,7 @@ func (s *service) getSupportBundle(w http.ResponseWriter, r *http.Request) {
|
||||
// Errors as a JSON
|
||||
if errs := s.guiErrors.Since(time.Time{}); len(errs) > 0 {
|
||||
if jsonError, err := json.MarshalIndent(errs, "", " "); err != nil {
|
||||
l.Warnln("Support bundle: failed to create errors.json:", err)
|
||||
slog.Warn("Failed to create errors.json in support bundle", slogutil.Error(err))
|
||||
} else {
|
||||
files = append(files, fileEntry{name: "errors.json.txt", data: jsonError})
|
||||
}
|
||||
@@ -1181,7 +1146,7 @@ func (s *service) getSupportBundle(w http.ResponseWriter, r *http.Request) {
|
||||
if panicFiles, err := filepath.Glob(filepath.Join(locations.GetBaseDir(locations.ConfigBaseDir), "panic*")); err == nil {
|
||||
for _, f := range panicFiles {
|
||||
if panicFile, err := os.ReadFile(f); err != nil {
|
||||
l.Warnf("Support bundle: failed to load %s: %s", filepath.Base(f), err)
|
||||
slog.Warn("Failed to load panic file for support bundle", slogutil.FilePath(filepath.Base(f)), slogutil.Error(err))
|
||||
} else {
|
||||
files = append(files, fileEntry{name: filepath.Base(f), data: panicFile})
|
||||
}
|
||||
@@ -1204,15 +1169,15 @@ func (s *service) getSupportBundle(w http.ResponseWriter, r *http.Request) {
|
||||
}, "", " "); err == nil {
|
||||
files = append(files, fileEntry{name: "version-platform.json.txt", data: versionPlatform})
|
||||
} else {
|
||||
l.Warnln("Failed to create versionPlatform.json: ", err)
|
||||
slog.Warn("Failed to create versionPlatform.json in support bundle", slogutil.Error(err))
|
||||
}
|
||||
|
||||
// Report Data as a JSON
|
||||
if r, err := s.urService.ReportDataPreview(r.Context(), ur.Version); err != nil {
|
||||
l.Warnln("Support bundle: failed to create usage-reporting.json.txt:", err)
|
||||
slog.Warn("Failed to create usage-reporting.json.txt in support bundle", slogutil.Error(err))
|
||||
} else {
|
||||
if usageReportingData, err := json.MarshalIndent(r, "", " "); err != nil {
|
||||
l.Warnln("Support bundle: failed to serialize usage-reporting.json.txt", err)
|
||||
slog.Warn("Failed to serialize usage-reporting.json.txt in support bundle", slogutil.Error(err))
|
||||
} else {
|
||||
files = append(files, fileEntry{name: "usage-reporting.json.txt", data: usageReportingData})
|
||||
}
|
||||
@@ -1227,7 +1192,7 @@ func (s *service) getSupportBundle(w http.ResponseWriter, r *http.Request) {
|
||||
// Connection data as JSON
|
||||
connStats := s.model.ConnectionStats()
|
||||
if connStatsJSON, err := json.MarshalIndent(connStats, "", " "); err != nil {
|
||||
l.Warnln("Support bundle: failed to serialize connection-stats.json.txt", err)
|
||||
slog.Warn("Failed to serialize connection-stats.json.txt in support bundle", slogutil.Error(err))
|
||||
} else {
|
||||
files = append(files, fileEntry{name: "connection-stats.json.txt", data: connStatsJSON})
|
||||
}
|
||||
@@ -1273,7 +1238,7 @@ func (s *service) getSupportBundle(w http.ResponseWriter, r *http.Request) {
|
||||
// Add buffer files to buffer zip
|
||||
var zipFilesBuffer bytes.Buffer
|
||||
if err := writeZip(&zipFilesBuffer, files); err != nil {
|
||||
l.Warnln("Support bundle: failed to create support bundle zip:", err)
|
||||
slog.Warn("Failed to create support bundle zip (buffer)", slogutil.Error(err))
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
@@ -1284,7 +1249,7 @@ func (s *service) getSupportBundle(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Write buffer zip to local zip file (back up)
|
||||
if err := os.WriteFile(zipFilePath, zipFilesBuffer.Bytes(), 0o600); err != nil {
|
||||
l.Warnln("Support bundle: support bundle zip could not be created:", err)
|
||||
slog.Warn("Failed to create support bundle zip (file)", slogutil.FilePath(zipFilePath), slogutil.Error(err))
|
||||
}
|
||||
|
||||
// Serve the buffer zip to client for download
|
||||
@@ -1293,26 +1258,6 @@ func (s *service) getSupportBundle(w http.ResponseWriter, r *http.Request) {
|
||||
io.Copy(w, &zipFilesBuffer)
|
||||
}
|
||||
|
||||
func (*service) getSystemHTTPMetrics(w http.ResponseWriter, _ *http.Request) {
|
||||
stats := make(map[string]interface{})
|
||||
metrics.Each(func(name string, intf interface{}) {
|
||||
if m, ok := intf.(*metrics.StandardTimer); ok {
|
||||
pct := m.Percentiles([]float64{0.50, 0.95, 0.99})
|
||||
for i := range pct {
|
||||
pct[i] /= 1e6 // ns to ms
|
||||
}
|
||||
stats[name] = map[string]interface{}{
|
||||
"count": m.Count(),
|
||||
"sumMs": m.Sum() / 1e6, // ns to ms
|
||||
"ratesPerS": []float64{m.Rate1(), m.Rate5(), m.Rate15()},
|
||||
"percentilesMs": pct,
|
||||
}
|
||||
}
|
||||
})
|
||||
bs, _ := json.MarshalIndent(stats, "", " ")
|
||||
w.Write(bs)
|
||||
}
|
||||
|
||||
func (s *service) getSystemDiscovery(w http.ResponseWriter, _ *http.Request) {
|
||||
devices := make(map[string]discover.CacheEntry)
|
||||
|
||||
@@ -1534,7 +1479,7 @@ func (s *service) postSystemUpgrade(w http.ResponseWriter, _ *http.Request) {
|
||||
if upgrade.CompareVersions(rel.Tag, build.Version) > upgrade.Equal {
|
||||
err = upgrade.To(rel)
|
||||
if err != nil {
|
||||
l.Warnln("upgrading:", err)
|
||||
slog.Error("Failed to upgrade", slogutil.Error(err))
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
@@ -1637,35 +1582,6 @@ func (*service) getQR(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write(code.PNG())
|
||||
}
|
||||
|
||||
func (s *service) getPeerCompletion(w http.ResponseWriter, _ *http.Request) {
|
||||
tot := map[string]float64{}
|
||||
count := map[string]float64{}
|
||||
|
||||
for _, folder := range s.cfg.Folders() {
|
||||
for _, device := range folder.DeviceIDs() {
|
||||
deviceStr := device.String()
|
||||
if s.model.ConnectedTo(device) {
|
||||
comp, err := s.model.Completion(device, folder.ID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
tot[deviceStr] += comp.CompletionPct
|
||||
} else {
|
||||
tot[deviceStr] = 0
|
||||
}
|
||||
count[deviceStr]++
|
||||
}
|
||||
}
|
||||
|
||||
comp := map[string]int{}
|
||||
for device := range tot {
|
||||
comp[device] = int(tot[device] / count[device])
|
||||
}
|
||||
|
||||
sendJSON(w, comp)
|
||||
}
|
||||
|
||||
func (s *service) getFolderVersions(w http.ResponseWriter, r *http.Request) {
|
||||
qs := r.URL.Query()
|
||||
versions, err := s.model.GetFolderVersions(qs.Get("folder"))
|
||||
|
||||
@@ -9,6 +9,7 @@ package api
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"slices"
|
||||
@@ -16,6 +17,7 @@ import (
|
||||
"time"
|
||||
|
||||
ldap "github.com/go-ldap/ldap/v3"
|
||||
"github.com/syncthing/syncthing/internal/slogutil"
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
@@ -43,11 +45,11 @@ func emitLoginAttempt(success bool, username string, r *http.Request, evLogger e
|
||||
if success {
|
||||
return
|
||||
}
|
||||
l := slog.Default().With(slogutil.Address(remoteAddress), slog.String("username", username))
|
||||
if proxy != "" {
|
||||
l.Infof("Wrong credentials supplied during API authorization from %s proxied by %s", remoteAddress, proxy)
|
||||
} else {
|
||||
l.Infof("Wrong credentials supplied during API authorization from %s", remoteAddress)
|
||||
l = l.With("proxy", proxy)
|
||||
}
|
||||
l.Warn("Bad credentials supplied during API authorization")
|
||||
}
|
||||
|
||||
func remoteAddress(r *http.Request) (remoteAddr, proxy string) {
|
||||
@@ -203,7 +205,7 @@ func attemptBasicAuth(r *http.Request, guiCfg config.GUIConfiguration, ldapCfg c
|
||||
return "", false
|
||||
}
|
||||
|
||||
l.Debugln("Sessionless HTTP request with authentication; this is expensive.")
|
||||
slog.Debug("Sessionless HTTP request with authentication; this is expensive.")
|
||||
|
||||
if auth(username, password, guiCfg, ldapCfg) {
|
||||
return username, true
|
||||
@@ -254,14 +256,14 @@ func authLDAP(username string, password string, cfg config.LDAPConfiguration) bo
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
l.Warnln("LDAP Dial:", err)
|
||||
slog.Error("Failed to dial LDAP server", slogutil.Error(err))
|
||||
return false
|
||||
}
|
||||
|
||||
if cfg.Transport == config.LDAPTransportStartTLS {
|
||||
err = connection.StartTLS(&tls.Config{InsecureSkipVerify: cfg.InsecureSkipVerify})
|
||||
if err != nil {
|
||||
l.Warnln("LDAP Start TLS:", err)
|
||||
slog.Error("Failed to handshake start TLS With LDAP server", slogutil.Error(err))
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -271,7 +273,7 @@ func authLDAP(username string, password string, cfg config.LDAPConfiguration) bo
|
||||
bindDN := formatOptionalPercentS(cfg.BindDN, escapeForLDAPDN(username))
|
||||
err = connection.Bind(bindDN, password)
|
||||
if err != nil {
|
||||
l.Warnln("LDAP Bind:", err)
|
||||
slog.Error("Failed to bind with LDAP server", slogutil.Error(err))
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -281,7 +283,7 @@ func authLDAP(username string, password string, cfg config.LDAPConfiguration) bo
|
||||
}
|
||||
|
||||
if cfg.SearchFilter == "" || cfg.SearchBaseDN == "" {
|
||||
l.Warnln("LDAP configuration: both searchFilter and searchBaseDN must be set, or neither.")
|
||||
slog.Error("Bad LDAP configuration: both searchFilter and searchBaseDN must be set, or neither")
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -296,11 +298,11 @@ func authLDAP(username string, password string, cfg config.LDAPConfiguration) bo
|
||||
|
||||
res, err := connection.Search(searchReq)
|
||||
if err != nil {
|
||||
l.Warnln("LDAP Search:", err)
|
||||
slog.Warn("Failed LDAP search", slogutil.Error(err))
|
||||
return false
|
||||
}
|
||||
if len(res.Entries) != 1 {
|
||||
l.Infof("Wrong number of LDAP search results, %d != 1", len(res.Entries))
|
||||
slog.Warn("Incorrect number of LDAP search results (expected one)", slog.Int("results", len(res.Entries)))
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -12,12 +12,12 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/api/auto"
|
||||
"github.com/syncthing/syncthing/lib/assets"
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
)
|
||||
|
||||
const themePrefix = "theme-assets/"
|
||||
@@ -36,7 +36,6 @@ func newStaticsServer(theme, assetDir string) *staticsServer {
|
||||
s := &staticsServer{
|
||||
assetDir: assetDir,
|
||||
assets: auto.Assets(),
|
||||
mut: sync.NewRWMutex(),
|
||||
theme: theme,
|
||||
lastThemeChange: time.Now().UTC(),
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
|
||||
"github.com/syncthing/syncthing/internal/db"
|
||||
"github.com/syncthing/syncthing/internal/db/sqlite"
|
||||
"github.com/syncthing/syncthing/internal/slogutil"
|
||||
"github.com/syncthing/syncthing/lib/assets"
|
||||
"github.com/syncthing/syncthing/lib/build"
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
@@ -38,14 +39,11 @@ import (
|
||||
eventmocks "github.com/syncthing/syncthing/lib/events/mocks"
|
||||
"github.com/syncthing/syncthing/lib/fs"
|
||||
"github.com/syncthing/syncthing/lib/locations"
|
||||
"github.com/syncthing/syncthing/lib/logger"
|
||||
loggermocks "github.com/syncthing/syncthing/lib/logger/mocks"
|
||||
"github.com/syncthing/syncthing/lib/model"
|
||||
modelmocks "github.com/syncthing/syncthing/lib/model/mocks"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/rand"
|
||||
"github.com/syncthing/syncthing/lib/svcutil"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
"github.com/syncthing/syncthing/lib/tlsutil"
|
||||
"github.com/syncthing/syncthing/lib/ur"
|
||||
)
|
||||
@@ -96,7 +94,7 @@ func TestStopAfterBrokenConfig(t *testing.T) {
|
||||
|
||||
srv.started = make(chan string)
|
||||
|
||||
sup := suture.New("test", svcutil.SpecWithDebugLogger(l))
|
||||
sup := suture.New("test", svcutil.SpecWithDebugLogger())
|
||||
sup.Add(srv)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
sup.ServeBackground(ctx)
|
||||
@@ -150,7 +148,6 @@ func TestAssetsDir(t *testing.T) {
|
||||
|
||||
e := &staticsServer{
|
||||
theme: "foo",
|
||||
mut: sync.NewRWMutex(),
|
||||
assetDir: "testdata",
|
||||
assets: map[string]assets.Asset{
|
||||
"foo/a": foo, // overridden in foo/a
|
||||
@@ -360,7 +357,7 @@ func TestAPIServiceRequests(t *testing.T) {
|
||||
Prefix: "{",
|
||||
},
|
||||
{
|
||||
URL: "/rest/system/debug",
|
||||
URL: "/rest/system/loglevels",
|
||||
Code: 200,
|
||||
Type: "application/json",
|
||||
Prefix: "{",
|
||||
@@ -1044,16 +1041,8 @@ func startHTTPWithShutdownTimeout(t *testing.T, cfg config.Wrapper, shutdownTime
|
||||
diskEventSub := new(eventmocks.BufferedSubscription)
|
||||
discoverer := new(discovermocks.Manager)
|
||||
connections := new(connmocks.Service)
|
||||
errorLog := new(loggermocks.Recorder)
|
||||
systemLog := new(loggermocks.Recorder)
|
||||
for _, l := range []*loggermocks.Recorder{errorLog, systemLog} {
|
||||
l.SinceReturns([]logger.Line{
|
||||
{
|
||||
When: time.Now(),
|
||||
Message: "Test message",
|
||||
},
|
||||
})
|
||||
}
|
||||
errorLog := slogutil.NewRecorder(0)
|
||||
systemLog := slogutil.NewRecorder(0)
|
||||
addrChan := make(chan string)
|
||||
mockedSummary := &modelmocks.FolderSummaryService{}
|
||||
mockedSummary.SummaryReturns(new(model.FolderSummary), nil)
|
||||
|
||||
@@ -9,10 +9,12 @@ package api
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
|
||||
"github.com/syncthing/syncthing/internal/slogutil"
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/structutil"
|
||||
@@ -311,7 +313,7 @@ func (c *configMuxBuilder) adjustConfig(w http.ResponseWriter, r *http.Request)
|
||||
to, err := config.ReadJSON(r.Body, c.id)
|
||||
r.Body.Close()
|
||||
if err != nil {
|
||||
l.Warnln("Decoding posted config:", err)
|
||||
slog.Error("Failed to decode posted config", slogutil.Error(err))
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
@@ -415,7 +417,7 @@ func (c *configMuxBuilder) adjustGUI(w http.ResponseWriter, r *http.Request, gui
|
||||
func (c *configMuxBuilder) postAdjustGui(from *config.GUIConfiguration, to *config.GUIConfiguration) error {
|
||||
if to.Password != from.Password {
|
||||
if err := to.SetPassword(to.Password); err != nil {
|
||||
l.Warnln("hashing password:", err)
|
||||
slog.Error("Failed to hash password", slogutil.Error(err))
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -456,7 +458,7 @@ func unmarshalToRawMessages(body io.ReadCloser) ([]json.RawMessage, error) {
|
||||
func (c *configMuxBuilder) finish(w http.ResponseWriter, waiter config.Waiter) {
|
||||
waiter.Wait()
|
||||
if err := c.cfg.Save(); err != nil {
|
||||
l.Warnln("Saving config:", err)
|
||||
slog.Error("Failed to save config", slogutil.Error(err))
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,9 @@
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/syncthing/syncthing/lib/logger"
|
||||
)
|
||||
import "github.com/syncthing/syncthing/internal/slogutil"
|
||||
|
||||
var l = logger.DefaultLogger.NewFacility("api", "REST API")
|
||||
var l = slogutil.NewAdapter("REST API")
|
||||
|
||||
func shouldDebugHTTP() bool {
|
||||
return l.ShouldDebug("api")
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
@@ -19,7 +20,6 @@ import (
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
"github.com/syncthing/syncthing/lib/rand"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
)
|
||||
|
||||
type tokenManager struct {
|
||||
@@ -49,7 +49,6 @@ func newTokenManager(key string, miscDB *db.Typed, lifetime time.Duration, maxIt
|
||||
lifetime: lifetime,
|
||||
maxItems: maxItems,
|
||||
timeNow: time.Now,
|
||||
mut: sync.NewMutex(),
|
||||
tokens: &tokens,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ type cast struct {
|
||||
// methods to get a functional implementation of Interface.
|
||||
func newCast(name string) *cast {
|
||||
// Only log restarts in debug mode.
|
||||
spec := svcutil.SpecWithDebugLogger(l)
|
||||
spec := svcutil.SpecWithDebugLogger()
|
||||
// Don't retry too frenetically: an error to open a socket or
|
||||
// whatever is usually something that is either permanent or takes
|
||||
// a while to get solved...
|
||||
|
||||
@@ -8,10 +8,12 @@ package beacon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/wlynxg/anet"
|
||||
"github.com/syncthing/syncthing/internal/slogutil"
|
||||
"github.com/syncthing/syncthing/lib/netutil"
|
||||
)
|
||||
|
||||
func NewBroadcast(port int) Interface {
|
||||
@@ -46,7 +48,7 @@ func writeBroadcasts(ctx context.Context, inbox <-chan []byte, port int) error {
|
||||
return doneCtx.Err()
|
||||
}
|
||||
|
||||
intfs, err := anet.Interfaces()
|
||||
intfs, err := netutil.Interfaces()
|
||||
if err != nil {
|
||||
l.Debugln("Failed to list interfaces:", err)
|
||||
// net.Interfaces() is broken on Android. see https://github.com/golang/go/issues/40569
|
||||
@@ -61,7 +63,7 @@ func writeBroadcasts(ctx context.Context, inbox <-chan []byte, port int) error {
|
||||
continue
|
||||
}
|
||||
|
||||
addrs, err := anet.InterfaceAddrsByInterface(&intf)
|
||||
addrs, err := netutil.InterfaceAddrsByInterface(&intf)
|
||||
if err != nil {
|
||||
l.Debugln("Failed to list interface addresses:", err)
|
||||
// Interface discovery might work while retrieving the addresses doesn't. So log the error and carry on.
|
||||
@@ -109,7 +111,7 @@ func writeBroadcasts(ctx context.Context, inbox <-chan []byte, port int) error {
|
||||
}
|
||||
|
||||
if success == 0 {
|
||||
l.Debugln("couldn't send any broadcasts")
|
||||
slog.DebugContext(ctx, "Couldn't send any broadcasts", slogutil.Error(err))
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -146,7 +148,7 @@ func readBroadcasts(ctx context.Context, outbox chan<- recv, port int) error {
|
||||
case <-doneCtx.Done():
|
||||
return doneCtx.Err()
|
||||
default:
|
||||
l.Debugln("dropping message")
|
||||
slog.DebugContext(ctx, "Dropping message")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
|
||||
package beacon
|
||||
|
||||
import (
|
||||
"github.com/syncthing/syncthing/lib/logger"
|
||||
)
|
||||
import "github.com/syncthing/syncthing/internal/slogutil"
|
||||
|
||||
var l = logger.DefaultLogger.NewFacility("beacon", "Multicast and broadcast discovery")
|
||||
var l = slogutil.NewAdapter("Multicast and broadcast discovery")
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user