Compare commits

...

60 Commits

Author SHA1 Message Date
Jakob Borg
7c6fb018ca Fix UPnP line endings (ref #211) 2014-05-28 16:04:20 +02:00
Jakob Borg
9c5c06bf31 Update GUI assets 2014-05-28 14:27:08 +02:00
Jakob Borg
61e3daaead Add shortcut for syncing identical files 2014-05-28 14:27:08 +02:00
Jakob Borg
9c0fde795e Update test for relaxed compareClusterConfig 2014-05-28 14:27:08 +02:00
Jakob Borg
ce4f565e2f Add forgotten file 2014-05-28 14:27:08 +02:00
Jakob Borg
5369a62fd5 Allow repo mismatches to proceed (ref #223) 2014-05-28 12:39:33 +02:00
Jakob Borg
b44016ff70 Don't ping timeout during long transfers (fixes #280) 2014-05-28 13:25:06 +02:00
Jakob Borg
9f76c87880 Merge pull request #305 from jedie/versioning_name
"Simple File Versioning" -> "File Versioning"
2014-05-28 11:25:34 +02:00
Jakob Borg
42ae2898e1 Revert "More memory efficient index sending"
This reverts commit 593f098276.
2014-05-28 10:11:17 +02:00
JensDiemer
dd649a6be4 "Simple File Versioning" -> "File Versioning"
see: http://discourse.syncthing.net/t/v0-8-10-simple-file-versioning/259/7?u=jedie
2014-05-28 10:03:56 +02:00
Jakob Borg
593f098276 More memory efficient index sending 2014-05-28 09:31:46 +02:00
Jakob Borg
4a87221f16 Silence Windows chtime warnings (fixes #288) 2014-05-28 09:27:00 +02:00
Jakob Borg
7745ed34d3 Don't stop discovery on send errors (fixes #240) 2014-05-28 07:03:47 +02:00
Jakob Borg
8fe546c4a2 Don't start repo with non-directory root (fixes #276) 2014-05-28 06:55:30 +02:00
Jakob Borg
381f6aeaf6 Handle and prevent invalid repo ID. Validate node ID format. (fixes #286) 2014-05-28 05:27:34 +02:00
Jakob Borg
9154bacced Recompile assets for previous 2014-05-27 11:17:22 +02:00
Jakob Borg
dc0dc8efb4 Merge pull request #301 from jedie/reformat_table2
reformat "folder" and "shared with" table items
2014-05-27 11:12:21 +02:00
JensDiemer
b062d5dd7f reformat "folder" and "shared with" table items
using white-space:nowrap;
2014-05-27 10:58:55 +02:00
Jakob Borg
c519e582b5 Expand tilde on Windows as well (fixes #289) 2014-05-26 16:58:03 +02:00
Jakob Borg
6b9dce36bf Default listen host should be 0.0.0.0 (again) (ref #216) 2014-05-26 15:01:04 +02:00
Jakob Borg
8e0520887a Send default permissions 0777 on directories when IgnorePerms set (ref #284) 2014-05-26 11:09:35 +02:00
Jakob Borg
cfd1fdb38e Don't set permissions 000 on directories with NoPermissionBits set (ref #284) 2014-05-26 11:08:54 +02:00
Jakob Borg
c6ba0208d0 Don't require SSE in 32 bit builds (fixes #277) 2014-05-25 21:36:38 +02:00
Jakob Borg
3d055bbb79 Simple file versioning (fixes #218) 2014-05-25 20:49:08 +02:00
Jakob Borg
dd971b56e5 Correct tests for uppercase-only node IDs 2014-05-25 14:54:50 +02:00
Jakob Borg
4031f5e24b Fix version comparison in upgrade 2014-05-24 23:22:08 +02:00
Jakob Borg
1cd7cc6869 Configuration directory is machine local (Windows) 2014-05-24 22:45:50 +02:00
Jakob Borg
9de2864db3 Repair and clean HTML structure 2014-05-24 21:56:09 +02:00
Jakob Borg
c27861cbaf Show node ID/name/address mapping at startup (ref #249) 2014-05-24 21:39:08 +02:00
Jakob Borg
c2f75d3689 Show counters for total data transferred (fixes #265) 2014-05-24 21:34:11 +02:00
Jakob Borg
5454ca1cf7 Sort list of sharing nodes (fixes #266) 2014-05-24 21:13:35 +02:00
Jakob Borg
8644bf30a9 Syncthing might be restarted after shutdown (fixes #274) 2014-05-24 21:08:53 +02:00
Jakob Borg
db3341a178 In Sync is now Up to Date (fixes #268) 2014-05-24 21:06:46 +02:00
Jakob Borg
e2cb0219c7 Node IDs are always upper case (ref #269) 2014-05-24 21:01:21 +02:00
Jakob Borg
217f29de76 Don't mess up unset properties of new nodes/repos 2014-05-24 21:00:47 +02:00
Jakob Borg
8661afcb4f Expand ~/ on Windows as well 2014-05-24 13:34:40 +02:00
Jakob Borg
ed07fc0f2c Simplify node/repo headers on extra-small screens 2014-05-24 12:38:44 +02:00
Jakob Borg
4af3f77a9a Wait for parent to release sockets (fixes #267, fixes #241) 2014-05-24 12:28:36 +02:00
Jakob Borg
8c4f07ef1b Crash slightly more controlled under weird circumstances... 2014-05-24 12:08:28 +02:00
Jakob Borg
1a231d39a5 Default permission bits are 0666 2014-05-24 08:53:54 +02:00
Jakob Borg
17e3d14272 Correct formatting of warning messages 2014-05-24 08:26:05 +02:00
Jakob Borg
03182c7714 Get tests in line with reality 2014-05-23 15:54:45 +02:00
Jakob Borg
963078f6ac Don't reuse certificate serials 2014-05-23 14:43:17 +02:00
Jakob Borg
8356b58b1d Implement IgnorePerms 2014-05-23 14:31:16 +02:00
Jakob Borg
303ce02271 Internal support for ignoring permissions 2014-05-23 13:10:26 +02:00
Jakob Borg
bcdc3ecdae There should be only One 2014-05-23 12:55:24 +02:00
Jakob Borg
b60d648e22 Convenience functions for flag testing 2014-05-23 12:53:26 +02:00
Jakob Borg
7bc36cbbd1 Add bit 17, No Permission Bits 2014-05-23 12:53:11 +02:00
Jakob Borg
04130fcb15 Allow GUI development with standard binary 2014-05-22 16:12:19 +02:00
Jakob Borg
52d8e4c691 Set local discovery port in GUI 2014-05-22 09:38:11 +02:00
Jakob Borg
ae0193b724 Configurable local announcement port (fixes #256) 2014-05-22 09:35:54 +02:00
Jakob Borg
2e1c33206f Fix discosrv build, build as part of all (fixes #257) 2014-05-22 08:46:19 +02:00
Jakob Borg
0c642ec7cf Un-ignore Godeps 2014-05-21 22:23:18 +02:00
Jakob Borg
b3ca96eeba Merge pull request #255 from KayoticSully/master
Resolves Issue #239
2014-05-21 22:21:33 +02:00
Jakob Borg
ae0e033178 Add KayoticSully 2014-05-21 22:20:53 +02:00
Ryan Sullivan
a97985b428 Added suggestions to settings fix. 2014-05-21 15:54:16 -04:00
Ryan Sullivan
63c0f11458 Merge remote-tracking branch 'upstream/master'
Conflicts:
	auto/gui.files.go
2014-05-21 15:15:37 -04:00
Ryan Sullivan
b336b2c336 Merge remote-tracking branch 'upstream/master'
Conflicts:
	auto/gui.files.go
2014-05-21 14:38:54 -04:00
Ryan Sullivan
8a5a573851 Fixed issue #239 Saving an unchanged config does not prompt for reboot 2014-05-21 14:35:51 -04:00
Ryan Sullivan
358862c7ad Ignore sublime files and Godeps changes 2014-05-21 13:50:06 -04:00
42 changed files with 1096 additions and 634 deletions

3
.gitignore vendored
View File

@@ -5,3 +5,6 @@ stcli.exe
*.tar.gz
*.zip
*.asc
*.sublime*
discosrv
stpidx

View File

@@ -4,4 +4,5 @@ Brandon Philips <brandon@ifup.org>
James Patterson <jamespatterson@operamail.com>
Jens Diemer <github.com@jensdiemer.de>
Philippe Schommers <philippe@schommers.be>
Ryan Sullivan <kayoticsully@gmail.com>
Veeti Paananen <veeti.paananen@rojekti.fi>

View File

File diff suppressed because one or more lines are too long

View File

@@ -102,9 +102,7 @@ func (b *Beacon) writer() {
if debug {
l.Debugln(err)
}
return
}
if debug {
} else if debug {
l.Debugf("sent %d bytes to %s", len(bs), dst)
}
}

View File

@@ -1,6 +1,7 @@
#!/usr/bin/env bash
export COPYFILE_DISABLE=true
export GO386=387 # Don't use SSE on 32 bit builds
distFiles=(README.md LICENSE) # apart from the binary itself
version=$(git describe --always --dirty)
@@ -88,7 +89,8 @@ case "$1" in
;;
guidev)
build -tags guidev
echo "Syncthing is already built for GUI developments. Try:"
echo " STGUIASSETS=~/someDir/gui syncthing"
;;
test)
@@ -112,6 +114,10 @@ case "$1" in
test || exit 1
assets
godep go build ./discover/cmd/discosrv
godep go build ./cmd/stpidx
godep go build ./cmd/stcli
for os in darwin-amd64 linux-386 linux-amd64 freebsd-amd64 windows-amd64 windows-386 ; do
export GOOS=${os%-*}
export GOARCH=${os#*-}

View File

@@ -4,17 +4,21 @@ import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"math/rand"
"mime"
"net"
"net/http"
"path/filepath"
"runtime"
"sync"
"time"
"crypto/tls"
"code.google.com/p/go.crypto/bcrypt"
"github.com/calmh/syncthing/auto"
"github.com/calmh/syncthing/config"
"github.com/calmh/syncthing/logger"
"github.com/calmh/syncthing/model"
@@ -31,8 +35,7 @@ var (
configInSync = true
guiErrors = []guiError{}
guiErrorsMut sync.Mutex
static = embeddedStatic()
staticFunc = static.(func(http.ResponseWriter, *http.Request, *log.Logger))
static func(http.ResponseWriter, *http.Request, *log.Logger)
)
const (
@@ -43,7 +46,7 @@ func init() {
l.AddHandler(logger.LevelWarn, showGuiError)
}
func startGUI(cfg config.GUIConfiguration, m *model.Model) error {
func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) error {
var listener net.Listener
var err error
if cfg.UseTLS {
@@ -70,6 +73,12 @@ func startGUI(cfg config.GUIConfiguration, m *model.Model) error {
}
}
if len(assetDir) > 0 {
static = martini.Static(assetDir).(func(http.ResponseWriter, *http.Request, *log.Logger))
} else {
static = embeddedStatic()
}
router := martini.NewRouter()
router.Get("/", getRoot)
router.Get("/rest/version", restGetVersion)
@@ -108,7 +117,7 @@ func startGUI(cfg config.GUIConfiguration, m *model.Model) error {
func getRoot(w http.ResponseWriter, r *http.Request) {
r.URL.Path = "/index.html"
staticFunc(w, r, nil)
static(w, r, nil)
}
func restMiddleware(w http.ResponseWriter, r *http.Request) {
@@ -175,23 +184,24 @@ func restGetConfig(w http.ResponseWriter) {
}
func restPostConfig(req *http.Request) {
var prevPassHash = cfg.GUI.Password
err := json.NewDecoder(req.Body).Decode(&cfg)
var newCfg config.Configuration
err := json.NewDecoder(req.Body).Decode(&newCfg)
if err != nil {
l.Warnln(err)
} else {
if cfg.GUI.Password == "" {
if newCfg.GUI.Password == "" {
// Leave it empty
} else if cfg.GUI.Password != unchangedPassword {
hash, err := bcrypt.GenerateFromPassword([]byte(cfg.GUI.Password), 0)
} else if newCfg.GUI.Password == unchangedPassword {
newCfg.GUI.Password = cfg.GUI.Password
} else {
hash, err := bcrypt.GenerateFromPassword([]byte(newCfg.GUI.Password), 0)
if err != nil {
l.Warnln(err)
} else {
cfg.GUI.Password = string(hash)
newCfg.GUI.Password = string(hash)
}
} else {
cfg.GUI.Password = prevPassHash
}
cfg = newCfg
saveConfig()
configInSync = false
}
@@ -235,7 +245,7 @@ func restGetSystem(w http.ResponseWriter) {
res["goroutines"] = runtime.NumGoroutine()
res["alloc"] = m.Alloc
res["sys"] = m.Sys
res["tilde"] = expandTilde("~/")
res["tilde"] = expandTilde("~")
if cfg.Options.GlobalAnnEnabled && discoverer != nil {
res["extAnnounceOK"] = discoverer.ExtAnnounceOK()
}
@@ -340,3 +350,29 @@ func basic(username string, passhash string) http.HandlerFunc {
}
}
}
func embeddedStatic() func(http.ResponseWriter, *http.Request, *log.Logger) {
var modt = time.Now().UTC().Format(http.TimeFormat)
return func(res http.ResponseWriter, req *http.Request, log *log.Logger) {
file := req.URL.Path
if file[0] == '/' {
file = file[1:]
}
bs, ok := auto.Assets[file]
if !ok {
return
}
mtype := mime.TypeByExtension(filepath.Ext(req.URL.Path))
if len(mtype) != 0 {
res.Header().Set("Content-Type", mtype)
}
res.Header().Set("Content-Length", fmt.Sprintf("%d", len(bs)))
res.Header().Set("Last-Modified", modt)
res.Write(bs)
}
}

View File

@@ -1,9 +0,0 @@
//+build guidev
package main
import "github.com/codegangsta/martini"
func embeddedStatic() interface{} {
return martini.Static("gui")
}

View File

@@ -1,40 +0,0 @@
//+build !guidev
package main
import (
"fmt"
"log"
"mime"
"net/http"
"path/filepath"
"time"
"github.com/calmh/syncthing/auto"
)
func embeddedStatic() interface{} {
var modt = time.Now().UTC().Format(http.TimeFormat)
return func(res http.ResponseWriter, req *http.Request, log *log.Logger) {
file := req.URL.Path
if file[0] == '/' {
file = file[1:]
}
bs, ok := auto.Assets[file]
if !ok {
return
}
mtype := mime.TypeByExtension(filepath.Ext(req.URL.Path))
if len(mtype) != 0 {
res.Header().Set("Content-Type", mtype)
}
res.Header().Set("Content-Length", fmt.Sprintf("%d", len(bs)))
res.Header().Set("Last-Modified", modt)
res.Write(bs)
}
}

View File

@@ -24,6 +24,7 @@ import (
"github.com/calmh/syncthing/discover"
"github.com/calmh/syncthing/logger"
"github.com/calmh/syncthing/model"
"github.com/calmh/syncthing/osutil"
"github.com/calmh/syncthing/protocol"
"github.com/calmh/syncthing/upnp"
"github.com/juju/ratelimit"
@@ -84,7 +85,9 @@ const (
- "xdr" (the xdr package)
- "all" (all of the above)
STCPUPROFILE Write CPU profile to the specified file.`
STCPUPROFILE Write CPU profile to the specified file.
STGUIASSETS Directory to load GUI assets from. Overrides compiled in assets.`
)
func main() {
@@ -98,11 +101,6 @@ func main() {
flag.Usage = usageFor(flag.CommandLine, usage, extraUsage)
flag.Parse()
if len(os.Getenv("STRESTART")) > 0 {
// Give the parent process time to exit and release sockets etc.
time.Sleep(1 * time.Second)
}
if showVersion {
fmt.Println(LongVersion)
return
@@ -133,7 +131,12 @@ func main() {
// continue. We don't much care if this fails at this point, we will
// be checking that later.
oldDefault := expandTilde("~/.syncthing")
var oldDefault string
if runtime.GOOS == "windows" {
oldDefault = filepath.Join(os.Getenv("AppData"), "Syncthing")
} else {
oldDefault = expandTilde("~/.syncthing")
}
if _, err := os.Stat(oldDefault); err == nil {
os.MkdirAll(filepath.Dir(confDir), 0700)
if err := os.Rename(oldDefault, confDir); err == nil {
@@ -200,9 +203,9 @@ func main() {
l.FatalErr(err)
cfg.GUI.Address = fmt.Sprintf("127.0.0.1:%d", port)
port, err = getFreePort("", 22000)
port, err = getFreePort("0.0.0.0", 22000)
l.FatalErr(err)
cfg.Options.ListenAddress = []string{fmt.Sprintf(":%d", port)}
cfg.Options.ListenAddress = []string{fmt.Sprintf("0.0.0.0:%d", port)}
saveConfig()
l.Infof("Edit %s to taste or use the GUI\n", cfgFile)
@@ -224,6 +227,10 @@ func main() {
}()
}
if len(os.Getenv("STRESTART")) > 0 {
waitForParentExit()
}
// The TLS configuration is used for both the listening socket and outgoing
// connections.
@@ -250,8 +257,8 @@ func main() {
if repo.Invalid != "" {
continue
}
dir := expandTilde(repo.Directory)
m.AddRepo(repo.ID, dir, repo.Nodes)
repo.Directory = expandTilde(repo.Directory)
m.AddRepo(repo)
}
// GUI
@@ -279,7 +286,7 @@ func main() {
}
l.Infof("Starting web GUI on %s://%s:%d/", proto, hostShow, addr.Port)
err := startGUI(cfg.GUI, m)
err := startGUI(cfg.GUI, os.Getenv("STGUIASSETS"), m)
if err != nil {
l.Fatalln("Cannot start GUI:", err)
}
@@ -360,7 +367,28 @@ func main() {
defer pprof.StopCPUProfile()
}
for _, node := range cfg.Nodes {
if len(node.Name) > 0 {
l.Infof("Node %s is %q at %v", node.NodeID, node.Name, node.Addresses)
}
}
<-stop
l.Okln("Exiting")
}
func waitForParentExit() {
l.Infoln("Waiting for parent to exit...")
// Wait for the listen address to become free, indicating that the parent has exited.
for {
ln, err := net.Listen("tcp", cfg.Options.ListenAddress[0])
if err == nil {
ln.Close()
break
}
time.Sleep(250 * time.Millisecond)
}
l.Okln("Continuing")
}
func setupUPnP() int {
@@ -471,7 +499,7 @@ func saveConfigLoop(cfgFile string) {
continue
}
err = model.Rename(cfgFile+".tmp", cfgFile)
err = osutil.Rename(cfgFile+".tmp", cfgFile)
if err != nil {
l.Warnln(err)
}
@@ -615,7 +643,7 @@ next:
}
func discovery(extPort int) *discover.Discoverer {
disc, err := discover.NewDiscoverer(myID, cfg.Options.ListenAddress)
disc, err := discover.NewDiscoverer(myID, cfg.Options.ListenAddress, cfg.Options.LocalAnnPort)
if err != nil {
l.Warnf("No discovery possible (%v)", err)
return nil
@@ -648,7 +676,7 @@ func ensureDir(dir string, mode int) {
func getDefaultConfDir() string {
switch runtime.GOOS {
case "windows":
return filepath.Join(os.Getenv("AppData"), "Syncthing")
return filepath.Join(os.Getenv("LocalAppData"), "Syncthing")
case "darwin":
return expandTilde("~/Library/Application Support/Syncthing")
@@ -663,7 +691,12 @@ func getDefaultConfDir() string {
}
func expandTilde(p string) string {
if runtime.GOOS == "windows" || !strings.HasPrefix(p, "~/") {
if p == "~" {
return getHomeDir()
}
p = filepath.FromSlash(p)
if !strings.HasPrefix(p, fmt.Sprintf("~%c", os.PathSeparator)) {
return p
}

View File

@@ -11,6 +11,7 @@ import (
"encoding/binary"
"encoding/pem"
"math/big"
mr "math/rand"
"os"
"path/filepath"
"strings"
@@ -50,7 +51,7 @@ func newCertificate(dir string, prefix string) {
notAfter := time.Date(2049, 12, 31, 23, 59, 59, 0, time.UTC)
template := x509.Certificate{
SerialNumber: new(big.Int).SetInt64(0),
SerialNumber: new(big.Int).SetInt64(mr.Int63()),
Subject: pkix.Name{
CommonName: tlsName,
},

View File

@@ -1,11 +1,10 @@
// +build !windows
package main
import (
"archive/tar"
"compress/gzip"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
@@ -14,8 +13,10 @@ import (
"path"
"path/filepath"
"runtime"
"strconv"
"strings"
"bytes"
"bitbucket.org/kardianos/osext"
)
@@ -33,6 +34,10 @@ type githubAsset struct {
var GoArchExtra string // "", "v5", "v6", "v7"
func upgrade() error {
if runtime.GOOS == "windows" {
return errors.New("Upgrade currently unsupported on Windows")
}
path, err := osext.Executable()
if err != nil {
return err
@@ -52,14 +57,15 @@ func upgrade() error {
}
rel := rels[0]
if rel.Tag > Version {
l.Infof("Attempting upgrade to %s...", rel.Tag)
} else if rel.Tag == Version {
l.Okf("Already running the latest version, %s. Not upgrading.", Version)
return nil
} else {
switch compareVersions(rel.Tag, Version) {
case -1:
l.Okf("Current version %s is newer than latest release %s. Not upgrading.", Version, rel.Tag)
return nil
case 0:
l.Okf("Already running the latest version, %s. Not upgrading.", Version)
return nil
default:
l.Infof("Attempting upgrade to %s...", rel.Tag)
}
expectedRelease := fmt.Sprintf("syncthing-%s-%s%s-%s.", runtime.GOOS, runtime.GOARCH, GoArchExtra, rel.Tag)
@@ -147,3 +153,18 @@ func readTarGZ(url string, dir string) (string, error) {
return "", fmt.Errorf("No upgrade found")
}
func compareVersions(a, b string) int {
return bytes.Compare(versionParts(a), versionParts(b))
}
func versionParts(v string) []byte {
parts := strings.Split(v, "-")
fields := strings.Split(parts[0], ".")
res := make([]byte, len(fields))
for i, s := range fields {
v, _ := strconv.Atoi(s)
res[i] = byte(v)
}
return res
}

View File

@@ -0,0 +1,27 @@
package main
import "testing"
var testcases = []struct {
a, b string
r int
}{
{"0.1.2", "0.1.2", 0},
{"0.1.3", "0.1.2", 1},
{"0.1.1", "0.1.2", -1},
{"0.3.0", "0.1.2", 1},
{"0.0.9", "0.1.2", -1},
{"1.1.2", "0.1.2", 1},
{"0.1.2", "1.1.2", -1},
{"0.1.10", "0.1.9", 1},
{"0.10.0", "0.2.0", 1},
{"30.10.0", "4.9.0", 1},
}
func TestCompareVersions(t *testing.T) {
for _, tc := range testcases {
if r := compareVersions(tc.a, tc.b); r != tc.r {
t.Errorf("compareVersions(%q, %q): %d != %d", tc.a, tc.b, r, tc.r)
}
}
}

View File

@@ -1,9 +0,0 @@
// +build windows
package main
import "errors"
func upgrade() error {
return errors.New("Upgrade currently unsupported on Windows")
}

View File

@@ -27,12 +27,56 @@ type Configuration struct {
}
type RepositoryConfiguration struct {
ID string `xml:"id,attr"`
Directory string `xml:"directory,attr"`
Nodes []NodeConfiguration `xml:"node"`
ReadOnly bool `xml:"ro,attr"`
Invalid string `xml:"-"` // Set at runtime when there is an error, not saved
nodeIDs []string
ID string `xml:"id,attr"`
Directory string `xml:"directory,attr"`
Nodes []NodeConfiguration `xml:"node"`
ReadOnly bool `xml:"ro,attr"`
IgnorePerms bool `xml:"ignorePerms,attr"`
Invalid string `xml:"-"` // Set at runtime when there is an error, not saved
Versioning VersioningConfiguration `xml:"versioning"`
nodeIDs []string
}
type VersioningConfiguration struct {
Type string `xml:"type,attr"`
Params map[string]string
}
type InternalVersioningConfiguration struct {
Type string `xml:"type,attr,omitempty"`
Params []InternalParam `xml:"param"`
}
type InternalParam struct {
Key string `xml:"key,attr"`
Val string `xml:"val,attr"`
}
func (c *VersioningConfiguration) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
var tmp InternalVersioningConfiguration
tmp.Type = c.Type
for k, v := range c.Params {
tmp.Params = append(tmp.Params, InternalParam{k, v})
}
return e.EncodeElement(tmp, start)
}
func (c *VersioningConfiguration) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
var tmp InternalVersioningConfiguration
err := d.DecodeElement(&tmp, &start)
if err != nil {
return err
}
c.Type = tmp.Type
c.Params = make(map[string]string, len(tmp.Params))
for _, p := range tmp.Params {
c.Params[p.Key] = p.Val
}
return nil
}
func (r *RepositoryConfiguration) NodeIDs() []string {
@@ -55,6 +99,7 @@ type OptionsConfiguration struct {
GlobalAnnServer string `xml:"globalAnnounceServer" default:"announce.syncthing.net:22025"`
GlobalAnnEnabled bool `xml:"globalAnnounceEnabled" default:"true"`
LocalAnnEnabled bool `xml:"localAnnounceEnabled" default:"true"`
LocalAnnPort int `xml:"localAnnouncePort" default:"21025"`
ParallelRequests int `xml:"parallelRequests" default:"16"`
MaxSendKbps int `xml:"maxSendKbps"`
RescanIntervalS int `xml:"rescanIntervalS" default:"60"`
@@ -189,6 +234,7 @@ func Load(rd io.Reader, myID string) (Configuration, error) {
// Strip spaces and dashes
node.NodeID = strings.Replace(node.NodeID, "-", "", -1)
node.NodeID = strings.Replace(node.NodeID, " ", "", -1)
node.NodeID = strings.ToUpper(node.NodeID)
}
// Check for missing, bad or duplicate repository ID:s
@@ -198,7 +244,7 @@ func Load(rd io.Reader, myID string) (Configuration, error) {
repo := &cfg.Repositories[i]
if len(repo.Directory) == 0 {
repo.Invalid = "empty directory"
repo.Invalid = "no directory configured"
continue
}

View File

@@ -14,6 +14,7 @@ func TestDefaultValues(t *testing.T) {
GlobalAnnServer: "announce.syncthing.net:22025",
GlobalAnnEnabled: true,
LocalAnnEnabled: true,
LocalAnnPort: 21025,
ParallelRequests: 16,
MaxSendKbps: 0,
RescanIntervalS: 60,
@@ -37,10 +38,10 @@ func TestNodeConfig(t *testing.T) {
v1data := []byte(`
<configuration version="1">
<repository id="test" directory="~/Sync">
<node id="node1" name="node one">
<node id="NODE1" name="node one">
<address>a</address>
</node>
<node id="node2" name="node two">
<node id="NODE2" name="node two">
<address>b</address>
</node>
</repository>
@@ -53,20 +54,20 @@ func TestNodeConfig(t *testing.T) {
v2data := []byte(`
<configuration version="2">
<repository id="test" directory="~/Sync" ro="true">
<node id="node1"/>
<node id="node2"/>
<node id="NODE1"/>
<node id="NODE2"/>
</repository>
<node id="node1" name="node one">
<node id="NODE1" name="node one">
<address>a</address>
</node>
<node id="node2" name="node two">
<node id="NODE2" name="node two">
<address>b</address>
</node>
</configuration>
`)
for i, data := range [][]byte{v1data, v2data} {
cfg, err := Load(bytes.NewReader(data), "node1")
cfg, err := Load(bytes.NewReader(data), "NODE1")
if err != nil {
t.Error(err)
}
@@ -75,23 +76,23 @@ func TestNodeConfig(t *testing.T) {
{
ID: "test",
Directory: "~/Sync",
Nodes: []NodeConfiguration{{NodeID: "node1"}, {NodeID: "node2"}},
Nodes: []NodeConfiguration{{NodeID: "NODE1"}, {NodeID: "NODE2"}},
ReadOnly: true,
},
}
expectedNodes := []NodeConfiguration{
{
NodeID: "node1",
NodeID: "NODE1",
Name: "node one",
Addresses: []string{"a"},
},
{
NodeID: "node2",
NodeID: "NODE2",
Name: "node two",
Addresses: []string{"b"},
},
}
expectedNodeIDs := []string{"node1", "node2"}
expectedNodeIDs := []string{"NODE1", "NODE2"}
if cfg.Version != 2 {
t.Errorf("%d: Incorrect version %d != 2", i, cfg.Version)
@@ -145,6 +146,7 @@ func TestOverriddenValues(t *testing.T) {
<globalAnnounceServer>syncthing.nym.se:22025</globalAnnounceServer>
<globalAnnounceEnabled>false</globalAnnounceEnabled>
<localAnnounceEnabled>false</localAnnounceEnabled>
<localAnnouncePort>42123</localAnnouncePort>
<parallelRequests>32</parallelRequests>
<maxSendKbps>1234</maxSendKbps>
<rescanIntervalS>600</rescanIntervalS>
@@ -161,6 +163,7 @@ func TestOverriddenValues(t *testing.T) {
GlobalAnnServer: "syncthing.nym.se:22025",
GlobalAnnEnabled: false,
LocalAnnEnabled: false,
LocalAnnPort: 42123,
ParallelRequests: 32,
MaxSendKbps: 1234,
RescanIntervalS: 600,
@@ -197,25 +200,25 @@ func TestNodeAddresses(t *testing.T) {
name, _ := os.Hostname()
expected := []NodeConfiguration{
{
NodeID: "n1",
NodeID: "N1",
Addresses: []string{"dynamic"},
},
{
NodeID: "n2",
NodeID: "N2",
Addresses: []string{"dynamic"},
},
{
NodeID: "n3",
NodeID: "N3",
Addresses: []string{"dynamic"},
},
{
NodeID: "n4",
NodeID: "N4",
Name: name, // Set when auto created
Addresses: []string{"dynamic"},
},
}
cfg, err := Load(bytes.NewReader(data), "n4")
cfg, err := Load(bytes.NewReader(data), "N4")
if err != nil {
t.Error(err)
}

View File

@@ -5,6 +5,7 @@ import (
"encoding/hex"
"flag"
"fmt"
"io"
"log"
"net"
"os"
@@ -16,18 +17,18 @@ import (
"github.com/juju/ratelimit"
)
type Node struct {
Addresses []Address
Updated time.Time
type node struct {
addresses []address
updated time.Time
}
type Address struct {
IP []byte
Port uint16
type address struct {
ip []byte
port uint16
}
var (
nodes = make(map[string]Node)
nodes = make(map[string]node)
lock sync.Mutex
queries = 0
announces = 0
@@ -134,7 +135,7 @@ func limit(addr *net.UDPAddr) bool {
func handleAnnounceV2(addr *net.UDPAddr, buf []byte) {
var pkt discover.AnnounceV2
err := pkt.UnmarshalXDR(buf)
if err != nil {
if err != nil && err != io.EOF {
log.Println("AnnounceV2 Unmarshal:", err)
log.Println(hex.Dump(buf))
return
@@ -152,25 +153,25 @@ func handleAnnounceV2(addr *net.UDPAddr, buf []byte) {
ip = addr.IP.To16()
}
var addrs []Address
for _, addr := range pkt.Addresses {
var addrs []address
for _, addr := range pkt.This.Addresses {
tip := addr.IP
if len(tip) == 0 {
tip = ip
}
addrs = append(addrs, Address{
IP: tip,
Port: addr.Port,
addrs = append(addrs, address{
ip: tip,
port: addr.Port,
})
}
node := Node{
Addresses: addrs,
Updated: time.Now(),
node := node{
addresses: addrs,
updated: time.Now(),
}
lock.Lock()
nodes[pkt.NodeID] = node
nodes[pkt.This.ID] = node
lock.Unlock()
}
@@ -191,19 +192,21 @@ func handleQueryV2(conn *net.UDPConn, addr *net.UDPAddr, buf []byte) {
queries++
lock.Unlock()
if ok && len(node.Addresses) > 0 {
pkt := discover.AnnounceV2{
Magic: discover.AnnouncementMagicV2,
NodeID: pkt.NodeID,
if ok && len(node.addresses) > 0 {
ann := discover.AnnounceV2{
Magic: discover.AnnouncementMagicV2,
This: discover.Node{
ID: pkt.NodeID,
},
}
for _, addr := range node.Addresses {
pkt.Addresses = append(pkt.Addresses, discover.Address{IP: addr.IP, Port: addr.Port})
for _, addr := range node.addresses {
ann.This.Addresses = append(ann.This.Addresses, discover.Address{IP: addr.ip, Port: addr.port})
}
if debug {
log.Printf("-> %v %#v", addr, pkt)
}
tb := pkt.MarshalXDR()
tb := ann.MarshalXDR()
_, _, err = conn.WriteMsgUDP(tb, nil, addr)
if err != nil {
log.Println("QueryV2 response write:", err)
@@ -235,7 +238,7 @@ func logStats(file string, intv int) {
var deleted = 0
for id, node := range nodes {
if time.Since(node.Updated) > 60*time.Minute {
if time.Since(node.updated) > 60*time.Minute {
delete(nodes, id)
deleted++
}

View File

@@ -13,10 +13,6 @@ import (
"github.com/calmh/syncthing/buffers"
)
const (
AnnouncementPort = 21025
)
type Discoverer struct {
myID string
listenAddrs []string
@@ -42,8 +38,8 @@ var (
// When we hit this many errors in succession, we stop.
const maxErrors = 30
func NewDiscoverer(id string, addresses []string) (*Discoverer, error) {
b, err := beacon.New(21025)
func NewDiscoverer(id string, addresses []string, localPort int) (*Discoverer, error) {
b, err := beacon.New(localPort)
if err != nil {
return nil, err
}
@@ -191,9 +187,8 @@ func (d *Discoverer) sendExternalAnnouncements() {
} else {
buf = d.announcementPkt()
}
var errCounter = 0
for errCounter < maxErrors {
for {
var ok bool
if debug {
@@ -205,11 +200,8 @@ func (d *Discoverer) sendExternalAnnouncements() {
if debug {
l.Debugln("discover: warning:", err)
}
errCounter++
ok = false
} else {
errCounter = 0
// Verify that the announce server responds positively for our node ID
time.Sleep(1 * time.Second)
@@ -218,7 +210,6 @@ func (d *Discoverer) sendExternalAnnouncements() {
l.Debugln("discover: external lookup check:", res)
}
ok = len(res) > 0
}
d.extAnnounceOKmut.Lock()
@@ -231,7 +222,6 @@ func (d *Discoverer) sendExternalAnnouncements() {
time.Sleep(60 * time.Second)
}
}
l.Warnf("Global discovery: %v: stopping due to too many errors: %v", remote, err)
}
func (d *Discoverer) recvAnnouncements() {

View File

@@ -76,7 +76,7 @@ func (m *Set) ReplaceWithDelete(id uint, fs []scanner.File) {
for _, ck := range m.remoteKey[cid.LocalID] {
if _, ok := nf[ck.Name]; !ok {
cf := m.files[ck].File
if cf.Flags&protocol.FlagDeleted != protocol.FlagDeleted {
if !protocol.IsDeleted(cf.Flags) {
cf.Flags |= protocol.FlagDeleted
cf.Blocks = nil
cf.Size = 0
@@ -193,7 +193,7 @@ func (m *Set) equals(id uint, fs []scanner.File) bool {
curWithoutDeleted := make(map[string]key)
for _, k := range m.remoteKey[id] {
f := m.files[k].File
if f.Flags&protocol.FlagDeleted == 0 {
if !protocol.IsDeleted(f.Flags) {
curWithoutDeleted[f.Name] = k
}
}
@@ -210,6 +210,9 @@ func (m *Set) equals(id uint, fs []scanner.File) bool {
func (m *Set) update(cid uint, fs []scanner.File) {
remFiles := m.remoteKey[cid]
if remFiles == nil {
l.Fatalln("update before replace for cid", cid)
}
for _, f := range fs {
n := f.Name
fk := keyFor(f)

View File

@@ -30,8 +30,9 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
{id: 'ParallelRequests', descr: 'Max Outstanding Requests', type: 'number', restart: true},
{id: 'MaxChangeKbps', descr: 'Max File Change Rate (KiB/s)', type: 'number', restart: true},
{id: 'GlobalAnnEnabled', descr: 'Global Announce', type: 'bool', restart: true},
{id: 'LocalAnnEnabled', descr: 'Local Announce', type: 'bool', restart: true},
{id: 'GlobalAnnEnabled', descr: 'Global Discovery', type: 'bool', restart: true},
{id: 'LocalAnnEnabled', descr: 'Local Discovery', type: 'bool', restart: true},
{id: 'LocalAnnPort', descr: 'Local Discovery Port', type: 'number', restart: true},
{id: 'StartBrowser', descr: 'Start Browser', type: 'bool'},
{id: 'UPnPEnabled', descr: 'Enable UPnP', type: 'bool'},
];
@@ -52,6 +53,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
if (restarting) {
$scope.init();
$('#restarting').modal('hide');
$('#shutdown').modal('hide');
restarting = false;
}
}
@@ -84,9 +86,6 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
id;
prevDate = now;
$scope.inbps = 0;
$scope.outbps = 0;
for (id in data) {
if (!data.hasOwnProperty(id)) {
continue;
@@ -98,8 +97,6 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
data[id].inbps = 0;
data[id].outbps = 0;
}
$scope.inbps += data[id].inbps;
$scope.outbps += data[id].outbps;
}
$scope.connections = data;
});
@@ -162,7 +159,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
var conn = $scope.connections[nodeCfg.NodeID];
if (conn) {
if (conn.Completion === 100) {
return 'In Sync';
return 'Up to Date';
} else {
return 'Syncing (' + conn.Completion + '%)';
}
@@ -254,13 +251,25 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
};
$scope.editSettings = function () {
// Make a working copy
$scope.config.workingOptions = angular.copy($scope.config.Options);
$scope.config.workingGUI = angular.copy($scope.config.GUI);
$('#settings').modal({backdrop: 'static', keyboard: true});
}
$scope.saveSettings = function () {
$scope.configInSync = false;
$scope.config.Options.ListenAddress = $scope.config.Options.ListenStr.split(',').map(function (x) { return x.trim(); });
$http.post(urlbase + '/config', JSON.stringify($scope.config), {headers: {'Content-Type': 'application/json'}});
// Make sure something changed
var changed = ! angular.equals($scope.config.Options, $scope.config.workingOptions) ||
! angular.equals($scope.config.GUI, $scope.config.workingGUI);
if(changed){
$scope.config.Options = angular.copy($scope.config.workingOptions);
$scope.config.GUI = angular.copy($scope.config.workingGUI);
$scope.configInSync = false;
$scope.config.Options.ListenAddress = $scope.config.Options.ListenStr.split(',').map(function (x) { return x.trim(); });
$http.post(urlbase + '/config', JSON.stringify($scope.config), {headers: {'Content-Type': 'application/json'}});
}
$('#settings').modal("hide");
};
@@ -327,7 +336,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
$scope.configInSync = false;
$('#editNode').modal('hide');
nodeCfg = $scope.currentNode;
nodeCfg.NodeID = nodeCfg.NodeID.replace(/ /g, '').replace(/-/g, '').trim();
nodeCfg.NodeID = nodeCfg.NodeID.replace(/ /g, '').replace(/-/g, '').toUpperCase().trim();
nodeCfg.Addresses = nodeCfg.AddressesStr.split(',').map(function (x) { return x.trim(); });
done = false;
@@ -396,10 +405,16 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
}
$scope.editRepo = function (nodeCfg) {
$scope.currentRepo = $.extend({selectedNodes: {}}, nodeCfg);
$scope.currentRepo = angular.copy(nodeCfg);
$scope.currentRepo.selectedNodes = {};
$scope.currentRepo.Nodes.forEach(function (n) {
$scope.currentRepo.selectedNodes[n.NodeID] = true;
});
if ($scope.currentRepo.Versioning && $scope.currentRepo.Versioning.Type === "simple") {
$scope.currentRepo.simpleFileVersioning = true;
$scope.currentRepo.simpleKeep = +$scope.currentRepo.Versioning.Params.keep;
}
$scope.currentRepo.simpleKeep = $scope.currentRepo.simpleKeep || 5;
$scope.editingExisting = true;
$scope.repoEditor.$setPristine();
$('#editRepo').modal({backdrop: 'static', keyboard: true});
@@ -427,12 +442,34 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
}
delete repoCfg.selectedNodes;
if (repoCfg.simpleFileVersioning) {
repoCfg.Versioning = {
'Type': 'simple',
'Params': {
'keep': '' + repoCfg.simpleKeep,
}
};
delete repoCfg.simpleFileVersioning;
delete repoCfg.simpleKeep;
} else {
delete repoCfg.Versioning;
}
$scope.repos[repoCfg.ID] = repoCfg;
$scope.config.Repositories = repoList($scope.repos);
$http.post(urlbase + '/config', JSON.stringify($scope.config), {headers: {'Content-Type': 'application/json'}});
};
$scope.sharesRepo = function(repoCfg) {
var names = [];
repoCfg.Nodes.forEach(function (node) {
names.push($scope.nodeName($scope.findNode(node.NodeID)));
});
names.sort();
return names.join(", ");
};
$scope.deleteRepo = function () {
$('#editRepo').modal('hide');
if (!$scope.editingExisting) {
@@ -611,6 +648,12 @@ syncthing.filter('shortPath', function () {
}
});
syncthing.filter('clean', function () {
return function (input) {
return encodeURIComponent(input).replace(/%/g, '');
}
});
syncthing.directive('optionEditor', function () {
return {
restrict: 'C',
@@ -643,3 +686,25 @@ syncthing.directive('uniqueRepo', function() {
}
};
});
syncthing.directive('validNodeid', function() {
return {
require: 'ngModel',
link: function(scope, elm, attrs, ctrl) {
ctrl.$parsers.unshift(function(viewValue) {
if (scope.editingExisting) {
// we shouldn't validate
ctrl.$setValidity('validNodeid', true);
} else {
var cleaned = viewValue.replace(/ /g, '').replace(/-/g, '').toUpperCase().trim();
if (cleaned.match(/^[A-Z2-7]{52}$/)) {
ctrl.$setValidity('validNodeid', true);
} else {
ctrl.$setValidity('validNodeid', false);
}
}
return viewValue;
});
}
};
});

View File

@@ -60,6 +60,7 @@
}
.table th {
white-space:nowrap;
font-weight: 400;
}
@@ -77,20 +78,20 @@
<div class="container">
<span class="navbar-brand"><img class="logo" src="st-logo-128.png" width="32" height="32" /> Syncthing<small class="hidden-xs"> <span class="text-muted">|</span> {{thisNodeName()}}</small></span>
<ul class="nav navbar-nav navbar-right">
<li class="dropdown">
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Edit&nbsp;<b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="" ng-click="addRepo()"><span class="glyphicon glyphicon-hdd"></span>&emsp;Add Repository</a></li>
<li><a href="" ng-click="addNode()"><span class="glyphicon glyphicon-retweet"></span>&emsp;Add Node</a></li>
<li class="divider"></li>
<li><a href="" ng-click="editSettings()"><span class="glyphicon glyphicon-cog"></span>&emsp;Settings</a></li>
<li><a href="" ng-click="idNode()"><span class="glyphicon glyphicon-qrcode"></span>&emsp;Show ID</a></span>
<li><a href="" ng-click="idNode()"><span class="glyphicon glyphicon-qrcode"></span>&emsp;Show ID</a></li>
<li class="divider"></li>
<li><a href="" ng-click="shutdown()"><span class="glyphicon glyphicon-off"></span>&emsp;Shutdown</a></li>
<li><a href="" ng-click="restart()"><span class="glyphicon glyphicon-refresh"></span>&emsp;Restart</a></li>
</ul>
</li>
</ul>
</ul>
</div>
</nav>
@@ -120,164 +121,171 @@
<!-- Repository list (top left) -->
<div class="col-md-6">
<div class="panel-group" id="repositories">
<div class="panel panel-{{repoClass(repo.ID)}}" ng-repeat="repo in repoList()">
<div class="panel-heading">
<h3 class="panel-title">
<a data-toggle="collapse" data-parent="#repositories" href="#repo-{{repo.ID}}">
<span class="glyphicon glyphicon-hdd"></span> {{repo.Directory | shortPath}}
<span class="pull-right">{{repoStatus(repo.ID)}}</span>
</a>
</h3>
</div>
<div id="repo-{{repo.ID}}" class="panel-collapse collapse">
<div class="panel-body">
<div class="table-responsive">
<table class="table table-condensed table-striped">
<tbody>
<tr>
<th><span class="glyphicon glyphicon-tag"></span>&emsp;Repository ID</th>
<td class="text-right">{{repo.ID}}</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-folder-open"></span>&emsp;Folder</th>
<td class="text-right">{{repo.Directory}}</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-comment"></span>&emsp;Synchronization</th>
<td class="text-right">{{repoStatus(repo.ID)}}</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-globe"></span>&emsp;Global Repository</th>
<td class="text-right">{{model[repo.ID].globalFiles | alwaysNumber}} files, {{model[repo.ID].globalBytes | binary}}B</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-home"></span>&emsp;Local Repository</th>
<td class="text-right">{{model[repo.ID].localFiles | alwaysNumber}} files, {{model[repo.ID].localBytes | binary}}B</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-cloud-download"></span>&emsp;Out of Sync</th>
<td class="text-right">{{model[repo.ID].needFiles | alwaysNumber}} files, {{model[repo.ID].needBytes | binary}}B</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-lock"></span>&emsp;Master Repository</th>
<td class="text-right">
<span ng-if="repo.ReadOnly">Yes</span>
<span ng-if="!repo.ReadOnly">No</span>
</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-share-alt"></span>&emsp;Shared With</th>
<td class="text-right">
<span ng-repeat="n in repo.Nodes">
{{nodeName(findNode(n.NodeID))}}<span ng-if="!$last">, </span>
</span>
</td>
</tr>
</tbody>
</table>
<div class="panel-group" id="repositories">
<div class="panel panel-{{repoClass(repo.ID)}}" ng-repeat="repo in repoList()">
<div class="panel-heading">
<h3 class="panel-title">
<a data-toggle="collapse" data-parent="#repositories" href="#repo-{{repo.ID | clean}}">
<span class="glyphicon glyphicon-hdd"></span> {{repo.Directory | shortPath}}
<span class="pull-right hidden-xs">{{repoStatus(repo.ID)}}</span>
</a>
</h3>
</div>
<div id="repo-{{repo.ID | clean}}" class="panel-collapse collapse">
<div class="panel-body">
<div class="table-responsive">
<table class="table table-condensed table-striped">
<tbody>
<tr>
<th><span class="glyphicon glyphicon-tag"></span>&emsp;Repository ID</th>
<td class="text-right">{{repo.ID}}</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-folder-open"></span>&emsp;Folder</th>
<td class="text-right">{{repo.Directory}}</td>
</tr>
<tr ng-if="model[repo.ID].invalid">
<th><span class="glyphicon glyphicon-warning-sign"></span>&emsp;Error</th>
<td class="text-right">{{model[repo.ID].invalid}}</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-comment"></span>&emsp;Synchronization</th>
<td class="text-right">{{repoStatus(repo.ID)}}</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-globe"></span>&emsp;Global Repository</th>
<td class="text-right">{{model[repo.ID].globalFiles | alwaysNumber}} files, {{model[repo.ID].globalBytes | binary}}B</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-home"></span>&emsp;Local Repository</th>
<td class="text-right">{{model[repo.ID].localFiles | alwaysNumber}} files, {{model[repo.ID].localBytes | binary}}B</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-cloud-download"></span>&emsp;Out of Sync</th>
<td class="text-right">{{model[repo.ID].needFiles | alwaysNumber}} files, {{model[repo.ID].needBytes | binary}}B</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-lock"></span>&emsp;Master Repository</th>
<td class="text-right">
<span ng-if="repo.ReadOnly">Yes</span>
<span ng-if="!repo.ReadOnly">No</span>
</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-unchecked"></span>&emsp;Ignore Permissions</th>
<td class="text-right">
<span ng-if="repo.IgnorePerms">Yes</span>
<span ng-if="!repo.IgnorePerms">No</span>
</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-share-alt"></span>&emsp;Shared With</th>
<td class="text-right">{{sharesRepo(repo)}}</td>
</tr>
</tbody>
</table>
</div>
<span class="pull-right"><a class="btn btn-sm btn-primary" href="" ng-click="editRepo(repo)"><span class="glyphicon glyphicon-pencil"></span>&emsp;Edit</a></span>
</div>
</div>
<span class="pull-right"><a class="btn btn-sm btn-primary" href="" ng-click="editRepo(repo)"><span class="glyphicon glyphicon-pencil"></span>&emsp;Edit</a></span>
</div>
</div>
</div>
</div>
</div>
<!-- Node list (top right) -->
<div class="col-md-6">
<div class="panel-group" id="nodes">
<div class="panel panel-default" ng-repeat="nodeCfg in [thisNode()]">
<div class="panel-heading">
<h3 class="panel-title">
<a data-toggle="collapse" data-parent="#nodes" href="#node-{{nodeCfg.NodeID}}"><span class="glyphicon glyphicon-home"></span> {{nodeName(nodeCfg)}}</a>
</h3>
</div>
<div id="node-{{nodeCfg.NodeID}}" class="panel-collapse collapse in">
<div class="panel-body">
<div class="table-responsive">
<table class="table table-condensed table-striped">
<tbody>
<tr>
<th><span class="glyphicon glyphicon-th"></span>&emsp;RAM Utilization</th>
<td class="text-right">{{system.sys | binary}}B</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-tasks"></span>&emsp;CPU Utilization</th>
<td class="text-right">{{system.cpuPercent | alwaysNumber | natural:1}}%</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-cloud-download"></span>&emsp;Download Rate</th>
<td class="text-right">{{inbps | metric}}bps</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-cloud-upload"></span>&emsp;Upload Rate</th>
<td class="text-right">{{outbps | metric}}bps </td>
</tr>
<tr ng-if="system.extAnnounceOK != undefined">
<th><span class="glyphicon glyphicon-bullhorn"></span>&emsp;Announce Server</th>
<td class="text-right">
<span class="data text-success" ng-if="system.extAnnounceOK">Online</span>
<span class="data text-danger" ng-if="!system.extAnnounceOK">Offline</span>
</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-tag"></span>&emsp;Version</th>
<td class="text-right">{{version}}</td>
</tr>
</tbody>
</table>
<div class="panel-group" id="nodes">
<div class="panel panel-default" ng-repeat="nodeCfg in [thisNode()]">
<div class="panel-heading">
<h3 class="panel-title">
<a data-toggle="collapse" data-parent="#nodes" href="#node-{{nodeCfg.NodeID | clean}}"><span class="glyphicon glyphicon-home"></span> {{nodeName(nodeCfg)}}</a>
</h3>
</div>
<div id="node-{{nodeCfg.NodeID | clean}}" class="panel-collapse collapse in">
<div class="panel-body">
<div class="table-responsive">
<table class="table table-condensed table-striped">
<tbody>
<tr>
<th><span class="glyphicon glyphicon-th"></span>&emsp;RAM Utilization</th>
<td class="text-right">{{system.sys | binary}}B</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-tasks"></span>&emsp;CPU Utilization</th>
<td class="text-right">{{system.cpuPercent | alwaysNumber | natural:1}}%</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-cloud-download"></span>&emsp;Download Rate</th>
<td class="text-right">{{connections['total'].inbps | metric}}bps ({{connections['total'].InBytesTotal | binary}}B)</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-cloud-upload"></span>&emsp;Upload Rate</th>
<td class="text-right">{{connections['total'].outbps | metric}}bps ({{connections['total'].OutBytesTotal | binary}}B)</td>
</tr>
<tr ng-if="system.extAnnounceOK != undefined">
<th><span class="glyphicon glyphicon-bullhorn"></span>&emsp;Announce Server</th>
<td class="text-right">
<span class="data text-success" ng-if="system.extAnnounceOK">Online</span>
<span class="data text-danger" ng-if="!system.extAnnounceOK">Offline</span>
</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-tag"></span>&emsp;Version</th>
<td class="text-right">{{version}}</td>
</tr>
</tbody>
</table>
</div>
<span class="pull-right"><a class="btn btn-sm btn-primary" href="" ng-click="editNode(nodeCfg)"><span class="glyphicon glyphicon-pencil"></span>&emsp;Edit</a></span>
</div>
</div>
<span class="pull-right"><a class="btn btn-sm btn-primary" href="" ng-click="editNode(nodeCfg)"><span class="glyphicon glyphicon-pencil"></span>&emsp;Edit</a></span>
</div>
</div>
</div>
<div class="panel panel-{{nodeClass(nodeCfg)}}" ng-repeat="nodeCfg in otherNodes()">
<div class="panel-heading">
<h3 class="panel-title">
<a data-toggle="collapse" data-parent="#nodes" href="#node-{{nodeCfg.NodeID}}">
<span class="glyphicon glyphicon-retweet"></span>
{{nodeName(nodeCfg)}}
<span class="pull-right">{{nodeStatus(nodeCfg)}}</span>
</a>
</h3>
</div>
<div id="node-{{nodeCfg.NodeID}}" class="panel-collapse collapse">
<div class="panel-body">
<div class="table-responsive">
<table class="table table-condensed table-striped">
<tbody>
<tr>
<th><span class="glyphicon glyphicon-link"></span>&emsp;Address</th>
<td class="text-right">{{nodeAddr(nodeCfg)}}</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-comment"></span>&emsp;Synchronization</th>
<td class="text-right">{{nodeStatus(nodeCfg)}}</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-cloud-download"></span>&emsp;Download Rate</th>
<td class="text-right">{{connections[nodeCfg.NodeID].inbps | metric}}bps</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-cloud-upload"></span>&emsp;Upload Rate</th>
<td class="text-right">{{connections[nodeCfg.NodeID].outbps | metric}}bps </td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-tag"></span>&emsp;Version</th>
<td class="text-right">{{nodeVer(nodeCfg)}}</td>
</tr>
</tbody>
</table>
<div class="panel panel-{{nodeClass(nodeCfg)}}" ng-repeat="nodeCfg in otherNodes()">
<div class="panel-heading">
<h3 class="panel-title">
<a data-toggle="collapse" data-parent="#nodes" href="#node-{{nodeCfg.NodeID}}">
<span class="glyphicon glyphicon-retweet"></span>
{{nodeName(nodeCfg)}}
<span class="pull-right hidden-xs">{{nodeStatus(nodeCfg)}}</span>
</a>
</h3>
</div>
<div id="node-{{nodeCfg.NodeID}}" class="panel-collapse collapse">
<div class="panel-body">
<div class="table-responsive">
<table class="table table-condensed table-striped">
<tbody>
<tr>
<th><span class="glyphicon glyphicon-link"></span>&emsp;Address</th>
<td class="text-right">{{nodeAddr(nodeCfg)}}</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-comment"></span>&emsp;Synchronization</th>
<td class="text-right">{{nodeStatus(nodeCfg)}}</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-cloud-download"></span>&emsp;Download Rate</th>
<td class="text-right">{{connections[nodeCfg.NodeID].inbps | metric}}bps ({{connections[nodeCfg.NodeID].InBytesTotal | binary}}B)</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-cloud-upload"></span>&emsp;Upload Rate</th>
<td class="text-right">{{connections[nodeCfg.NodeID].outbps | metric}}bps ({{connections[nodeCfg.NodeID].OutBytesTotal | binary}}B)</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-tag"></span>&emsp;Version</th>
<td class="text-right">{{nodeVer(nodeCfg)}}</td>
</tr>
</tbody>
</table>
</div>
<span class="pull-right"><a class="btn btn-sm btn-primary" href="" ng-click="editNode(nodeCfg)"><span class="glyphicon glyphicon-pencil"></span>&emsp;Edit</a></span>
</div>
</div>
<span class="pull-right"><a class="btn btn-sm btn-primary" href="" ng-click="editNode(nodeCfg)"><span class="glyphicon glyphicon-pencil"></span>&emsp;Edit</a></span>
</div>
</div>
</div>
</div>
</div> <!-- /row -->
<!-- Errors -->
@@ -386,14 +394,14 @@
</h4>
</div>
<div class="modal-body">
<div class="well well-sm text-monospace text-center">
{{myID | chunkID}}
</div>
<img ng-if="myID" class="center-block img-thumbnail" src="qr/{{myID | chunkID}}"/>
<div class="well well-sm text-monospace text-center">
{{myID | chunkID}}
</div>
<img ng-if="myID" class="center-block img-thumbnail" src="qr/{{myID | chunkID}}"/>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal"><span class="glyphicon glyphicon-remove"></span>&emsp;Close</button>
</div>
</div>
</div>
</div>
</div>
@@ -411,13 +419,14 @@
<form role="form" name="nodeEditor">
<div class="form-group" ng-class="{'has-error': nodeEditor.nodeID.$invalid && nodeEditor.nodeID.$dirty}">
<label for="nodeID">Node ID</label>
<input ng-if="!editingExisting" name="nodeID" id="nodeID" class="form-control text-monospace" type="text" ng-model="currentNode.NodeID" required></input>
<input ng-if="!editingExisting" name="nodeID" id="nodeID" class="form-control text-monospace" type="text" ng-model="currentNode.NodeID" required valid-nodeid></input>
<div ng-if="editingExisting" class="well well-sm text-monospace">{{currentNode.NodeID | chunkID}}</div>
<p class="help-block">
<span ng-if="nodeEditor.nodeID.$valid || nodeEditor.nodeID.$pristine">The node ID to enter here can be found in the "Edit > Show ID" dialog on the other node. Spaces and dashes are optional (ignored).
<span ng-show="!editingExisting">When adding a new node, keep in mind that <em>this node</em> must be added on the other side too.</span>
<span ng-show="!editingExisting">When adding a new node, keep in mind that <em>this node</em> must be added on the other side too.</span>
</span>
<span ng-if="nodeEditor.nodeID.$error.required && nodeEditor.nodeID.$dirty">The node ID cannot be blank.</span>
<span ng-if="nodeEditor.nodeID.$error.validNodeid && nodeEditor.nodeID.$dirty">The entered node ID does not look valid. It should be a 52 character string consisting of letters and numbers, with spaces and dashes being optional.</span>
</p>
</div>
<div class="form-group">
@@ -452,39 +461,76 @@
</div>
<div class="modal-body">
<form role="form" name="repoEditor">
<div class="form-group" ng-class="{'has-error': repoEditor.repoID.$invalid && repoEditor.repoID.$dirty}">
<label for="repoID">Repository ID</label>
<input name="repoID" placeholder="documents" ng-disabled="editingExisting" id="repoID" class="form-control" type="text" ng-model="currentRepo.ID" required unique-repo></input>
<p class="help-block">
<span ng-if="repoEditor.repoID.$valid || repoEditor.repoID.$pristine">Short identifier for the repository. Must be the same on all cluster nodes.</span>
<span ng-if="repoEditor.repoID.$error.uniqueRepo">The repository ID must be unique.</span>
<span ng-if="repoEditor.repoID.$error.required && repoEditor.repoID.$dirty">The repository ID cannot be blank.</span>
</p>
</div>
<div class="form-group" ng-class="{'has-error': repoEditor.repoPath.$invalid && repoEditor.repoPath.$dirty}">
<label for="repoPath">Repository Path</label>
<input name="repoPath" placeholder="~/Documents" id="repoPath" class="form-control" type="text" ng-model="currentRepo.Directory" required></input>
<p class="help-block">
<span ng-if="repoEditor.repoPath.$valid || repoEditor.repoPath.$pristine">Path to the repository on the local computer. Will be created if it does not exist. The tilde character <code>~</code> can be used as a shortcut for <code>{{system.tilde}}</code>.</span>
<span ng-if="repoEditor.repoPath.$error.required && repoEditor.repoPath.$dirty">The repository path cannot be blank.</span>
</p>
</div>
<div class="form-group">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="currentRepo.ReadOnly"> Repository Master
</label>
<div class="row">
<div class="col-md-12">
<div class="form-group" ng-class="{'has-error': repoEditor.repoID.$invalid && repoEditor.repoID.$dirty}">
<label for="repoID">Repository ID</label>
<input name="repoID" placeholder="documents" ng-disabled="editingExisting" id="repoID" class="form-control" type="text" ng-model="currentRepo.ID" required unique-repo ng-pattern="/^[a-zaZ0-9-_.]{1,64}$/"></input>
<p class="help-block">
<span ng-if="repoEditor.repoID.$valid || repoEditor.repoID.$pristine">Short identifier for the repository. Must be the same on all cluster nodes.</span>
<span ng-if="repoEditor.repoID.$error.uniqueRepo">The repository ID must be unique.</span>
<span ng-if="repoEditor.repoID.$error.required && repoEditor.repoID.$dirty">The repository ID cannot be blank.</span>
<span ng-if="repoEditor.repoID.$error.pattern && repoEditor.repoID.$dirty">The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the <code>-_.</code> characters only.</span>
</p>
</div>
<div class="form-group" ng-class="{'has-error': repoEditor.repoPath.$invalid && repoEditor.repoPath.$dirty}">
<label for="repoPath">Repository Path</label>
<input name="repoPath" placeholder="~/Documents" id="repoPath" class="form-control" type="text" ng-model="currentRepo.Directory" required></input>
<p class="help-block">
<span ng-if="repoEditor.repoPath.$valid || repoEditor.repoPath.$pristine">Path to the repository on the local computer. Will be created if it does not exist. The tilde character <code>~</code> can be used as a shortcut for <code>{{system.tilde}}</code>.</span>
<span ng-if="repoEditor.repoPath.$error.required && repoEditor.repoPath.$dirty">The repository path cannot be blank.</span>
</p>
</div>
</div>
<p class="help-block">Files are protected from changes made on other nodes, but changes made on <em>this</em> node will be sent to the rest of the cluster.</p>
</div>
<div class="form-group">
<label for="nodes">Nodes</label>
<div class="checkbox" ng-repeat="node in otherNodes()">
<label>
<input type="checkbox" ng-model="currentRepo.selectedNodes[node.NodeID]"> {{nodeName(node)}}
</label>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="currentRepo.ReadOnly"> Repository Master
</label>
</div>
<p class="help-block">Files are protected from changes made on other nodes, but changes made on <em>this</em> node will be sent to the rest of the cluster.</p>
</div>
<div class="form-group">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="currentRepo.IgnorePerms"> Ignore Permissions
</label>
</div>
<p class="help-block">File permission bits are ignored when looking for changes. Use on FAT filesystems.</p>
</div>
<div class="form-group">
<label for="nodes">Nodes</label>
<div class="checkbox" ng-repeat="node in otherNodes()">
<label>
<input type="checkbox" ng-model="currentRepo.selectedNodes[node.NodeID]"> {{nodeName(node)}}
</label>
</div>
<p class="help-block">Select the nodes to share this repository with.</p>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="currentRepo.simpleFileVersioning"> File Versioning
</label>
</div>
<p class="help-block">Files are moved to date stamped versions in a <code>.stversions</code> folder when replaced or deleted by syncthing.</p>
</div>
<div class="form-group" ng-if="currentRepo.simpleFileVersioning" ng-class="{'has-error': repoEditor.simpleKeep.$invalid && repoEditor.simpleKeep.$dirty}">
<label for="simpleKeep">Keep Versions</label>
<input name="simpleKeep" id="simpleKeep" class="form-control" type="number" ng-model="currentRepo.simpleKeep" required min="1"></input>
<p class="help-block">
<span ng-if="repoEditor.simpleKeep.$valid || repoEditor.simpleKeep.$pristine">The number of old versions to keep, per file.</span>
<span ng-if="repoEditor.simpleKeep.$error.required && repoEditor.simpleKeep.$dirty">The number of versions must be a number and cannot be blank.</span>
<span ng-if="repoEditor.simpleKeep.$error.min && repoEditor.simpleKeep.$dirty">You must keep at least one version.</span>
</p>
</div>
</div>
<p class="help-block">Select the nodes to share this repository with.</p>
</div>
</form>
<div ng-show="!editingExisting">
@@ -515,11 +561,11 @@
<div class="form-group" ng-repeat="setting in settings">
<div ng-if="setting.type == 'text' || setting.type == 'number'">
<label for="{{setting.id}}">{{setting.descr}}</label>
<input id="{{setting.id}}" class="form-control" type="{{setting.type}}" ng-model="config.Options[setting.id]"></input>
<input id="{{setting.id}}" class="form-control" type="{{setting.type}}" ng-model="config.workingOptions[setting.id]"></input>
</div>
<div class="checkbox" ng-if="setting.type == 'bool'">
<label>
{{setting.descr}} <input id="{{setting.id}}" type="checkbox" ng-model="config.Options[setting.id]"></input>
{{setting.descr}} <input id="{{setting.id}}" type="checkbox" ng-model="config.workingOptions[setting.id]"></input>
</label>
</div>
</div>
@@ -528,11 +574,11 @@
<div class="form-group" ng-repeat="setting in guiSettings">
<div ng-if="setting.type == 'text' || setting.type == 'number' || setting.type == 'password'">
<label for="{{setting.id}}">{{setting.descr}}</label>
<input id="{{setting.id}}" class="form-control" type="{{setting.type}}" ng-model="config.GUI[setting.id]"></input>
<input id="{{setting.id}}" class="form-control" type="{{setting.type}}" ng-model="config.workingGUI[setting.id]"></input>
</div>
<div class="checkbox" ng-if="setting.type == 'bool'">
<label>
{{setting.descr}} <input id="{{setting.id}}" type="checkbox" ng-model="config.GUI[setting.id]"></input>
{{setting.descr}} <input id="{{setting.id}}" type="checkbox" ng-model="config.workingGUI[setting.id]"></input>
</label>
</div>
</div>

View File

@@ -16,6 +16,7 @@ type bqBlock struct {
file scanner.File
block scanner.Block // get this block from the network
copy []scanner.Block // copy these blocks from the old version of the file
first bool
last bool
}
@@ -47,24 +48,30 @@ func (q *blockQueue) addBlock(a bqAdd) {
return
}
}
l := len(a.need)
if len(a.have) > 0 {
// First queue a copy operation
q.queued = append(q.queued, bqBlock{
file: a.file,
copy: a.have,
file: a.file,
copy: a.have,
first: true,
last: l == 0,
})
}
// Queue the needed blocks individually
l := len(a.need)
for i, b := range a.need {
q.queued = append(q.queued, bqBlock{
file: a.file,
block: b,
first: len(a.have) == 0 && i == 0,
last: i == l-1,
})
}
if l == 0 {
if len(a.need)+len(a.have) == 0 {
// If we didn't have anything to fetch, queue an empty block with the "last" flag set to close the file.
q.queued = append(q.queued, bqBlock{
file: a.file,

View File

@@ -17,6 +17,7 @@ import (
"github.com/calmh/syncthing/config"
"github.com/calmh/syncthing/files"
"github.com/calmh/syncthing/lamport"
"github.com/calmh/syncthing/osutil"
"github.com/calmh/syncthing/protocol"
"github.com/calmh/syncthing/scanner"
)
@@ -43,12 +44,12 @@ type Model struct {
clientName string
clientVersion string
repoDirs map[string]string // repo -> dir
repoFiles map[string]*files.Set // repo -> files
repoNodes map[string][]string // repo -> nodeIDs
nodeRepos map[string][]string // nodeID -> repos
suppressor map[string]*suppressor // repo -> suppressor
rmut sync.RWMutex // protects the above
repoCfgs map[string]config.RepositoryConfiguration // repo -> cfg
repoFiles map[string]*files.Set // repo -> files
repoNodes map[string][]string // repo -> nodeIDs
nodeRepos map[string][]string // nodeID -> repos
suppressor map[string]*suppressor // repo -> suppressor
rmut sync.RWMutex // protects the above
repoState map[string]repoState // repo -> state
smut sync.RWMutex
@@ -80,7 +81,7 @@ func NewModel(indexDir string, cfg *config.Configuration, clientName, clientVers
cfg: cfg,
clientName: clientName,
clientVersion: clientVersion,
repoDirs: make(map[string]string),
repoCfgs: make(map[string]config.RepositoryConfiguration),
repoFiles: make(map[string]*files.Set),
repoNodes: make(map[string][]string),
nodeRepos: make(map[string][]string),
@@ -104,10 +105,10 @@ func (m *Model) StartRepoRW(repo string, threads int) {
m.rmut.RLock()
defer m.rmut.RUnlock()
if dir, ok := m.repoDirs[repo]; !ok {
if cfg, ok := m.repoCfgs[repo]; !ok {
panic("cannot start without repo")
} else {
newPuller(repo, dir, m, threads, m.cfg)
newPuller(cfg, m, threads, m.cfg)
}
}
@@ -149,9 +150,9 @@ func (m *Model) ConnectionStats() map[string]ConnectionInfo {
for _, repo := range m.nodeRepos[node] {
for _, f := range m.repoFiles[repo].Global() {
if f.Flags&protocol.FlagDeleted == 0 {
if !protocol.IsDeleted(f.Flags) {
size := f.Size
if f.Flags&protocol.FlagDirectory != 0 {
if protocol.IsDirectory(f.Flags) {
size = zeroEntrySize
}
tot += size
@@ -160,9 +161,9 @@ func (m *Model) ConnectionStats() map[string]ConnectionInfo {
}
for _, f := range m.repoFiles[repo].Need(m.cm.Get(node)) {
if f.Flags&protocol.FlagDeleted == 0 {
if !protocol.IsDeleted(f.Flags) {
size := f.Size
if f.Flags&protocol.FlagDirectory != 0 {
if protocol.IsDirectory(f.Flags) {
size = zeroEntrySize
}
have -= size
@@ -181,14 +182,23 @@ func (m *Model) ConnectionStats() map[string]ConnectionInfo {
m.rmut.RUnlock()
m.pmut.RUnlock()
in, out := protocol.TotalInOut()
res["total"] = ConnectionInfo{
Statistics: protocol.Statistics{
At: time.Now(),
InBytesTotal: int(in),
OutBytesTotal: int(out),
},
}
return res
}
func sizeOf(fs []scanner.File) (files, deleted int, bytes int64) {
for _, f := range fs {
if f.Flags&protocol.FlagDeleted == 0 {
if !protocol.IsDeleted(f.Flags) {
files++
if f.Flags&protocol.FlagDirectory == 0 {
if !protocol.IsDirectory(f.Flags) {
bytes += f.Size
} else {
bytes += zeroEntrySize
@@ -252,7 +262,7 @@ func (m *Model) Index(nodeID string, repo string, fs []protocol.FileInfo) {
lamport.Default.Tick(f.Version)
if debug {
var flagComment string
if f.Flags&protocol.FlagDeleted != 0 {
if protocol.IsDeleted(f.Flags) {
flagComment = " (deleted)"
}
l.Debugf("IDX(in): %s %q/%q m=%d f=%o%s v=%d (%d blocks)", nodeID, repo, f.Name, f.Modified, f.Flags, flagComment, f.Version, len(f.Blocks))
@@ -265,7 +275,8 @@ func (m *Model) Index(nodeID string, repo string, fs []protocol.FileInfo) {
if r, ok := m.repoFiles[repo]; ok {
r.Replace(id, files)
} else {
l.Warnf("Index from %s for nonexistant repo %q; dropping", nodeID, repo)
l.Warnf("Index from %s for unexpected repo %q; verify configuration", nodeID, repo)
}
m.rmut.RUnlock()
}
@@ -283,7 +294,7 @@ func (m *Model) IndexUpdate(nodeID string, repo string, fs []protocol.FileInfo)
lamport.Default.Tick(f.Version)
if debug {
var flagComment string
if f.Flags&protocol.FlagDeleted != 0 {
if protocol.IsDeleted(f.Flags) {
flagComment = " (deleted)"
}
l.Debugf("IDXUP(in): %s %q/%q m=%d f=%o%s v=%d (%d blocks)", nodeID, repo, f.Name, f.Modified, f.Flags, flagComment, f.Version, len(f.Blocks))
@@ -368,7 +379,7 @@ func (m *Model) Request(nodeID, repo, name string, offset int64, size int) ([]by
}
lf := r.Get(cid.LocalID, name)
if lf.Suppressed || lf.Flags&protocol.FlagDeleted != 0 {
if lf.Suppressed || protocol.IsDeleted(lf.Flags) {
if debug {
l.Debugf("REQ(in): %s: %q / %q o=%d s=%d; invalid: %v", nodeID, repo, name, offset, size, lf)
}
@@ -386,7 +397,7 @@ func (m *Model) Request(nodeID, repo, name string, offset int64, size int) ([]by
l.Debugf("REQ(in): %s: %q / %q o=%d s=%d", nodeID, repo, name, offset, size)
}
m.rmut.RLock()
fn := filepath.Join(m.repoDirs[repo], name)
fn := filepath.Join(m.repoCfgs[repo].Directory, name)
m.rmut.RUnlock()
fd, err := os.Open(fn) // XXX: Inefficient, should cache fd?
if err != nil {
@@ -502,7 +513,7 @@ func (m *Model) protocolIndex(repo string) []protocol.FileInfo {
mf := fileInfoFromFile(f)
if debug {
var flagComment string
if mf.Flags&protocol.FlagDeleted != 0 {
if protocol.IsDeleted(mf.Flags) {
flagComment = " (deleted)"
}
l.Debugf("IDX(out): %q/%q m=%d f=%o%s v=%d (%d blocks)", repo, mf.Name, mf.Modified, mf.Flags, flagComment, mf.Version, len(mf.Blocks))
@@ -582,23 +593,23 @@ func (m *Model) broadcastIndexLoop() {
}
}
func (m *Model) AddRepo(id, dir string, nodes []config.NodeConfiguration) {
func (m *Model) AddRepo(cfg config.RepositoryConfiguration) {
if m.started {
panic("cannot add repo to started model")
}
if len(id) == 0 {
if len(cfg.ID) == 0 {
panic("cannot add empty repo id")
}
m.rmut.Lock()
m.repoDirs[id] = dir
m.repoFiles[id] = files.NewSet()
m.suppressor[id] = &suppressor{threshold: int64(m.cfg.Options.MaxChangeKbps)}
m.repoCfgs[cfg.ID] = cfg
m.repoFiles[cfg.ID] = files.NewSet()
m.suppressor[cfg.ID] = &suppressor{threshold: int64(m.cfg.Options.MaxChangeKbps)}
m.repoNodes[id] = make([]string, len(nodes))
for i, node := range nodes {
m.repoNodes[id][i] = node.NodeID
m.nodeRepos[node.NodeID] = append(m.nodeRepos[node.NodeID], id)
m.repoNodes[cfg.ID] = make([]string, len(cfg.Nodes))
for i, node := range cfg.Nodes {
m.repoNodes[cfg.ID][i] = node.NodeID
m.nodeRepos[node.NodeID] = append(m.nodeRepos[node.NodeID], cfg.ID)
}
m.addedRepo = true
@@ -607,8 +618,8 @@ func (m *Model) AddRepo(id, dir string, nodes []config.NodeConfiguration) {
func (m *Model) ScanRepos() {
m.rmut.RLock()
var repos = make([]string, 0, len(m.repoDirs))
for repo := range m.repoDirs {
var repos = make([]string, 0, len(m.repoCfgs))
for repo := range m.repoCfgs {
repos = append(repos, repo)
}
m.rmut.RUnlock()
@@ -618,7 +629,10 @@ func (m *Model) ScanRepos() {
for _, repo := range repos {
repo := repo
go func() {
m.ScanRepo(repo)
err := m.ScanRepo(repo)
if err != nil {
invalidateRepo(m.cfg, repo, err)
}
wg.Done()
}()
}
@@ -627,9 +641,9 @@ func (m *Model) ScanRepos() {
func (m *Model) CleanRepos() {
m.rmut.RLock()
var dirs = make([]string, 0, len(m.repoDirs))
for _, dir := range m.repoDirs {
dirs = append(dirs, dir)
var dirs = make([]string, 0, len(m.repoCfgs))
for _, cfg := range m.repoCfgs {
dirs = append(dirs, cfg.Directory)
}
m.rmut.RUnlock()
@@ -651,12 +665,13 @@ func (m *Model) CleanRepos() {
func (m *Model) ScanRepo(repo string) error {
m.rmut.RLock()
w := &scanner.Walker{
Dir: m.repoDirs[repo],
Dir: m.repoCfgs[repo].Directory,
IgnoreFile: ".stignore",
BlockSize: scanner.StandardBlockSize,
TempNamer: defTempNamer,
Suppressor: m.suppressor[repo],
CurrentFiler: cFiler{m, repo},
IgnorePerms: m.repoCfgs[repo].IgnorePerms,
}
m.rmut.RUnlock()
m.setState(repo, RepoScanning)
@@ -671,7 +686,7 @@ func (m *Model) ScanRepo(repo string) error {
func (m *Model) SaveIndexes(dir string) {
m.rmut.RLock()
for repo := range m.repoDirs {
for repo := range m.repoCfgs {
fs := m.protocolIndex(repo)
m.saveIndex(repo, dir, fs)
}
@@ -680,7 +695,7 @@ func (m *Model) SaveIndexes(dir string) {
func (m *Model) LoadIndexes(dir string) {
m.rmut.RLock()
for repo := range m.repoDirs {
for repo := range m.repoCfgs {
fs := m.loadIndex(repo, dir)
m.SeedLocal(repo, fs)
}
@@ -688,7 +703,7 @@ func (m *Model) LoadIndexes(dir string) {
}
func (m *Model) saveIndex(repo string, dir string, fs []protocol.FileInfo) {
id := fmt.Sprintf("%x", sha1.Sum([]byte(m.repoDirs[repo])))
id := fmt.Sprintf("%x", sha1.Sum([]byte(m.repoCfgs[repo].Directory)))
name := id + ".idx.gz"
name = filepath.Join(dir, name)
@@ -706,11 +721,11 @@ func (m *Model) saveIndex(repo string, dir string, fs []protocol.FileInfo) {
gzw.Close()
idxf.Close()
Rename(name+".tmp", name)
osutil.Rename(name+".tmp", name)
}
func (m *Model) loadIndex(repo string, dir string) []protocol.FileInfo {
id := fmt.Sprintf("%x", sha1.Sum([]byte(m.repoDirs[repo])))
id := fmt.Sprintf("%x", sha1.Sum([]byte(m.repoCfgs[repo].Directory)))
name := id + ".idx.gz"
name = filepath.Join(dir, name)

View File

@@ -49,7 +49,7 @@ func init() {
func TestRequest(t *testing.T) {
m := NewModel("/tmp", &config.Configuration{}, "syncthing", "dev")
m.AddRepo("default", "testdata", nil)
m.AddRepo(config.RepositoryConfiguration{ID: "default", Directory: "testdata"})
m.ScanRepo("default")
bs, err := m.Request("some node", "default", "foo", 0, 6)
@@ -85,7 +85,7 @@ func genFiles(n int) []protocol.FileInfo {
func BenchmarkIndex10000(b *testing.B) {
m := NewModel("/tmp", nil, "syncthing", "dev")
m.AddRepo("default", "testdata", nil)
m.AddRepo(config.RepositoryConfiguration{ID: "default", Directory: "testdata"})
m.ScanRepo("default")
files := genFiles(10000)
@@ -97,7 +97,7 @@ func BenchmarkIndex10000(b *testing.B) {
func BenchmarkIndex00100(b *testing.B) {
m := NewModel("/tmp", nil, "syncthing", "dev")
m.AddRepo("default", "testdata", nil)
m.AddRepo(config.RepositoryConfiguration{ID: "default", Directory: "testdata"})
m.ScanRepo("default")
files := genFiles(100)
@@ -109,7 +109,7 @@ func BenchmarkIndex00100(b *testing.B) {
func BenchmarkIndexUpdate10000f10000(b *testing.B) {
m := NewModel("/tmp", nil, "syncthing", "dev")
m.AddRepo("default", "testdata", nil)
m.AddRepo(config.RepositoryConfiguration{ID: "default", Directory: "testdata"})
m.ScanRepo("default")
files := genFiles(10000)
m.Index("42", "default", files)
@@ -122,7 +122,7 @@ func BenchmarkIndexUpdate10000f10000(b *testing.B) {
func BenchmarkIndexUpdate10000f00100(b *testing.B) {
m := NewModel("/tmp", nil, "syncthing", "dev")
m.AddRepo("default", "testdata", nil)
m.AddRepo(config.RepositoryConfiguration{ID: "default", Directory: "testdata"})
m.ScanRepo("default")
files := genFiles(10000)
m.Index("42", "default", files)
@@ -136,7 +136,7 @@ func BenchmarkIndexUpdate10000f00100(b *testing.B) {
func BenchmarkIndexUpdate10000f00001(b *testing.B) {
m := NewModel("/tmp", nil, "syncthing", "dev")
m.AddRepo("default", "testdata", nil)
m.AddRepo(config.RepositoryConfiguration{ID: "default", Directory: "testdata"})
m.ScanRepo("default")
files := genFiles(10000)
m.Index("42", "default", files)
@@ -183,7 +183,7 @@ func (FakeConnection) Statistics() protocol.Statistics {
func BenchmarkRequest(b *testing.B) {
m := NewModel("/tmp", nil, "syncthing", "dev")
m.AddRepo("default", "testdata", nil)
m.AddRepo(config.RepositoryConfiguration{ID: "default", Directory: "testdata"})
m.ScanRepo("default")
const n = 1000

View File

@@ -5,13 +5,16 @@ import (
"errors"
"os"
"path/filepath"
"runtime"
"time"
"github.com/calmh/syncthing/buffers"
"github.com/calmh/syncthing/cid"
"github.com/calmh/syncthing/config"
"github.com/calmh/syncthing/osutil"
"github.com/calmh/syncthing/protocol"
"github.com/calmh/syncthing/scanner"
"github.com/calmh/syncthing/versioner"
)
type requestResult struct {
@@ -63,8 +66,7 @@ var errNoNode = errors.New("no available source node")
type puller struct {
cfg *config.Configuration
repo string
dir string
repoCfg config.RepositoryConfiguration
bq *blockQueue
model *Model
oustandingPerNode activityMap
@@ -72,13 +74,13 @@ type puller struct {
requestSlots chan bool
blocks chan bqBlock
requestResults chan requestResult
versioner versioner.Versioner
}
func newPuller(repo, dir string, model *Model, slots int, cfg *config.Configuration) *puller {
func newPuller(repoCfg config.RepositoryConfiguration, model *Model, slots int, cfg *config.Configuration) *puller {
p := &puller{
repoCfg: repoCfg,
cfg: cfg,
repo: repo,
dir: dir,
bq: newBlockQueue(),
model: model,
oustandingPerNode: make(activityMap),
@@ -88,19 +90,27 @@ func newPuller(repo, dir string, model *Model, slots int, cfg *config.Configurat
requestResults: make(chan requestResult),
}
if len(repoCfg.Versioning.Type) > 0 {
factory, ok := versioner.Factories[repoCfg.Versioning.Type]
if !ok {
l.Fatalf("Requested versioning type %q that does not exist", repoCfg.Versioning.Type)
}
p.versioner = factory(repoCfg.Versioning.Params)
}
if slots > 0 {
// Read/write
for i := 0; i < slots; i++ {
p.requestSlots <- true
}
if debug {
l.Debugf("starting puller; repo %q dir %q slots %d", repo, dir, slots)
l.Debugf("starting puller; repo %q dir %q slots %d", repoCfg.ID, repoCfg.Directory, slots)
}
go p.run()
} else {
// Read only
if debug {
l.Debugf("starting puller; repo %q dir %q (read only)", repo, dir)
l.Debugf("starting puller; repo %q dir %q (read only)", repoCfg.ID, repoCfg.Directory)
}
go p.runRO()
}
@@ -114,7 +124,7 @@ func (p *puller) run() {
<-p.requestSlots
b := p.bq.get()
if debug {
l.Debugf("filler: queueing %q / %q offset %d copy %d", p.repo, b.file.Name, b.block.Offset, len(b.copy))
l.Debugf("filler: queueing %q / %q offset %d copy %d", p.repoCfg.ID, b.file.Name, b.block.Offset, len(b.copy))
}
p.blocks <- b
}
@@ -130,13 +140,13 @@ func (p *puller) run() {
for {
select {
case res := <-p.requestResults:
p.model.setState(p.repo, RepoSyncing)
p.model.setState(p.repoCfg.ID, RepoSyncing)
changed = true
p.requestSlots <- true
p.handleRequestResult(res)
case b := <-p.blocks:
p.model.setState(p.repo, RepoSyncing)
p.model.setState(p.repoCfg.ID, RepoSyncing)
changed = true
if p.handleBlock(b) {
// Block was fully handled, free up the slot
@@ -149,7 +159,7 @@ func (p *puller) run() {
break pull
}
if debug {
l.Debugf("%q: idle but have %d open files", p.repo, len(p.openFiles))
l.Debugf("%q: idle but have %d open files", p.repoCfg.ID, len(p.openFiles))
i := 5
for _, f := range p.openFiles {
l.Debugf(" %v", f)
@@ -163,22 +173,22 @@ func (p *puller) run() {
}
if changed {
p.model.setState(p.repo, RepoCleaning)
p.model.setState(p.repoCfg.ID, RepoCleaning)
p.fixupDirectories()
changed = false
}
p.model.setState(p.repo, RepoIdle)
p.model.setState(p.repoCfg.ID, RepoIdle)
// Do a rescan if it's time for it
select {
case <-walkTicker:
if debug {
l.Debugf("%q: time for rescan", p.repo)
l.Debugf("%q: time for rescan", p.repoCfg.ID)
}
err := p.model.ScanRepo(p.repo)
err := p.model.ScanRepo(p.repoCfg.ID)
if err != nil {
invalidateRepo(p.cfg, p.repo, err)
invalidateRepo(p.cfg, p.repoCfg.ID, err)
return
}
@@ -195,11 +205,11 @@ func (p *puller) runRO() {
for _ = range walkTicker {
if debug {
l.Debugf("%q: time for rescan", p.repo)
l.Debugf("%q: time for rescan", p.repoCfg.ID)
}
err := p.model.ScanRepo(p.repo)
err := p.model.ScanRepo(p.repoCfg.ID)
if err != nil {
invalidateRepo(p.cfg, p.repo, err)
invalidateRepo(p.cfg, p.repoCfg.ID, err)
return
}
}
@@ -214,7 +224,7 @@ func (p *puller) fixupDirectories() {
return nil
}
rn, err := filepath.Rel(p.dir, path)
rn, err := filepath.Rel(p.repoCfg.Directory, path)
if err != nil {
return nil
}
@@ -223,7 +233,11 @@ func (p *puller) fixupDirectories() {
return nil
}
cur := p.model.CurrentRepoFile(p.repo, rn)
if filepath.Base(rn) == ".stversions" {
return nil
}
cur := p.model.CurrentRepoFile(p.repoCfg.ID, rn)
if cur.Name != rn {
// No matching dir in current list; weird
if debug {
@@ -232,7 +246,7 @@ func (p *puller) fixupDirectories() {
return nil
}
if cur.Flags&protocol.FlagDeleted != 0 {
if protocol.IsDeleted(cur.Flags) {
if debug {
l.Debugf("queue delete dir: %v", cur)
}
@@ -245,10 +259,10 @@ func (p *puller) fixupDirectories() {
return nil
}
if cur.Flags&uint32(os.ModePerm) != uint32(info.Mode()&os.ModePerm) {
if !p.repoCfg.IgnorePerms && protocol.HasPermissionBits(cur.Flags) && !scanner.PermsEqual(cur.Flags, uint32(info.Mode())) {
err := os.Chmod(path, os.FileMode(cur.Flags)&os.ModePerm)
if err != nil {
l.Warnln("Restoring folder flags: %q: %v", path, err)
l.Warnf("Restoring folder flags: %q: %v", path, err)
} else {
changed++
if debug {
@@ -261,7 +275,10 @@ func (p *puller) fixupDirectories() {
t := time.Unix(cur.Modified, 0)
err := os.Chtimes(path, t, t)
if err != nil {
l.Warnln("Restoring folder modtime: %q: %v", path, err)
if runtime.GOOS != "windows" {
// https://code.google.com/p/go/issues/detail?id=8090
l.Warnf("Restoring folder modtime: %q: %v", path, err)
}
} else {
changed++
if debug {
@@ -276,7 +293,7 @@ func (p *puller) fixupDirectories() {
for {
deleteDirs = nil
changed = 0
filepath.Walk(p.dir, walkFn)
filepath.Walk(p.repoCfg.Directory, walkFn)
var deleted = 0
// Delete any queued directories
@@ -286,10 +303,10 @@ func (p *puller) fixupDirectories() {
l.Debugln("delete dir:", dir)
}
err := os.Remove(dir)
if err != nil {
l.Warnln(err)
} else {
if err == nil {
deleted++
} else if p.versioner == nil { // Failures are expected in the presence of versioning
l.Warnln(err)
}
}
@@ -320,7 +337,7 @@ func (p *puller) handleRequestResult(res requestResult) {
p.openFiles[f.Name] = of
if debug {
l.Debugf("pull: wrote %q / %q offset %d outstanding %d done %v", p.repo, f.Name, res.offset, of.outstanding, of.done)
l.Debugf("pull: wrote %q / %q offset %d outstanding %d done %v", p.repoCfg.ID, f.Name, res.offset, of.outstanding, of.done)
}
if of.done && of.outstanding == 0 {
@@ -336,9 +353,9 @@ func (p *puller) handleBlock(b bqBlock) bool {
// For directories, making sure they exist is enough.
// Deleted directories we mark as handled and delete later.
if f.Flags&protocol.FlagDirectory != 0 {
if f.Flags&protocol.FlagDeleted == 0 {
path := filepath.Join(p.dir, f.Name)
if protocol.IsDirectory(f.Flags) {
if !protocol.IsDeleted(f.Flags) {
path := filepath.Join(p.repoCfg.Directory, f.Name)
_, err := os.Stat(path)
if err != nil && os.IsNotExist(err) {
if debug {
@@ -352,7 +369,30 @@ func (p *puller) handleBlock(b bqBlock) bool {
} else if debug {
l.Debugf("ignore delete dir: %v", f)
}
p.model.updateLocal(p.repo, f)
p.model.updateLocal(p.repoCfg.ID, f)
return true
}
if len(b.copy) > 0 && len(b.copy) == len(b.file.Blocks) && b.last {
// We are supposed to copy the entire file, and then fetch nothing.
// We don't actually need to make the copy.
if debug {
l.Debugln("taking shortcut:", f)
}
fp := filepath.Join(p.repoCfg.Directory, f.Name)
t := time.Unix(f.Modified, 0)
err := os.Chtimes(fp, t, t)
if debug && err != nil {
l.Debugf("pull: error: %q / %q: %v", p.repoCfg.ID, f.Name, err)
}
if !p.repoCfg.IgnorePerms && protocol.HasPermissionBits(f.Flags) {
err = os.Chmod(fp, os.FileMode(f.Flags&0777))
if debug && err != nil {
l.Debugf("pull: error: %q / %q: %v", p.repoCfg.ID, f.Name, err)
}
}
p.model.updateLocal(p.repoCfg.ID, f)
return true
}
@@ -361,12 +401,12 @@ func (p *puller) handleBlock(b bqBlock) bool {
if !ok {
if debug {
l.Debugf("pull: %q: opening file %q", p.repo, f.Name)
l.Debugf("pull: %q: opening file %q", p.repoCfg.ID, f.Name)
}
of.availability = uint64(p.model.repoFiles[p.repo].Availability(f.Name))
of.filepath = filepath.Join(p.dir, f.Name)
of.temp = filepath.Join(p.dir, defTempNamer.TempName(f.Name))
of.availability = uint64(p.model.repoFiles[p.repoCfg.ID].Availability(f.Name))
of.filepath = filepath.Join(p.repoCfg.Directory, f.Name)
of.temp = filepath.Join(p.repoCfg.Directory, defTempNamer.TempName(f.Name))
dirName := filepath.Dir(of.filepath)
_, err := os.Stat(dirName)
@@ -374,26 +414,26 @@ func (p *puller) handleBlock(b bqBlock) bool {
err = os.MkdirAll(dirName, 0777)
}
if err != nil {
l.Debugf("pull: error: %q / %q: %v", p.repo, f.Name, err)
l.Debugf("pull: error: %q / %q: %v", p.repoCfg.ID, f.Name, err)
}
of.file, of.err = os.Create(of.temp)
if of.err != nil {
if debug {
l.Debugf("pull: error: %q / %q: %v", p.repo, f.Name, of.err)
l.Debugf("pull: error: %q / %q: %v", p.repoCfg.ID, f.Name, of.err)
}
if !b.last {
p.openFiles[f.Name] = of
}
return true
}
defTempNamer.Hide(of.temp)
osutil.HideFile(of.temp)
}
if of.err != nil {
// We have already failed this file.
if debug {
l.Debugf("pull: error: %q / %q has already failed: %v", p.repo, f.Name, of.err)
l.Debugf("pull: error: %q / %q has already failed: %v", p.repoCfg.ID, f.Name, of.err)
}
if b.last {
delete(p.openFiles, f.Name)
@@ -424,14 +464,14 @@ func (p *puller) handleCopyBlock(b bqBlock) {
of := p.openFiles[f.Name]
if debug {
l.Debugf("pull: copying %d blocks for %q / %q", len(b.copy), p.repo, f.Name)
l.Debugf("pull: copying %d blocks for %q / %q", len(b.copy), p.repoCfg.ID, f.Name)
}
var exfd *os.File
exfd, of.err = os.Open(of.filepath)
if of.err != nil {
if debug {
l.Debugf("pull: error: %q / %q: %v", p.repo, f.Name, of.err)
l.Debugf("pull: error: %q / %q: %v", p.repoCfg.ID, f.Name, of.err)
}
of.file.Close()
of.file = nil
@@ -450,7 +490,7 @@ func (p *puller) handleCopyBlock(b bqBlock) {
buffers.Put(bs)
if of.err != nil {
if debug {
l.Debugf("pull: error: %q / %q: %v", p.repo, f.Name, of.err)
l.Debugf("pull: error: %q / %q: %v", p.repoCfg.ID, f.Name, of.err)
}
exfd.Close()
of.file.Close()
@@ -493,10 +533,10 @@ func (p *puller) handleRequestBlock(b bqBlock) bool {
go func(node string, b bqBlock) {
if debug {
l.Debugf("pull: requesting %q / %q offset %d size %d from %q outstanding %d", p.repo, f.Name, b.block.Offset, b.block.Size, node, of.outstanding)
l.Debugf("pull: requesting %q / %q offset %d size %d from %q outstanding %d", p.repoCfg.ID, f.Name, b.block.Offset, b.block.Size, node, of.outstanding)
}
bs, err := p.model.requestGlobal(node, p.repo, f.Name, b.block.Offset, int(b.block.Size), nil)
bs, err := p.model.requestGlobal(node, p.repoCfg.ID, f.Name, b.block.Offset, int(b.block.Size), nil)
p.requestResults <- requestResult{
node: node,
file: f,
@@ -520,31 +560,35 @@ func (p *puller) handleEmptyBlock(b bqBlock) {
}
}
if f.Flags&protocol.FlagDeleted != 0 {
if protocol.IsDeleted(f.Flags) {
if debug {
l.Debugf("pull: delete %q", f.Name)
}
os.Remove(of.temp)
os.Chmod(of.filepath, 0666)
if err := os.Remove(of.filepath); err == nil || os.IsNotExist(err) {
p.model.updateLocal(p.repo, f)
if p.versioner != nil {
if err := p.versioner.Archive(of.filepath); err == nil {
p.model.updateLocal(p.repoCfg.ID, f)
}
} else if err := os.Remove(of.filepath); err == nil || os.IsNotExist(err) {
p.model.updateLocal(p.repoCfg.ID, f)
}
} else {
if debug {
l.Debugf("pull: no blocks to fetch and nothing to copy for %q / %q", p.repo, f.Name)
l.Debugf("pull: no blocks to fetch and nothing to copy for %q / %q", p.repoCfg.ID, f.Name)
}
t := time.Unix(f.Modified, 0)
if os.Chtimes(of.temp, t, t) != nil {
delete(p.openFiles, f.Name)
return
}
if os.Chmod(of.temp, os.FileMode(f.Flags&0777)) != nil {
if !p.repoCfg.IgnorePerms && protocol.HasPermissionBits(f.Flags) && os.Chmod(of.temp, os.FileMode(f.Flags&0777)) != nil {
delete(p.openFiles, f.Name)
return
}
defTempNamer.Show(of.temp)
if Rename(of.temp, of.filepath) == nil {
p.model.updateLocal(p.repo, f)
osutil.ShowFile(of.temp)
if osutil.Rename(of.temp, of.filepath) == nil {
p.model.updateLocal(p.repoCfg.ID, f)
}
}
delete(p.openFiles, f.Name)
@@ -552,8 +596,8 @@ func (p *puller) handleEmptyBlock(b bqBlock) {
func (p *puller) queueNeededBlocks() {
queued := 0
for _, f := range p.model.NeedFilesRepo(p.repo) {
lf := p.model.CurrentRepoFile(p.repo, f.Name)
for _, f := range p.model.NeedFilesRepo(p.repoCfg.ID) {
lf := p.model.CurrentRepoFile(p.repoCfg.ID, f.Name)
have, need := scanner.BlockDiff(lf.Blocks, f.Blocks)
if debug {
l.Debugf("need:\n local: %v\n global: %v\n haveBlocks: %v\n needBlocks: %v", lf, f, have, need)
@@ -566,13 +610,13 @@ func (p *puller) queueNeededBlocks() {
})
}
if debug && queued > 0 {
l.Debugf("%q: queued %d blocks", p.repo, queued)
l.Debugf("%q: queued %d blocks", p.repoCfg.ID, queued)
}
}
func (p *puller) closeFile(f scanner.File) {
if debug {
l.Debugf("pull: closing %q / %q", p.repo, f.Name)
l.Debugf("pull: closing %q / %q", p.repoCfg.ID, f.Name)
}
of := p.openFiles[f.Name]
@@ -584,7 +628,7 @@ func (p *puller) closeFile(f scanner.File) {
fd, err := os.Open(of.temp)
if err != nil {
if debug {
l.Debugf("pull: error: %q / %q: %v", p.repo, f.Name, err)
l.Debugf("pull: error: %q / %q: %v", p.repoCfg.ID, f.Name, err)
}
return
}
@@ -593,29 +637,49 @@ func (p *puller) closeFile(f scanner.File) {
if l0, l1 := len(hb), len(f.Blocks); l0 != l1 {
if debug {
l.Debugf("pull: %q / %q: nblocks %d != %d", p.repo, f.Name, l0, l1)
l.Debugf("pull: %q / %q: nblocks %d != %d", p.repoCfg.ID, f.Name, l0, l1)
}
return
}
for i := range hb {
if bytes.Compare(hb[i].Hash, f.Blocks[i].Hash) != 0 {
l.Debugf("pull: %q / %q: block %d hash mismatch", p.repo, f.Name, i)
l.Debugf("pull: %q / %q: block %d hash mismatch", p.repoCfg.ID, f.Name, i)
return
}
}
t := time.Unix(f.Modified, 0)
os.Chtimes(of.temp, t, t)
os.Chmod(of.temp, os.FileMode(f.Flags&0777))
defTempNamer.Show(of.temp)
if debug {
l.Debugf("pull: rename %q / %q: %q", p.repo, f.Name, of.filepath)
err = os.Chtimes(of.temp, t, t)
if debug && err != nil {
l.Debugf("pull: error: %q / %q: %v", p.repoCfg.ID, f.Name, err)
}
if err := Rename(of.temp, of.filepath); err == nil {
p.model.updateLocal(p.repo, f)
if !p.repoCfg.IgnorePerms && protocol.HasPermissionBits(f.Flags) {
err = os.Chmod(of.temp, os.FileMode(f.Flags&0777))
if debug && err != nil {
l.Debugf("pull: error: %q / %q: %v", p.repoCfg.ID, f.Name, err)
}
}
osutil.ShowFile(of.temp)
if p.versioner != nil {
err := p.versioner.Archive(of.filepath)
if err != nil {
if debug {
l.Debugf("pull: error: %q / %q: %v", p.repoCfg.ID, f.Name, err)
}
return
}
}
if debug {
l.Debugf("pull: rename %q / %q: %q", p.repoCfg.ID, f.Name, of.filepath)
}
if err := osutil.Rename(of.temp, of.filepath); err == nil {
p.model.updateLocal(p.repoCfg.ID, f)
} else {
l.Debugf("pull: error: %q / %q: %v", p.repo, f.Name, err)
l.Debugf("pull: error: %q / %q: %v", p.repoCfg.ID, f.Name, err)
}
}

View File

@@ -23,11 +23,3 @@ func (t tempNamer) TempName(name string) string {
tname := fmt.Sprintf("%s.%s", t.prefix, filepath.Base(name))
return filepath.Join(tdir, tname)
}
func (t tempNamer) Hide(path string) error {
return nil
}
func (t tempNamer) Show(path string) error {
return nil
}

View File

@@ -6,7 +6,6 @@ import (
"fmt"
"path/filepath"
"strings"
"syscall"
)
type tempNamer struct {
@@ -24,33 +23,3 @@ func (t tempNamer) TempName(name string) string {
tname := fmt.Sprintf("%s.%s.tmp", t.prefix, filepath.Base(name))
return filepath.Join(tdir, tname)
}
func (t tempNamer) Hide(path string) error {
p, err := syscall.UTF16PtrFromString(path)
if err != nil {
return err
}
attrs, err := syscall.GetFileAttributes(p)
if err != nil {
return err
}
attrs |= syscall.FILE_ATTRIBUTE_HIDDEN
return syscall.SetFileAttributes(p, attrs)
}
func (t tempNamer) Show(path string) error {
p, err := syscall.UTF16PtrFromString(path)
if err != nil {
return err
}
attrs, err := syscall.GetFileAttributes(p)
if err != nil {
return err
}
attrs &^= syscall.FILE_ATTRIBUTE_HIDDEN
return syscall.SetFileAttributes(p, attrs)
}

View File

@@ -2,26 +2,12 @@ package model
import (
"fmt"
"os"
"path/filepath"
"runtime"
"github.com/calmh/syncthing/protocol"
"github.com/calmh/syncthing/scanner"
)
func Rename(from, to string) error {
if runtime.GOOS == "windows" {
os.Chmod(to, 0666) // Make sure the file is user writeable
err := os.Remove(to)
if err != nil && !os.IsNotExist(err) {
l.Warnln(err)
}
}
defer os.Remove(from) // Don't leave a dangling temp file in case of rename error
return os.Rename(from, to)
}
func fileFromFileInfo(f protocol.FileInfo) scanner.File {
var blocks = make([]scanner.Block, len(f.Blocks))
var offset int64
@@ -95,17 +81,8 @@ func compareClusterConfig(local, remote protocol.ClusterConfigMessage) error {
}
}
}
} else {
return ClusterConfigMismatch(fmt.Errorf("remote is missing repository %q", repo))
}
}
for repo := range rm {
if _, ok := lm[repo]; !ok {
return ClusterConfigMismatch(fmt.Errorf("remote has extra repository %q", repo))
}
}
return nil
}

View File

@@ -20,24 +20,6 @@ var testcases = []struct {
remote: protocol.ClusterConfigMessage{ClientName: "c", ClientVersion: "d"},
err: "",
},
{
local: protocol.ClusterConfigMessage{
Repositories: []protocol.Repository{
{ID: "foo"},
},
},
remote: protocol.ClusterConfigMessage{ClientName: "c", ClientVersion: "d"},
err: `remote is missing repository "foo"`,
},
{
local: protocol.ClusterConfigMessage{ClientName: "c", ClientVersion: "d"},
remote: protocol.ClusterConfigMessage{
Repositories: []protocol.Repository{
{ID: "foo"},
},
},
err: `remote has extra repository "foo"`,
},
{
local: protocol.ClusterConfigMessage{
Repositories: []protocol.Repository{
@@ -53,38 +35,6 @@ var testcases = []struct {
},
err: "",
},
{
local: protocol.ClusterConfigMessage{
Repositories: []protocol.Repository{
{ID: "quux"},
{ID: "foo"},
{ID: "bar"},
},
},
remote: protocol.ClusterConfigMessage{
Repositories: []protocol.Repository{
{ID: "bar"},
{ID: "quux"},
},
},
err: `remote is missing repository "foo"`,
},
{
local: protocol.ClusterConfigMessage{
Repositories: []protocol.Repository{
{ID: "quux"},
{ID: "bar"},
},
},
remote: protocol.ClusterConfigMessage{
Repositories: []protocol.Repository{
{ID: "bar"},
{ID: "foo"},
{ID: "quux"},
},
},
err: `remote has extra repository "foo"`,
},
{
local: protocol.ClusterConfigMessage{
Repositories: []protocol.Repository{

11
osutil/hidden_unix.go Normal file
View File

@@ -0,0 +1,11 @@
// +build !windows
package osutil
func HideFile(path string) error {
return nil
}
func ShowFile(path string) error {
return nil
}

35
osutil/hidden_windows.go Normal file
View File

@@ -0,0 +1,35 @@
// +build windows
package osutil
import "syscall"
func HideFile(path string) error {
p, err := syscall.UTF16PtrFromString(path)
if err != nil {
return err
}
attrs, err := syscall.GetFileAttributes(p)
if err != nil {
return err
}
attrs |= syscall.FILE_ATTRIBUTE_HIDDEN
return syscall.SetFileAttributes(p, attrs)
}
func ShowFile(path string) error {
p, err := syscall.UTF16PtrFromString(path)
if err != nil {
return err
}
attrs, err := syscall.GetFileAttributes(p)
if err != nil {
return err
}
attrs &^= syscall.FILE_ATTRIBUTE_HIDDEN
return syscall.SetFileAttributes(p, attrs)
}

18
osutil/osutil.go Normal file
View File

@@ -0,0 +1,18 @@
package osutil
import (
"os"
"runtime"
)
func Rename(from, to string) error {
if runtime.GOOS == "windows" {
os.Chmod(to, 0666) // Make sure the file is user writeable
err := os.Remove(to)
if err != nil && !os.IsNotExist(err) {
return err
}
}
defer os.Remove(from) // Don't leave a dangling temp file in case of rename error
return os.Rename(from, to)
}

View File

@@ -388,7 +388,7 @@ The Flags field is made up of the following single bit flags:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Reserved |I|D| Unix Perm. & Mode |
| Reserved |P|I|D| Unix Perm. & Mode |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- The lower 12 bits hold the common Unix permission and mode bits. An
@@ -404,7 +404,13 @@ The Flags field is made up of the following single bit flags:
synchronization. A peer MAY set this bit to indicate that it can
temporarily not serve data for the file.
- Bit 0 through 17 are reserved for future use and SHALL be set to
- Bit 17 ("P") is set when there is no permission information for the
file. This is the case when it originates on a non-permission-
supporting file system. Changes to only permission bits SHOULD be
disregarded on files with this bit set. The permissions bits MUST be
set to the octal value 0666.
- Bit 0 through 16 are reserved for future use and SHALL be set to
zero.
The hash algorithm is implied by the Hash length. Currently, the hash

View File

@@ -10,9 +10,15 @@ type countingReader struct {
tot uint64
}
var (
totalIncoming uint64
totalOutgoing uint64
)
func (c *countingReader) Read(bs []byte) (int, error) {
n, err := c.Reader.Read(bs)
atomic.AddUint64(&c.tot, uint64(n))
atomic.AddUint64(&totalIncoming, uint64(n))
return n, err
}
@@ -28,9 +34,14 @@ type countingWriter struct {
func (c *countingWriter) Write(bs []byte) (int, error) {
n, err := c.Writer.Write(bs)
atomic.AddUint64(&c.tot, uint64(n))
atomic.AddUint64(&totalOutgoing, uint64(n))
return n, err
}
func (c *countingWriter) Tot() uint64 {
return atomic.LoadUint64(&c.tot)
}
func TotalInOut() (uint64, uint64) {
return atomic.LoadUint64(&totalIncoming), atomic.LoadUint64(&totalOutgoing)
}

13
protocol/debug.go Normal file
View File

@@ -0,0 +1,13 @@
package protocol
import (
"os"
"strings"
"github.com/calmh/syncthing/logger"
)
var (
debug = strings.Contains(os.Getenv("STTRACE"), "protocol") || os.Getenv("STTRACE") == "all"
l = logger.DefaultLogger
)

View File

@@ -25,9 +25,10 @@ const (
)
const (
FlagDeleted uint32 = 1 << 12
FlagInvalid = 1 << 13
FlagDirectory = 1 << 14
FlagDeleted uint32 = 1 << 12
FlagInvalid = 1 << 13
FlagDirectory = 1 << 14
FlagNoPermBits = 1 << 15
)
const (
@@ -91,8 +92,8 @@ type asyncResult struct {
}
const (
pingTimeout = 4 * time.Minute
pingIdleTime = 5 * time.Minute
pingTimeout = 300 * time.Second
pingIdleTime = 600 * time.Second
)
func NewConnection(nodeID string, reader io.Reader, writer io.Writer, receiver Model) Connection {
@@ -475,11 +476,27 @@ func (c *rawConnection) pingerLoop() {
for {
select {
case <-ticker:
if d := time.Since(c.xr.LastRead()); d < pingIdleTime {
if debug {
l.Debugln(c.id, "ping skipped after rd", d)
}
continue
}
if d := time.Since(c.xw.LastWrite()); d < pingIdleTime {
if debug {
l.Debugln(c.id, "ping skipped after wr", d)
}
continue
}
go func() {
if debug {
l.Debugln(c.id, "ping ->")
}
rc <- c.ping()
}()
select {
case ok := <-rc:
l.Debugln(c.id, "<- pong")
if !ok {
c.close(fmt.Errorf("ping failure"))
}
@@ -515,3 +532,19 @@ func (c *rawConnection) Statistics() Statistics {
OutBytesTotal: int(c.cw.Tot()),
}
}
func IsDeleted(bits uint32) bool {
return bits&FlagDeleted != 0
}
func IsInvalid(bits uint32) bool {
return bits&FlagInvalid != 0
}
func IsDirectory(bits uint32) bool {
return bits&FlagDirectory != 0
}
func HasPermissionBits(bits uint32) bool {
return bits&FlagNoPermBits == 0
}

View File

@@ -29,6 +29,10 @@ type Walker struct {
// Suppressed files will be returned with empty metadata and the Suppressed flag set.
// Requires CurrentFiler to be set.
Suppressor Suppressor
// If IgnorePerms is true, changes to permission bits will not be
// detected. Scanned files will get zero permission bits and the
// NoPermissionBits flag set.
IgnorePerms bool
}
type TempNamer interface {
@@ -140,15 +144,7 @@ func (w *Walker) walkAndHashFiles(res *[]File, ign map[string][]string) filepath
return nil
}
if _, sn := filepath.Split(rn); sn == w.IgnoreFile {
// An ignore-file; these are ignored themselves
if debug {
l.Debugln("ignorefile:", rn)
}
return nil
}
if w.ignoreFile(ign, rn) {
if sn := filepath.Base(rn); sn == w.IgnoreFile || sn == ".stversions" || w.ignoreFile(ign, rn) {
// An ignored file
if debug {
l.Debugln("ignored:", rn)
@@ -162,16 +158,23 @@ func (w *Walker) walkAndHashFiles(res *[]File, ign map[string][]string) filepath
if info.Mode().IsDir() {
if w.CurrentFiler != nil {
cf := w.CurrentFiler.CurrentFile(rn)
if cf.Modified == info.ModTime().Unix() && cf.Flags&protocol.FlagDirectory != 0 && permsEqual(cf.Flags, uint32(info.Mode())) {
permUnchanged := w.IgnorePerms || !protocol.HasPermissionBits(cf.Flags) || PermsEqual(cf.Flags, uint32(info.Mode()))
if cf.Modified == info.ModTime().Unix() && protocol.IsDirectory(cf.Flags) && permUnchanged {
if debug {
l.Debugln("unchanged:", cf)
}
*res = append(*res, cf)
} else {
var flags uint32 = protocol.FlagDirectory
if w.IgnorePerms {
flags |= protocol.FlagNoPermBits | 0777
} else {
flags |= uint32(info.Mode() & os.ModePerm)
}
f := File{
Name: rn,
Version: lamport.Default.Tick(0),
Flags: uint32(info.Mode()&os.ModePerm) | protocol.FlagDirectory,
Flags: flags,
Modified: info.ModTime().Unix(),
}
if debug {
@@ -186,7 +189,8 @@ func (w *Walker) walkAndHashFiles(res *[]File, ign map[string][]string) filepath
if info.Mode().IsRegular() {
if w.CurrentFiler != nil {
cf := w.CurrentFiler.CurrentFile(rn)
if cf.Flags&protocol.FlagDeleted == 0 && cf.Modified == info.ModTime().Unix() && permsEqual(cf.Flags, uint32(info.Mode())) {
permUnchanged := w.IgnorePerms || !protocol.HasPermissionBits(cf.Flags) || PermsEqual(cf.Flags, uint32(info.Mode()))
if !protocol.IsDeleted(cf.Flags) && cf.Modified == info.ModTime().Unix() && permUnchanged {
if debug {
l.Debugln("unchanged:", cf)
}
@@ -235,11 +239,16 @@ func (w *Walker) walkAndHashFiles(res *[]File, ign map[string][]string) filepath
t1 := time.Now()
l.Debugln("hashed:", rn, ";", len(blocks), "blocks;", info.Size(), "bytes;", int(float64(info.Size())/1024/t1.Sub(t0).Seconds()), "KB/s")
}
var flags = uint32(info.Mode() & os.ModePerm)
if w.IgnorePerms {
flags = protocol.FlagNoPermBits | 0666
}
f := File{
Name: rn,
Version: lamport.Default.Tick(0),
Size: info.Size(),
Flags: uint32(info.Mode()),
Flags: flags,
Modified: info.ModTime().Unix(),
Blocks: blocks,
}
@@ -275,15 +284,17 @@ func (w *Walker) ignoreFile(patterns map[string][]string, file string) bool {
}
func checkDir(dir string) error {
if info, err := os.Stat(dir); err != nil {
if info, err := os.Lstat(dir); err != nil {
return err
} else if !info.IsDir() {
return errors.New(dir + ": not a directory")
} else if debug {
l.Debugln("checkDir", dir, info)
}
return nil
}
func permsEqual(a, b uint32) bool {
func PermsEqual(a, b uint32) bool {
switch runtime.GOOS {
case "windows":
// There is only writeable and read only, represented for user, group

View File

@@ -60,14 +60,14 @@ func Discover() (*IGD, error) {
return nil, err
}
search := []byte(`
M-SEARCH * HTTP/1.1
searchStr := `M-SEARCH * HTTP/1.1
Host: 239.255.255.250:1900
St: urn:schemas-upnp-org:device:InternetGatewayDevice:1
Man: "ssdp:discover"
Mx: 3
`)
`
search := []byte(strings.Replace(searchStr, "\n", "\r\n", -1))
_, err = socket.WriteTo(search, ssdp)
if err != nil {

13
versioner/debug.go Normal file
View File

@@ -0,0 +1,13 @@
package versioner
import (
"os"
"strings"
"github.com/calmh/syncthing/logger"
)
var (
debug = strings.Contains(os.Getenv("STTRACE"), "versioner") || os.Getenv("STTRACE") == "all"
l = logger.DefaultLogger
)

84
versioner/simple.go Normal file
View File

@@ -0,0 +1,84 @@
package versioner
import (
"os"
"path/filepath"
"sort"
"strconv"
"time"
"github.com/calmh/syncthing/osutil"
)
func init() {
// Register the constructor for this type of versioner with the name "simple"
Factories["simple"] = NewSimple
}
// The type holds our configuration
type Simple struct {
keep int
}
// The constructor function takes a map of parameters and creates the type.
func NewSimple(params map[string]string) Versioner {
keep, err := strconv.Atoi(params["keep"])
if err != nil {
keep = 5 // A reasonable default
}
s := Simple{
keep: keep,
}
if debug {
l.Debugf("instantiated %#v", s)
}
return s
}
// Move away the named file to a version archive. If this function returns
// nil, the named file does not exist any more (has been archived).
func (v Simple) Archive(path string) error {
_, err := os.Stat(path)
if err != nil && os.IsNotExist(err) {
return nil
}
if debug {
l.Debugln("archiving", path)
}
file := filepath.Base(path)
dir := filepath.Join(filepath.Dir(path), ".stversions")
err = os.MkdirAll(dir, 0755)
if err != nil && !os.IsExist(err) {
return err
} else {
osutil.HideFile(dir)
}
ver := file + "~" + time.Now().Format("20060102-150405")
err = osutil.Rename(path, filepath.Join(dir, ver))
if err != nil {
return err
}
versions, err := filepath.Glob(filepath.Join(dir, file+"~*"))
if err != nil {
l.Warnln(err)
return nil
}
if len(versions) > v.keep {
sort.Strings(versions)
for _, toRemove := range versions[:len(versions)-v.keep] {
err = os.Remove(toRemove)
if err != nil {
l.Warnln(err)
}
}
}
return nil
}

7
versioner/versioner.go Normal file
View File

@@ -0,0 +1,7 @@
package versioner
type Versioner interface {
Archive(path string) error
}
var Factories = map[string]func(map[string]string) Versioner{}

View File

@@ -3,15 +3,17 @@ package xdr
import (
"errors"
"io"
"time"
)
var ErrElementSizeExceeded = errors.New("element size exceeded")
type Reader struct {
r io.Reader
tot int
err error
b [8]byte
r io.Reader
tot int
err error
b [8]byte
last time.Time
}
func NewReader(r io.Reader) *Reader {
@@ -44,6 +46,7 @@ func (r *Reader) ReadBytesMaxInto(max int, dst []byte) []byte {
if r.err != nil {
return nil
}
r.last = time.Now()
l := int(r.ReadUint32())
if r.err != nil {
return nil
@@ -74,6 +77,7 @@ func (r *Reader) ReadUint16() uint16 {
if r.err != nil {
return 0
}
r.last = time.Now()
_, r.err = io.ReadFull(r.r, r.b[:4])
r.tot += 4
v := uint16(r.b[1]) | uint16(r.b[0])<<8
@@ -88,6 +92,7 @@ func (r *Reader) ReadUint32() uint32 {
if r.err != nil {
return 0
}
r.last = time.Now()
n, r.err = io.ReadFull(r.r, r.b[:4])
if n < 4 {
return 0
@@ -105,6 +110,7 @@ func (r *Reader) ReadUint64() uint64 {
if r.err != nil {
return 0
}
r.last = time.Now()
n, r.err = io.ReadFull(r.r, r.b[:8])
r.tot += n
v := uint64(r.b[7]) | uint64(r.b[6])<<8 | uint64(r.b[5])<<16 | uint64(r.b[4])<<24 |
@@ -122,3 +128,7 @@ func (r *Reader) Tot() int {
func (r *Reader) Error() error {
return r.err
}
func (r *Reader) LastRead() time.Time {
return r.last
}

View File

@@ -1,6 +1,9 @@
package xdr
import "io"
import (
"io"
"time"
)
func pad(l int) int {
d := l % 4
@@ -13,10 +16,11 @@ func pad(l int) int {
var padBytes = []byte{0, 0, 0}
type Writer struct {
w io.Writer
tot int
err error
b [8]byte
w io.Writer
tot int
err error
b [8]byte
last time.Time
}
func NewWriter(w io.Writer) *Writer {
@@ -34,6 +38,7 @@ func (w *Writer) WriteBytes(bs []byte) (int, error) {
return 0, w.err
}
w.last = time.Now()
w.WriteUint32(uint32(len(bs)))
if w.err != nil {
return 0, w.err
@@ -65,6 +70,7 @@ func (w *Writer) WriteUint16(v uint16) (int, error) {
return 0, w.err
}
w.last = time.Now()
if debug {
dl.Debugf("wr uint16=%d", v)
}
@@ -85,6 +91,7 @@ func (w *Writer) WriteUint32(v uint32) (int, error) {
return 0, w.err
}
w.last = time.Now()
if debug {
dl.Debugf("wr uint32=%d", v)
}
@@ -105,6 +112,7 @@ func (w *Writer) WriteUint64(v uint64) (int, error) {
return 0, w.err
}
w.last = time.Now()
if debug {
dl.Debugf("wr uint64=%d", v)
}
@@ -131,3 +139,7 @@ func (w *Writer) Tot() int {
func (w *Writer) Error() error {
return w.err
}
func (w *Writer) LastWrite() time.Time {
return w.last
}