mirror of
https://github.com/syncthing/syncthing.git
synced 2026-01-03 19:39:20 -05:00
Compare commits
87 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d87051ca99 | ||
|
|
3798cebad0 | ||
|
|
a477989950 | ||
|
|
5065d1d0b4 | ||
|
|
829990c9ef | ||
|
|
ac037e0fa3 | ||
|
|
da42d51008 | ||
|
|
99027813ef | ||
|
|
9112ba8f0b | ||
|
|
843fd9bdbd | ||
|
|
26c33c4a69 | ||
|
|
2db76ae786 | ||
|
|
a0b15d006d | ||
|
|
23b27fa24a | ||
|
|
b6f580cbc2 | ||
|
|
f2459ef331 | ||
|
|
0a37fac794 | ||
|
|
2d9a822ed7 | ||
|
|
98622ca4d0 | ||
|
|
f7a25adcbd | ||
|
|
9bf13b253c | ||
|
|
2e8b639a34 | ||
|
|
672f7a010f | ||
|
|
37e15c4368 | ||
|
|
4d7837ba96 | ||
|
|
a6c8423905 | ||
|
|
832ed556d9 | ||
|
|
7c6fb018ca | ||
|
|
9c5c06bf31 | ||
|
|
61e3daaead | ||
|
|
9c0fde795e | ||
|
|
ce4f565e2f | ||
|
|
5369a62fd5 | ||
|
|
b44016ff70 | ||
|
|
9f76c87880 | ||
|
|
42ae2898e1 | ||
|
|
dd649a6be4 | ||
|
|
593f098276 | ||
|
|
4a87221f16 | ||
|
|
7745ed34d3 | ||
|
|
8fe546c4a2 | ||
|
|
381f6aeaf6 | ||
|
|
9154bacced | ||
|
|
dc0dc8efb4 | ||
|
|
b062d5dd7f | ||
|
|
c519e582b5 | ||
|
|
6b9dce36bf | ||
|
|
8e0520887a | ||
|
|
cfd1fdb38e | ||
|
|
c6ba0208d0 | ||
|
|
3d055bbb79 | ||
|
|
dd971b56e5 | ||
|
|
4031f5e24b | ||
|
|
1cd7cc6869 | ||
|
|
9de2864db3 | ||
|
|
c27861cbaf | ||
|
|
c2f75d3689 | ||
|
|
5454ca1cf7 | ||
|
|
8644bf30a9 | ||
|
|
db3341a178 | ||
|
|
e2cb0219c7 | ||
|
|
217f29de76 | ||
|
|
8661afcb4f | ||
|
|
ed07fc0f2c | ||
|
|
4af3f77a9a | ||
|
|
8c4f07ef1b | ||
|
|
1a231d39a5 | ||
|
|
17e3d14272 | ||
|
|
03182c7714 | ||
|
|
963078f6ac | ||
|
|
8356b58b1d | ||
|
|
303ce02271 | ||
|
|
bcdc3ecdae | ||
|
|
b60d648e22 | ||
|
|
7bc36cbbd1 | ||
|
|
04130fcb15 | ||
|
|
52d8e4c691 | ||
|
|
ae0193b724 | ||
|
|
2e1c33206f | ||
|
|
0c642ec7cf | ||
|
|
b3ca96eeba | ||
|
|
ae0e033178 | ||
|
|
a97985b428 | ||
|
|
63c0f11458 | ||
|
|
b336b2c336 | ||
|
|
8a5a573851 | ||
|
|
358862c7ad |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -5,3 +5,7 @@ stcli.exe
|
||||
*.tar.gz
|
||||
*.zip
|
||||
*.asc
|
||||
*.sublime*
|
||||
discosrv
|
||||
stpidx
|
||||
.jshintrc
|
||||
@@ -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>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package beacon
|
||||
|
||||
import "net"
|
||||
@@ -102,9 +106,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/calmh/syncthing/beacon"
|
||||
)
|
||||
|
||||
func main() {
|
||||
b, err := beacon.NewBeacon(21025)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
go func() {
|
||||
for {
|
||||
bs, addr := b.Recv()
|
||||
log.Printf("Received %d bytes from %s: %x %x", len(bs), addr, bs[:8], bs[8:])
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
bs := [16]byte{}
|
||||
binary.BigEndian.PutUint64(bs[:], uint64(time.Now().UnixNano()))
|
||||
log.Printf("My ID: %x", bs[:8])
|
||||
for {
|
||||
binary.BigEndian.PutUint64(bs[8:], uint64(time.Now().UnixNano()))
|
||||
b.Send(bs[:])
|
||||
log.Printf("Sent %d bytes", len(bs[:]))
|
||||
time.Sleep(10 * time.Second)
|
||||
}
|
||||
}()
|
||||
select {}
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package beacon
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// Package beacon implements an UDP broadcast beacon
|
||||
package beacon
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// Package buffers manages a set of reusable byte buffers.
|
||||
package buffers
|
||||
|
||||
|
||||
15
build.sh
15
build.sh
@@ -1,8 +1,9 @@
|
||||
#!/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
|
||||
distFiles=(README.md LICENSE CONTRIBUTORS) # apart from the binary itself
|
||||
version=$(git describe --always --dirty)
|
||||
date=$(git show -s --format=%ct)
|
||||
user=$(whoami)
|
||||
@@ -59,7 +60,10 @@ zipDist() {
|
||||
name="$1"
|
||||
rm -rf "$name"
|
||||
mkdir -p "$name"
|
||||
cp syncthing.exe "${distFiles[@]}" "$name"
|
||||
for f in "${distFiles[@]}" ; do
|
||||
sed 's/$/
|
||||
/' < "$f" > "$name/$f.txt"
|
||||
done
|
||||
cp syncthing.exe "$name"
|
||||
sign "$name/syncthing.exe"
|
||||
zip -r "$name.zip" "$name"
|
||||
@@ -88,7 +92,8 @@ case "$1" in
|
||||
build -race
|
||||
;;
|
||||
|
||||
build -tags guidev
|
||||
guidev)
|
||||
echo "Syncthing is already built for GUI developments. Try:"
|
||||
echo " STGUIASSETS=~/someDir/gui syncthing"
|
||||
;;
|
||||
|
||||
@@ -112,6 +117,10 @@ case "$1" in
|
||||
rm -f *.tar.gz *.zip
|
||||
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%-*}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// Package cid provides a manager for mappings between node ID:s and connection ID:s.
|
||||
package cid
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package cid
|
||||
|
||||
import "testing"
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,20 +1,28 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
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 +39,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 +50,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 +77,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 +121,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 +188,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 +249,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 +354,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
//+build guidev
|
||||
|
||||
package main
|
||||
|
||||
import "github.com/codegangsta/martini"
|
||||
|
||||
func embeddedStatic() interface{} {
|
||||
return martini.Static("gui")
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
//+build solaris
|
||||
|
||||
package main
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
//+build !windows,!solaris
|
||||
|
||||
package main
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
//+build locktrace
|
||||
|
||||
package main
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -24,6 +28,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"
|
||||
@@ -48,7 +53,7 @@ func init() {
|
||||
LongVersion = fmt.Sprintf("syncthing %s (%s %s-%s) %s@%s %s", Version, runtime.Version(), runtime.GOOS, runtime.GOARCH, BuildUser, BuildHost, date)
|
||||
|
||||
if os.Getenv("STTRACE") != "" {
|
||||
l.SetFlags(log.Ltime | log.Ldate | log.Lmicroseconds | log.Lshortfile)
|
||||
logFlags = log.Ltime | log.Ldate | log.Lmicroseconds | log.Lshortfile
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +61,7 @@ var (
|
||||
cfg config.Configuration
|
||||
myID string
|
||||
confDir string
|
||||
logFlags int = log.Ltime
|
||||
rateBucket *ratelimit.Bucket
|
||||
stop = make(chan bool)
|
||||
discoverer *discover.Discoverer
|
||||
@@ -63,7 +69,19 @@ var (
|
||||
|
||||
const (
|
||||
usage = "syncthing [options]"
|
||||
extraUsage = `The following enviroment variables are interpreted by syncthing:
|
||||
extraUsage = `The value for the -logflags option is a sum of the following:
|
||||
|
||||
1 Date
|
||||
2 Time
|
||||
4 Microsecond time
|
||||
8 Long filename
|
||||
16 Short filename
|
||||
|
||||
I.e. to prefix each log line with date and time, set -logflags=3 (1 + 2 from
|
||||
above). The value 0 is used to disable all of the above. The default is to
|
||||
show time only (2).
|
||||
|
||||
The following enviroment variables are interpreted by syncthing:
|
||||
|
||||
STNORESTART Do not attempt to restart when requested to, instead just exit.
|
||||
Set this variable when running under a service manager such as
|
||||
@@ -84,7 +102,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() {
|
||||
@@ -95,19 +115,17 @@ func main() {
|
||||
flag.BoolVar(&reset, "reset", false, "Prepare to resync from cluster")
|
||||
flag.BoolVar(&showVersion, "version", false, "Show version")
|
||||
flag.BoolVar(&doUpgrade, "upgrade", false, "Perform upgrade")
|
||||
flag.IntVar(&logFlags, "logflags", logFlags, "Set log flags")
|
||||
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
|
||||
}
|
||||
|
||||
l.SetFlags(logFlags)
|
||||
|
||||
if doUpgrade {
|
||||
err := upgrade()
|
||||
if err != nil {
|
||||
@@ -133,7 +151,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 +223,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 +247,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 +277,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 +306,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 +387,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 +519,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 +663,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 +696,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 +711,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
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build !windows
|
||||
|
||||
package main
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build windows
|
||||
|
||||
package main
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -11,6 +15,7 @@ import (
|
||||
"encoding/binary"
|
||||
"encoding/pem"
|
||||
"math/big"
|
||||
mr "math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -50,7 +55,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,
|
||||
},
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
// +build !windows
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
@@ -6,6 +8,7 @@ import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
@@ -14,8 +17,10 @@ import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"bytes"
|
||||
"bitbucket.org/kardianos/osext"
|
||||
)
|
||||
|
||||
@@ -33,6 +38,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 +61,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 +157,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
|
||||
}
|
||||
31
cmd/syncthing/upgrade_test.go
Normal file
31
cmd/syncthing/upgrade_test.go
Normal file
@@ -0,0 +1,31 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
// +build windows
|
||||
|
||||
package main
|
||||
|
||||
import "errors"
|
||||
|
||||
func upgrade() error {
|
||||
return errors.New("Upgrade currently unsupported on Windows")
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// Package config implements reading and writing of the syncthing configuration file.
|
||||
package config
|
||||
|
||||
@@ -27,12 +31,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 +103,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 +238,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 +248,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
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
@@ -14,6 +18,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 +42,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 +58,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 +80,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 +150,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 +167,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 +204,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)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -5,6 +9,7 @@ import (
|
||||
"encoding/hex"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
@@ -16,18 +21,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 +139,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 +157,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 +196,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 +242,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++
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package discover
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package discover
|
||||
|
||||
import (
|
||||
@@ -13,10 +17,6 @@ import (
|
||||
"github.com/calmh/syncthing/buffers"
|
||||
)
|
||||
|
||||
const (
|
||||
AnnouncementPort = 21025
|
||||
)
|
||||
|
||||
type Discoverer struct {
|
||||
myID string
|
||||
listenAddrs []string
|
||||
@@ -42,8 +42,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 +191,8 @@ func (d *Discoverer) sendExternalAnnouncements() {
|
||||
} else {
|
||||
buf = d.announcementPkt()
|
||||
}
|
||||
var errCounter = 0
|
||||
|
||||
for errCounter < maxErrors {
|
||||
for {
|
||||
var ok bool
|
||||
|
||||
if debug {
|
||||
@@ -205,11 +204,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 +214,6 @@ func (d *Discoverer) sendExternalAnnouncements() {
|
||||
l.Debugln("discover: external lookup check:", res)
|
||||
}
|
||||
ok = len(res) > 0
|
||||
|
||||
}
|
||||
|
||||
d.extAnnounceOKmut.Lock()
|
||||
@@ -231,7 +226,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() {
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// Package discover implements the node discovery protocol.
|
||||
package discover
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package discover
|
||||
|
||||
const (
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package discover
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package files
|
||||
|
||||
import (
|
||||
|
||||
11
files/set.go
11
files/set.go
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// Package files provides a set type to track local/remote files with newness checks.
|
||||
package files
|
||||
|
||||
@@ -76,7 +80,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 +197,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 +214,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)
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
//+build anal
|
||||
|
||||
package files
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
//+build !anal
|
||||
|
||||
package files
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package files
|
||||
|
||||
import (
|
||||
|
||||
131
gui/app.js
131
gui/app.js
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
/*jslint browser: true, continue: true, plusplus: true */
|
||||
/*global $: false, angular: false */
|
||||
|
||||
@@ -16,6 +20,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
|
||||
$scope.myID = '';
|
||||
$scope.nodes = [];
|
||||
$scope.configInSync = true;
|
||||
$scope.protocolChanged = false;
|
||||
$scope.errors = [];
|
||||
$scope.seenError = '';
|
||||
$scope.model = {};
|
||||
@@ -30,8 +35,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 +58,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
|
||||
if (restarting) {
|
||||
$scope.init();
|
||||
$('#restarting').modal('hide');
|
||||
$('#shutdown').modal('hide');
|
||||
restarting = false;
|
||||
}
|
||||
}
|
||||
@@ -84,9 +91,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 +102,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;
|
||||
});
|
||||
@@ -125,7 +127,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.repoClass = function (repo) {
|
||||
if (typeof $scope.model[repo] === 'undefined') {
|
||||
@@ -144,7 +146,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
|
||||
return 'primary';
|
||||
}
|
||||
return 'info';
|
||||
}
|
||||
};
|
||||
|
||||
$scope.syncPercentage = function (repo) {
|
||||
if (typeof $scope.model[repo] === 'undefined') {
|
||||
@@ -162,7 +164,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 +256,31 @@ 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){
|
||||
// see if protocol will need to be changed on restart
|
||||
if($scope.config.GUI.UseTLS !== $scope.config.workingGUI.UseTLS){
|
||||
$scope.protocolChanged = true;
|
||||
}
|
||||
|
||||
// Apply new settings locally
|
||||
$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");
|
||||
};
|
||||
|
||||
@@ -269,6 +289,21 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
|
||||
$('#restarting').modal({backdrop: 'static', keyboard: false});
|
||||
$http.post(urlbase + '/restart');
|
||||
$scope.configInSync = true;
|
||||
|
||||
// Switch webpage protocol if needed
|
||||
if($scope.protocolChanged){
|
||||
var protocol = 'http';
|
||||
|
||||
if($scope.config.GUI.UseTLS){
|
||||
protocol = 'https';
|
||||
}
|
||||
|
||||
setTimeout(function(){
|
||||
window.location.protocol = protocol;
|
||||
}, 1000);
|
||||
|
||||
$scope.protocolChanged = false;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.shutdown = function () {
|
||||
@@ -327,7 +362,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;
|
||||
@@ -393,13 +428,19 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
|
||||
|
||||
$scope.repoList = function () {
|
||||
return repoList($scope.repos);
|
||||
}
|
||||
};
|
||||
|
||||
$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 +468,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) {
|
||||
@@ -507,7 +570,7 @@ function repoMap(l) {
|
||||
function repoList(m) {
|
||||
var l = [];
|
||||
for (var id in m) {
|
||||
l.push(m[id])
|
||||
l.push(m[id]);
|
||||
}
|
||||
l.sort(repoCompare);
|
||||
return l;
|
||||
@@ -596,7 +659,7 @@ syncthing.filter('chunkID', function () {
|
||||
if (!parts)
|
||||
return "";
|
||||
return parts.join('-');
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
syncthing.filter('shortPath', function () {
|
||||
@@ -608,7 +671,13 @@ syncthing.filter('shortPath', function () {
|
||||
return input;
|
||||
}
|
||||
return ".../" + parts.slice(parts.length-2).join("/");
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
syncthing.filter('clean', function () {
|
||||
return function (input) {
|
||||
return encodeURIComponent(input).replace(/%/g, '');
|
||||
};
|
||||
});
|
||||
|
||||
syncthing.directive('optionEditor', function () {
|
||||
@@ -643,3 +712,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;
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
434
gui/index.html
434
gui/index.html
@@ -1,4 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
Use of this source code is governed by an MIT-style license that can be
|
||||
found in the LICENSE file.
|
||||
-->
|
||||
<html lang="en" ng-app="syncthing" ng-controller="SyncthingCtrl" class="ng-cloak">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
@@ -60,12 +65,22 @@
|
||||
}
|
||||
|
||||
.table th {
|
||||
white-space:nowrap;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.table td {
|
||||
padding-left: 20px !important;
|
||||
}
|
||||
|
||||
@media (max-width:767px) {
|
||||
.table-responsive>.table>tbody>tr>td {
|
||||
/* revert a bootstrap setting e.g.:
|
||||
* for mobile phones to allow linebreaks in long repro folder/shared with
|
||||
* columns. */
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
@@ -77,20 +92,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 <b class="caret"></b></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="" ng-click="addRepo()"><span class="glyphicon glyphicon-hdd"></span> Add Repository</a></li>
|
||||
<li><a href="" ng-click="addNode()"><span class="glyphicon glyphicon-retweet"></span> Add Node</a></li>
|
||||
<li class="divider"></li>
|
||||
<li><a href="" ng-click="editSettings()"><span class="glyphicon glyphicon-cog"></span> Settings</a></li>
|
||||
<li><a href="" ng-click="idNode()"><span class="glyphicon glyphicon-qrcode"></span> Show ID</a></span>
|
||||
<li><a href="" ng-click="idNode()"><span class="glyphicon glyphicon-qrcode"></span> Show ID</a></li>
|
||||
<li class="divider"></li>
|
||||
<li><a href="" ng-click="shutdown()"><span class="glyphicon glyphicon-off"></span> Shutdown</a></li>
|
||||
<li><a href="" ng-click="restart()"><span class="glyphicon glyphicon-refresh"></span> Restart</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -120,164 +135,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> Repository ID</th>
|
||||
<td class="text-right">{{repo.ID}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-folder-open"></span> Folder</th>
|
||||
<td class="text-right">{{repo.Directory}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-comment"></span> Synchronization</th>
|
||||
<td class="text-right">{{repoStatus(repo.ID)}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-globe"></span> 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> 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> 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> 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> 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-{{$index}}">
|
||||
<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-{{$index}}" 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> Repository ID</th>
|
||||
<td class="text-right">{{repo.ID}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-folder-open"></span> 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> Error</th>
|
||||
<td class="text-right">{{model[repo.ID].invalid}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-comment"></span> Synchronization</th>
|
||||
<td class="text-right">{{repoStatus(repo.ID)}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-globe"></span> 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> 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> 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> 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> 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> 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> 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> 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> RAM Utilization</th>
|
||||
<td class="text-right">{{system.sys | binary}}B</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-tasks"></span> CPU Utilization</th>
|
||||
<td class="text-right">{{system.cpuPercent | alwaysNumber | natural:1}}%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-cloud-download"></span> Download Rate</th>
|
||||
<td class="text-right">{{inbps | metric}}bps</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-cloud-upload"></span> 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> 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> 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-this"><span class="glyphicon glyphicon-home"></span> {{nodeName(nodeCfg)}}</a>
|
||||
</h3>
|
||||
</div>
|
||||
<div id="node-this" 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> RAM Utilization</th>
|
||||
<td class="text-right">{{system.sys | binary}}B</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-tasks"></span> CPU Utilization</th>
|
||||
<td class="text-right">{{system.cpuPercent | alwaysNumber | natural:1}}%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-cloud-download"></span> 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> 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> 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> 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> 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> 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> Address</th>
|
||||
<td class="text-right">{{nodeAddr(nodeCfg)}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-comment"></span> Synchronization</th>
|
||||
<td class="text-right">{{nodeStatus(nodeCfg)}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-cloud-download"></span> Download Rate</th>
|
||||
<td class="text-right">{{connections[nodeCfg.NodeID].inbps | metric}}bps</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-cloud-upload"></span> Upload Rate</th>
|
||||
<td class="text-right">{{connections[nodeCfg.NodeID].outbps | metric}}bps </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-tag"></span> 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-{{$index}}">
|
||||
<span class="glyphicon glyphicon-retweet"></span>
|
||||
{{nodeName(nodeCfg)}}
|
||||
<span class="pull-right hidden-xs">{{nodeStatus(nodeCfg)}}</span>
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
<div id="node-{{$index}}" 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> Address</th>
|
||||
<td class="text-right">{{nodeAddr(nodeCfg)}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-comment"></span> Synchronization</th>
|
||||
<td class="text-right">{{nodeStatus(nodeCfg)}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-cloud-download"></span> 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> 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> 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> 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> Edit</a></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div> <!-- /row -->
|
||||
|
||||
<!-- Errors -->
|
||||
@@ -386,14 +408,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> Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -411,13 +433,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 +475,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-zA-Z0-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 +575,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 +588,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>
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
# Use of this source code is governed by an MIT-style license that can be
|
||||
# found in the LICENSE file.
|
||||
|
||||
iterations=${1:-5}
|
||||
|
||||
id1=I6KAH7666SLLL5PFXSOAUFJCDZYAOMLEKCP2GB3BV5RQST3PSROA
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build ignore
|
||||
|
||||
package main
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build ignore
|
||||
|
||||
package main
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build ignore
|
||||
|
||||
package main
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
# Use of this source code is governed by an MIT-style license that can be
|
||||
# found in the LICENSE file.
|
||||
|
||||
iterations=${1:-5}
|
||||
|
||||
id1=I6KAH7666SLLL5PFXSOAUFJCDZYAOMLEKCP2GB3BV5RQST3PSROA
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// Package lamport implements a simple Lamport Clock for versioning
|
||||
package lamport
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// Package logger implements a standardized logger with callback functionality
|
||||
package logger
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
@@ -16,6 +20,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 +52,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,
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// Package model implements repository abstraction and file pulling mechanisms
|
||||
package model
|
||||
|
||||
140
model/model.go
140
model/model.go
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
@@ -17,6 +21,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 +48,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 +85,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 +109,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 +154,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 +165,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 +186,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: in,
|
||||
OutBytesTotal: 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 +266,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 +279,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 +298,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 +383,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 +401,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 +517,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))
|
||||
@@ -556,7 +571,10 @@ func (m *Model) broadcastIndexLoop() {
|
||||
idx := m.protocolIndex(repo)
|
||||
indexWg.Add(1)
|
||||
go func() {
|
||||
m.saveIndex(repo, m.indexDir, idx)
|
||||
err := m.saveIndex(repo, m.indexDir, idx)
|
||||
if err != nil {
|
||||
l.Infof("Saving index for %q: %v", repo, err)
|
||||
}
|
||||
indexWg.Done()
|
||||
}()
|
||||
|
||||
@@ -582,23 +600,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 +625,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 +636,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 +648,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 +672,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,46 +693,66 @@ 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)
|
||||
err := m.saveIndex(repo, dir, fs)
|
||||
if err != nil {
|
||||
l.Infof("Saving index for %q: %v", repo, err)
|
||||
}
|
||||
}
|
||||
m.rmut.RUnlock()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
m.rmut.RUnlock()
|
||||
}
|
||||
|
||||
func (m *Model) saveIndex(repo string, dir string, fs []protocol.FileInfo) {
|
||||
id := fmt.Sprintf("%x", sha1.Sum([]byte(m.repoDirs[repo])))
|
||||
func (m *Model) saveIndex(repo string, dir string, fs []protocol.FileInfo) error {
|
||||
id := fmt.Sprintf("%x", sha1.Sum([]byte(m.repoCfgs[repo].Directory)))
|
||||
name := id + ".idx.gz"
|
||||
name = filepath.Join(dir, name)
|
||||
|
||||
idxf, err := os.Create(name + ".tmp")
|
||||
if err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
gzw := gzip.NewWriter(idxf)
|
||||
|
||||
protocol.IndexMessage{
|
||||
n, err := protocol.IndexMessage{
|
||||
Repository: repo,
|
||||
Files: fs,
|
||||
}.EncodeXDR(gzw)
|
||||
gzw.Close()
|
||||
idxf.Close()
|
||||
if err != nil {
|
||||
gzw.Close()
|
||||
idxf.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
Rename(name+".tmp", name)
|
||||
err = gzw.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = idxf.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if debug {
|
||||
l.Debugln("wrote index,", n, "bytes uncompressed")
|
||||
}
|
||||
|
||||
return 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)
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
@@ -49,7 +53,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 +89,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 +101,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 +113,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 +126,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 +140,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 +187,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
|
||||
|
||||
212
model/puller.go
212
model/puller.go
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
@@ -5,13 +9,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 +70,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 +78,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 +94,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 +128,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 +144,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 +163,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 +177,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 +209,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
|
||||
}
|
||||
}
|
||||
@@ -210,11 +224,15 @@ func (p *puller) fixupDirectories() {
|
||||
var changed = 0
|
||||
|
||||
var walkFn = func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
rn, err := filepath.Rel(p.dir, path)
|
||||
rn, err := filepath.Rel(p.repoCfg.Directory, path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -223,7 +241,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 +254,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 +267,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 +283,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 +301,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 +311,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 +345,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 +361,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 +377,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 +409,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 +422,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 +472,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 +498,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 +541,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 +568,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 +604,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 +618,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 +636,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 +645,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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build !windows
|
||||
|
||||
package model
|
||||
@@ -23,11 +27,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
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build windows
|
||||
|
||||
package model
|
||||
@@ -6,7 +10,6 @@ import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
type tempNamer struct {
|
||||
@@ -24,33 +27,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)
|
||||
}
|
||||
|
||||
@@ -1,27 +1,17 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
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 +85,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
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
@@ -20,24 +24,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 +39,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{
|
||||
|
||||
15
osutil/hidden_unix.go
Normal file
15
osutil/hidden_unix.go
Normal file
@@ -0,0 +1,15 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build !windows
|
||||
|
||||
package osutil
|
||||
|
||||
func HideFile(path string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func ShowFile(path string) error {
|
||||
return nil
|
||||
}
|
||||
39
osutil/hidden_windows.go
Normal file
39
osutil/hidden_windows.go
Normal file
@@ -0,0 +1,39 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +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)
|
||||
}
|
||||
22
osutil/osutil.go
Normal file
22
osutil/osutil.go
Normal file
@@ -0,0 +1,22 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -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
|
||||
@@ -569,7 +575,7 @@ Message Limits
|
||||
|
||||
An implementation MAY impose reasonable limits on the length of message
|
||||
fields to aid robustness in the face of corruption or broken
|
||||
implementations. These limits, if imposed, SHOULD not be more
|
||||
implementations. These limits, if imposed, SHOULD NOT be more
|
||||
restrictive than the following:
|
||||
|
||||
### Index and Index Update Messages
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package protocol
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package protocol
|
||||
|
||||
import (
|
||||
@@ -10,9 +14,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 +38,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)
|
||||
}
|
||||
|
||||
17
protocol/debug.go
Normal file
17
protocol/debug.go
Normal file
@@ -0,0 +1,17 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
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
|
||||
)
|
||||
@@ -1,2 +1,6 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// Package protocol implements the Block Exchange Protocol.
|
||||
package protocol
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package protocol
|
||||
|
||||
import "github.com/calmh/syncthing/xdr"
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package protocol
|
||||
|
||||
type IndexMessage struct {
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package protocol
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build darwin
|
||||
|
||||
package protocol
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build !windows,!darwin
|
||||
|
||||
package protocol
|
||||
|
||||
@@ -1,24 +1,48 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build windows
|
||||
|
||||
package protocol
|
||||
|
||||
// Windows uses backslashes as file separator
|
||||
// Windows uses backslashes as file separator and disallows a bunch of
|
||||
// characters in the filename
|
||||
|
||||
import "path/filepath"
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var disallowedCharacters = string([]rune{
|
||||
'<', '>', ':', '"', '|', '?', '*',
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
|
||||
11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
|
||||
21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
|
||||
31,
|
||||
})
|
||||
|
||||
type nativeModel struct {
|
||||
next Model
|
||||
}
|
||||
|
||||
func (m nativeModel) Index(nodeID string, repo string, files []FileInfo) {
|
||||
for i := range files {
|
||||
files[i].Name = filepath.FromSlash(files[i].Name)
|
||||
for i, f := range files {
|
||||
if strings.ContainsAny(f.Name, disallowedCharacters) {
|
||||
files[i].Flags |= FlagInvalid
|
||||
l.Warnf("File name %q contains invalid characters; marked as invalid.", f.Name)
|
||||
}
|
||||
files[i].Name = filepath.FromSlash(f.Name)
|
||||
}
|
||||
m.next.Index(nodeID, repo, files)
|
||||
}
|
||||
|
||||
func (m nativeModel) IndexUpdate(nodeID string, repo string, files []FileInfo) {
|
||||
for i := range files {
|
||||
for i, f := range files {
|
||||
if strings.ContainsAny(f.Name, disallowedCharacters) {
|
||||
files[i].Flags |= FlagInvalid
|
||||
l.Warnf("File name %q contains invalid characters; marked as invalid.", f.Name)
|
||||
}
|
||||
files[i].Name = filepath.FromSlash(files[i].Name)
|
||||
}
|
||||
m.next.IndexUpdate(nodeID, repo, files)
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package protocol
|
||||
|
||||
import (
|
||||
@@ -25,9 +29,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 +96,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 +480,29 @@ 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:
|
||||
if debug {
|
||||
l.Debugln(c.id, "<- pong")
|
||||
}
|
||||
if !ok {
|
||||
c.close(fmt.Errorf("ping failure"))
|
||||
}
|
||||
@@ -504,14 +527,30 @@ func (c *rawConnection) processRequest(msgID int, req RequestMessage) {
|
||||
|
||||
type Statistics struct {
|
||||
At time.Time
|
||||
InBytesTotal int
|
||||
OutBytesTotal int
|
||||
InBytesTotal uint64
|
||||
OutBytesTotal uint64
|
||||
}
|
||||
|
||||
func (c *rawConnection) Statistics() Statistics {
|
||||
return Statistics{
|
||||
At: time.Now(),
|
||||
InBytesTotal: int(c.cr.Tot()),
|
||||
OutBytesTotal: int(c.cw.Tot()),
|
||||
InBytesTotal: c.cr.Tot(),
|
||||
OutBytesTotal: 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
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package protocol
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package protocol
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package scanner
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package scanner
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package scanner
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// Package scanner implements a file system scanner and hasher.
|
||||
package scanner
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package scanner
|
||||
|
||||
import "fmt"
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package scanner
|
||||
|
||||
import (
|
||||
@@ -29,6 +33,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 +148,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 +162,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 +193,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 +243,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 +288,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
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package scanner
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package upnp
|
||||
|
||||
import (
|
||||
|
||||
15
upnp/upnp.go
15
upnp/upnp.go
@@ -1,9 +1,12 @@
|
||||
// Package upnp implements UPnP Internet Gateway upnpDevice port mappings
|
||||
package upnp
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// Adapted from https://github.com/jackpal/Taipei-Torrent/blob/dd88a8bfac6431c01d959ce3c745e74b8a911793/IGD.go
|
||||
// Copyright (c) 2010 Jack Palevich (https://github.com/jackpal/Taipei-Torrent/blob/dd88a8bfac6431c01d959ce3c745e74b8a911793/LICENSE)
|
||||
// Copyright (c) 2014 Jakob Borg
|
||||
|
||||
// Package upnp implements UPnP Internet Gateway upnpDevice port mappings
|
||||
package upnp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@@ -60,14 +63,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 {
|
||||
|
||||
17
versioner/debug.go
Normal file
17
versioner/debug.go
Normal file
@@ -0,0 +1,17 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
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
|
||||
)
|
||||
88
versioner/simple.go
Normal file
88
versioner/simple.go
Normal file
@@ -0,0 +1,88 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
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
|
||||
}
|
||||
11
versioner/versioner.go
Normal file
11
versioner/versioner.go
Normal file
@@ -0,0 +1,11 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package versioner
|
||||
|
||||
type Versioner interface {
|
||||
Archive(path string) error
|
||||
}
|
||||
|
||||
var Factories = map[string]func(map[string]string) Versioner{}
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package xdr
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// Package xdr implements an XDR (RFC 4506) encoder/decoder.
|
||||
package xdr
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
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 +50,9 @@ func (r *Reader) ReadBytesMaxInto(max int, dst []byte) []byte {
|
||||
if r.err != nil {
|
||||
return nil
|
||||
}
|
||||
r.last = time.Now()
|
||||
s := r.tot
|
||||
|
||||
l := int(r.ReadUint32())
|
||||
if r.err != nil {
|
||||
return nil
|
||||
@@ -52,19 +61,28 @@ func (r *Reader) ReadBytesMaxInto(max int, dst []byte) []byte {
|
||||
r.err = ErrElementSizeExceeded
|
||||
return nil
|
||||
}
|
||||
|
||||
if l+pad(l) > len(dst) {
|
||||
dst = make([]byte, l+pad(l))
|
||||
} else {
|
||||
dst = dst[:l+pad(l)]
|
||||
}
|
||||
|
||||
var n int
|
||||
n, r.err = io.ReadFull(r.r, dst)
|
||||
if r.err != nil {
|
||||
if debug {
|
||||
dl.Debugf("@0x%x: rd bytes (%d): %v", s, len(dst), r.err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
r.tot += n
|
||||
|
||||
if debug {
|
||||
if n > maxDebugBytes {
|
||||
dl.Debugf("rd bytes (%d): %x...", n, dst[:maxDebugBytes])
|
||||
dl.Debugf("@0x%x: rd bytes (%d): %x...", s, len(dst), dst[:maxDebugBytes])
|
||||
} else {
|
||||
dl.Debugf("rd bytes (%d): %x", n, dst)
|
||||
dl.Debugf("@0x%x: rd bytes (%d): %x", s, len(dst), dst)
|
||||
}
|
||||
}
|
||||
return dst[:l]
|
||||
@@ -74,43 +92,74 @@ func (r *Reader) ReadUint16() uint16 {
|
||||
if r.err != nil {
|
||||
return 0
|
||||
}
|
||||
_, r.err = io.ReadFull(r.r, r.b[:4])
|
||||
r.tot += 4
|
||||
r.last = time.Now()
|
||||
s := r.tot
|
||||
|
||||
var n int
|
||||
n, r.err = io.ReadFull(r.r, r.b[:4])
|
||||
r.tot += n
|
||||
if r.err != nil {
|
||||
if debug {
|
||||
dl.Debugf("@0x%x: rd uint16: %v", r.tot, r.err)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
v := uint16(r.b[1]) | uint16(r.b[0])<<8
|
||||
|
||||
if debug {
|
||||
dl.Debugf("rd uint16=%d", v)
|
||||
dl.Debugf("@0x%x: rd uint16=%d (0x%04x)", s, v, v)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func (r *Reader) ReadUint32() uint32 {
|
||||
var n int
|
||||
if r.err != nil {
|
||||
return 0
|
||||
}
|
||||
r.last = time.Now()
|
||||
s := r.tot
|
||||
|
||||
var n int
|
||||
n, r.err = io.ReadFull(r.r, r.b[:4])
|
||||
if n < 4 {
|
||||
r.tot += n
|
||||
if r.err != nil {
|
||||
if debug {
|
||||
dl.Debugf("@0x%x: rd uint32: %v", r.tot, r.err)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
r.tot += n
|
||||
|
||||
v := uint32(r.b[3]) | uint32(r.b[2])<<8 | uint32(r.b[1])<<16 | uint32(r.b[0])<<24
|
||||
|
||||
if debug {
|
||||
dl.Debugf("rd uint32=%d", v)
|
||||
dl.Debugf("@0x%x: rd uint32=%d (0x%08x)", s, v, v)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func (r *Reader) ReadUint64() uint64 {
|
||||
var n int
|
||||
if r.err != nil {
|
||||
return 0
|
||||
}
|
||||
r.last = time.Now()
|
||||
s := r.tot
|
||||
|
||||
var n int
|
||||
n, r.err = io.ReadFull(r.r, r.b[:8])
|
||||
r.tot += n
|
||||
if r.err != nil {
|
||||
if debug {
|
||||
dl.Debugf("@0x%x: rd uint64: %v", r.tot, r.err)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
v := uint64(r.b[7]) | uint64(r.b[6])<<8 | uint64(r.b[5])<<16 | uint64(r.b[4])<<24 |
|
||||
uint64(r.b[3])<<32 | uint64(r.b[2])<<40 | uint64(r.b[1])<<48 | uint64(r.b[0])<<56
|
||||
|
||||
if debug {
|
||||
dl.Debugf("rd uint64=%d", v)
|
||||
dl.Debugf("@0x%x: rd uint64=%d (0x%016x)", s, v, v)
|
||||
}
|
||||
return v
|
||||
}
|
||||
@@ -122,3 +171,7 @@ func (r *Reader) Tot() int {
|
||||
func (r *Reader) Error() error {
|
||||
return r.err
|
||||
}
|
||||
|
||||
func (r *Reader) LastRead() time.Time {
|
||||
return r.last
|
||||
}
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package xdr
|
||||
|
||||
import "io"
|
||||
import (
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
func pad(l int) int {
|
||||
d := l % 4
|
||||
@@ -13,10 +20,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 +42,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 +74,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 +95,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 +116,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 +143,7 @@ func (w *Writer) Tot() int {
|
||||
func (w *Writer) Error() error {
|
||||
return w.err
|
||||
}
|
||||
|
||||
func (w *Writer) LastWrite() time.Time {
|
||||
return w.last
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package xdr
|
||||
|
||||
import (
|
||||
|
||||
Reference in New Issue
Block a user