Compare commits
48
Commits
v1.15.0-rc.2
...
v1.15.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13679284ac | ||
|
|
7931af1078 | ||
|
|
fb4fdaf4c0 | ||
|
|
0d7a77ba85 | ||
|
|
924b96856f | ||
|
|
f7929229c8 | ||
|
|
6b25eb2e79 | ||
|
|
bc08a951f1 | ||
|
|
a87c5515bd | ||
|
|
ebcd22b02b | ||
|
|
4b02b7e6f1 | ||
|
|
fdd823d2cb | ||
|
|
8ef504f745 | ||
|
|
960e850a78 | ||
|
|
ea701a4e9e | ||
|
|
6c573a5762 | ||
|
|
3ac858b150 | ||
|
|
f4372710bf | ||
|
|
f39477bbd5 | ||
|
|
6e5514419d | ||
|
|
f014b7b919 | ||
|
|
81484699db | ||
|
|
6d93d9c488 | ||
|
|
0930bccf88 | ||
|
|
e321bd3941 | ||
|
|
4b02937862 | ||
|
|
21e6849f2d | ||
|
|
39c2d1bc1a | ||
|
|
cd21b8dfa5 | ||
|
|
40fbdc87ce | ||
|
|
1814f4693d | ||
|
|
3f2b584c4e | ||
|
|
e0dd737822 | ||
|
|
d2d4fcc1df | ||
|
|
273ee09925 | ||
|
|
bb886868d2 | ||
|
|
f80ee472c2 | ||
|
|
a12ede3bbe | ||
|
|
97a8777d03 | ||
|
|
8a4c00d82e | ||
|
|
31f859e909 | ||
|
|
4d979a1ce9 | ||
|
|
4465cdf8bc | ||
|
|
3938b61c3f | ||
|
|
cdef503db6 | ||
|
|
df08984a58 | ||
|
|
cf838c71f7 | ||
|
|
9a001051d6 |
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
<a href="https://docs.syncthing.net/users/foldertypes.html" target="_blank"><span class="fas fa-question-circle"></span> <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 & 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
@@ -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
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,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,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
@@ -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,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,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,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,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,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,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,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
Reference in New Issue
Block a user