Compare commits

...
48 Commits
Author SHA1 Message Date
Jakob Borg 13679284ac cmd/syncthing: Rename --conf back to --config
This was inadvertently changed in the flag migration.
2021-04-06 10:42:29 +02:00
André ColombandJakob Borg 7931af1078 lib/db: Fix comparison of pending folder timestamps (fixes #7532) (#7535) 2021-04-06 10:37:11 +02:00
Jakob Borg fb4fdaf4c0 Merge pull request from GHSA-x462-89pf-6r5h 2021-04-06 08:03:22 +02:00
Simon FreiandGitHub 0d7a77ba85 lib/model: Reset conn when enc token is missing (fixes #7198) (#7518) 2021-03-23 10:38:40 +01:00
Simon FreiandGitHub 924b96856f lib: Handle adding enc folders on an existing conn (fixes #7509) (#7510) 2021-03-22 21:50:19 +01:00
Audrius ButkeviciusandGitHub f7929229c8 cmd/strelaysrv: Increase default strelaysrv network buffer size (fixes #7514) (#7517)
Increases throughput at the cost of more memory per connection
2021-03-22 21:47:51 +01:00
Simon FreiandGitHub 6b25eb2e79 lib/ur: Prevent panic when blocksResult is nil (ref #7495) (#7515) 2021-03-22 15:07:41 +01:00
Simon FreiandGitHub bc08a951f1 lib/model: Encrypted fileinfo trailer needs to be in wire format (#7505) 2021-03-21 10:34:08 +01:00
Lars LehtonenandGitHub a87c5515bd lib/model: Error check in test (#7508) 2021-03-21 10:32:17 +01:00
Jakob BorgandGitHub ebcd22b02b lib/osutil: Fix raising max FDs on macOS (#7507)
There was a logic mistake, so the limit in question wasn't used. On my
macOS this doesn't seem to matter, the hard limit returned is 2^63-1 and
setting the soft limit to that works. However I'm assuming that's not
the case for older macOSes since it was so nicely documented, so we
should still have this working. (10240 FDs should be enough for
anybody.)
2021-03-20 16:32:36 +01:00
Audrius ButkeviciusandGitHub 4b02b7e6f1 lib/connections: Silence "connected to myself" messages. (#7500) 2021-03-17 23:53:20 +01:00
Jakob Borg fdd823d2cb lib/osutil: Remove unused code 2021-03-17 23:18:07 +01:00
Jakob BorgandGitHub 8ef504f745 all: Simplify some method calls (#7499)
strings.Replace(a, b, c, -1) -> strings.ReplaceAll(a, b, c)

(Go 1.12) and who knows what was up with that dialQueue.Sort() thing.
2021-03-17 23:12:26 +01:00
Jakob BorgandGitHub 960e850a78 cmd/uraggregate: Ignore errors harder (#7498)
We were already ignoring them, it just didn't look like it.
2021-03-17 22:55:43 +01:00
Jakob BorgandGitHub ea701a4e9e cmd/syncthing: Error handling on upgrade (#7497)
dat innocuous little colon
2021-03-17 22:52:43 +01:00
deepsource-autofix[bot]GitHubdeepsource-autofix[bot] <62050782+deepsource-autofix[bot]@users.noreply.github.com>
6c573a5762 Remove unnecessary guard around delete (#7496)
Co-authored-by: deepsource-autofix[bot] <62050782+deepsource-autofix[bot]@users.noreply.github.com>
2021-03-17 22:23:36 +01:00
Jakob BorgandGitHub 3ac858b150 all: Remove miscellaneous vestigial code (#7495) 2021-03-17 22:23:12 +01:00
Jakob BorgandGitHub f4372710bf all: Remove crypto/md5 (#7493)
This is a mostly pointless change to make security scanners and static
analysis tools happy, as they all hate seeing md5. None of our md5 uses
were security relevant, but still. Only visible effect of this change is
that our temp file names for very long file names become slightly longer
than they were previously...
2021-03-17 22:22:49 +01:00
Jakob BorgandGitHub f39477bbd5 lib/api: Missing error handling in API delete-device (#7494) 2021-03-17 22:08:44 +01:00
Jakob BorgandGitHub 6e5514419d lib/db: Fix some omitted error checks, unused variable (#7489) 2021-03-17 21:41:07 +01:00
tomasz1986andGitHub f014b7b919 gui: Improve Receive Encrypted folder type requirements text (#7473)
The current text gives an impression that we are currently using a
Receive Encrypted folder, even if we are not. Thus, make the current
text displayed only when the folder is in fact Receive Encrypted, and
add a new string to be displayed when using different folder types.

Signed-off-by: Tomasz Wilczyński <twilczynski@naver.com>
2021-03-17 21:36:11 +01:00
Jakob BorgandGitHub 81484699db lib/model: Actually break puller loop on context cancel (#7492)
Current break does nothing (breaks the select).
2021-03-17 21:34:52 +01:00
Jakob Borg 6d93d9c488 deepsource: Test patterns 2021-03-17 21:12:51 +01:00
deepsource-autofix[bot]GitHubdeepsource-autofix[bot] <62050782+deepsource-autofix[bot]@users.noreply.github.com>
0930bccf88 cmd/ursrv, lib/scanner: Remove unnecessary slicing of slices (#7491)
Co-authored-by: deepsource-autofix[bot] <62050782+deepsource-autofix[bot]@users.noreply.github.com>
2021-03-17 21:04:36 +01:00
Jakob BorgandGitHub e321bd3941 lib/*/auto: Add noassets files (#7490)
This adds a couple of dummy asset files protected by the "noassets"
build tag. The purpose is that it should be possible for, for example,
CI tools and static analysis things to compile and analyze the source
tree without our custom asset generation step. Also makes `go test -tags
noassets ./...` work without building assets first.
2021-03-17 21:03:35 +01:00
Simon FreiandGitHub 4b02937862 lib/model: Add missing lock on availability func (#7487) 2021-03-17 20:46:13 +01:00
Jakob Borg 21e6849f2d Add DeepSource for static analysis 2021-03-17 20:05:20 +01:00
Simon Frei 39c2d1bc1a cmd/syncthing: Check os.Args length (fixes #7486) 2021-03-17 15:51:14 +01:00
Simon FreiandGitHub cd21b8dfa5 cmd/syncthing/cli: Remove db reset and adjust others (#7484) 2021-03-17 09:06:37 +01:00
Simon FreiandGitHub 40fbdc87ce cmd/syncthing: Refactor cli subcommand to allow flags (#7454) 2021-03-17 09:04:50 +01:00
Jakob Borg 1814f4693d gui, man, authors: Update docs, translations, and contributors 2021-03-17 07:45:40 +01:00
Simon FreiandGitHub 3f2b584c4e lib/model: Don't use ignore patterns for recv-enc folders (fixes #7469) (#7472) 2021-03-16 15:04:11 +01:00
Jakob BorgandGitHub e0dd737822 gui: Fix validation and help texts in versioning editor (fixes #7481) (#7482) 2021-03-16 09:30:07 +01:00
Simon FreiandGitHub d2d4fcc1df lib/protocol: Improve messages when an error occurs receiving (ref #7466) (#7470) 2021-03-15 19:14:09 +01:00
Simon FreiandGitHub 273ee09925 lib/db, lib/model: Allow needing invalid files (fixes #7474) (#7476) 2021-03-15 07:58:01 +01:00
tomasz1986andGitHub bb886868d2 gui: Use grey background for disabled options in form-control (#7468)
Disabled options are currently barely distinguishable from enabled
ones. This changes their background to grey, following the Bootstrap
defaults already used for disabled <select>.

Signed-off-by: Tomasz Wilczyński <twilczynski@naver.com>
2021-03-14 08:14:19 +01:00
Simon FreiandGitHub f80ee472c2 lib/protocol: Set invalid flag on encrypted file infos (fixes #7466) (#7467) 2021-03-13 16:57:36 +01:00
tomasz1986andGitHub a12ede3bbe gui: Fix missing trashcanClean in simple versioning (#7465) 2021-03-13 10:14:26 +01:00
Simon FreiandGitHub 97a8777d03 lib/fs: Check both old and new path when renaming (fixes #7426) (#7463) 2021-03-12 21:15:50 +01:00
Simon FreiandGitHub 8a4c00d82e lib/model: Send failure report on CC encryption check error (#7460) 2021-03-12 12:21:54 +01:00
Simon FreiandGitHub 31f859e909 lib/model: Return correct error in puller-iteration (ref #7424) (#7461) 2021-03-12 12:21:28 +01:00
Jakob BorgandGitHub 4d979a1ce9 all: Truncate some timestamps (fixes #7457) (#7459)
This truncates times meant for API consumption to second precision,
where fractions won't typically matter or add any value. Exception to
this is timestamps on logs and events, and of course I'm not touching
things like file metadata.

I'm not 100% certain this is an exhaustive change, but it's the things I
found by grepping and following the breadcrumbs from lib/api...

I also considered general-but-ugly solutions, like having the API
serializer itself do reflection magic or even regexps on returned
objects, but decided against it because aurgh...
2021-03-12 10:35:10 +01:00
Simon FreiandGitHub 4465cdf8bc lib/api: Fix body of renamed config/restart-required endpoint (ref #7402) (#7453) 2021-03-11 15:54:05 +01:00
Simon FreiandGitHub 3938b61c3f lib/fs: Expose fs option on interface (fixes #7385, ref #7381) (#7389) 2021-03-11 15:23:56 +01:00
Jakob BorgandGitHub cdef503db6 all: Make config.Wrapper an actual suture.Service (fixes #7451) (#7452) 2021-03-11 14:51:00 +01:00
Jakob BorgandGitHub df08984a58 lib/api: Sanitize names used in certificates (fixes #7434) (#7435) 2021-03-11 13:15:03 +01:00
Simon FreiandGitHub cf838c71f7 gui: Check if versioning object exists (fixes #7449) (#7450) 2021-03-11 11:21:35 +01:00
Simon FreiandGitHub 9a001051d6 cmd/ursrv, lib/ur: Collect and present encryption usage (#7448) 2021-03-10 22:26:56 +00:00
103 changed files with 1095 additions and 585 deletions
+12
View File
@@ -0,0 +1,12 @@
version = 1
exclude_patterns = ["*.pb.go"]
test_patterns = ["*_test.go"]
[[analyzers]]
name = "go"
enabled = true
[analyzers.meta]
import_paths = ["github.com/syncthing/syncthing"]
build_tags = ["noassets"]
+7 -10
View File
@@ -7,7 +7,6 @@
package main
import (
"crypto/md5"
"errors"
"flag"
"fmt"
@@ -15,6 +14,8 @@ import (
"log"
"os"
"path/filepath"
"github.com/syncthing/syncthing/lib/sha256"
)
func main() {
@@ -74,7 +75,7 @@ type fileInfo struct {
name string
mode os.FileMode
mod int64
hash [16]byte
hash [sha256.Size]byte
}
func (f fileInfo) String() string {
@@ -106,11 +107,7 @@ func startWalker(dir string, res chan<- fileInfo, abort <-chan struct{}) chan er
if err != nil {
return err
}
h := md5.New()
h.Write([]byte(tgt))
hash := h.Sum(nil)
copy(f.hash[:], hash)
f.hash = sha256.Sum256([]byte(tgt))
} else if info.IsDir() {
f = fileInfo{
name: rn,
@@ -123,7 +120,7 @@ func startWalker(dir string, res chan<- fileInfo, abort <-chan struct{}) chan er
mode: info.Mode(),
mod: info.ModTime().Unix(),
}
sum, err := md5file(path)
sum, err := sha256file(path)
if err != nil {
return err
}
@@ -150,14 +147,14 @@ func startWalker(dir string, res chan<- fileInfo, abort <-chan struct{}) chan er
return errc
}
func md5file(fname string) (hash [16]byte, err error) {
func sha256file(fname string) (hash [sha256.Size]byte, err error) {
f, err := os.Open(fname)
if err != nil {
return
}
defer f.Close()
h := md5.New()
h := sha256.New()
io.Copy(h, f)
hb := h.Sum(nil)
copy(hash[:], hb)
+15
View File
@@ -0,0 +1,15 @@
// Copyright (C) 2021 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/.
//+build noassets
package auto
import "github.com/syncthing/syncthing/lib/assets"
func Assets() map[string]assets.Asset {
return nil
}
+3 -2
View File
@@ -10,7 +10,6 @@ import (
"encoding/json"
"flag"
"fmt"
"github.com/syncthing/syncthing/lib/protocol"
"io"
"io/ioutil"
"log"
@@ -23,6 +22,8 @@ import (
"strings"
"time"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/golang/groupcache/lru"
"github.com/oschwald/geoip2-golang"
"github.com/prometheus/client_golang/prometheus"
@@ -475,7 +476,7 @@ func handleRelayTest(request request) {
updateMetrics(request.relay.uri.Host, *stats, location)
}
request.relay.Stats = stats
request.relay.StatsRetrieved = time.Now()
request.relay.StatsRetrieved = time.Now().Truncate(time.Second)
request.relay.Location = location
timer, ok := evictionTimers[request.relay.uri.Host]
+3 -1
View File
@@ -99,7 +99,7 @@ func main() {
flag.IntVar(&natRenewal, "nat-renewal", 30, "NAT renewal frequency in minutes")
flag.IntVar(&natTimeout, "nat-timeout", 10, "NAT discovery timeout in seconds")
flag.BoolVar(&pprofEnabled, "pprof", false, "Enable the built in profiling on the status server")
flag.IntVar(&networkBufferSize, "network-buffer", 2048, "Network buffer size (two of these per proxied connection)")
flag.IntVar(&networkBufferSize, "network-buffer", 65536, "Network buffer size (two of these per proxied connection)")
showVersion := flag.Bool("version", false, "Show version")
flag.Parse()
@@ -186,6 +186,7 @@ func main() {
}
wrapper := config.Wrap("config", config.New(id), id, events.NoopLogger)
go wrapper.Serve(context.TODO())
wrapper.Modify(func(cfg *config.Configuration) {
cfg.Options.NATLeaseM = natLease
cfg.Options.NATRenewalM = natRenewal
@@ -232,6 +233,7 @@ func main() {
uri, err := url.Parse(fmt.Sprintf("relay://%s/?id=%s&pingInterval=%s&networkTimeout=%s&sessionLimitBps=%d&globalLimitBps=%d&statusAddr=%s&providedBy=%s", mapping.Address(), id, pingInterval, networkTimeout, sessionLimitBps, globalLimitBps, statusAddr, providedBy))
if err != nil {
log.Fatalln("Failed to construct URI", err)
return
}
log.Println("URI:", uri.String())
+1 -1
View File
@@ -37,7 +37,7 @@ type result struct {
func main() {
flag.Parse()
prefix := strings.ToUpper(strings.Replace(flag.Arg(0), "-", "", -1))
prefix := strings.ToUpper(strings.ReplaceAll(flag.Arg(0), "-", ""))
if len(prefix) > 7 {
prefix = prefix[:7] + "-" + prefix[7:]
}
+22 -23
View File
@@ -7,31 +7,15 @@
package main
import (
"bytes"
"crypto/md5"
"flag"
"fmt"
"io"
"os"
"time"
"github.com/syncthing/syncthing/lib/sha256"
)
func getmd5(filePath string) ([]byte, error) {
var result []byte
file, err := os.Open(filePath)
if err != nil {
return result, err
}
defer file.Close()
hash := md5.New()
if _, err := io.Copy(hash, file); err != nil {
return result, err
}
return hash.Sum(result), nil
}
func main() {
period := flag.Duration("period", 200*time.Millisecond, "Sleep period between checks")
flag.Parse()
@@ -46,7 +30,7 @@ func main() {
exists := true
size := int64(0)
mtime := time.Time{}
hash := []byte{}
var hash [sha256.Size]byte
for {
time.Sleep(*period)
@@ -72,7 +56,7 @@ func main() {
if !exists {
size = 0
mtime = time.Time{}
hash = []byte{}
hash = [sha256.Size]byte{}
continue
}
@@ -83,12 +67,12 @@ func main() {
newSize := fi.Size()
newMtime := fi.ModTime()
newHash, err := getmd5(file)
newHash, err := sha256file(file)
if err != nil {
fmt.Println("getmd5:", err)
fmt.Println("sha256file:", err)
}
if newSize != size || newMtime != mtime || !bytes.Equal(newHash, hash) {
if newSize != size || newMtime != mtime || newHash != hash {
fmt.Println(file, "Size:", newSize, "Mtime:", newMtime, "Hash:", fmt.Sprintf("%x", newHash))
hash = newHash
size = newSize
@@ -96,3 +80,18 @@ func main() {
}
}
}
func sha256file(fname string) (hash [sha256.Size]byte, err error) {
f, err := os.Open(fname)
if err != nil {
return
}
defer f.Close()
h := sha256.New()
io.Copy(h, f)
hb := h.Sum(nil)
copy(hash[:], hb)
return
}
+60 -32
View File
@@ -10,33 +10,39 @@ import (
"bufio"
"crypto/tls"
"encoding/json"
"log"
"io/ioutil"
"os"
"reflect"
"strings"
"github.com/AudriusButkevicius/recli"
"github.com/alecthomas/kong"
"github.com/flynn-archive/go-shlex"
"github.com/mattn/go-isatty"
"github.com/pkg/errors"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/locations"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/svcutil"
"github.com/urfave/cli"
)
type CLI struct {
GUIAddress string `name:"gui-address" placeholder:"URL" help:"Override GUI address (e.g. \"http://192.0.2.42:8443\")"`
GUIAPIKey string `name:"gui-apikey" placeholder:"API-KEY" help:"Override GUI API key"`
HomeDir string `name:"home" placeholder:"PATH" help:"Set configuration and data directory"`
ConfDir string `name:"conf" placeholder:"PATH" help:"Set configuration directory (config and keys)"`
Args []string `arg:"" optional:""`
type preCli struct {
GUIAddress string `name:"gui-address"`
GUIAPIKey string `name:"gui-apikey"`
HomeDir string `name:"home"`
ConfDir string `name:"conf"`
}
func (c *CLI) Run() error {
func Run() error {
// This is somewhat a hack around a chicken and egg problem. We need to set
// the home directory and potentially other flags to know where the
// syncthing instance is running in order to get it's config ... which we
// then use to construct the actual CLI ... at which point it's too late to
// add flags there...
c := preCli{}
parseFlags(&c)
// Not set as default above because the strings can be really long.
var err error
homeSet := c.HomeDir != ""
@@ -50,8 +56,7 @@ func (c *CLI) Run() error {
err = locations.SetBaseDir(locations.ConfigBaseDir, c.ConfDir)
}
if err != nil {
log.Println("Command line options:", err)
os.Exit(svcutil.ExitError.AsInt())
return errors.Wrap(err, "Command line options:")
}
guiCfg := config.GUIConfiguration{
RawAddress: c.GUIAddress,
@@ -136,28 +141,26 @@ func (c *CLI) Run() error {
// Construct the actual CLI
app := cli.NewApp()
app.Name = "syncthing cli"
app.HelpName = app.Name
app.Author = "The Syncthing Authors"
app.Usage = "Syncthing command line interface"
app.Flags = fakeFlags
app.Metadata = map[string]interface{}{
"client": client,
}
app.Commands = []cli.Command{
{
Name: "config",
HideHelp: true,
Usage: "Configuration modification command group",
Subcommands: commands,
app.Commands = []cli.Command{{
Name: "cli",
Usage: "Syncthing command line interface",
Flags: fakeFlags,
Subcommands: []cli.Command{
{
Name: "config",
HideHelp: true,
Usage: "Configuration modification command group",
Subcommands: commands,
},
showCommand,
operationCommand,
errorsCommand,
},
showCommand,
operationCommand,
errorsCommand,
}
// It expects to be give os.Args which has argv[0] set to executable name, so fake it.
c.Args = append([]string{"cli"}, c.Args...)
}}
tty := isatty.IsTerminal(os.Stdin.Fd()) || isatty.IsCygwinTerminal(os.Stdin.Fd())
if !tty {
@@ -171,7 +174,7 @@ func (c *CLI) Run() error {
if len(input) == 0 {
continue
}
err = app.Run(append(c.Args, input...))
err = app.Run(append(os.Args, input...))
if err != nil {
return err
}
@@ -181,7 +184,7 @@ func (c *CLI) Run() error {
return err
}
} else {
err = app.Run(c.Args)
err = app.Run(os.Args)
if err != nil {
return err
}
@@ -206,3 +209,28 @@ func (c *CLI) Run() error {
}
return nil
}
func parseFlags(c *preCli) error {
// kong only needs to parse the global arguments after "cli" and before the
// subcommand (if any).
if len(os.Args) <= 2 {
return nil
}
args := os.Args[2:]
for i := 0; i < len(args); i++ {
if !strings.HasPrefix(args[i], "--") {
args = args[:i]
break
}
if !strings.Contains(args[i], "=") {
i++
}
}
// We don't want kong to print anything nor os.Exit (e.g. on -h)
parser, err := kong.New(c, kong.Writers(ioutil.Discard, ioutil.Discard), kong.Exit(func(int) {}))
if err != nil {
return err
}
_, err = parser.Parse(args)
return err
}
+1 -6
View File
@@ -27,11 +27,6 @@ var operationCommand = cli.Command{
Usage: "Shutdown syncthing",
Action: expects(0, emptyPost("system/shutdown")),
},
{
Name: "reset",
Usage: "Reset syncthing deleting all folders and devices",
Action: expects(0, emptyPost("system/reset")),
},
{
Name: "upgrade",
Usage: "Upgrade syncthing (if a newer version is available)",
@@ -39,7 +34,7 @@ var operationCommand = cli.Command{
},
{
Name: "folder-override",
Usage: "Override changes on folder (remote for sendonly, local for receiveonly)",
Usage: "Override changes on folder (remote for sendonly, local for receiveonly). WARNING: Destructive - deletes/changes your data.",
ArgsUsage: "[folder id]",
Action: expects(1, foldersOverride),
},
+1 -1
View File
@@ -23,7 +23,7 @@ var showCommand = cli.Command{
{
Name: "config-status",
Usage: "Show configuration status, whether or not a restart is required for changes to take effect",
Action: expects(0, dumpOutput("system/config/insync")),
Action: expects(0, dumpOutput("config/restart-required")),
},
{
Name: "system",
+6 -1
View File
@@ -18,6 +18,7 @@ import (
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/fs"
"github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/scanner"
)
@@ -79,7 +80,7 @@ func (c *CLI) walk() error {
dstFs = fs.NewFilesystem(fs.FilesystemTypeBasic, c.To)
}
return srcFs.Walk("/", func(path string, info fs.FileInfo, err error) error {
return srcFs.Walk(".", func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
@@ -142,6 +143,10 @@ func (c *CLI) process(srcFs fs.Filesystem, dstFs fs.Filesystem, path string) err
return fmt.Errorf("%s: loading metadata trailer: %w", path, err)
}
// Workaround for a bug in <= v1.15.0-rc.5 where we stored names
// in native format, while protocol expects wire format (slashes).
encFi.Name = osutil.NormalizedFilename(encFi.Name)
plainFi, err := protocol.DecryptFileInfo(*encFi, c.folderKey)
if err != nil {
return fmt.Errorf("%s: decrypting metadata: %w", path, err)
+18 -10
View File
@@ -38,6 +38,7 @@ import (
"github.com/syncthing/syncthing/lib/build"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/db"
"github.com/syncthing/syncthing/lib/db/backend"
"github.com/syncthing/syncthing/lib/dialer"
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/fs"
@@ -131,10 +132,11 @@ var (
// The entrypoint struct is the main entry point for the command line parser. The
// commands and options here are top level commands to syncthing.
// Cli is just a placeholder for the help text (see main).
var entrypoint struct {
Serve serveOptions `cmd:"" help:"Run Syncthing"`
Decrypt decrypt.CLI `cmd:"" help:"Decrypt or verify an encrypted folder"`
Cli cli.CLI `cmd:"" help:"Command line interface for Syncthing"`
Cli struct{} `cmd:"" help:"Command line interface for Syncthing"`
}
// serveOptions are the options for the `syncthing serve` command.
@@ -144,7 +146,7 @@ type serveOptions struct {
Audit bool `help:"Write events to audit file"`
AuditFile string `name:"auditfile" placeholder:"PATH" help:"Specify audit file (use \"-\" for stdout, \"--\" for stderr)"`
BrowserOnly bool `help:"Open GUI in browser"`
ConfDir string `name:"conf" placeholder:"PATH" help:"Set configuration directory (config and keys)"`
ConfDir string `name:"config" placeholder:"PATH" help:"Set configuration directory (config and keys)"`
DataDir string `name:"data" placeholder:"PATH" help:"Set data directory (database and logs)"`
DeviceID bool `help:"Show the device ID"`
GenerateDir string `name:"generate" placeholder:"PATH" help:"Generate key and config in specified dir, then exit"`
@@ -211,6 +213,17 @@ func defaultVars() kong.Vars {
}
func main() {
// The "cli" subcommand uses a different command line parser, and e.g. help
// gets mangled when integrating it as a subcommand -> detect it here at the
// beginning.
if len(os.Args) > 1 && os.Args[1] == "cli" {
if err := cli.Run(); err != nil {
fmt.Println(err)
os.Exit(1)
}
return
}
// First some massaging of the raw command line to fit the new model.
// Basically this means adding the default command at the front, and
// converting -options to --options.
@@ -248,10 +261,6 @@ func main() {
}
func helpHandler(options kong.HelpOptions, ctx *kong.Context) error {
// If we're looking for CLI help, pass the arguments down to the CLI library to print it's own help.
if ctx.Command() == "cli" {
return ctx.Run()
}
if err := kong.DefaultHelpPrinter(options, ctx); err != nil {
return err
}
@@ -384,7 +393,8 @@ func (options serveOptions) Run() error {
release, err := checkUpgrade()
if err == nil {
// Use leveldb database locks to protect against concurrent upgrades
ldb, err := syncthing.OpenDBBackend(locations.Get(locations.Database), config.TuningAuto)
var ldb backend.Backend
ldb, err = syncthing.OpenDBBackend(locations.Get(locations.Database), config.TuningAuto)
if err != nil {
err = upgradeViaRest()
} else {
@@ -603,9 +613,7 @@ func syncthingMain(options serveOptions) {
l.Warnln("Failed to initialize config:", err)
os.Exit(svcutil.ExitError.AsInt())
}
if cfgService, ok := cfgWrapper.(suture.Service); ok {
earlyService.Add(cfgService)
}
earlyService.Add(cfgWrapper)
// Candidate builds should auto upgrade. Make sure the option is set,
// unless we are in a build where it's disabled or the STNOUPGRADE
+6 -6
View File
@@ -135,30 +135,30 @@ func setupDB(db *sql.DB) error {
row := db.QueryRow(`SELECT 'UniqueDayVersionIndex'::regclass`)
if err := row.Scan(&t); err != nil {
_, err = db.Exec(`CREATE UNIQUE INDEX UniqueDayVersionIndex ON VersionSummary (Day, Version)`)
_, _ = db.Exec(`CREATE UNIQUE INDEX UniqueDayVersionIndex ON VersionSummary (Day, Version)`)
}
row = db.QueryRow(`SELECT 'VersionDayIndex'::regclass`)
if err := row.Scan(&t); err != nil {
_, err = db.Exec(`CREATE INDEX VersionDayIndex ON VersionSummary (Day)`)
_, _ = db.Exec(`CREATE INDEX VersionDayIndex ON VersionSummary (Day)`)
}
row = db.QueryRow(`SELECT 'MovementDayIndex'::regclass`)
if err := row.Scan(&t); err != nil {
_, err = db.Exec(`CREATE INDEX MovementDayIndex ON UserMovement (Day)`)
_, _ = db.Exec(`CREATE INDEX MovementDayIndex ON UserMovement (Day)`)
}
row = db.QueryRow(`SELECT 'PerformanceDayIndex'::regclass`)
if err := row.Scan(&t); err != nil {
_, err = db.Exec(`CREATE INDEX PerformanceDayIndex ON Performance (Day)`)
_, _ = db.Exec(`CREATE INDEX PerformanceDayIndex ON Performance (Day)`)
}
row = db.QueryRow(`SELECT 'BlockStatsDayIndex'::regclass`)
if err := row.Scan(&t); err != nil {
_, err = db.Exec(`CREATE INDEX BlockStatsDayIndex ON BlockStats (Day)`)
_, _ = db.Exec(`CREATE INDEX BlockStatsDayIndex ON BlockStats (Day)`)
}
return err
return nil
}
func maxIndexedDay(db *sql.DB, table string) time.Time {
+4 -3
View File
@@ -89,7 +89,7 @@ var funcs = map[string]interface{}{
parts = append(parts, part)
}
if len(input) > 0 {
parts = append(parts, input[:])
parts = append(parts, input)
}
return parts[whichPart-1]
},
@@ -725,8 +725,8 @@ func getReport(db *sql.DB) map[string]interface{} {
if rep.NATType != "" {
natType := rep.NATType
natType = strings.Replace(natType, "unknown", "Unknown", -1)
natType = strings.Replace(natType, "Symetric", "Symmetric", -1)
natType = strings.ReplaceAll(natType, "unknown", "Unknown")
natType = strings.ReplaceAll(natType, "Symetric", "Symmetric")
add(featureGroups["Various"]["v3"], "NAT Type", natType, 1)
}
@@ -745,6 +745,7 @@ func getReport(db *sql.DB) map[string]interface{} {
inc(features["Folder"]["v3"], "Weak hash, custom threshold", rep.FolderUsesV3.CustomWeakHashThreshold)
inc(features["Folder"]["v3"], "Filesystem watcher", rep.FolderUsesV3.FsWatcherEnabled)
inc(features["Folder"]["v3"], "Case sensitive FS", rep.FolderUsesV3.CaseSensitiveFS)
inc(features["Folder"]["v3"], "Mode, receive encrypted", rep.FolderUsesV3.ReceiveEncrypted)
add(featureGroups["Folder"]["v3"], "Conflicts", "Disabled", rep.FolderUsesV3.ConflictsDisabled)
add(featureGroups["Folder"]["v3"], "Conflicts", "Unlimited", rep.FolderUsesV3.ConflictsUnlimited)
+6
View File
@@ -437,3 +437,9 @@ ul.three-columns li, ul.two-columns li {
.form-horizontal {
margin-bottom: 10px;
}
/* Use the same style as Bootstrap uses for disabled <select>. */
.form-control option[disabled] {
background-color: #eeeeee;
opacity: 1;
}
+1 -1
View File
@@ -212,7 +212,7 @@
"Outgoing Rate Limit (KiB/s)": "Limite du débit d'envoi (Kio/s)",
"Override Changes": "Écraser les changements",
"Path": "Chemin",
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Dans l'écran de \"Personnalisation\" des valeurs par défaut (menu Actions/Configuration), ce champ indique le chemin dans lequel les partages acceptés automatiquement seront créés, ainsi que chemin de base suggéré lors de l'enregistrement des nouveaux partages. Le caractère tilde (~) est un raccourci pour {{tilde}}.\nEn création/acceptation manuelle d'un nouveau partage, c'est le chemin vers le répertoire à partager dans l'appareil local. Il sera créé s'il n'existe pas. Vous pouvez entrer un chemin absolu (p.ex \"/home/moi/Sync/Exemple\") ou relatif à celui du programme (p.ex \"..\\Partages\\Exemple\" - utile pour installation portable). Le caractère tilde (~, ou ~+Espace sous Windows) peut être utilisé comme raccourci vers",
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Dans l'écran de \"Personnalisation\" des valeurs par défaut (menu Actions/Configuration), ce champ indique le chemin dans lequel les partages acceptés automatiquement seront créés, ainsi que chemin de base suggéré lors de l'enregistrement des nouveaux partages. Le caractère tilde (~) est un raccourci pour votre répertoire personnel {{tilde}} .\nEn création/acceptation manuelle d'un nouveau partage, c'est le chemin vers le répertoire à partager dans l'appareil local. Il sera créé s'il n'existe pas. Vous pouvez entrer un chemin absolu (p.ex \"/home/moi/Sync/Exemple\") ou relatif à celui du programme (p.ex \"..\\Partages\\Exemple\" - utile pour installation portable). Le caractère tilde (~, taper ~+Espace sous Windows) peut être utilisé comme raccourci vers",
"Path where new auto accepted folders will be created, as well as the default suggested path when adding new folders via the UI. Tilde character (~) expands to {%tilde%}.": "Chemin dans lequel les partages acceptés automatiquement seront créés, ainsi que chemin suggéré lors de l'enregistrement des nouveaux partages via cette interface graphique. Le caractère tilde (~) est un raccourci pour {{tilde}}.",
"Path where versions should be stored (leave empty for the default .stversions directory in the shared folder).": "Chemin où les versions seront conservées (laisser vide pour le chemin par défaut de .stversions (caché) dans le partage).\nChemin relatif ou absolu (recommandé), mais dans un répertoire non synchronisé (par masque ou hors du chemin du partage).\nSur la même partition ou système de fichiers (recommandé).",
"Pause": "Pause",
@@ -2053,6 +2053,9 @@ angular.module('syncthing.core')
folderCfg.devices = newDevices;
delete $scope.currentSharing;
if (!folderCfg.versioning) {
folderCfg.versioning = {params: {}};
}
folderCfg.versioning.type = folderCfg._guiVersioning.selector;
if ($scope.internalVersioningEnabled()) {
folderCfg.versioning.cleanupIntervalS = folderCfg._guiVersioning.cleanupIntervalS;
@@ -98,7 +98,7 @@
<option value="external" translate>External File Versioning</option>
</select>
</div>
<div class="form-group" ng-if="currentFolder._guiVersioning.selector=='trashcan' || currentFolder._guiVersioning.selectorector=='simple'" ng-class="{'has-error': folderEditor._guiVersioning.trashcanClean.$invalid && folderEditor._guiVersioning.trashcanClean.$dirty}">
<div class="form-group" ng-if="currentFolder._guiVersioning.selector=='trashcan' || currentFolder._guiVersioning.selector=='simple'" ng-class="{'has-error': folderEditor.trashcanClean.$invalid && folderEditor.trashcanClean.$dirty}">
<p translate class="help-block">Files are moved to .stversions directory when replaced or deleted by Syncthing.</p>
<label translate for="trashcanClean">Clean out after</label>
<div class="input-group">
@@ -106,30 +106,30 @@
<div class="input-group-addon" translate>days</div>
</div>
<p class="help-block">
<span translate ng-if="folderEditor._guiVersioning.trashcanClean.$valid || folderEditor._guiVersioning.trashcanClean.$pristine">The number of days to keep files in the trash can. Zero means forever.</span>
<span translate ng-if="folderEditor._guiVersioning.trashcanClean.$error.required && folderEditor._guiVersioning.trashcanClean.$dirty">The number of days must be a number and cannot be blank.</span>
<span translate ng-if="folderEditor._guiVersioning.trashcanClean.$error.min && folderEditor._guiVersioning.trashcanClean.$dirty">A negative number of days doesn't make sense.</span>
<span translate ng-if="folderEditor.trashcanClean.$valid || folderEditor.trashcanClean.$pristine">The number of days to keep files in the trash can. Zero means forever.</span>
<span translate ng-if="folderEditor.trashcanClean.$error.required && folderEditor.trashcanClean.$dirty">The number of days must be a number and cannot be blank.</span>
<span translate ng-if="folderEditor.trashcanClean.$error.min && folderEditor.trashcanClean.$dirty">A negative number of days doesn't make sense.</span>
</p>
</div>
<div class="form-group" ng-if="currentFolder._guiVersioning.selector=='simple'" ng-class="{'has-error': folderEditor._guiVersioning.simpleKeep.$invalid && folderEditor._guiVersioning.simpleKeep.$dirty}">
<div class="form-group" ng-if="currentFolder._guiVersioning.selector=='simple'" ng-class="{'has-error': folderEditor.simpleKeep.$invalid && folderEditor.simpleKeep.$dirty}">
<p translate class="help-block">Files are moved to date stamped versions in a .stversions directory when replaced or deleted by Syncthing.</p>
<label translate for="simpleKeep">Keep Versions</label>
<input name="simpleKeep" id="simpleKeep" class="form-control" type="number" ng-model="currentFolder._guiVersioning.simpleKeep" required="" aria-required="true" min="1" />
<p class="help-block">
<span translate ng-if="folderEditor._guiVersioning.simpleKeep.$valid || folderEditor._guiVersioning.simpleKeep.$pristine">The number of old versions to keep, per file.</span>
<span translate ng-if="folderEditor._guiVersioning.simpleKeep.$error.required && folderEditor._guiVersioning.simpleKeep.$dirty">The number of versions must be a number and cannot be blank.</span>
<span translate ng-if="folderEditor._guiVersioning.simpleKeep.$error.min && folderEditor._guiVersioning.simpleKeep.$dirty">You must keep at least one version.</span>
<span translate ng-if="folderEditor.simpleKeep.$valid || folderEditor.simpleKeep.$pristine">The number of old versions to keep, per file.</span>
<span translate ng-if="folderEditor.simpleKeep.$error.required && folderEditor.simpleKeep.$dirty">The number of versions must be a number and cannot be blank.</span>
<span translate ng-if="folderEditor.simpleKeep.$error.min && folderEditor.simpleKeep.$dirty">You must keep at least one version.</span>
</p>
</div>
<div class="form-group" ng-if="currentFolder._guiVersioning.selector=='staggered'" ng-class="{'has-error': folderEditor._guiVersioning.staggeredMaxAge.$invalid && folderEditor._guiVersioning.staggeredMaxAge.$dirty}">
<div class="form-group" ng-if="currentFolder._guiVersioning.selector=='staggered'" ng-class="{'has-error': folderEditor.staggeredMaxAge.$invalid && folderEditor.staggeredMaxAge.$dirty}">
<p class="help-block"><span translate>Files are moved to date stamped versions in a .stversions directory when replaced or deleted by Syncthing.</span> <span translate>Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.</span></p>
<p translate class="help-block">The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.</p>
<label translate for="staggeredMaxAge">Maximum Age</label>
<input name="staggeredMaxAge" id="staggeredMaxAge" class="form-control" type="number" ng-model="currentFolder._guiVersioning.staggeredMaxAge" required="" aria-required="true" min="0" />
<p class="help-block">
<span translate ng-if="folderEditor._guiVersioning.staggeredMaxAge.$valid || folderEditor._guiVersioning.staggeredMaxAge.$pristine">The maximum time to keep a version (in days, set to 0 to keep versions forever).</span>
<span translate ng-if="folderEditor._guiVersioning.staggeredMaxAge.$error.required && folderEditor._guiVersioning.staggeredMaxAge.$dirty">The maximum age must be a number and cannot be blank.</span>
<span translate ng-if="folderEditor._guiVersioning.staggeredMaxAge.$error.min && folderEditor._guiVersioning.staggeredMaxAge.$dirty">A negative number of days doesn't make sense.</span>
<span translate ng-if="folderEditor.staggeredMaxAge.$valid || folderEditor.staggeredMaxAge.$pristine">The maximum time to keep a version (in days, set to 0 to keep versions forever).</span>
<span translate ng-if="folderEditor.staggeredMaxAge.$error.required && folderEditor.staggeredMaxAge.$dirty">The maximum age must be a number and cannot be blank.</span>
<span translate ng-if="folderEditor.staggeredMaxAge.$error.min && folderEditor.staggeredMaxAge.$dirty">A negative number of days doesn't make sense.</span>
</p>
</div>
<div class="form-group" ng-if="internalVersioningEnabled()">
@@ -146,16 +146,16 @@
<span translate ng-if="folderEditor.externalCommand.$error.required && folderEditor.externalCommand.$dirty">The path cannot be blank.</span>
</p>
</div>
<div class="form-group" ng-if="internalVersioningEnabled()" ng-class="{'has-error': folderEditor._guiVersioning.cleanupIntervalS.$invalid && folderEditor._guiVersioning.cleanupIntervalS.$dirty}">
<label translate for="versioningCleanupIntervalS">Cleanup Interval</label>
<div class="form-group" ng-if="internalVersioningEnabled()" ng-class="{'has-error': folderEditor.cleanupIntervalS.$invalid && folderEditor.cleanupIntervalS.$dirty}">
<label translate for="cleanupIntervalS">Cleanup Interval</label>
<div class="input-group">
<input name="versioningCleanupIntervalS" id="versioningCleanupIntervalS" class="form-control text-right" type="number" ng-model="currentFolder._guiVersioning.cleanupIntervalS" required="" min="0" max="31536000" aria-required="true" />
<input name="cleanupIntervalS" id="cleanupIntervalS" class="form-control text-right" type="number" ng-model="currentFolder._guiVersioning.cleanupIntervalS" required="" min="0" max="31536000" aria-required="true" />
<div class="input-group-addon" translate>seconds</div>
</div>
<p class="help-block">
<span translate ng-if="folderEditor._guiVersioning.cleanupIntervalS.$valid || folderEditor._guiVersioning.cleanupIntervalS.$pristine"class="help-block">The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.</span>
<span translate ng-if="folderEditor._guiVersioning.cleanupIntervalS.$error.required && folderEditor._guiVersioning.cleanupIntervalS.$dirty">The cleanup interval cannot be blank.</span>
<span translate ng-if="folderEditor._guiVersioning.cleanupIntervalS.$error.min && folderEditor._guiVersioning.cleanupIntervalS.$dirty">The interval must be a positive number of seconds.</span>
<span translate ng-if="folderEditor.cleanupIntervalS.$valid || folderEditor.cleanupIntervalS.$pristine" class="help-block">The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.</span>
<span translate ng-if="folderEditor.cleanupIntervalS.$error.required && folderEditor.cleanupIntervalS.$dirty">The cleanup interval cannot be blank.</span>
<span translate ng-if="folderEditor.cleanupIntervalS.$error.min && folderEditor.cleanupIntervalS.$dirty">The interval must be a positive number of seconds.</span>
</p>
</div>
</div>
@@ -2093,6 +2093,9 @@ angular.module('syncthing.core')
folderCfg.devices = newDevices;
delete $scope.currentSharing;
if (!folderCfg.versioning) {
folderCfg.versioning = {params: {}};
}
folderCfg.versioning.type = folderCfg._guiVersioning.selector;
if ($scope.internalVersioningEnabled()) {
folderCfg.versioning.cleanupIntervalS = folderCfg._guiVersioning.cleanupIntervalS;
@@ -86,7 +86,7 @@
<option value="external" translate>External File Versioning</option>
</select>
</div>
<div class="form-group" ng-if="currentFolder._guiVersioning.selector=='trashcan' || currentFolder._guiVersioning.selectorector=='simple'" ng-class="{'has-error': folderEditor._guiVersioning.trashcanClean.$invalid && folderEditor._guiVersioning.trashcanClean.$dirty}">
<div class="form-group" ng-if="currentFolder._guiVersioning.selector=='trashcan' || currentFolder._guiVersioning.selector=='simple'" ng-class="{'has-error': folderEditor.trashcanClean.$invalid && folderEditor.trashcanClean.$dirty}">
<p translate class="help-block">Files are moved to .stversions directory when replaced or deleted by Syncthing.</p>
<label translate for="trashcanClean">Clean out after</label>
<div class="input-group">
@@ -94,30 +94,30 @@
<div class="input-group-addon" translate>days</div>
</div>
<p class="help-block">
<span translate ng-if="folderEditor._guiVersioning.trashcanClean.$valid || folderEditor._guiVersioning.trashcanClean.$pristine">The number of days to keep files in the trash can. Zero means forever.</span>
<span translate ng-if="folderEditor._guiVersioning.trashcanClean.$error.required && folderEditor._guiVersioning.trashcanClean.$dirty">The number of days must be a number and cannot be blank.</span>
<span translate ng-if="folderEditor._guiVersioning.trashcanClean.$error.min && folderEditor._guiVersioning.trashcanClean.$dirty">A negative number of days doesn't make sense.</span>
<span translate ng-if="folderEditor.trashcanClean.$valid || folderEditor.trashcanClean.$pristine">The number of days to keep files in the trash can. Zero means forever.</span>
<span translate ng-if="folderEditor.trashcanClean.$error.required && folderEditor.trashcanClean.$dirty">The number of days must be a number and cannot be blank.</span>
<span translate ng-if="folderEditor.trashcanClean.$error.min && folderEditor.trashcanClean.$dirty">A negative number of days doesn't make sense.</span>
</p>
</div>
<div class="form-group" ng-if="currentFolder._guiVersioning.selector=='simple'" ng-class="{'has-error': folderEditor._guiVersioning.simpleKeep.$invalid && folderEditor._guiVersioning.simpleKeep.$dirty}">
<div class="form-group" ng-if="currentFolder._guiVersioning.selector=='simple'" ng-class="{'has-error': folderEditor.simpleKeep.$invalid && folderEditor.simpleKeep.$dirty}">
<p translate class="help-block">Files are moved to date stamped versions in a .stversions directory when replaced or deleted by Syncthing.</p>
<label translate for="simpleKeep">Keep Versions</label>
<input name="simpleKeep" id="simpleKeep" class="form-control" type="number" ng-model="currentFolder._guiVersioning.simpleKeep" required="" aria-required="true" min="1" />
<p class="help-block">
<span translate ng-if="folderEditor._guiVersioning.simpleKeep.$valid || folderEditor._guiVersioning.simpleKeep.$pristine">The number of old versions to keep, per file.</span>
<span translate ng-if="folderEditor._guiVersioning.simpleKeep.$error.required && folderEditor._guiVersioning.simpleKeep.$dirty">The number of versions must be a number and cannot be blank.</span>
<span translate ng-if="folderEditor._guiVersioning.simpleKeep.$error.min && folderEditor._guiVersioning.simpleKeep.$dirty">You must keep at least one version.</span>
<span translate ng-if="folderEditor.simpleKeep.$valid || folderEditor.simpleKeep.$pristine">The number of old versions to keep, per file.</span>
<span translate ng-if="folderEditor.simpleKeep.$error.required && folderEditor.simpleKeep.$dirty">The number of versions must be a number and cannot be blank.</span>
<span translate ng-if="folderEditor.simpleKeep.$error.min && folderEditor.simpleKeep.$dirty">You must keep at least one version.</span>
</p>
</div>
<div class="form-group" ng-if="currentFolder._guiVersioning.selector=='staggered'" ng-class="{'has-error': folderEditor._guiVersioning.staggeredMaxAge.$invalid && folderEditor._guiVersioning.staggeredMaxAge.$dirty}">
<div class="form-group" ng-if="currentFolder._guiVersioning.selector=='staggered'" ng-class="{'has-error': folderEditor.staggeredMaxAge.$invalid && folderEditor.staggeredMaxAge.$dirty}">
<p class="help-block"><span translate>Files are moved to date stamped versions in a .stversions directory when replaced or deleted by Syncthing.</span> <span translate>Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.</span></p>
<p translate class="help-block">The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.</p>
<label translate for="staggeredMaxAge">Maximum Age</label>
<input name="staggeredMaxAge" id="staggeredMaxAge" class="form-control" type="number" ng-model="currentFolder._guiVersioning.staggeredMaxAge" required="" aria-required="true" min="0" />
<p class="help-block">
<span translate ng-if="folderEditor._guiVersioning.staggeredMaxAge.$valid || folderEditor._guiVersioning.staggeredMaxAge.$pristine">The maximum time to keep a version (in days, set to 0 to keep versions forever).</span>
<span translate ng-if="folderEditor._guiVersioning.staggeredMaxAge.$error.required && folderEditor._guiVersioning.staggeredMaxAge.$dirty">The maximum age must be a number and cannot be blank.</span>
<span translate ng-if="folderEditor._guiVersioning.staggeredMaxAge.$error.min && folderEditor._guiVersioning.staggeredMaxAge.$dirty">A negative number of days doesn't make sense.</span>
<span translate ng-if="folderEditor.staggeredMaxAge.$valid || folderEditor.staggeredMaxAge.$pristine">The maximum time to keep a version (in days, set to 0 to keep versions forever).</span>
<span translate ng-if="folderEditor.staggeredMaxAge.$error.required && folderEditor.staggeredMaxAge.$dirty">The maximum age must be a number and cannot be blank.</span>
<span translate ng-if="folderEditor.staggeredMaxAge.$error.min && folderEditor.staggeredMaxAge.$dirty">A negative number of days doesn't make sense.</span>
</p>
</div>
<div class="form-group" ng-if="internalVersioningEnabled()">
@@ -134,16 +134,16 @@
<span translate ng-if="folderEditor.externalCommand.$error.required && folderEditor.externalCommand.$dirty">The path cannot be blank.</span>
</p>
</div>
<div class="form-group" ng-if="internalVersioningEnabled()" ng-class="{'has-error': folderEditor._guiVersioning.cleanupIntervalS.$invalid && folderEditor._guiVersioning.cleanupIntervalS.$dirty}">
<label translate for="versioningCleanupIntervalS">Cleanup Interval</label>
<div class="form-group" ng-if="internalVersioningEnabled()" ng-class="{'has-error': folderEditor.cleanupIntervalS.$invalid && folderEditor.cleanupIntervalS.$dirty}">
<label translate for="cleanupIntervalS">Cleanup Interval</label>
<div class="input-group">
<input name="versioningCleanupIntervalS" id="versioningCleanupIntervalS" class="form-control text-right" type="number" ng-model="currentFolder._guiVersioning.cleanupIntervalS" required="" min="0" max="31536000" aria-required="true" />
<input name="cleanupIntervalS" id="cleanupIntervalS" class="form-control text-right" type="number" ng-model="currentFolder._guiVersioning.cleanupIntervalS" required="" min="0" max="31536000" aria-required="true" />
<div class="input-group-addon" translate>seconds</div>
</div>
<p class="help-block">
<span translate ng-if="folderEditor._guiVersioning.cleanupIntervalS.$valid || folderEditor._guiVersioning.cleanupIntervalS.$pristine"class="help-block">The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.</span>
<span translate ng-if="folderEditor._guiVersioning.cleanupIntervalS.$error.required && folderEditor._guiVersioning.cleanupIntervalS.$dirty">The cleanup interval cannot be blank.</span>
<span translate ng-if="folderEditor._guiVersioning.cleanupIntervalS.$error.min && folderEditor._guiVersioning.cleanupIntervalS.$dirty">The interval must be a positive number of seconds.</span>
<span translate ng-if="folderEditor.cleanupIntervalS.$valid || folderEditor.cleanupIntervalS.$pristine"class="help-block">The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.</span>
<span translate ng-if="folderEditor.cleanupIntervalS.$error.required && folderEditor.cleanupIntervalS.$dirty">The cleanup interval cannot be blank.</span>
<span translate ng-if="folderEditor.cleanupIntervalS.$error.min && folderEditor.cleanupIntervalS.$dirty">The interval must be a positive number of seconds.</span>
</p>
</div>
</div>
@@ -205,7 +205,7 @@
<div class="col-md-6 form-group">
<label translate>Folder Type</label>
&nbsp;<a href="https://docs.syncthing.net/users/foldertypes.html" target="_blank"><span class="fas fa-question-circle"></span>&nbsp;<span translate>Help</span></a>
<select class="form-control" ng-change="setDefaultsForFolderType()" ng-model="currentFolder.type" ng-disabled="editingExisting && currentFolder.type === 'receiveencrypted'">
<select class="form-control" ng-change="setDefaultsForFolderType()" ng-model="currentFolder.type" ng-disabled="editingExisting && currentFolder.type == 'receiveencrypted'">
<option value="sendreceive" translate>Send &amp; Receive</option>
<option value="sendonly" translate>Send Only</option>
<option value="receiveonly" translate>Receive Only</option>
@@ -214,7 +214,8 @@
<p ng-if="currentFolder.type == 'sendonly'" translate class="help-block">Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.</p>
<p ng-if="currentFolder.type == 'receiveonly'" translate class="help-block">Files are synchronized from the cluster, but any changes made locally will not be sent to other devices.</p>
<p ng-if="currentFolder.type == 'receiveencrypted'" translate class="help-block" translate-value-receive-encrypted="{{'Receive Encrypted' | translate}}">Stores and syncs only encrypted data. Folders on all connected devices need to be set up with the same password or be of type "{%receiveEncrypted%}" too.</p>
<p ng-if="editingExisting" translate class="help-block" translate-value-receive-encrypted="{{'Receive Encrypted' | translate}}">Folder type "{%receiveEncrypted%}" cannot be changed after adding the folder. You need to remove the folder, delete or decrypt the data on disk, and add the folder again.</p>
<p ng-if="editingExisting && currentFolder.type == 'receiveencrypted'" translate class="help-block" translate-value-receive-encrypted="{{'Receive Encrypted' | translate}}">Folder type "{%receiveEncrypted%}" cannot be changed after adding the folder. You need to remove the folder, delete or decrypt the data on disk, and add the folder again.</p>
<p ng-if="editingExisting && currentFolder.type != 'receiveencrypted'" translate class="help-block" translate-value-receive-encrypted="{{'Receive Encrypted' | translate}}">Folder type "{%receiveEncrypted%}" can only be set when adding a new folder.</p>
</div>
<div class="col-md-6 form-group">
<label translate>File Pull Order</label>
+41 -5
View File
@@ -23,18 +23,21 @@ import (
"os"
"path/filepath"
"reflect"
"regexp"
"runtime"
"runtime/pprof"
"sort"
"strconv"
"strings"
"time"
"unicode"
"github.com/julienschmidt/httprouter"
metrics "github.com/rcrowley/go-metrics"
"github.com/thejerf/suture/v4"
"github.com/vitrun/qart/qr"
"golang.org/x/text/runes"
"golang.org/x/text/transform"
"golang.org/x/text/unicode/norm"
"github.com/syncthing/syncthing/lib/build"
"github.com/syncthing/syncthing/lib/config"
@@ -56,9 +59,6 @@ import (
"github.com/syncthing/syncthing/lib/ur"
)
// matches a bcrypt hash and not too much else
var bcryptExpr = regexp.MustCompile(`^\$2[aby]\$\d+\$.{50,}`)
const (
DefaultEventMask = events.AllEvents &^ events.LocalChangeDetected &^ events.RemoteChangeDetected
DiskEventMask = events.LocalChangeDetected | events.RemoteChangeDetected
@@ -153,6 +153,10 @@ func (s *service) getListener(guiCfg config.GUIConfiguration) (net.Listener, err
if err != nil {
name = s.tlsDefaultCommonName
}
name, err = sanitizedHostname(name)
if err != nil {
name = s.tlsDefaultCommonName
}
cert, err = tlsutil.NewCertificate(httpsCertFile, httpsKeyFile, name, httpsCertLifetimeDays)
}
@@ -298,7 +302,7 @@ func (s *service) Serve(ctx context.Context) error {
configBuilder.registerConfig("/rest/config")
configBuilder.registerConfigInsync("/rest/config/insync") // deprecated
configBuilder.registerConfigInsync("/rest/config/restart-required")
configBuilder.registerConfigRequiresRestart("/rest/config/restart-required")
configBuilder.registerFolders("/rest/config/folders")
configBuilder.registerDevices("/rest/config/devices")
configBuilder.registerFolder("/rest/config/folders/:id")
@@ -1895,6 +1899,38 @@ func errorString(err error) *string {
return nil
}
// sanitizedHostname returns the given name in a suitable form for use as
// the common name in a certificate, or an error.
func sanitizedHostname(name string) (string, error) {
// Remove diacritics and non-alphanumerics. This works by first
// transforming into normalization form D (things with diacriticals are
// split into the base character and the mark) and then removing
// undesired characters.
t := transform.Chain(
// Split runes with diacritics into base character and mark.
norm.NFD,
// Leave only [A-Za-z0-9-.].
runes.Remove(runes.Predicate(func(r rune) bool {
return r > unicode.MaxASCII ||
!unicode.IsLetter(r) && !unicode.IsNumber(r) &&
r != '.' && r != '-'
})))
name, _, err := transform.String(t, name)
if err != nil {
return "", err
}
// Name should not start or end with a dash or dot.
name = strings.Trim(name, "-.")
// Name should not be empty.
if name == "" {
return "", errors.New("no suitable name")
}
return strings.ToLower(name), nil
}
func isFolderNotFound(err error) bool {
for _, target := range []error{
model.ErrFolderMissing,
+24 -5
View File
@@ -1262,11 +1262,9 @@ func TestConfigChanges(t *testing.T) {
defer os.Remove(tmpFile.Name())
w := config.Wrap(tmpFile.Name(), cfg, protocol.LocalDeviceID, events.NoopLogger)
tmpFile.Close()
if cfgService, ok := w.(suture.Service); ok {
cfgCtx, cfgCancel := context.WithCancel(context.Background())
go cfgService.Serve(cfgCtx)
defer cfgCancel()
}
cfgCtx, cfgCancel := context.WithCancel(context.Background())
go w.Serve(cfgCtx)
defer cfgCancel()
baseURL, cancel, err := startHTTP(w)
if err != nil {
t.Fatal("Unexpected error from getting base URL:", err)
@@ -1370,6 +1368,27 @@ func TestConfigChanges(t *testing.T) {
}
}
func TestSanitizedHostname(t *testing.T) {
cases := []struct {
in, out string
}{
{"foo.BAR-baz", "foo.bar-baz"},
{"~.~-Min 1:a Räksmörgås-dator 😀😎 ~.~-", "min1araksmorgas-dator"},
{"Vicenç-PC", "vicenc-pc"},
{"~.~-~.~-", ""},
{"", ""},
}
for _, tc := range cases {
res, err := sanitizedHostname(tc.in)
if tc.out == "" && err == nil {
t.Errorf("%q should cause error", tc.in)
} else if res != tc.out {
t.Errorf("%q => %q, expected %q", tc.in, res, tc.out)
}
}
}
func equalStrings(a, b []string) bool {
if len(a) != len(b) {
return false
+32
View File
@@ -0,0 +1,32 @@
// Copyright (C) 2021 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/.
//+build noassets
package auto
import (
"bytes"
"compress/gzip"
"github.com/syncthing/syncthing/lib/assets"
)
func Assets() map[string]assets.Asset {
// Return a minimal index.html and nothing else, to allow the trivial
// test to pass.
buf := new(bytes.Buffer)
gw := gzip.NewWriter(buf)
_, _ = gw.Write([]byte("<html></html>"))
_ = gw.Flush()
return map[string]assets.Asset{
"default/index.html": {
Gzipped: true,
Content: buf.String(),
},
}
}
+10
View File
@@ -51,6 +51,12 @@ func (c *configMuxBuilder) registerConfigInsync(path string) {
})
}
func (c *configMuxBuilder) registerConfigRequiresRestart(path string) {
c.HandlerFunc(http.MethodGet, path, func(w http.ResponseWriter, _ *http.Request) {
sendJSON(w, map[string]bool{"requiresRestart": c.cfg.RequiresRestart()})
})
}
func (c *configMuxBuilder) registerFolders(path string) {
c.HandlerFunc(http.MethodGet, path, func(w http.ResponseWriter, _ *http.Request) {
sendJSON(w, c.cfg.FolderList())
@@ -181,6 +187,10 @@ func (c *configMuxBuilder) registerDevice(path string) {
c.Handle(http.MethodDelete, path, func(w http.ResponseWriter, _ *http.Request, p httprouter.Params) {
id, err := protocol.DeviceIDFromString(p.ByName("id"))
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
waiter, err := c.cfg.RemoveDevice(id)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
+1 -8
View File
@@ -23,7 +23,6 @@ import (
"testing"
"github.com/d4l3k/messagediff"
"github.com/thejerf/suture/v4"
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/fs"
@@ -1301,16 +1300,10 @@ func startWrapper(wrapper Wrapper) *testWrapper {
Wrapper: wrapper,
done: make(chan struct{}),
}
s, ok := wrapper.(suture.Service)
if !ok {
tw.cancel = func() {}
close(tw.done)
return tw
}
ctx, cancel := context.WithCancel(context.Background())
tw.cancel = cancel
go func() {
s.Serve(ctx)
wrapper.Serve(ctx)
close(tw.done)
}()
return tw
+2 -2
View File
@@ -47,11 +47,11 @@ func (f FolderConfiguration) Filesystem() fs.Filesystem {
// cfg.Folders["default"].Filesystem() should be valid.
var opts []fs.Option
if f.FilesystemType == fs.FilesystemTypeBasic && f.JunctionsAsDirs {
opts = append(opts, fs.WithJunctionsAsDirs())
opts = append(opts, new(fs.OptionJunctionsAsDirs))
}
filesystem := fs.NewFilesystem(f.FilesystemType, f.Path, opts...)
if !f.CaseSensitiveFS {
filesystem = fs.NewCaseFilesystem(filesystem, opts...)
filesystem = fs.NewCaseFilesystem(filesystem)
}
return filesystem
}
+75
View File
@@ -2,6 +2,7 @@
package mocks
import (
"context"
"sync"
"github.com/syncthing/syncthing/lib/config"
@@ -258,6 +259,17 @@ type Wrapper struct {
saveReturnsOnCall map[int]struct {
result1 error
}
ServeStub func(context.Context) error
serveMutex sync.RWMutex
serveArgsForCall []struct {
arg1 context.Context
}
serveReturns struct {
result1 error
}
serveReturnsOnCall map[int]struct {
result1 error
}
SubscribeStub func(config.Committer) config.Configuration
subscribeMutex sync.RWMutex
subscribeArgsForCall []struct {
@@ -1577,6 +1589,67 @@ func (fake *Wrapper) SaveReturnsOnCall(i int, result1 error) {
}{result1}
}
func (fake *Wrapper) Serve(arg1 context.Context) error {
fake.serveMutex.Lock()
ret, specificReturn := fake.serveReturnsOnCall[len(fake.serveArgsForCall)]
fake.serveArgsForCall = append(fake.serveArgsForCall, struct {
arg1 context.Context
}{arg1})
stub := fake.ServeStub
fakeReturns := fake.serveReturns
fake.recordInvocation("Serve", []interface{}{arg1})
fake.serveMutex.Unlock()
if stub != nil {
return stub(arg1)
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *Wrapper) ServeCallCount() int {
fake.serveMutex.RLock()
defer fake.serveMutex.RUnlock()
return len(fake.serveArgsForCall)
}
func (fake *Wrapper) ServeCalls(stub func(context.Context) error) {
fake.serveMutex.Lock()
defer fake.serveMutex.Unlock()
fake.ServeStub = stub
}
func (fake *Wrapper) ServeArgsForCall(i int) context.Context {
fake.serveMutex.RLock()
defer fake.serveMutex.RUnlock()
argsForCall := fake.serveArgsForCall[i]
return argsForCall.arg1
}
func (fake *Wrapper) ServeReturns(result1 error) {
fake.serveMutex.Lock()
defer fake.serveMutex.Unlock()
fake.ServeStub = nil
fake.serveReturns = struct {
result1 error
}{result1}
}
func (fake *Wrapper) ServeReturnsOnCall(i int, result1 error) {
fake.serveMutex.Lock()
defer fake.serveMutex.Unlock()
fake.ServeStub = nil
if fake.serveReturnsOnCall == nil {
fake.serveReturnsOnCall = make(map[int]struct {
result1 error
})
}
fake.serveReturnsOnCall[i] = struct {
result1 error
}{result1}
}
func (fake *Wrapper) Subscribe(arg1 config.Committer) config.Configuration {
fake.subscribeMutex.Lock()
ret, specificReturn := fake.subscribeReturnsOnCall[len(fake.subscribeArgsForCall)]
@@ -1719,6 +1792,8 @@ func (fake *Wrapper) Invocations() map[string][][]interface{} {
defer fake.requiresRestartMutex.RUnlock()
fake.saveMutex.RLock()
defer fake.saveMutex.RUnlock()
fake.serveMutex.RLock()
defer fake.serveMutex.RUnlock()
fake.subscribeMutex.RLock()
defer fake.subscribeMutex.RUnlock()
fake.unsubscribeMutex.RLock()
+3
View File
@@ -20,6 +20,7 @@ import (
"github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/sync"
"github.com/thejerf/suture/v4"
)
const (
@@ -111,6 +112,8 @@ type Wrapper interface {
Subscribe(c Committer) Configuration
Unsubscribe(c Committer)
suture.Service
}
type wrapper struct {
+2 -7
View File
@@ -17,7 +17,6 @@ import (
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/thejerf/suture/v4"
"golang.org/x/time/rate"
)
@@ -45,12 +44,8 @@ func initConfig() (config.Wrapper, context.CancelFunc) {
dev3Conf = newDeviceConfiguration(wrapper, device3, "device3")
dev4Conf = newDeviceConfiguration(wrapper, device4, "device4")
var cancel context.CancelFunc = func() {}
if wrapperService, ok := wrapper.(suture.Service); ok {
var ctx context.Context
ctx, cancel = context.WithCancel(context.Background())
go wrapperService.Serve(ctx)
}
ctx, cancel := context.WithCancel(context.Background())
go wrapper.Serve(ctx)
dev2Conf.MaxRecvKbps = rand.Int() % 100000
dev2Conf.MaxSendKbps = rand.Int() % 100000
+1 -4
View File
@@ -90,10 +90,7 @@ func (d *quicDialer) Dial(ctx context.Context, _ protocol.DeviceID, uri *url.URL
return newInternalConn(&quicTlsConn{session, stream, createdConn}, connTypeQUICClient, quicPriority), nil
}
type quicDialerFactory struct {
cfg config.Wrapper
tlsCfg *tls.Config
}
type quicDialerFactory struct{}
func (quicDialerFactory) New(opts config.OptionsConfiguration, tlsCfg *tls.Config) genericDialer {
return &quicDialer{commonDialer{
+2 -2
View File
@@ -79,7 +79,7 @@ func (t *quicListener) OnExternalAddressChanged(address *stun.Host, via string)
}
func (t *quicListener) serve(ctx context.Context) error {
network := strings.Replace(t.uri.Scheme, "quic", "udp", -1)
network := strings.ReplaceAll(t.uri.Scheme, "quic", "udp")
packetConn, err := net.ListenPacket(network, t.uri.Host)
if err != nil {
@@ -174,7 +174,7 @@ func (t *quicListener) WANAddresses() []*url.URL {
func (t *quicListener) LANAddresses() []*url.URL {
addrs := []*url.URL{t.uri}
network := strings.Replace(t.uri.Scheme, "quic", "udp", -1)
network := strings.ReplaceAll(t.uri.Scheme, "quic", "udp")
addrs = append(addrs, getURLsForAllAdaptersIfUnspecified(network, t.uri)...)
return addrs
}
+4 -10
View File
@@ -244,7 +244,7 @@ func (s *service) handle(ctx context.Context) error {
// though, especially in the presence of NAT hairpinning, multiple
// clients between the same NAT gateway, and global discovery.
if remoteID == s.myID {
l.Infof("Connected to myself (%s) at %s - should not happen", remoteID, c)
l.Debugf("Connected to myself (%s) at %s", remoteID, c)
c.Close()
continue
}
@@ -335,13 +335,7 @@ func (s *service) handle(ctx context.Context) error {
isLAN := s.isLAN(c.RemoteAddr())
rd, wr := s.limiter.getLimiters(remoteID, c, isLAN)
var protoConn protocol.Connection
passwords := s.cfg.FolderPasswords(remoteID)
if len(passwords) > 0 {
protoConn = protocol.NewEncryptedConnection(passwords, remoteID, rd, wr, c, s.model, c, deviceCfg.Compression)
} else {
protoConn = protocol.NewConnection(remoteID, rd, wr, c, s.model, c, deviceCfg.Compression)
}
protoConn := protocol.NewConnection(remoteID, rd, wr, c, s.model, c, deviceCfg.Compression, s.cfg.FolderPasswords(remoteID))
l.Infof("Established secure connection to %s at %s", remoteID, c)
@@ -478,7 +472,7 @@ func (s *service) dialDevices(ctx context.Context, now time.Time, cfg config.Con
// doesn't have much effect, but it may result in getting up and running
// quicker if only a subset of configured devices are actually reachable
// (by prioritizing those that were reachable recently).
dialQueue.Sort(queue)
queue.Sort()
// Perform dials according to the queue, stopping when we've reached the
// allowed additional number of connections (if limited).
@@ -1023,7 +1017,7 @@ func (s *service) validateIdentity(c internalConn, expectedID protocol.DeviceID)
// though, especially in the presence of NAT hairpinning, multiple
// clients between the same NAT gateway, and global discovery.
if remoteID == s.myID {
l.Infof("Connected to myself (%s) at %s - should not happen", remoteID, c)
l.Debugf("Connected to myself (%s) at %s", remoteID, c)
c.Close()
return errors.New("connected to self")
}
+1 -1
View File
@@ -89,7 +89,7 @@ func newInternalConn(tc tlsConn, connType connType, priority int) internalConn {
tlsConn: tc,
connType: connType,
priority: priority,
establishedAt: time.Now(),
establishedAt: time.Now().Truncate(time.Second),
}
}
+1 -1
View File
@@ -15,7 +15,7 @@ import (
"github.com/syncthing/syncthing/lib/protocol"
)
var files, filesUpdated, oneFile, firstHalf, secondHalf, changed100, unchanged100 []protocol.FileInfo
var files, oneFile, firstHalf, secondHalf, changed100, unchanged100 []protocol.FileInfo
func lazyInitBenchFiles() {
if files != nil {
+3
View File
@@ -262,6 +262,9 @@ func TestUpdate0to3(t *testing.T) {
t.Fatal(err)
}
key, err = trans.keyer.GenerateDeviceFileKey(key, folder, vl.Versions[0].Device, name)
if err != nil {
t.Fatal(err)
}
fi, ok, err := trans.getFileTrunc(key, false)
if err != nil {
t.Fatal(err)
+7
View File
@@ -846,6 +846,10 @@ func (db *Lowlevel) getMetaAndCheck(folder string) (*metadataTracker, error) {
db.gcMut.RLock()
defer db.gcMut.RUnlock()
return db.getMetaAndCheckGCLocked(folder)
}
func (db *Lowlevel) getMetaAndCheckGCLocked(folder string) (*metadataTracker, error) {
fixed, err := db.checkLocalNeed([]byte(folder))
if err != nil {
return nil, fmt.Errorf("checking local need: %w", err)
@@ -928,6 +932,9 @@ func (db *Lowlevel) recalcMeta(folderStr string) (*metadataTracker, error) {
meta.addFile(protocol.GlobalDeviceID, f)
return true
})
if err != nil {
return nil, err
}
meta.emptyNeeded(protocol.LocalDeviceID)
err = t.withNeed(folder, protocol.LocalDeviceID[:], true, func(f protocol.FileIntf) bool {
+2 -3
View File
@@ -148,10 +148,9 @@ func (m *metadataTracker) countsPtr(dev protocol.DeviceID, flag uint32) *Counts
// the metadatatracker, even if there's no change to the need
// bucket itself.
nkey := metaKey{dev, needFlag}
nidx, ok := m.indexes[nkey]
if !ok {
if _, ok := m.indexes[nkey]; !ok {
// Initially a new device needs everything, except deletes
nidx = len(m.counts.Counts)
nidx := len(m.counts.Counts)
m.counts.Counts = append(m.counts.Counts, m.allNeededCounts(dev))
m.indexes[nkey] = nidx
}
+3 -3
View File
@@ -15,7 +15,7 @@ import (
func (db *Lowlevel) AddOrUpdatePendingDevice(device protocol.DeviceID, name, address string) error {
key := db.keyer.GeneratePendingDeviceKey(nil, device[:])
od := ObservedDevice{
Time: time.Now().Round(time.Second),
Time: time.Now().Truncate(time.Second),
Name: name,
Address: address,
}
@@ -72,7 +72,7 @@ func (db *Lowlevel) AddOrUpdatePendingFolder(id, label string, device protocol.D
return err
}
of := ObservedFolder{
Time: time.Now().Round(time.Second),
Time: time.Now().Truncate(time.Second),
Label: label,
ReceiveEncrypted: receiveEncrypted,
}
@@ -124,7 +124,7 @@ func (db *Lowlevel) RemovePendingFoldersBeforeTime(device protocol.DeviceID, old
return nil, err
}
defer iter.Release()
oldest = oldest.Round(time.Second)
oldest = oldest.Truncate(time.Second)
var res []string
for iter.Next() {
var of ObservedFolder
+15 -4
View File
@@ -8,7 +8,6 @@ package db
import (
"bytes"
"errors"
"fmt"
"sort"
"strings"
@@ -21,7 +20,7 @@ import (
// do not put restrictions on downgrades (e.g. for repairs after a bugfix).
const (
dbVersion = 14
dbMigrationVersion = 15
dbMigrationVersion = 16
dbMinSyncthingVersion = "v1.9.0"
)
@@ -32,8 +31,6 @@ type migration struct {
migration func(prevSchema int) error
}
var errFolderMissing = errors.New("folder present in global list but missing in keyer index")
type databaseDowngradeError struct {
minSyncthingVersion string
}
@@ -104,6 +101,7 @@ func (db *schemaUpdater) updateSchema() error {
{13, 13, "v1.7.0", db.updateSchemaTo13},
{14, 14, "v1.9.0", db.updateSchemaTo14},
{14, 15, "v1.9.0", db.migration15},
{14, 16, "v1.9.0", db.checkRepairMigration},
}
for _, m := range migrations {
@@ -729,6 +727,9 @@ func (db *schemaUpdater) updateSchemaTo14(_ int) error {
defer t.close()
key, err = t.keyer.GenerateDeviceFileKey(key, folder, protocol.LocalDeviceID[:], nil)
if err != nil {
return err
}
it, err := t.NewPrefixIterator(key)
if err != nil {
return err
@@ -782,6 +783,16 @@ func (db *schemaUpdater) migration15(_ int) error {
return nil
}
func (db *schemaUpdater) checkRepairMigration(_ int) error {
for _, folder := range db.ListFolders() {
_, err := db.getMetaAndCheckGCLocked(folder)
if err != nil {
return err
}
}
return nil
}
func (db *schemaUpdater) rewriteGlobals(t readWriteTransaction) error {
it, err := t.NewPrefixIterator([]byte{KeyTypeGlobal})
if err != nil {
+4 -3
View File
@@ -469,9 +469,10 @@ func TestNeedWithInvalid(t *testing.T) {
}
expectedNeed := fileList{
protocol.FileInfo{Name: "b", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1001}}}, Blocks: genBlocks(2)},
protocol.FileInfo{Name: "c", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1002}}}, Blocks: genBlocks(7)},
protocol.FileInfo{Name: "d", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1003}}}, Blocks: genBlocks(7)},
remote0Have[0],
remote1Have[0],
remote0Have[2],
remote1Have[2],
}
replace(s, protocol.LocalDeviceID, localHave)
-27
View File
@@ -396,25 +396,6 @@ func (vl *VersionList) findDevice(device []byte) (bool, int, int, bool) {
return false, -1, -1, false
}
func (vl *VersionList) popVersion(version protocol.Vector) (FileVersion, bool) {
i := vl.versionIndex(version)
if i == -1 {
return FileVersion{}, false
}
fv := vl.RawVersions[i]
vl.popVersionAt(i)
return fv, true
}
func (vl *VersionList) versionIndex(version protocol.Vector) int {
for i, v := range vl.RawVersions {
if version.Equal(v.Version) {
return i
}
}
return -1
}
func (vl *VersionList) popVersionAt(i int) {
vl.RawVersions = append(vl.RawVersions[:i], vl.RawVersions[i+1:]...)
}
@@ -496,14 +477,6 @@ func popDeviceAt(devices [][]byte, i int) [][]byte {
return append(devices[:i], devices[i+1:]...)
}
func popDevice(devices [][]byte, device []byte) ([][]byte, bool) {
i := deviceIndex(devices, device)
if i == -1 {
return devices, false
}
return popDeviceAt(devices, i), true
}
func newFileVersion(device []byte, version protocol.Vector, invalid, deleted bool) FileVersion {
fv := FileVersion{
Version: version,
+2 -4
View File
@@ -762,10 +762,8 @@ func (t readWriteTransaction) updateLocalNeed(keyBuf, folder, name []byte, add b
}
func Need(global FileVersion, haveLocal bool, localVersion protocol.Vector) bool {
// We never need an invalid file or a file without a valid version (just
// another way of expressing "invalid", really, until we fix that
// part...).
if global.IsInvalid() || global.Version.IsEmpty() {
// We never need a file without a valid version.
if global.Version.IsEmpty() {
return false
}
// We don't need a deleted file if we don't have it.
+1 -2
View File
@@ -237,7 +237,6 @@ type logger struct {
events chan Event
funcs chan func(context.Context)
toUnsubscribe chan *subscription
stop chan struct{}
}
type Event struct {
@@ -317,7 +316,7 @@ loop:
func (l *logger) Log(t EventType, data interface{}) {
l.events <- Event{
Time: time.Now(),
Time: time.Now(), // intentionally high precision
Type: t,
Data: data,
// SubscriptionID and GlobalID are set in sendEvent
+18 -11
View File
@@ -26,24 +26,26 @@ var (
errNotRelative = errors.New("not a relative path")
)
func WithJunctionsAsDirs() Option {
return Option{
apply: func(fs Filesystem) {
if basic, ok := fs.(*BasicFilesystem); !ok {
l.Warnln("WithJunctionsAsDirs must only be used with FilesystemTypeBasic")
} else {
basic.junctionsAsDirs = true
}
},
id: "junctionsAsDirs",
type OptionJunctionsAsDirs struct{}
func (o *OptionJunctionsAsDirs) apply(fs Filesystem) {
if basic, ok := fs.(*BasicFilesystem); !ok {
l.Warnln("WithJunctionsAsDirs must only be used with FilesystemTypeBasic")
} else {
basic.junctionsAsDirs = true
}
}
func (o *OptionJunctionsAsDirs) String() string {
return "junctionsAsDirs"
}
// The BasicFilesystem implements all aspects by delegating to package os.
// All paths are relative to the root and cannot (should not) escape the root directory.
type BasicFilesystem struct {
root string
junctionsAsDirs bool
options []Option
}
func newBasicFilesystem(root string, opts ...Option) *BasicFilesystem {
@@ -82,7 +84,8 @@ func newBasicFilesystem(root string, opts ...Option) *BasicFilesystem {
}
fs := &BasicFilesystem{
root: root,
root: root,
options: opts,
}
for _, opt := range opts {
opt.apply(fs)
@@ -311,6 +314,10 @@ func (f *BasicFilesystem) URI() string {
return strings.TrimPrefix(f.root, `\\?\`)
}
func (f *BasicFilesystem) Options() []Option {
return f.options
}
func (f *BasicFilesystem) SameFile(fi1, fi2 FileInfo) bool {
// Like os.SameFile, we always return false unless fi1 and fi2 were created
// by this package's Stat/Lstat method.
+15 -10
View File
@@ -55,22 +55,22 @@ type caseFilesystemRegistry struct {
startCleaner sync.Once
}
func newFSKey(fs Filesystem, opts ...Option) fskey {
func newFSKey(fs Filesystem) fskey {
k := fskey{
fstype: fs.Type(),
uri: fs.URI(),
}
if len(opts) > 0 {
k.opts = opts[0].id
if opts := fs.Options(); len(opts) > 0 {
k.opts = opts[0].String()
for _, o := range opts[1:] {
k.opts += "&" + o.id
k.opts += "&" + o.String()
}
}
return k
}
func (r *caseFilesystemRegistry) get(fs Filesystem, opts ...Option) Filesystem {
k := newFSKey(fs, opts...)
func (r *caseFilesystemRegistry) get(fs Filesystem) Filesystem {
k := newFSKey(fs)
// Use double locking when getting a caseFs. In the common case it will
// already exist and we take the read lock fast path. If it doesn't, we
@@ -136,10 +136,8 @@ type caseFilesystem struct {
// from the real path. It is safe to use with any filesystem, i.e. also a
// case-sensitive one. However it will add some overhead and thus shouldn't be
// used if the filesystem is known to already behave case-sensitively.
func NewCaseFilesystem(fs Filesystem, opts ...Option) Filesystem {
return wrapFilesystem(fs, func(fs Filesystem) Filesystem {
return globalCaseFilesystemRegistry.get(fs, opts...)
})
func NewCaseFilesystem(fs Filesystem) Filesystem {
return wrapFilesystem(fs, globalCaseFilesystemRegistry.get)
}
func (f *caseFilesystem) Chmod(name string, mode FileMode) error {
@@ -226,6 +224,13 @@ func (f *caseFilesystem) Rename(oldpath, newpath string) error {
if err := f.checkCase(oldpath); err != nil {
return err
}
if err := f.checkCase(newpath); err != nil {
// Case-only rename is ok
e := &ErrCaseConflict{}
if !errors.As(err, &e) || e.Real != oldpath {
return err
}
}
if err := f.Filesystem.Rename(oldpath, newpath); err != nil {
return err
}
+4 -1
View File
@@ -45,7 +45,10 @@ func (fs *errorFilesystem) Roots() ([]string, error) { retur
func (fs *errorFilesystem) Usage(name string) (Usage, error) { return Usage{}, fs.err }
func (fs *errorFilesystem) Type() FilesystemType { return fs.fsType }
func (fs *errorFilesystem) URI() string { return fs.uri }
func (fs *errorFilesystem) SameFile(fi1, fi2 FileInfo) bool { return false }
func (fs *errorFilesystem) Options() []Option {
return nil
}
func (fs *errorFilesystem) SameFile(fi1, fi2 FileInfo) bool { return false }
func (fs *errorFilesystem) Watch(path string, ignore Matcher, ctx context.Context, ignorePerms bool) (<-chan Event, <-chan error, error) {
return nil, nil, fs.err
}
+4
View File
@@ -640,6 +640,10 @@ func (fs *fakefs) URI() string {
return fs.uri
}
func (fs *fakefs) Options() []Option {
return nil
}
func (fs *fakefs) SameFile(fi1, fi2 FileInfo) bool {
// BUG: real systems base file sameness on path, inodes, etc
// we try our best, but FileInfo just doesn't have enough data
+11 -5
View File
@@ -47,6 +47,7 @@ type Filesystem interface {
Usage(name string) (Usage, error)
Type() FilesystemType
URI() string
Options() []Option
SameFile(fi1, fi2 FileInfo) bool
}
@@ -178,9 +179,15 @@ var IsPermission = os.IsPermission
// IsPathSeparator is the equivalent of os.IsPathSeparator
var IsPathSeparator = os.IsPathSeparator
type Option struct {
apply func(Filesystem)
id string
// Option modifies a filesystem at creation. An option might be specific
// to a filesystem-type.
//
// String is used to detect options with the same effect, i.e. must be different
// for options with different effects. Meaning if an option has parameters, a
// representation of those must be part of the returned string.
type Option interface {
String() string
apply(Filesystem)
}
func NewFilesystem(fsType FilesystemType, uri string, opts ...Option) Filesystem {
@@ -245,8 +252,7 @@ func Canonicalize(file string) (string, error) {
file = filepath.Clean(file)
// It is not acceptable to attempt to traverse upwards.
switch file {
case "..":
if file == ".." {
return "", errNotRelative
}
if strings.HasPrefix(file, ".."+pathSep) {
+3 -4
View File
@@ -7,11 +7,12 @@
package fs
import (
"crypto/md5"
"fmt"
"path/filepath"
"runtime"
"strings"
"github.com/syncthing/syncthing/lib/sha256"
)
const (
@@ -50,9 +51,7 @@ func TempNameWithPrefix(name, prefix string) string {
tdir := filepath.Dir(name)
tbase := filepath.Base(name)
if len(tbase) > maxFilenameLength {
hash := md5.New()
hash.Write([]byte(name))
tbase = fmt.Sprintf("%x", hash.Sum(nil))
tbase = fmt.Sprintf("%x", sha256.Sum256([]byte(name)))
}
tname := fmt.Sprintf("%s%s.tmp", prefix, tbase)
return filepath.Join(tdir, tname)
+22 -7
View File
@@ -63,10 +63,20 @@ type WalkFunc func(path string, info FileInfo, err error) error
type walkFilesystem struct {
Filesystem
checkInfiniteRecursion bool
}
func NewWalkFilesystem(next Filesystem) Filesystem {
return &walkFilesystem{next}
fs := &walkFilesystem{
Filesystem: next,
}
for _, opt := range next.Options() {
if _, ok := opt.(*OptionJunctionsAsDirs); ok {
fs.checkInfiniteRecursion = true
break
}
}
return fs
}
// walk recursively descends path, calling walkFn.
@@ -89,11 +99,13 @@ func (f *walkFilesystem) walk(path string, info FileInfo, walkFn WalkFunc, ances
return nil
}
if !ancestors.Contains(info) {
ancestors.Push(info)
defer ancestors.Pop()
} else {
return walkFn(path, info, ErrInfiniteRecursion)
if f.checkInfiniteRecursion {
if !ancestors.Contains(info) {
ancestors.Push(info)
defer ancestors.Pop()
} else {
return walkFn(path, info, ErrInfiniteRecursion)
}
}
names, err := f.DirNames(path)
@@ -131,6 +143,9 @@ func (f *walkFilesystem) Walk(root string, walkFn WalkFunc) error {
if err != nil {
return walkFn(root, nil, err)
}
ancestors := &ancestorDirList{fs: f.Filesystem}
var ancestors *ancestorDirList
if f.checkInfiniteRecursion {
ancestors = &ancestorDirList{fs: f.Filesystem}
}
return f.walk(root, info, walkFn, ancestors)
}
+2 -2
View File
@@ -57,7 +57,7 @@ func testWalkTraverseDirJunct(t *testing.T, fsType FilesystemType, uri string) {
t.Skip("Directory junctions are available and tested on windows only")
}
fs := NewFilesystem(fsType, uri, WithJunctionsAsDirs())
fs := NewFilesystem(fsType, uri, new(OptionJunctionsAsDirs))
if err := fs.MkdirAll("target/foo", 0); err != nil {
t.Fatal(err)
@@ -90,7 +90,7 @@ func testWalkInfiniteRecursion(t *testing.T, fsType FilesystemType, uri string)
t.Skip("Infinite recursion detection is tested on windows only")
}
fs := NewFilesystem(fsType, uri, WithJunctionsAsDirs())
fs := NewFilesystem(fsType, uri, new(OptionJunctionsAsDirs))
if err := fs.MkdirAll("target/foo", 0); err != nil {
t.Fatal(err)
+2 -2
View File
@@ -9,7 +9,6 @@ package ignore
import (
"bufio"
"bytes"
"crypto/md5"
"errors"
"fmt"
"io"
@@ -22,6 +21,7 @@ import (
"github.com/syncthing/syncthing/lib/fs"
"github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/sha256"
"github.com/syncthing/syncthing/lib/sync"
)
@@ -373,7 +373,7 @@ func (m *Matcher) SkipIgnoredDirs() bool {
}
func hashPatterns(patterns []Pattern) string {
h := md5.New()
h := sha256.New()
for _, pat := range patterns {
h.Write([]byte(pat.String()))
h.Write([]byte("\n"))
+4 -3
View File
@@ -607,8 +607,9 @@ func TestHashOfEmpty(t *testing.T) {
firstHash := p1.Hash()
// Reloading with a non-existent file should empty the patterns and
// recalculate the hash. d41d8cd98f00b204e9800998ecf8427e is the md5 of
// nothing.
// recalculate the hash.
// e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 is
// the sah256 of nothing.
p1.Load("file/does/not/exist")
secondHash := p1.Hash()
@@ -616,7 +617,7 @@ func TestHashOfEmpty(t *testing.T) {
if firstHash == secondHash {
t.Error("hash did not change")
}
if secondHash != "d41d8cd98f00b204e9800998ecf8427e" {
if secondHash != "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" {
t.Error("second hash is not hash of empty string")
}
if len(p1.patterns) != 0 {
+2 -2
View File
@@ -115,7 +115,7 @@ func expandLocations() error {
newLocations := make(map[LocationEnum]string)
for key, dir := range locationTemplates {
for varName, value := range baseDirs {
dir = strings.Replace(dir, "${"+string(varName)+"}", value, -1)
dir = strings.ReplaceAll(dir, "${"+string(varName)+"}", value)
}
var err error
dir, err = fs.ExpandTilde(dir)
@@ -197,5 +197,5 @@ func GetTimestamped(key LocationEnum) string {
// 2006 replaced by 2015...
tpl := locations[key]
now := time.Now().Format("20060102-150405")
return strings.Replace(tpl, "${timestamp}", now, -1)
return strings.ReplaceAll(tpl, "${timestamp}", now)
}
+1 -1
View File
@@ -337,7 +337,7 @@ func (r *recorder) Clear() {
func (r *recorder) append(l LogLevel, msg string) {
line := Line{
When: time.Now(),
When: time.Now(), // intentionally high precision
Message: msg,
Level: l,
}
+2 -4
View File
@@ -7,10 +7,11 @@
package model
import (
"github.com/syncthing/syncthing/lib/protocol"
"reflect"
"sort"
"testing"
"github.com/syncthing/syncthing/lib/protocol"
)
var (
@@ -46,9 +47,6 @@ func Test_chunk(t *testing.T) {
}
func Test_inOrderBlockPullReorderer_Reorder(t *testing.T) {
type args struct {
blocks []protocol.BlockInfo
}
tests := []struct {
name string
blocks []protocol.BlockInfo
+41 -27
View File
@@ -33,7 +33,7 @@ func newFakeConnection(id protocol.DeviceID, model Model) *fakeConnection {
})
f.IDReturns(id)
f.CloseCalls(func(err error) {
model.Closed(f, err)
model.Closed(id, err)
f.ClosedReturns(true)
})
return f
@@ -62,33 +62,34 @@ func (f *fakeConnection) DownloadProgress(_ context.Context, folder string, upda
})
}
func (f *fakeConnection) addFileLocked(name string, flags uint32, ftype protocol.FileInfoType, data []byte, version protocol.Vector) {
func (f *fakeConnection) addFileLocked(name string, flags uint32, ftype protocol.FileInfoType, data []byte, version protocol.Vector, localFlags uint32) {
blockSize := protocol.BlockSize(int64(len(data)))
blocks, _ := scanner.Blocks(context.TODO(), bytes.NewReader(data), blockSize, int64(len(data)), nil, true)
if ftype == protocol.FileInfoTypeFile || ftype == protocol.FileInfoTypeDirectory {
f.files = append(f.files, protocol.FileInfo{
Name: name,
Type: ftype,
Size: int64(len(data)),
ModifiedS: time.Now().Unix(),
Permissions: flags,
Version: version,
Sequence: time.Now().UnixNano(),
RawBlockSize: blockSize,
Blocks: blocks,
})
} else {
// Symlink
f.files = append(f.files, protocol.FileInfo{
Name: name,
Type: ftype,
Version: version,
Sequence: time.Now().UnixNano(),
SymlinkTarget: string(data),
NoPermissions: true,
})
file := protocol.FileInfo{
Name: name,
Type: ftype,
Version: version,
Sequence: time.Now().UnixNano(),
LocalFlags: localFlags,
}
switch ftype {
case protocol.FileInfoTypeFile, protocol.FileInfoTypeDirectory:
file.ModifiedS = time.Now().Unix()
file.Permissions = flags
if ftype == protocol.FileInfoTypeFile {
file.Size = int64(len(data))
file.RawBlockSize = blockSize
file.Blocks = blocks
}
default: // Symlink
file.Name = name
file.Type = ftype
file.Version = version
file.SymlinkTarget = string(data)
file.NoPermissions = true
}
f.files = append(f.files, file)
if f.fileData == nil {
f.fileData = make(map[string][]byte)
@@ -96,13 +97,22 @@ func (f *fakeConnection) addFileLocked(name string, flags uint32, ftype protocol
f.fileData[name] = data
}
func (f *fakeConnection) addFileWithLocalFlags(name string, ftype protocol.FileInfoType, localFlags uint32) {
f.mut.Lock()
defer f.mut.Unlock()
var version protocol.Vector
version = version.Update(f.id.Short())
f.addFileLocked(name, 0, ftype, nil, version, localFlags)
}
func (f *fakeConnection) addFile(name string, flags uint32, ftype protocol.FileInfoType, data []byte) {
f.mut.Lock()
defer f.mut.Unlock()
var version protocol.Vector
version = version.Update(f.id.Short())
f.addFileLocked(name, flags, ftype, data, version)
f.addFileLocked(name, flags, ftype, data, version, 0)
}
func (f *fakeConnection) updateFile(name string, flags uint32, ftype protocol.FileInfoType, data []byte) {
@@ -112,7 +122,7 @@ func (f *fakeConnection) updateFile(name string, flags uint32, ftype protocol.Fi
for i, fi := range f.files {
if fi.Name == name {
f.files = append(f.files[:i], f.files[i+1:]...)
f.addFileLocked(name, flags, ftype, data, fi.Version.Update(f.id.Short()))
f.addFileLocked(name, flags, ftype, data, fi.Version.Update(f.id.Short()), 0)
return
}
}
@@ -137,7 +147,11 @@ func (f *fakeConnection) deleteFile(name string) {
}
func (f *fakeConnection) sendIndexUpdate() {
f.model.IndexUpdate(f.id, f.folder, f.files)
toSend := make([]protocol.FileInfo, len(f.files))
for i := range f.files {
toSend[i] = prepareFileInfoForIndex(f.files[i])
}
f.model.IndexUpdate(f.id, f.folder, toSend)
}
func addFakeConn(m *testModel, dev protocol.DeviceID) *fakeConnection {
+4 -2
View File
@@ -296,8 +296,10 @@ func (f *folder) getHealthErrorAndLoadIgnores() error {
if err := f.getHealthErrorWithoutIgnores(); err != nil {
return err
}
if err := f.ignores.Load(".stignore"); err != nil && !fs.IsNotExist(err) {
return errors.Wrap(err, "loading ignores")
if f.Type != config.FolderTypeReceiveEncrypted {
if err := f.ignores.Load(".stignore"); err != nil && !fs.IsNotExist(err) {
return errors.Wrap(err, "loading ignores")
}
}
return nil
}
+8 -2
View File
@@ -307,7 +307,7 @@ func (f *sendReceiveFolder) pullerIteration(scanChan chan<- string) (int, error)
f.queue.Reset()
return changed, nil
return changed, err
}
func (f *sendReceiveFolder) processNeeded(snap *db.Snapshot, dbUpdateChan chan<- dbUpdateJob, copyChan chan<- copyBlocksState, scanChan chan<- string) (int, map[string]protocol.FileInfo, []protocol.FileInfo, error) {
@@ -357,6 +357,11 @@ func (f *sendReceiveFolder) processNeeded(snap *db.Snapshot, dbUpdateChan chan<-
changed--
}
case file.IsInvalid():
// Global invalid file just exists for need accounting
l.Debugln(f, "Handling global invalid item", file)
dbUpdateChan <- dbUpdateJob{file, dbUpdateInvalidate}
case file.IsDeleted():
if file.IsDirectory() {
// Perform directory deletions at the end, as we may have
@@ -1491,11 +1496,12 @@ func (f *sendReceiveFolder) pullBlock(state pullBlockState, snap *db.Snapshot, o
var lastError error
candidates := f.model.availabilityInSnapshot(f.FolderConfiguration, snap, state.file, state.block)
loop:
for {
select {
case <-f.ctx.Done():
state.fail(errors.Wrap(f.ctx.Err(), "folder stopped"))
break
break loop
default:
}
+2 -2
View File
@@ -100,7 +100,7 @@ func (s *stateTracker) setState(newState folderState) {
}
s.current = newState
s.changed = time.Now()
s.changed = time.Now().Truncate(time.Second)
s.evLogger.Log(events.StateChanged, eventData)
}
@@ -139,7 +139,7 @@ func (s *stateTracker) setError(err error) {
}
s.err = err
s.changed = time.Now()
s.changed = time.Now().Truncate(time.Second)
s.evLogger.Log(events.StateChanged, eventData)
}
+17 -16
View File
@@ -25,7 +25,6 @@ type indexSender struct {
conn protocol.Connection
folder string
folderIsReceiveEncrypted bool
dev string
fset *db.FileSet
prevSequence int64
evLogger events.Logger
@@ -178,18 +177,7 @@ func (s *indexSender) sendIndexTo(ctx context.Context) error {
return true
}
// Mark the file as invalid if any of the local bad stuff flags are set.
f.RawInvalid = f.IsInvalid()
// If the file is marked LocalReceive (i.e., changed locally on a
// receive only folder) we do not want it to ever become the
// globally best version, invalid or not.
if f.IsReceiveOnlyChanged() {
f.Version = protocol.Vector{}
}
// never sent externally
f.LocalFlags = 0
f.VersionHash = nil
f = prepareFileInfoForIndex(f)
previousWasDelete = f.IsDeleted()
@@ -211,6 +199,21 @@ func (s *indexSender) sendIndexTo(ctx context.Context) error {
return err
}
func prepareFileInfoForIndex(f protocol.FileInfo) protocol.FileInfo {
// Mark the file as invalid if any of the local bad stuff flags are set.
f.RawInvalid = f.IsInvalid()
// If the file is marked LocalReceive (i.e., changed locally on a
// receive only folder) we do not want it to ever become the
// globally best version, invalid or not.
if f.IsReceiveOnlyChanged() {
f.Version = protocol.Vector{}
}
// never sent externally
f.LocalFlags = 0
f.VersionHash = nil
return f
}
func (s *indexSender) String() string {
return fmt.Sprintf("indexSender@%p for %s to %s at %s", s, s.folder, s.conn.ID(), s.conn)
}
@@ -312,9 +315,7 @@ func (r *indexSenderRegistry) addLocked(folder config.FolderConfiguration, fset
r.sup.RemoveAndWait(is.token, 0)
delete(r.indexSenders, folder.ID)
}
if _, ok := r.startInfos[folder.ID]; ok {
delete(r.startInfos, folder.ID)
}
delete(r.startInfos, folder.ID)
is := &indexSender{
conn: r.conn,
+6 -6
View File
@@ -43,10 +43,10 @@ type Model struct {
arg1 string
arg2 string
}
ClosedStub func(protocol.Connection, error)
ClosedStub func(protocol.DeviceID, error)
closedMutex sync.RWMutex
closedArgsForCall []struct {
arg1 protocol.Connection
arg1 protocol.DeviceID
arg2 error
}
ClusterConfigStub func(protocol.DeviceID, protocol.ClusterConfig) error
@@ -684,10 +684,10 @@ func (fake *Model) BringToFrontArgsForCall(i int) (string, string) {
return argsForCall.arg1, argsForCall.arg2
}
func (fake *Model) Closed(arg1 protocol.Connection, arg2 error) {
func (fake *Model) Closed(arg1 protocol.DeviceID, arg2 error) {
fake.closedMutex.Lock()
fake.closedArgsForCall = append(fake.closedArgsForCall, struct {
arg1 protocol.Connection
arg1 protocol.DeviceID
arg2 error
}{arg1, arg2})
stub := fake.ClosedStub
@@ -704,13 +704,13 @@ func (fake *Model) ClosedCallCount() int {
return len(fake.closedArgsForCall)
}
func (fake *Model) ClosedCalls(stub func(protocol.Connection, error)) {
func (fake *Model) ClosedCalls(stub func(protocol.DeviceID, error)) {
fake.closedMutex.Lock()
defer fake.closedMutex.Unlock()
fake.ClosedStub = stub
}
func (fake *Model) ClosedArgsForCall(i int) (protocol.Connection, error) {
func (fake *Model) ClosedArgsForCall(i int) (protocol.DeviceID, error) {
fake.closedMutex.RLock()
defer fake.closedMutex.RUnlock()
argsForCall := fake.closedArgsForCall[i]
+58 -40
View File
@@ -184,7 +184,6 @@ var (
errNetworkNotAllowed = errors.New("network not allowed")
errNoVersioner = errors.New("folder has no versioner")
// errors about why a connection is closed
errIgnoredFolderRemoved = errors.New("folder no longer ignored")
errReplacingConnection = errors.New("replacing connection")
errStopped = errors.New("Syncthing is being stopped")
errEncryptionInvConfigLocal = errors.New("can't encrypt data for a device when the folder type is receiveEncrypted")
@@ -193,10 +192,12 @@ var (
errEncryptionNotEncryptedRemote = errors.New("folder is configured to be encrypted but not announced thus")
errEncryptionNotEncryptedUntrusted = errors.New("device is untrusted, but configured to receive not encrypted data")
errEncryptionPassword = errors.New("different encryption passwords used")
errEncryptionReceivedToken = errors.New("resetting connection to send info on new encrypted folder (new cluster config)")
errEncryptionNeedToken = errors.New("require password token for receive-encrypted token")
errMissingRemoteInClusterConfig = errors.New("remote device missing in cluster config")
errMissingLocalInClusterConfig = errors.New("local device missing in cluster config")
errConnLimitReached = errors.New("connection limit reached")
// messages for failure reports
failureUnexpectedGenerateCCError = "unexpected error occurred in generateClusterConfig"
)
// NewModel creates and starts a new model. The model starts in read-only mode,
@@ -294,7 +295,7 @@ func (m *model) initFolders(cfg config.Configuration) error {
ignoredDevices := observedDeviceSet(m.cfg.IgnoredDevices())
m.cleanPending(cfg.DeviceMap(), cfg.FolderMap(), ignoredDevices, nil)
m.resendClusterConfig(clusterConfigDevices.AsSlice())
m.sendClusterConfig(clusterConfigDevices.AsSlice())
return nil
}
@@ -331,8 +332,10 @@ func (m *model) StartDeadlockDetector(timeout time.Duration) {
// Need to hold lock on m.fmut when calling this.
func (m *model) addAndStartFolderLocked(cfg config.FolderConfiguration, fset *db.FileSet, cacheIgnoredFiles bool) {
ignores := ignore.New(cfg.Filesystem(), ignore.WithCache(cacheIgnoredFiles))
if err := ignores.Load(".stignore"); err != nil && !fs.IsNotExist(err) {
l.Warnln("Loading ignores:", err)
if cfg.Type != config.FolderTypeReceiveEncrypted {
if err := ignores.Load(".stignore"); err != nil && !fs.IsNotExist(err) {
l.Warnln("Loading ignores:", err)
}
}
m.addAndStartFolderLockedWithIgnores(cfg, fset, ignores)
@@ -691,7 +694,7 @@ func (m *model) UsageReportingStats(report *contract.Report, version int, previe
if strings.Contains(line, "**") {
report.IgnoreStats.DoubleStars++
// Remove not to trip up star checks.
line = strings.Replace(line, "**", "", -1)
line = strings.ReplaceAll(line, "**", "")
}
if strings.Contains(line, "*") {
@@ -769,7 +772,7 @@ func (m *model) ConnectionStats() map[string]interface{} {
in, out := protocol.TotalInOut()
res["total"] = ConnectionInfo{
Statistics: protocol.Statistics{
At: time.Now(),
At: time.Now().Truncate(time.Second),
InBytesTotal: in,
OutBytesTotal: out,
},
@@ -1364,12 +1367,12 @@ func (m *model) ccHandleFolders(folders []protocol.Folder, deviceCfg config.Devi
}
m.folderEncryptionFailures[folder.ID][deviceID] = err
msg := fmt.Sprintf("Failure checking encryption consistency with device %v for folder %v: %v", deviceID, cfg.Description(), err)
if sameError || err == errEncryptionReceivedToken {
if sameError {
l.Debugln(msg)
} else {
l.Warnln(msg)
}
m.evLogger.Log(events.Failure, err.Error())
return tempIndexFolders, paused, err
}
if devErrs, ok := m.folderEncryptionFailures[folder.ID]; ok {
@@ -1509,7 +1512,7 @@ func (m *model) ccCheckEncryption(fcfg config.FolderConfiguration, folderDevice
m.fmut.Unlock()
// We can only announce ourselfs once we have the token,
// thus we need to resend CCs now that we have it.
m.resendClusterConfig(fcfg.DeviceIDs())
m.sendClusterConfig(fcfg.DeviceIDs())
return nil
}
}
@@ -1519,7 +1522,7 @@ func (m *model) ccCheckEncryption(fcfg config.FolderConfiguration, folderDevice
return nil
}
func (m *model) resendClusterConfig(ids []protocol.DeviceID) {
func (m *model) sendClusterConfig(ids []protocol.DeviceID) {
if len(ids) == 0 {
return
}
@@ -1533,7 +1536,16 @@ func (m *model) resendClusterConfig(ids []protocol.DeviceID) {
m.pmut.RUnlock()
// Generating cluster-configs acquires fmut -> must happen outside of pmut.
for _, conn := range ccConns {
cm := m.generateClusterConfig(conn.ID())
cm, passwords, err := m.generateClusterConfig(conn.ID())
if err != nil {
if err != errEncryptionNeedToken {
m.evLogger.Log(events.Failure, failureUnexpectedGenerateCCError)
continue
}
go conn.Close(err)
continue
}
conn.SetFolderPasswords(passwords)
go conn.ClusterConfig(cm)
}
}
@@ -1699,15 +1711,6 @@ func (m *model) handleAutoAccepts(deviceID protocol.DeviceID, folder protocol.Fo
}
}
func (m *model) newFolderConfiguration(id, label string, fsType fs.FilesystemType, path string) config.FolderConfiguration {
fcfg := m.cfg.DefaultFolder()
fcfg.ID = id
fcfg.Label = label
fcfg.FilesystemType = fsType
fcfg.Path = path
return fcfg
}
func (m *model) introduceDevice(device protocol.Device, introducerCfg config.DeviceConfiguration) config.DeviceConfiguration {
addresses := []string{"dynamic"}
for _, addr := range device.Addresses {
@@ -1736,9 +1739,7 @@ func (m *model) introduceDevice(device protocol.Device, introducerCfg config.Dev
}
// Closed is called when a connection has been closed
func (m *model) Closed(conn protocol.Connection, err error) {
device := conn.ID()
func (m *model) Closed(device protocol.DeviceID, err error) {
m.pmut.Lock()
conn, ok := m.conn[device]
if !ok {
@@ -2255,7 +2256,13 @@ func (m *model) AddConnection(conn protocol.Connection, hello protocol.Hello) {
m.pmut.Unlock()
// Acquires fmut, so has to be done outside of pmut.
cm := m.generateClusterConfig(deviceID)
cm, passwords, err := m.generateClusterConfig(deviceID)
// We ignore errEncryptionNeedToken on a new connection, as the missing
// token should be delivered in the cluster-config about to be received.
if err != nil && err != errEncryptionNeedToken {
m.evLogger.Log(events.Failure, failureUnexpectedGenerateCCError)
}
conn.SetFolderPasswords(passwords)
conn.ClusterConfig(cm)
if (device.Name == "" || m.cfg.Options().OverwriteRemoteDevNames) && hello.DeviceName != "" {
@@ -2415,15 +2422,17 @@ func (m *model) numHashers(folder string) int {
return 1
}
// generateClusterConfig returns a ClusterConfigMessage that is correct for
// the given peer device
func (m *model) generateClusterConfig(device protocol.DeviceID) protocol.ClusterConfig {
// generateClusterConfig returns a ClusterConfigMessage that is correct and the
// set of folder passwords for the given peer device
func (m *model) generateClusterConfig(device protocol.DeviceID) (protocol.ClusterConfig, map[string]string, error) {
var message protocol.ClusterConfig
m.fmut.RLock()
defer m.fmut.RUnlock()
for _, folderCfg := range m.cfg.FolderList() {
folders := m.cfg.FolderList()
passwords := make(map[string]string, len(folders))
for _, folderCfg := range folders {
if !folderCfg.SharedWith(device) {
continue
}
@@ -2432,10 +2441,10 @@ func (m *model) generateClusterConfig(device protocol.DeviceID) protocol.Cluster
var hasEncryptionToken bool
if folderCfg.Type == config.FolderTypeReceiveEncrypted {
if encryptionToken, hasEncryptionToken = m.folderEncryptionPasswordTokens[folderCfg.ID]; !hasEncryptionToken {
// We haven't gotten a token for us yet and without
// one the other side can't validate us - pretend
// we don't have the folder yet.
continue
// We haven't gotten a token yet and without one the other side
// can't validate us - reset the connection to trigger a new
// cluster-config and get the token.
return message, nil, errEncryptionNeedToken
}
}
@@ -2456,8 +2465,8 @@ func (m *model) generateClusterConfig(device protocol.DeviceID) protocol.Cluster
// another cluster config once the folder is started.
protocolFolder.Paused = folderCfg.Paused || fs == nil
for _, device := range folderCfg.Devices {
deviceCfg, _ := m.cfg.Device(device.DeviceID)
for _, folderDevice := range folderCfg.Devices {
deviceCfg, _ := m.cfg.Device(folderDevice.DeviceID)
protocolDevice := protocol.Device{
ID: deviceCfg.DeviceID,
@@ -2470,8 +2479,11 @@ func (m *model) generateClusterConfig(device protocol.DeviceID) protocol.Cluster
if deviceCfg.DeviceID == m.id && hasEncryptionToken {
protocolDevice.EncryptionPasswordToken = encryptionToken
} else if device.EncryptionPassword != "" {
protocolDevice.EncryptionPasswordToken = protocol.PasswordToken(folderCfg.ID, device.EncryptionPassword)
} else if folderDevice.EncryptionPassword != "" {
protocolDevice.EncryptionPasswordToken = protocol.PasswordToken(folderCfg.ID, folderDevice.EncryptionPassword)
if folderDevice.DeviceID == device {
passwords[folderCfg.ID] = folderDevice.EncryptionPassword
}
}
if fs != nil {
@@ -2490,7 +2502,7 @@ func (m *model) generateClusterConfig(device protocol.DeviceID) protocol.Cluster
message.Folders = append(message.Folders, protocolFolder)
}
return message
return message, passwords, nil
}
func (m *model) State(folder string) (string, time.Time, error) {
@@ -2715,10 +2727,16 @@ func (m *model) Availability(folder string, file protocol.FileInfo, block protoc
}
defer snap.Release()
return m.availabilityInSnapshot(cfg, snap, file, block), nil
return m.availabilityInSnapshotPRlocked(cfg, snap, file, block), nil
}
func (m *model) availabilityInSnapshot(cfg config.FolderConfiguration, snap *db.Snapshot, file protocol.FileInfo, block protocol.BlockInfo) []Availability {
m.pmut.RLock()
defer m.pmut.RUnlock()
return m.availabilityInSnapshotPRlocked(cfg, snap, file, block)
}
func (m *model) availabilityInSnapshotPRlocked(cfg config.FolderConfiguration, snap *db.Snapshot, file protocol.FileInfo, block protocol.BlockInfo) []Availability {
var availabilities []Availability
for _, device := range snap.Availability(file.Name) {
if _, ok := m.remotePausedFolders[device]; !ok {
@@ -2893,7 +2911,7 @@ func (m *model) CommitConfiguration(from, to config.Configuration) bool {
}
m.pmut.RUnlock()
// Generating cluster-configs acquires fmut -> must happen outside of pmut.
m.resendClusterConfig(clusterConfigDevices.AsSlice())
m.sendClusterConfig(clusterConfigDevices.AsSlice())
ignoredDevices := observedDeviceSet(to.IgnoredDevices)
m.cleanPending(toDevices, toFolders, ignoredDevices, removedFolders)
+16 -10
View File
@@ -341,7 +341,7 @@ func TestDeviceRename(t *testing.T) {
t.Errorf("Device already has a name")
}
m.Closed(conn, protocol.ErrTimeout)
m.Closed(conn.ID(), protocol.ErrTimeout)
hello.DeviceName = "tester"
m.AddConnection(conn, hello)
@@ -349,7 +349,7 @@ func TestDeviceRename(t *testing.T) {
t.Errorf("Device did not get a name")
}
m.Closed(conn, protocol.ErrTimeout)
m.Closed(conn.ID(), protocol.ErrTimeout)
hello.DeviceName = "tester2"
m.AddConnection(conn, hello)
@@ -367,7 +367,7 @@ func TestDeviceRename(t *testing.T) {
t.Errorf("Device name not saved in config")
}
m.Closed(conn, protocol.ErrTimeout)
m.Closed(conn.ID(), protocol.ErrTimeout)
waiter, err := cfg.Modify(func(cfg *config.Configuration) {
cfg.Options.OverwriteRemoteDevNames = true
@@ -428,7 +428,8 @@ func TestClusterConfig(t *testing.T) {
m.ServeBackground()
defer cleanupModel(m)
cm := m.generateClusterConfig(device2)
cm, _, err := m.generateClusterConfig(device2)
must(t, err)
if l := len(cm.Folders); l != 2 {
t.Fatalf("Incorrect number of folders %d != 2", l)
@@ -853,7 +854,8 @@ func TestIssue4897(t *testing.T) {
defer cleanupModel(m)
cancel()
cm := m.generateClusterConfig(device1)
cm, _, err := m.generateClusterConfig(device1)
must(t, err)
if l := len(cm.Folders); l != 1 {
t.Errorf("Cluster config contains %v folders, expected 1", l)
}
@@ -873,7 +875,7 @@ func TestIssue5063(t *testing.T) {
for _, c := range m.conn {
conn := c.(*fakeConnection)
conn.CloseCalls(func(_ error) {})
defer m.Closed(c, errStopped) // to unblock deferred m.Stop()
defer m.Closed(c.ID(), errStopped) // to unblock deferred m.Stop()
}
m.pmut.Unlock()
@@ -2428,8 +2430,8 @@ func TestNoRequestsFromPausedDevices(t *testing.T) {
t.Errorf("should have two available")
}
m.Closed(newFakeConnection(device1, m), errDeviceUnknown)
m.Closed(newFakeConnection(device2, m), errDeviceUnknown)
m.Closed(device1, errDeviceUnknown)
m.Closed(device2, errDeviceUnknown)
avail = m.testAvailability("default", file, file.Blocks[0])
if len(avail) != 0 {
@@ -3171,7 +3173,7 @@ func TestConnCloseOnRestart(t *testing.T) {
br := &testutils.BlockingRW{}
nw := &testutils.NoopRW{}
m.AddConnection(protocol.NewConnection(device1, br, nw, testutils.NoopCloser{}, m, new(protocolmocks.ConnectionInfo), protocol.CompressionNever), protocol.Hello{})
m.AddConnection(protocol.NewConnection(device1, br, nw, testutils.NoopCloser{}, m, new(protocolmocks.ConnectionInfo), protocol.CompressionNever, nil), protocol.Hello{})
m.pmut.RLock()
if len(m.closed) != 1 {
t.Fatalf("Expected just one conn (len(m.conn) == %v)", len(m.conn))
@@ -4142,7 +4144,8 @@ func TestCCFolderNotRunning(t *testing.T) {
defer cleanupModelAndRemoveDir(m, tfs.URI())
// A connection can happen before all the folders are started.
cc := m.generateClusterConfig(device1)
cc, _, err := m.generateClusterConfig(device1)
must(t, err)
if l := len(cc.Folders); l != 1 {
t.Fatalf("Expected 1 folder in CC, got %v", l)
}
@@ -4185,6 +4188,9 @@ func TestPendingFolder(t *testing.T) {
}
device3, err := protocol.DeviceIDFromString("AIBAEAQ-CAIBAEC-AQCAIBA-EAQCAIA-BAEAQCA-IBAEAQC-CAIBAEA-QCAIBA7")
if err != nil {
t.Fatal(err)
}
setDevice(t, w, config.DeviceConfiguration{DeviceID: device3})
if err := m.db.AddOrUpdatePendingFolder(pfolder, pfolder, device3, false); err != nil {
t.Fatal(err)
+42
View File
@@ -1399,3 +1399,45 @@ func TestRequestReceiveEncryptedLocalNoSend(t *testing.T) {
t.Fatal("timed out before receiving index")
}
}
func TestRequestIssue7474(t *testing.T) {
// Repro for https://github.com/syncthing/syncthing/issues/7474
// Devices A, B and C. B connected to A and C, but not A to C.
// A has valid file, B ignores it.
// In the test C is local, and B is the fake connection.
done := make(chan struct{})
defer close(done)
m, fc, fcfg, wcfgCancel := setupModelWithConnection(t)
defer wcfgCancel()
tfs := fcfg.Filesystem()
defer cleanupModelAndRemoveDir(m, tfs.URI())
indexChan := make(chan []protocol.FileInfo)
fc.setIndexFn(func(ctx context.Context, folder string, fs []protocol.FileInfo) error {
select {
case indexChan <- fs:
case <-done:
case <-ctx.Done():
}
return nil
})
name := "foo"
fc.addFileWithLocalFlags(name, protocol.FileInfoTypeFile, protocol.FlagLocalIgnored)
fc.sendIndexUpdate()
select {
case <-time.After(10 * time.Second):
t.Fatal("timed out before receiving index")
case fs := <-indexChan:
if len(fs) != 1 {
t.Fatalf("Expected one file in index, got %v", len(fs))
}
if !fs[0].IsInvalid() {
t.Error("Expected invalid file")
}
}
}
+9 -3
View File
@@ -13,6 +13,7 @@ import (
"github.com/pkg/errors"
"github.com/syncthing/syncthing/lib/fs"
"github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/sync"
)
@@ -346,8 +347,13 @@ func (s *sharedPullerState) finalClose() (bool, error) {
// folder from encrypted data we can extract this FileInfo from the end of
// the file and regain the original metadata.
func (s *sharedPullerState) finalizeEncrypted() error {
bs := make([]byte, encryptionTrailerSize(s.file))
n, err := s.file.MarshalTo(bs)
// Here the file is in native format, while encryption happens in
// wire format (always slashes).
wireFile := s.file
wireFile.Name = osutil.NormalizedFilename(wireFile.Name)
bs := make([]byte, encryptionTrailerSize(wireFile))
n, err := wireFile.MarshalTo(bs)
if err != nil {
return err
}
@@ -359,7 +365,7 @@ func (s *sharedPullerState) finalizeEncrypted() error {
return err
}
}
if _, err := s.writer.WriteAt(bs, s.file.Size); err != nil {
if _, err := s.writer.WriteAt(bs, wireFile.Size); err != nil {
return err
}
+3 -8
View File
@@ -13,8 +13,6 @@ import (
"testing"
"time"
"github.com/thejerf/suture/v4"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/db"
"github.com/syncthing/syncthing/lib/db/backend"
@@ -84,12 +82,9 @@ func createTmpWrapper(cfg config.Configuration) (config.Wrapper, context.CancelF
}
wrapper := config.Wrap(tmpFile.Name(), cfg, myID, events.NoopLogger)
tmpFile.Close()
if cfgService, ok := wrapper.(suture.Service); ok {
ctx, cancel := context.WithCancel(context.Background())
go cfgService.Serve(ctx)
return wrapper, cancel
}
return wrapper, func() {}
ctx, cancel := context.WithCancel(context.Background())
go wrapper.Serve(ctx)
return wrapper, cancel
}
func tmpDefaultWrapper() (config.Wrapper, config.FolderConfiguration, context.CancelFunc) {
-28
View File
@@ -1,28 +0,0 @@
// Copyright (C) 2015 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 http://mozilla.org/MPL/2.0/.
package osutil
import (
"net"
)
func interfaceAddresses(network string, intf *net.Interface) []string {
var out []string
addrs, err := intf.Addrs()
if err != nil {
return out
}
for _, addr := range addrs {
ipnet, ok := addr.(*net.IPNet)
if ok && (network == "tcp" || (network == "tcp4" && len(ipnet.IP) == net.IPv4len) || (network == "tcp6" && len(ipnet.IP) == net.IPv6len)) {
out = append(out, ipnet.IP.String())
}
}
return out
}
+1 -1
View File
@@ -36,7 +36,7 @@ func MaximizeOpenFileLimit() (int, error) {
// macOS doesn't like a soft limit greater then OPEN_MAX
// See also: man setrlimit
if runtime.GOOS == "darwin" && lim.Max > darwinOpenMax {
lim.Cur = darwinOpenMax
lim.Max = darwinOpenMax
}
// Try to increase the limit to the max.
+3 -3
View File
@@ -60,9 +60,9 @@ func benchmarkRequestsTLS(b *testing.B, conn0, conn1 net.Conn) {
func benchmarkRequestsConnPair(b *testing.B, conn0, conn1 net.Conn) {
// Start up Connections on them
c0 := NewConnection(LocalDeviceID, conn0, conn0, testutils.NoopCloser{}, new(fakeModel), new(mockedConnectionInfo), CompressionMetadata)
c0 := NewConnection(LocalDeviceID, conn0, conn0, testutils.NoopCloser{}, new(fakeModel), new(mockedConnectionInfo), CompressionMetadata, nil)
c0.Start()
c1 := NewConnection(LocalDeviceID, conn1, conn1, testutils.NoopCloser{}, new(fakeModel), new(mockedConnectionInfo), CompressionMetadata)
c1 := NewConnection(LocalDeviceID, conn1, conn1, testutils.NoopCloser{}, new(fakeModel), new(mockedConnectionInfo), CompressionMetadata, nil)
c1.Start()
// Satisfy the assertions in the protocol by sending an initial cluster config
@@ -188,7 +188,7 @@ func (m *fakeModel) ClusterConfig(deviceID DeviceID, config ClusterConfig) error
return nil
}
func (m *fakeModel) Closed(conn Connection, err error) {
func (m *fakeModel) Closed(DeviceID, error) {
}
func (m *fakeModel) DownloadProgress(deviceID DeviceID, folder string, updates []FileDownloadProgressUpdate) error {
+1 -1
View File
@@ -49,7 +49,7 @@ func (t *TestModel) Request(deviceID DeviceID, folder, name string, blockNo, siz
return &fakeRequestResponse{buf}, nil
}
func (t *TestModel) Closed(conn Connection, err error) {
func (t *TestModel) Closed(_ DeviceID, err error) {
t.closedErr = err
close(t.closedCh)
}
+5 -5
View File
@@ -203,15 +203,15 @@ func chunkify(s string) string {
}
func unchunkify(s string) string {
s = strings.Replace(s, "-", "", -1)
s = strings.Replace(s, " ", "", -1)
s = strings.ReplaceAll(s, "-", "")
s = strings.ReplaceAll(s, " ", "")
return s
}
func untypeoify(s string) string {
s = strings.Replace(s, "0", "O", -1)
s = strings.Replace(s, "1", "I", -1)
s = strings.Replace(s, "8", "B", -1)
s = strings.ReplaceAll(s, "0", "O")
s = strings.ReplaceAll(s, "1", "I")
s = strings.ReplaceAll(s, "8", "B")
return s
}
+43 -13
View File
@@ -14,6 +14,7 @@ import (
"fmt"
"io"
"strings"
"sync"
"time"
"github.com/gogo/protobuf/proto"
@@ -41,11 +42,11 @@ const (
// must decrypt those and answer requests by encrypting the data.
type encryptedModel struct {
model Model
folderKeys map[string]*[keySize]byte // folder ID -> key
folderKeys *folderKeyRegistry
}
func (e encryptedModel) Index(deviceID DeviceID, folder string, files []FileInfo) error {
if folderKey, ok := e.folderKeys[folder]; ok {
if folderKey, ok := e.folderKeys.get(folder); ok {
// incoming index data to be decrypted
if err := decryptFileInfos(files, folderKey); err != nil {
return err
@@ -55,7 +56,7 @@ func (e encryptedModel) Index(deviceID DeviceID, folder string, files []FileInfo
}
func (e encryptedModel) IndexUpdate(deviceID DeviceID, folder string, files []FileInfo) error {
if folderKey, ok := e.folderKeys[folder]; ok {
if folderKey, ok := e.folderKeys.get(folder); ok {
// incoming index data to be decrypted
if err := decryptFileInfos(files, folderKey); err != nil {
return err
@@ -65,7 +66,7 @@ func (e encryptedModel) IndexUpdate(deviceID DeviceID, folder string, files []Fi
}
func (e encryptedModel) Request(deviceID DeviceID, folder, name string, blockNo, size int32, offset int64, hash []byte, weakHash uint32, fromTemporary bool) (RequestResponse, error) {
folderKey, ok := e.folderKeys[folder]
folderKey, ok := e.folderKeys.get(folder)
if !ok {
return e.model.Request(deviceID, folder, name, blockNo, size, offset, hash, weakHash, fromTemporary)
}
@@ -123,7 +124,7 @@ func (e encryptedModel) Request(deviceID DeviceID, folder, name string, blockNo,
}
func (e encryptedModel) DownloadProgress(deviceID DeviceID, folder string, updates []FileDownloadProgressUpdate) error {
if _, ok := e.folderKeys[folder]; !ok {
if _, ok := e.folderKeys.get(folder); !ok {
return e.model.DownloadProgress(deviceID, folder, updates)
}
@@ -135,42 +136,46 @@ func (e encryptedModel) ClusterConfig(deviceID DeviceID, config ClusterConfig) e
return e.model.ClusterConfig(deviceID, config)
}
func (e encryptedModel) Closed(conn Connection, err error) {
e.model.Closed(conn, err)
func (e encryptedModel) Closed(device DeviceID, err error) {
e.model.Closed(device, err)
}
// The encryptedConnection sits between the model and the encrypted device. It
// encrypts outgoing metadata and decrypts incoming responses.
type encryptedConnection struct {
ConnectionInfo
conn Connection
folderKeys map[string]*[keySize]byte // folder ID -> key
conn *rawConnection
folderKeys *folderKeyRegistry
}
func (e encryptedConnection) Start() {
e.conn.Start()
}
func (e encryptedConnection) SetFolderPasswords(passwords map[string]string) {
e.folderKeys.setPasswords(passwords)
}
func (e encryptedConnection) ID() DeviceID {
return e.conn.ID()
}
func (e encryptedConnection) Index(ctx context.Context, folder string, files []FileInfo) error {
if folderKey, ok := e.folderKeys[folder]; ok {
if folderKey, ok := e.folderKeys.get(folder); ok {
encryptFileInfos(files, folderKey)
}
return e.conn.Index(ctx, folder, files)
}
func (e encryptedConnection) IndexUpdate(ctx context.Context, folder string, files []FileInfo) error {
if folderKey, ok := e.folderKeys[folder]; ok {
if folderKey, ok := e.folderKeys.get(folder); ok {
encryptFileInfos(files, folderKey)
}
return e.conn.IndexUpdate(ctx, folder, files)
}
func (e encryptedConnection) Request(ctx context.Context, folder string, name string, blockNo int, offset int64, size int, hash []byte, weakHash uint32, fromTemporary bool) ([]byte, error) {
folderKey, ok := e.folderKeys[folder]
folderKey, ok := e.folderKeys.get(folder)
if !ok {
return e.conn.Request(ctx, folder, name, blockNo, offset, size, hash, weakHash, fromTemporary)
}
@@ -205,7 +210,7 @@ func (e encryptedConnection) Request(ctx context.Context, folder string, name st
}
func (e encryptedConnection) DownloadProgress(ctx context.Context, folder string, updates []FileDownloadProgressUpdate) {
if _, ok := e.folderKeys[folder]; !ok {
if _, ok := e.folderKeys.get(folder); !ok {
e.conn.DownloadProgress(ctx, folder, updates)
}
@@ -314,6 +319,7 @@ func encryptFileInfo(fi FileInfo, folderKey *[keySize]byte) FileInfo {
Permissions: 0644,
ModifiedS: 1234567890, // Sat Feb 14 00:31:30 CET 2009
Deleted: fi.Deleted,
RawInvalid: fi.IsInvalid(),
Version: version,
Sequence: fi.Sequence,
RawBlockSize: fi.RawBlockSize + blockOverhead,
@@ -589,3 +595,27 @@ func isEncryptedParentFromComponents(pathComponents []string) bool {
}
return true
}
type folderKeyRegistry struct {
keys map[string]*[keySize]byte // folder ID -> key
mut sync.RWMutex
}
func newFolderKeyRegistry(passwords map[string]string) *folderKeyRegistry {
return &folderKeyRegistry{
keys: keysFromPasswords(passwords),
}
}
func (r *folderKeyRegistry) get(folder string) (*[keySize]byte, bool) {
r.mut.RLock()
key, ok := r.keys[folder]
r.mut.RUnlock()
return key, ok
}
func (r *folderKeyRegistry) setPasswords(passwords map[string]string) {
r.mut.Lock()
r.keys = keysFromPasswords(passwords)
r.mut.Unlock()
}
+22 -3
View File
@@ -113,9 +113,8 @@ func TestEnDecryptBytes(t *testing.T) {
}
}
func TestEnDecryptFileInfo(t *testing.T) {
var key [32]byte
fi := FileInfo{
func encFileInfo() FileInfo {
return FileInfo{
Name: "hello",
Size: 45,
Permissions: 0755,
@@ -133,6 +132,11 @@ func TestEnDecryptFileInfo(t *testing.T) {
},
},
}
}
func TestEnDecryptFileInfo(t *testing.T) {
var key [32]byte
fi := encFileInfo()
enc := encryptFileInfo(fi, &key)
if bytes.Equal(enc.Blocks[0].Hash, enc.Blocks[1].Hash) {
@@ -155,6 +159,21 @@ func TestEnDecryptFileInfo(t *testing.T) {
}
}
func TestEncryptedFileInfoConsistency(t *testing.T) {
var key [32]byte
files := []FileInfo{
encFileInfo(),
encFileInfo(),
}
files[1].SetIgnored()
for i, f := range files {
enc := encryptFileInfo(f, &key)
if err := checkFileInfoConsistency(enc); err != nil {
t.Errorf("%v: %v", i, err)
}
}
}
func TestIsEncryptedParent(t *testing.T) {
comp := rand.String(maxPathComponent)
cases := []struct {
+39
View File
@@ -135,6 +135,11 @@ type Connection struct {
result1 []byte
result2 error
}
SetFolderPasswordsStub func(map[string]string)
setFolderPasswordsMutex sync.RWMutex
setFolderPasswordsArgsForCall []struct {
arg1 map[string]string
}
StartStub func()
startMutex sync.RWMutex
startArgsForCall []struct {
@@ -817,6 +822,38 @@ func (fake *Connection) RequestReturnsOnCall(i int, result1 []byte, result2 erro
}{result1, result2}
}
func (fake *Connection) SetFolderPasswords(arg1 map[string]string) {
fake.setFolderPasswordsMutex.Lock()
fake.setFolderPasswordsArgsForCall = append(fake.setFolderPasswordsArgsForCall, struct {
arg1 map[string]string
}{arg1})
stub := fake.SetFolderPasswordsStub
fake.recordInvocation("SetFolderPasswords", []interface{}{arg1})
fake.setFolderPasswordsMutex.Unlock()
if stub != nil {
fake.SetFolderPasswordsStub(arg1)
}
}
func (fake *Connection) SetFolderPasswordsCallCount() int {
fake.setFolderPasswordsMutex.RLock()
defer fake.setFolderPasswordsMutex.RUnlock()
return len(fake.setFolderPasswordsArgsForCall)
}
func (fake *Connection) SetFolderPasswordsCalls(stub func(map[string]string)) {
fake.setFolderPasswordsMutex.Lock()
defer fake.setFolderPasswordsMutex.Unlock()
fake.SetFolderPasswordsStub = stub
}
func (fake *Connection) SetFolderPasswordsArgsForCall(i int) map[string]string {
fake.setFolderPasswordsMutex.RLock()
defer fake.setFolderPasswordsMutex.RUnlock()
argsForCall := fake.setFolderPasswordsArgsForCall[i]
return argsForCall.arg1
}
func (fake *Connection) Start() {
fake.startMutex.Lock()
fake.startArgsForCall = append(fake.startArgsForCall, struct {
@@ -1080,6 +1117,8 @@ func (fake *Connection) Invocations() map[string][][]interface{} {
defer fake.remoteAddrMutex.RUnlock()
fake.requestMutex.RLock()
defer fake.requestMutex.RUnlock()
fake.setFolderPasswordsMutex.RLock()
defer fake.setFolderPasswordsMutex.RUnlock()
fake.startMutex.RLock()
defer fake.startMutex.RUnlock()
fake.statisticsMutex.RLock()
+80 -75
View File
@@ -126,8 +126,8 @@ type Model interface {
Request(deviceID DeviceID, folder, name string, blockNo, size int32, offset int64, hash []byte, weakHash uint32, fromTemporary bool) (RequestResponse, error)
// A cluster configuration message was received
ClusterConfig(deviceID DeviceID, config ClusterConfig) error
// The peer device closed the connection
Closed(conn Connection, err error)
// The peer device closed the connection or an error occurred
Closed(device DeviceID, err error)
// The peer device sent progress updates for the files it is currently downloading
DownloadProgress(deviceID DeviceID, folder string, updates []FileDownloadProgressUpdate) error
}
@@ -140,6 +140,7 @@ type RequestResponse interface {
type Connection interface {
Start()
SetFolderPasswords(passwords map[string]string)
Close(err error)
ID() DeviceID
Index(ctx context.Context, folder string, files []FileInfo) error
@@ -225,24 +226,16 @@ const (
// Should not be modified in production code, just for testing.
var CloseTimeout = 10 * time.Second
func NewConnection(deviceID DeviceID, reader io.Reader, writer io.Writer, closer io.Closer, receiver Model, connInfo ConnectionInfo, compress Compression) Connection {
receiver = nativeModel{receiver}
rc := newRawConnection(deviceID, reader, writer, closer, receiver, connInfo, compress)
return wireFormatConnection{rc}
}
func NewEncryptedConnection(passwords map[string]string, deviceID DeviceID, reader io.Reader, writer io.Writer, closer io.Closer, receiver Model, connInfo ConnectionInfo, compress Compression) Connection {
keys := keysFromPasswords(passwords)
func NewConnection(deviceID DeviceID, reader io.Reader, writer io.Writer, closer io.Closer, receiver Model, connInfo ConnectionInfo, compress Compression, passwords map[string]string) Connection {
// Encryption / decryption is first (outermost) before conversion to
// native path formats.
nm := nativeModel{receiver}
em := encryptedModel{model: nm, folderKeys: keys}
em := &encryptedModel{model: nm, folderKeys: newFolderKeyRegistry(passwords)}
// We do the wire format conversion first (outermost) so that the
// metadata is in wire format when it reaches the encryption step.
rc := newRawConnection(deviceID, reader, writer, closer, em, connInfo, compress)
ec := encryptedConnection{ConnectionInfo: rc, conn: rc, folderKeys: keys}
ec := encryptedConnection{ConnectionInfo: rc, conn: rc, folderKeys: em.folderKeys}
wc := wireFormatConnection{ec}
return wc
@@ -296,7 +289,7 @@ func (c *rawConnection) Start() {
c.pingReceiver()
c.loopWG.Done()
}()
c.startTime = time.Now()
c.startTime = time.Now().Truncate(time.Second)
}
func (c *rawConnection) ID() DeviceID {
@@ -437,82 +430,61 @@ func (c *rawConnection) dispatcherLoop() (err error) {
case <-c.closed:
return ErrClosed
}
msgContext, err := messageContext(msg)
if err != nil {
return fmt.Errorf("protocol error: %w", err)
}
l.Debugf("handle %v message", msgContext)
switch msg := msg.(type) {
case *ClusterConfig:
l.Debugln("read ClusterConfig message")
if state == stateInitial {
state = stateReady
}
if err := c.receiver.ClusterConfig(c.id, *msg); err != nil {
return fmt.Errorf("receiving cluster config: %w", err)
}
case *Index:
l.Debugln("read Index message")
case *Close:
return fmt.Errorf("closed by remote: %v", msg.Reason)
default:
if state != stateReady {
return fmt.Errorf("protocol error: index message in state %d", state)
return newProtocolError(fmt.Errorf("invalid state %d", state), msgContext)
}
if err := checkIndexConsistency(msg.Files); err != nil {
return errors.Wrap(err, "protocol error: index")
}
if err := c.handleIndex(*msg); err != nil {
return fmt.Errorf("receiving index: %w", err)
}
state = stateReady
}
switch msg := msg.(type) {
case *Index:
err = checkIndexConsistency(msg.Files)
case *IndexUpdate:
l.Debugln("read IndexUpdate message")
if state != stateReady {
return fmt.Errorf("protocol error: index update message in state %d", state)
}
if err := checkIndexConsistency(msg.Files); err != nil {
return errors.Wrap(err, "protocol error: index update")
}
if err := c.handleIndexUpdate(*msg); err != nil {
return fmt.Errorf("receiving index update: %w", err)
}
state = stateReady
err = checkIndexConsistency(msg.Files)
case *Request:
err = checkFilename(msg.Name)
}
if err != nil {
return newProtocolError(err, msgContext)
}
switch msg := msg.(type) {
case *ClusterConfig:
err = c.receiver.ClusterConfig(c.id, *msg)
case *Index:
err = c.handleIndex(*msg)
case *IndexUpdate:
err = c.handleIndexUpdate(*msg)
case *Request:
l.Debugln("read Request message")
if state != stateReady {
return fmt.Errorf("protocol error: request message in state %d", state)
}
if err := checkFilename(msg.Name); err != nil {
return errors.Wrapf(err, "protocol error: request: %q", msg.Name)
}
go c.handleRequest(*msg)
case *Response:
l.Debugln("read Response message")
if state != stateReady {
return fmt.Errorf("protocol error: response message in state %d", state)
}
c.handleResponse(*msg)
case *DownloadProgress:
l.Debugln("read DownloadProgress message")
if state != stateReady {
return fmt.Errorf("protocol error: response message in state %d", state)
}
if err := c.receiver.DownloadProgress(c.id, msg.Folder, msg.Updates); err != nil {
return fmt.Errorf("receiving download progress: %w", err)
}
case *Ping:
l.Debugln("read Ping message")
if state != stateReady {
return fmt.Errorf("protocol error: ping message in state %d", state)
}
// Nothing
case *Close:
l.Debugln("read Close message")
return fmt.Errorf("closed by remote: %v", msg.Reason)
default:
l.Debugf("read unknown message: %+T", msg)
return fmt.Errorf("protocol error: %s: unknown or empty message", c.id)
err = c.receiver.DownloadProgress(c.id, msg.Folder, msg.Updates)
}
if err != nil {
return newHandleError(err, msgContext)
}
}
}
@@ -769,6 +741,8 @@ func (c *rawConnection) writerLoop() {
}
func (c *rawConnection) writeMessage(msg message) error {
msgContext, _ := messageContext(msg)
l.Debugf("Writing %v", msgContext)
if c.shouldCompressMessage(msg) {
return c.writeCompressedMessage(msg)
}
@@ -976,7 +950,7 @@ func (c *rawConnection) internalClose(err error) {
<-c.dispatcherLoopStopped
c.receiver.Closed(c, err)
c.receiver.Closed(c.ID(), err)
})
}
@@ -1040,7 +1014,7 @@ type Statistics struct {
func (c *rawConnection) Statistics() Statistics {
return Statistics{
At: time.Now(),
At: time.Now().Truncate(time.Second),
InBytesTotal: c.cr.Tot(),
OutBytesTotal: c.cw.Tot(),
StartedAt: c.startTime,
@@ -1078,3 +1052,34 @@ func (c *rawConnection) lz4Decompress(src []byte) ([]byte, error) {
}
return decoded, nil
}
func newProtocolError(err error, msgContext string) error {
return fmt.Errorf("protocol error on %v: %w", msgContext, err)
}
func newHandleError(err error, msgContext string) error {
return fmt.Errorf("handling %v: %w", msgContext, err)
}
func messageContext(msg message) (string, error) {
switch msg := msg.(type) {
case *ClusterConfig:
return "cluster-config", nil
case *Index:
return fmt.Sprintf("index for %v", msg.Folder), nil
case *IndexUpdate:
return fmt.Sprintf("index-update for %v", msg.Folder), nil
case *Request:
return fmt.Sprintf(`request for "%v" in %v`, msg.Name, msg.Folder), nil
case *Response:
return "response", nil
case *DownloadProgress:
return fmt.Sprintf("download-progress for %v", msg.Folder), nil
case *Ping:
return "ping", nil
case *Close:
return "close", nil
default:
return "", errors.New("unknown or empty message")
}
}
+25 -14
View File
@@ -31,10 +31,10 @@ func TestPing(t *testing.T) {
ar, aw := io.Pipe()
br, bw := io.Pipe()
c0 := NewConnection(c0ID, ar, bw, testutils.NoopCloser{}, newTestModel(), new(mockedConnectionInfo), CompressionAlways).(wireFormatConnection).Connection.(*rawConnection)
c0 := getRawConnection(NewConnection(c0ID, ar, bw, testutils.NoopCloser{}, newTestModel(), new(mockedConnectionInfo), CompressionAlways, nil))
c0.Start()
defer closeAndWait(c0, ar, bw)
c1 := NewConnection(c1ID, br, aw, testutils.NoopCloser{}, newTestModel(), new(mockedConnectionInfo), CompressionAlways).(wireFormatConnection).Connection.(*rawConnection)
c1 := getRawConnection(NewConnection(c1ID, br, aw, testutils.NoopCloser{}, newTestModel(), new(mockedConnectionInfo), CompressionAlways, nil))
c1.Start()
defer closeAndWait(c1, ar, bw)
c0.ClusterConfig(ClusterConfig{})
@@ -57,10 +57,10 @@ func TestClose(t *testing.T) {
ar, aw := io.Pipe()
br, bw := io.Pipe()
c0 := NewConnection(c0ID, ar, bw, testutils.NoopCloser{}, m0, new(mockedConnectionInfo), CompressionAlways).(wireFormatConnection).Connection.(*rawConnection)
c0 := getRawConnection(NewConnection(c0ID, ar, bw, testutils.NoopCloser{}, m0, new(mockedConnectionInfo), CompressionAlways, nil))
c0.Start()
defer closeAndWait(c0, ar, bw)
c1 := NewConnection(c1ID, br, aw, testutils.NoopCloser{}, m1, new(mockedConnectionInfo), CompressionAlways)
c1 := NewConnection(c1ID, br, aw, testutils.NoopCloser{}, m1, new(mockedConnectionInfo), CompressionAlways, nil)
c1.Start()
defer closeAndWait(c1, ar, bw)
c0.ClusterConfig(ClusterConfig{})
@@ -102,7 +102,7 @@ func TestCloseOnBlockingSend(t *testing.T) {
m := newTestModel()
rw := testutils.NewBlockingRW()
c := NewConnection(c0ID, rw, rw, testutils.NoopCloser{}, m, new(mockedConnectionInfo), CompressionAlways).(wireFormatConnection).Connection.(*rawConnection)
c := getRawConnection(NewConnection(c0ID, rw, rw, testutils.NoopCloser{}, m, new(mockedConnectionInfo), CompressionAlways, nil))
c.Start()
defer closeAndWait(c, rw)
@@ -153,10 +153,10 @@ func TestCloseRace(t *testing.T) {
ar, aw := io.Pipe()
br, bw := io.Pipe()
c0 := NewConnection(c0ID, ar, bw, testutils.NoopCloser{}, m0, new(mockedConnectionInfo), CompressionNever).(wireFormatConnection).Connection.(*rawConnection)
c0 := getRawConnection(NewConnection(c0ID, ar, bw, testutils.NoopCloser{}, m0, new(mockedConnectionInfo), CompressionNever, nil))
c0.Start()
defer closeAndWait(c0, ar, bw)
c1 := NewConnection(c1ID, br, aw, testutils.NoopCloser{}, m1, new(mockedConnectionInfo), CompressionNever)
c1 := NewConnection(c1ID, br, aw, testutils.NoopCloser{}, m1, new(mockedConnectionInfo), CompressionNever, nil)
c1.Start()
defer closeAndWait(c1, ar, bw)
c0.ClusterConfig(ClusterConfig{})
@@ -193,7 +193,7 @@ func TestClusterConfigFirst(t *testing.T) {
m := newTestModel()
rw := testutils.NewBlockingRW()
c := NewConnection(c0ID, rw, &testutils.NoopRW{}, testutils.NoopCloser{}, m, new(mockedConnectionInfo), CompressionAlways).(wireFormatConnection).Connection.(*rawConnection)
c := getRawConnection(NewConnection(c0ID, rw, &testutils.NoopRW{}, testutils.NoopCloser{}, m, new(mockedConnectionInfo), CompressionAlways, nil))
c.Start()
defer closeAndWait(c, rw)
@@ -245,7 +245,7 @@ func TestCloseTimeout(t *testing.T) {
m := newTestModel()
rw := testutils.NewBlockingRW()
c := NewConnection(c0ID, rw, rw, testutils.NoopCloser{}, m, new(mockedConnectionInfo), CompressionAlways).(wireFormatConnection).Connection.(*rawConnection)
c := getRawConnection(NewConnection(c0ID, rw, rw, testutils.NoopCloser{}, m, new(mockedConnectionInfo), CompressionAlways, nil))
c.Start()
defer closeAndWait(c, rw)
@@ -865,7 +865,7 @@ func TestClusterConfigAfterClose(t *testing.T) {
m := newTestModel()
rw := testutils.NewBlockingRW()
c := NewConnection(c0ID, rw, rw, testutils.NoopCloser{}, m, new(mockedConnectionInfo), CompressionAlways).(wireFormatConnection).Connection.(*rawConnection)
c := getRawConnection(NewConnection(c0ID, rw, rw, testutils.NoopCloser{}, m, new(mockedConnectionInfo), CompressionAlways, nil))
c.Start()
defer closeAndWait(c, rw)
@@ -889,7 +889,7 @@ func TestDispatcherToCloseDeadlock(t *testing.T) {
// the model callbacks (ClusterConfig).
m := newTestModel()
rw := testutils.NewBlockingRW()
c := NewConnection(c0ID, rw, &testutils.NoopRW{}, testutils.NoopCloser{}, m, new(mockedConnectionInfo), CompressionAlways).(wireFormatConnection).Connection.(*rawConnection)
c := getRawConnection(NewConnection(c0ID, rw, &testutils.NoopRW{}, testutils.NoopCloser{}, m, new(mockedConnectionInfo), CompressionAlways, nil))
m.ccFn = func(devID DeviceID, cc ClusterConfig) {
c.Close(errManual)
}
@@ -962,17 +962,28 @@ func TestIndexIDString(t *testing.T) {
}
}
func closeAndWait(c Connection, closers ...io.Closer) {
func closeAndWait(c interface{}, closers ...io.Closer) {
for _, closer := range closers {
closer.Close()
}
var raw *rawConnection
switch i := c.(type) {
case wireFormatConnection:
raw = i.Connection.(*rawConnection)
case *rawConnection:
raw = i
default:
raw = getRawConnection(c.(Connection))
}
raw.internalClose(ErrClosed)
raw.loopWG.Wait()
}
func getRawConnection(c Connection) *rawConnection {
var raw *rawConnection
switch i := c.(type) {
case wireFormatConnection:
raw = i.Connection.(encryptedConnection).conn
case encryptedConnection:
raw = i.conn
}
return raw
}
+4
View File
@@ -4,6 +4,7 @@ package protocol
import (
"errors"
"fmt"
"io"
)
@@ -86,6 +87,9 @@ func ReadMessage(r io.Reader) (interface{}, error) {
if header.magic != magic {
return nil, errors.New("magic mismatch")
}
if header.messageLength < 0 || header.messageLength > 1024 {
return nil, fmt.Errorf("bad length (%d)", header.messageLength)
}
buf = make([]byte, int(header.messageLength))
if _, err := io.ReadFull(r, buf); err != nil {
+6 -6
View File
@@ -195,17 +195,17 @@ func BenchmarkValidate(b *testing.B) {
for i := 0; i < blocksPerType; i++ {
var b block
b.data = make([]byte, 128<<10)
r.Read(b.data[:])
b.hash = sha256.Sum256(b.data[:])
b.weakhash = origAdler32.Checksum(b.data[:])
r.Read(b.data)
b.hash = sha256.Sum256(b.data)
b.weakhash = origAdler32.Checksum(b.data)
blocks = append(blocks, b)
}
// Blocks where the hash matches, but the weakhash doesn't.
for i := 0; i < blocksPerType; i++ {
var b block
b.data = make([]byte, 128<<10)
r.Read(b.data[:])
b.hash = sha256.Sum256(b.data[:])
r.Read(b.data)
b.hash = sha256.Sum256(b.data)
b.weakhash = 1 // Zeros causes Validate to skip the weakhash.
blocks = append(blocks, b)
}
@@ -215,7 +215,7 @@ func BenchmarkValidate(b *testing.B) {
for i := 0; i < b.N; i++ {
for _, b := range blocks {
Validate(b.data[:], b.hash[:], b.weakhash)
Validate(b.data, b.hash[:], b.weakhash)
}
}
}
+4
View File
@@ -97,6 +97,10 @@ func (s singleFileFS) Open(name string) (fs.File, error) {
return &fakeFile{s.name, s.filesize, 0}, nil
}
func (s singleFileFS) Options() []fs.Option {
return nil
}
type fakeInfo struct {
name string
size int64
+1 -1
View File
@@ -62,7 +62,7 @@ func (s *DeviceStatisticsReference) GetLastConnectionDuration() (time.Duration,
func (s *DeviceStatisticsReference) WasSeen() error {
l.Debugln("stats.DeviceStatisticsReference.WasSeen:", s.device)
return s.ns.PutTime(lastSeenKey, time.Now())
return s.ns.PutTime(lastSeenKey, time.Now().Truncate(time.Second))
}
func (s *DeviceStatisticsReference) LastConnectionDuration(d time.Duration) error {
+2 -2
View File
@@ -61,7 +61,7 @@ func (s *FolderStatisticsReference) GetLastFile() (LastFile, error) {
func (s *FolderStatisticsReference) ReceivedFile(file string, deleted bool) error {
l.Debugln("stats.FolderStatisticsReference.ReceivedFile:", s.folder, file)
if err := s.ns.PutTime("lastFileAt", time.Now()); err != nil {
if err := s.ns.PutTime("lastFileAt", time.Now().Truncate(time.Second)); err != nil {
return err
}
if err := s.ns.PutString("lastFileName", file); err != nil {
@@ -74,7 +74,7 @@ func (s *FolderStatisticsReference) ReceivedFile(file string, deleted bool) erro
}
func (s *FolderStatisticsReference) ScanCompleted() error {
return s.ns.PutTime("lastScan", time.Now())
return s.ns.PutTime("lastScan", time.Now().Truncate(time.Second))
}
func (s *FolderStatisticsReference) GetLastScanTime() (time.Time, error) {
+1 -1
View File
@@ -156,7 +156,7 @@ USER-AGENT: syncthing/1.0
`
searchStr := fmt.Sprintf(tpl, deviceType, timeout/time.Second)
search := []byte(strings.Replace(searchStr, "\n", "\r\n", -1) + "\r\n")
search := []byte(strings.ReplaceAll(searchStr, "\n", "\r\n") + "\r\n")
l.Debugln("Starting discovery of device type", deviceType, "on", intf.Name)
+1
View File
@@ -129,6 +129,7 @@ type Report struct {
BlockPullOrder map[string]int `json:"blockPullOrder,omitempty" since:"3"`
CopyRangeMethod map[string]int `json:"copyRangeMethod,omitempty" since:"3"`
CaseSensitiveFS int `json:"caseSensitiveFS,omitempty" since:"3"`
ReceiveEncrypted int `json:"receiveencrypted,omitempty" since:"3"`
} `json:"folderUsesV3,omitempty" since:"3"`
DeviceUsesV3 struct {
+10 -2
View File
@@ -36,7 +36,7 @@ import (
// are prompted for acceptance of the new report.
const Version = 3
var StartTime = time.Now()
var StartTime = time.Now().Truncate(time.Second)
type Service struct {
cfg config.Wrapper
@@ -267,6 +267,9 @@ func (s *Service) reportData(ctx context.Context, urVersion int, preview bool) (
if cfg.CaseSensitiveFS {
report.FolderUsesV3.CaseSensitiveFS++
}
if cfg.Type == config.FolderTypeReceiveEncrypted {
report.FolderUsesV3.ReceiveEncrypted++
}
}
sort.Ints(report.FolderUsesV3.FsWatcherDelays)
@@ -423,7 +426,12 @@ func CpuBench(ctx context.Context, iterations int, duration time.Duration, useWe
perf = v
}
}
blocksResult = nil
// not looking at the blocksResult makes it unused from a static
// analysis / compiler standpoint...
// blocksResult may be nil at this point if the context is cancelled
if blocksResult != nil {
blocksResult = nil
}
return perf
}
+2 -2
View File
@@ -36,7 +36,7 @@ func newExternal(cfg config.FolderConfiguration) Versioner {
command := cfg.Versioning.Params["command"]
if runtime.GOOS == "windows" {
command = strings.Replace(command, `\`, `\\`, -1)
command = strings.ReplaceAll(command, `\`, `\\`)
}
s := external{
@@ -81,7 +81,7 @@ func (v external) Archive(filePath string) error {
for i, word := range words {
for key, val := range context {
word = strings.Replace(word, key, val, -1)
word = strings.ReplaceAll(word, key, val)
}
words[i] = word
+1 -1
View File
@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "STDISCOSRV" "1" "Mar 09, 2021" "v1" "Syncthing"
.TH "STDISCOSRV" "1" "Mar 11, 2021" "v1" "Syncthing"
.SH NAME
stdiscosrv \- Syncthing Discovery Server
.
+1 -1
View File
@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "STRELAYSRV" "1" "Mar 09, 2021" "v1" "Syncthing"
.TH "STRELAYSRV" "1" "Mar 11, 2021" "v1" "Syncthing"
.SH NAME
strelaysrv \- Syncthing Relay Server
.
+1 -1
View File
@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-BEP" "7" "Mar 09, 2021" "v1" "Syncthing"
.TH "SYNCTHING-BEP" "7" "Mar 11, 2021" "v1" "Syncthing"
.SH NAME
syncthing-bep \- Block Exchange Protocol v1
.
+13 -6
View File
@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-CONFIG" "5" "Mar 09, 2021" "v1" "Syncthing"
.TH "SYNCTHING-CONFIG" "5" "Mar 11, 2021" "v1" "Syncthing"
.SH NAME
syncthing-config \- Syncthing Configuration
.
@@ -100,7 +100,16 @@ currently on disk and available from peers.
.UNINDENT
.SH CONFIG FILE FORMAT
.sp
The following shows an example of the default configuration file (IDs will differ):
The following shows an example of a default configuration file (IDs will differ):
.sp
\fBNOTE:\fP
.INDENT 0.0
.INDENT 3.5
The config examples are present for illustration. Do \fBnot\fP copy them
entirely to use as your config. They are likely out\-of\-date and the values
may no longer correspond to the defaults.
.UNINDENT
.UNINDENT
.INDENT 0.0
.INDENT 3.5
.sp
@@ -148,8 +157,7 @@ The following shows an example of the default configuration file (IDs will diffe
</gui>
<ldap></ldap>
<options>
<listenAddress>tcp://0.0.0.0:8384</listenAddress>
<listenAddress>dynamic+https://relays.syncthing.net/endpoint</listenAddress>
<listenAddress>default</listenAddress>
<globalAnnounceServer>default</globalAnnounceServer>
<globalAnnounceEnabled>true</globalAnnounceEnabled>
<localAnnounceEnabled>true</localAnnounceEnabled>
@@ -820,8 +828,7 @@ Skip verification (true or false).
.nf
.ft C
<options>
<listenAddress>tcp://0.0.0.0:8384</listenAddress>
<listenAddress>dynamic+https://relays.syncthing.net/endpoint</listenAddress>
<listenAddress>default</listenAddress>
<globalAnnounceServer>default</globalAnnounceServer>
<globalAnnounceEnabled>true</globalAnnounceEnabled>
<localAnnounceEnabled>true</localAnnounceEnabled>
+1 -1
View File
@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-DEVICE-IDS" "7" "Mar 09, 2021" "v1" "Syncthing"
.TH "SYNCTHING-DEVICE-IDS" "7" "Mar 11, 2021" "v1" "Syncthing"
.SH NAME
syncthing-device-ids \- Understanding Device IDs
.
+1 -1
View File
@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-EVENT-API" "7" "Mar 09, 2021" "v1" "Syncthing"
.TH "SYNCTHING-EVENT-API" "7" "Mar 11, 2021" "v1" "Syncthing"
.SH NAME
syncthing-event-api \- Event API
.
+1 -1
View File
@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-FAQ" "7" "Mar 09, 2021" "v1" "Syncthing"
.TH "SYNCTHING-FAQ" "7" "Mar 11, 2021" "v1" "Syncthing"
.SH NAME
syncthing-faq \- Frequently Asked Questions
.
+1 -1
View File
@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-GLOBALDISCO" "7" "Mar 09, 2021" "v1" "Syncthing"
.TH "SYNCTHING-GLOBALDISCO" "7" "Mar 11, 2021" "v1" "Syncthing"
.SH NAME
syncthing-globaldisco \- Global Discovery Protocol v3
.
+1 -1
View File
@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-LOCALDISCO" "7" "Mar 09, 2021" "v1" "Syncthing"
.TH "SYNCTHING-LOCALDISCO" "7" "Mar 11, 2021" "v1" "Syncthing"
.SH NAME
syncthing-localdisco \- Local Discovery Protocol v4
.
+1 -1
View File
@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-NETWORKING" "7" "Mar 09, 2021" "v1" "Syncthing"
.TH "SYNCTHING-NETWORKING" "7" "Mar 11, 2021" "v1" "Syncthing"
.SH NAME
syncthing-networking \- Firewall Setup
.
+1 -1
View File
@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-RELAY" "7" "Mar 09, 2021" "v1" "Syncthing"
.TH "SYNCTHING-RELAY" "7" "Mar 11, 2021" "v1" "Syncthing"
.SH NAME
syncthing-relay \- Relay Protocol v1
.
+1 -1
View File
@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-REST-API" "7" "Mar 09, 2021" "v1" "Syncthing"
.TH "SYNCTHING-REST-API" "7" "Mar 11, 2021" "v1" "Syncthing"
.SH NAME
syncthing-rest-api \- REST API
.
+1 -1
View File
@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-SECURITY" "7" "Mar 09, 2021" "v1" "Syncthing"
.TH "SYNCTHING-SECURITY" "7" "Mar 11, 2021" "v1" "Syncthing"
.SH NAME
syncthing-security \- Security Principles
.
+1 -1
View File
@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-STIGNORE" "5" "Mar 09, 2021" "v1" "Syncthing"
.TH "SYNCTHING-STIGNORE" "5" "Mar 11, 2021" "v1" "Syncthing"
.SH NAME
syncthing-stignore \- Prevent files from being synchronized to other nodes
.

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