mirror of
https://github.com/syncthing/syncthing.git
synced 2026-01-03 11:29:10 -05:00
Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50b37f1366 | ||
|
|
a7b6e35467 | ||
|
|
37d83a4e2e | ||
|
|
a720f90a70 | ||
|
|
4a6b43bcae | ||
|
|
2f5a822ca4 | ||
|
|
bc1d04f0b9 | ||
|
|
381795d6d0 | ||
|
|
6ade27641d | ||
|
|
53898d2c60 | ||
|
|
91c4ff6009 | ||
|
|
0aa067a726 | ||
|
|
5353659f9f | ||
|
|
7ac00e189b | ||
|
|
a2da31056b | ||
|
|
2383579a64 | ||
|
|
68750211ef | ||
|
|
db3e3ade80 | ||
|
|
e6f04ed238 | ||
|
|
a6eb690e31 | ||
|
|
77fe8449ba | ||
|
|
33e9a35f08 | ||
|
|
4ab4816556 | ||
|
|
8e8a579bb2 | ||
|
|
efbdf72d20 | ||
|
|
0e59b5678a | ||
|
|
de75550415 | ||
|
|
4dbce32738 | ||
|
|
b05fcbc9d7 | ||
|
|
d09c71b688 | ||
|
|
874d6760d4 | ||
|
|
26ebbee877 | ||
|
|
12eda0449a | ||
|
|
5a98f4e47c | ||
|
|
964c903a68 | ||
|
|
21b699826d | ||
|
|
5fa8f8e50c | ||
|
|
9ca87f5314 | ||
|
|
537c6b3b69 | ||
|
|
48a3fac2da | ||
|
|
fd73682806 | ||
|
|
34bd5b9dcf | ||
|
|
58c5e46206 | ||
|
|
4c61ab0f18 | ||
|
|
f241b63e0e | ||
|
|
2ffdb5a82a | ||
|
|
46e963443d | ||
|
|
66d4e9e5d7 | ||
|
|
de382e33a3 | ||
|
|
3c6738da73 | ||
|
|
18e5cb6793 | ||
|
|
9cd6b85c09 | ||
|
|
f40f3b3b7b | ||
|
|
7454670b0a | ||
|
|
e63596681d | ||
|
|
3dbaa76dcb | ||
|
|
8752003b50 | ||
|
|
8716ed5aa4 | ||
|
|
38ac4e8f79 | ||
|
|
70fc8a3064 | ||
|
|
7626c5d526 | ||
|
|
7e04c9d048 | ||
|
|
9eda8f2c7e |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -8,4 +8,5 @@ stcli.exe
|
||||
*.sublime*
|
||||
discosrv
|
||||
stpidx
|
||||
.jshintrc
|
||||
.jshintrc
|
||||
coverage.out
|
||||
|
||||
12
.travis.yml
12
.travis.yml
@@ -1,12 +1,20 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.2
|
||||
- tip
|
||||
|
||||
install:
|
||||
- export PATH=$PATH:$HOME/gopath/bin
|
||||
- ./build.sh setup
|
||||
- go get code.google.com/p/go.tools/cmd/cover
|
||||
- go get github.com/mattn/goveralls
|
||||
|
||||
script:
|
||||
- ./build.sh test
|
||||
- ./build.sh test-cov
|
||||
|
||||
after_success:
|
||||
- goveralls -coverprofile=coverage.out -service=travis-ci -package=calmh/syncthing -repotoken="$COVERALS_TOKEN"
|
||||
|
||||
env:
|
||||
global:
|
||||
secure: "zEV2h2XtKHNLVdXJjM4LA/VjMfLVydm6goF+ARit+nOSGxGoH7f7jIdzJzhxgh7shKG93q61eLO1Tug+WBMYB2EpBuYnTB5AIMYhCDwNI8C4uBV6c3brHfcrie7MASNao8TID2QScASKNFFWvjv/i1Ccn5ztxdcQuhSsNjGZp8A="
|
||||
|
||||
2
Godeps/Godeps.json
generated
2
Godeps/Godeps.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"ImportPath": "github.com/calmh/syncthing",
|
||||
"GoVersion": "go1.2.2",
|
||||
"GoVersion": "go1.3",
|
||||
"Packages": [
|
||||
"./cmd/syncthing",
|
||||
"./cmd/assets",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
syncthing [](https://travis-ci.org/calmh/syncthing)
|
||||
syncthing [](https://travis-ci.org/calmh/syncthing) [](https://coveralls.io/r/calmh/syncthing?branch=master)
|
||||
=========
|
||||
|
||||
This is the `syncthing` project. The following are the project goals:
|
||||
|
||||
BIN
assets/st-logo-text.pxm
Normal file
BIN
assets/st-logo-text.pxm
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -52,7 +52,7 @@ func (b *Beacon) Recv() ([]byte, net.Addr) {
|
||||
}
|
||||
|
||||
func (b *Beacon) reader() {
|
||||
var bs = make([]byte, 65536)
|
||||
bs := make([]byte, 65536)
|
||||
for {
|
||||
n, addr, err := b.conn.ReadFrom(bs)
|
||||
if err != nil {
|
||||
@@ -62,8 +62,11 @@ func (b *Beacon) reader() {
|
||||
if debug {
|
||||
l.Debugf("recv %d bytes from %s", n, addr)
|
||||
}
|
||||
|
||||
c := make([]byte, n)
|
||||
copy(c, bs)
|
||||
select {
|
||||
case b.outbox <- recv{bs[:n], addr}:
|
||||
case b.outbox <- recv{c, addr}:
|
||||
default:
|
||||
if debug {
|
||||
l.Debugln("dropping message")
|
||||
@@ -83,7 +86,7 @@ func (b *Beacon) writer() {
|
||||
|
||||
var dsts []net.IP
|
||||
for _, addr := range addrs {
|
||||
if iaddr, ok := addr.(*net.IPNet); ok && iaddr.IP.IsGlobalUnicast() {
|
||||
if iaddr, ok := addr.(*net.IPNet); ok && iaddr.IP.IsGlobalUnicast() && iaddr.IP.To4() != nil {
|
||||
baddr := bcast(iaddr)
|
||||
dsts = append(dsts, baddr.IP)
|
||||
}
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
// 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
|
||||
|
||||
const (
|
||||
largeMin = 1024
|
||||
)
|
||||
|
||||
var (
|
||||
smallBuffers = make(chan []byte, 32)
|
||||
largeBuffers = make(chan []byte, 32)
|
||||
)
|
||||
|
||||
func Get(size int) []byte {
|
||||
var ch = largeBuffers
|
||||
if size < largeMin {
|
||||
ch = smallBuffers
|
||||
}
|
||||
|
||||
var buf []byte
|
||||
select {
|
||||
case buf = <-ch:
|
||||
default:
|
||||
}
|
||||
|
||||
if len(buf) < size {
|
||||
return make([]byte, size)
|
||||
}
|
||||
return buf[:size]
|
||||
}
|
||||
|
||||
func Put(buf []byte) {
|
||||
buf = buf[:cap(buf)]
|
||||
if len(buf) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var ch = largeBuffers
|
||||
if len(buf) < largeMin {
|
||||
ch = smallBuffers
|
||||
}
|
||||
|
||||
select {
|
||||
case ch <- buf:
|
||||
default:
|
||||
}
|
||||
}
|
||||
27
build.sh
27
build.sh
@@ -9,7 +9,8 @@ date=$(git show -s --format=%ct)
|
||||
user=$(whoami)
|
||||
host=$(hostname)
|
||||
host=${host%%.*}
|
||||
ldflags="-w -X main.Version $version -X main.BuildStamp $date -X main.BuildUser $user -X main.BuildHost $host"
|
||||
bldenv=${ENVIRONMENT:-default}
|
||||
ldflags="-w -X main.Version $version -X main.BuildStamp $date -X main.BuildUser $user -X main.BuildHost $host -X main.BuildEnv $bldenv"
|
||||
|
||||
check() {
|
||||
if ! command -v godep >/dev/null ; then
|
||||
@@ -31,6 +32,21 @@ assets() {
|
||||
godep go run cmd/assets/assets.go gui > auto/gui.files.go
|
||||
}
|
||||
|
||||
test-cov() {
|
||||
echo "mode: set" > coverage.out
|
||||
fail=0
|
||||
|
||||
for dir in $(go list ./...) ; do
|
||||
godep go test -coverprofile=profile.out $dir || fail=1
|
||||
if [ -f profile.out ] ; then
|
||||
grep -v "mode: set" profile.out >> coverage.out
|
||||
rm profile.out
|
||||
fi
|
||||
done
|
||||
|
||||
exit $fail
|
||||
}
|
||||
|
||||
test() {
|
||||
check
|
||||
godep go test -cpu=1,2,4 ./...
|
||||
@@ -61,7 +77,8 @@ zipDist() {
|
||||
rm -rf "$name"
|
||||
mkdir -p "$name"
|
||||
for f in "${distFiles[@]}" ; do
|
||||
sed 's/$/
|
||||
sed 's/$/
|
||||
/' < "$f" > "$name/$f.txt"
|
||||
done
|
||||
cp syncthing.exe "$name"
|
||||
sign "$name/syncthing.exe"
|
||||
@@ -100,6 +117,10 @@ case "$1" in
|
||||
test
|
||||
;;
|
||||
|
||||
test-cov)
|
||||
test-cov
|
||||
;;
|
||||
|
||||
tar)
|
||||
rm -f *.tar.gz *.zip
|
||||
test || exit 1
|
||||
@@ -121,7 +142,7 @@ case "$1" in
|
||||
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 solaris-amd64 ; do
|
||||
export GOOS=${os%-*}
|
||||
export GOARCH=${os#*-}
|
||||
|
||||
|
||||
@@ -58,6 +58,8 @@ func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) erro
|
||||
if cfg.UseTLS {
|
||||
cert, err := loadCert(confDir, "https-")
|
||||
if err != nil {
|
||||
l.Infoln("Loading HTTPS certificate:", err)
|
||||
l.Infoln("Creating new HTTPS certificate")
|
||||
newCertificate(confDir, "https-")
|
||||
cert, err = loadCert(confDir, "https-")
|
||||
}
|
||||
@@ -89,6 +91,7 @@ func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) erro
|
||||
router.Get("/", getRoot)
|
||||
router.Get("/rest/version", restGetVersion)
|
||||
router.Get("/rest/model", restGetModel)
|
||||
router.Get("/rest/model/version", restGetModelVersion)
|
||||
router.Get("/rest/need", restGetNeed)
|
||||
router.Get("/rest/connections", restGetConnections)
|
||||
router.Get("/rest/config", restGetConfig)
|
||||
@@ -96,6 +99,7 @@ func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) erro
|
||||
router.Get("/rest/system", restGetSystem)
|
||||
router.Get("/rest/errors", restGetErrors)
|
||||
router.Get("/rest/discovery", restGetDiscovery)
|
||||
router.Get("/rest/report", restGetReport)
|
||||
router.Get("/qr/:text", getQR)
|
||||
|
||||
router.Post("/rest/config", restPostConfig)
|
||||
@@ -105,6 +109,7 @@ func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) erro
|
||||
router.Post("/rest/error", restPostError)
|
||||
router.Post("/rest/error/clear", restClearErrors)
|
||||
router.Post("/rest/discovery/hint", restPostDiscoveryHint)
|
||||
router.Post("/rest/model/override", restPostOverride)
|
||||
|
||||
mr := martini.New()
|
||||
mr.Use(csrfMiddleware)
|
||||
@@ -140,6 +145,17 @@ func restGetVersion() string {
|
||||
return Version
|
||||
}
|
||||
|
||||
func restGetModelVersion(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||
var qs = r.URL.Query()
|
||||
var repo = qs.Get("repo")
|
||||
var res = make(map[string]interface{})
|
||||
|
||||
res["version"] = m.Version(repo)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(res)
|
||||
}
|
||||
|
||||
func restGetModel(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||
var qs = r.URL.Query()
|
||||
var repo = qs.Get("repo")
|
||||
@@ -164,24 +180,31 @@ func restGetModel(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||
res["inSyncFiles"], res["inSyncBytes"] = globalFiles-needFiles, globalBytes-needBytes
|
||||
|
||||
res["state"] = m.State(repo)
|
||||
res["version"] = m.Version(repo)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(res)
|
||||
}
|
||||
|
||||
func restPostOverride(m *model.Model, r *http.Request) {
|
||||
var qs = r.URL.Query()
|
||||
var repo = qs.Get("repo")
|
||||
m.Override(repo)
|
||||
}
|
||||
|
||||
func restGetNeed(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||
var qs = r.URL.Query()
|
||||
var repo = qs.Get("repo")
|
||||
|
||||
files := m.NeedFilesRepo(repo)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(files)
|
||||
}
|
||||
|
||||
func restGetConnections(m *model.Model, w http.ResponseWriter) {
|
||||
var res = m.ConnectionStats()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(res)
|
||||
}
|
||||
|
||||
@@ -190,10 +213,11 @@ func restGetConfig(w http.ResponseWriter) {
|
||||
if encCfg.GUI.Password != "" {
|
||||
encCfg.GUI.Password = unchangedPassword
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(encCfg)
|
||||
}
|
||||
|
||||
func restPostConfig(req *http.Request) {
|
||||
func restPostConfig(req *http.Request, m *model.Model) {
|
||||
var newCfg config.Configuration
|
||||
err := json.NewDecoder(req.Body).Decode(&newCfg)
|
||||
if err != nil {
|
||||
@@ -240,7 +264,21 @@ func restPostConfig(req *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(cfg.Options, newCfg.Options) {
|
||||
if newCfg.Options.URAccepted > cfg.Options.URAccepted {
|
||||
// UR was enabled
|
||||
newCfg.Options.URAccepted = usageReportVersion
|
||||
err := sendUsageReport(m)
|
||||
if err != nil {
|
||||
l.Infoln("Usage report:", err)
|
||||
}
|
||||
go usageReportingLoop(m)
|
||||
} else if newCfg.Options.URAccepted < cfg.Options.URAccepted {
|
||||
// UR was disabled
|
||||
newCfg.Options.URAccepted = -1
|
||||
stopUsageReporting()
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(cfg.Options, newCfg.Options) || !reflect.DeepEqual(cfg.GUI, newCfg.GUI) {
|
||||
configInSync = false
|
||||
}
|
||||
|
||||
@@ -252,21 +290,25 @@ func restPostConfig(req *http.Request) {
|
||||
}
|
||||
|
||||
func restGetConfigInSync(w http.ResponseWriter) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(map[string]bool{"configInSync": configInSync})
|
||||
}
|
||||
|
||||
func restPostRestart(w http.ResponseWriter) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
flushResponse(`{"ok": "restarting"}`, w)
|
||||
go restart()
|
||||
}
|
||||
|
||||
func restPostReset(w http.ResponseWriter) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
flushResponse(`{"ok": "resetting repos"}`, w)
|
||||
resetRepositories()
|
||||
go restart()
|
||||
}
|
||||
|
||||
func restPostShutdown(w http.ResponseWriter) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
flushResponse(`{"ok": "shutting down"}`, w)
|
||||
go shutdown()
|
||||
}
|
||||
@@ -301,11 +343,12 @@ func restGetSystem(w http.ResponseWriter) {
|
||||
cpuUsageLock.RUnlock()
|
||||
res["cpuPercent"] = cpusum / 10
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(res)
|
||||
}
|
||||
|
||||
func restGetErrors(w http.ResponseWriter) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
guiErrorsMut.Lock()
|
||||
json.NewEncoder(w).Encode(guiErrors)
|
||||
guiErrorsMut.Unlock()
|
||||
@@ -342,9 +385,15 @@ func restPostDiscoveryHint(r *http.Request) {
|
||||
}
|
||||
|
||||
func restGetDiscovery(w http.ResponseWriter) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(discoverer.All())
|
||||
}
|
||||
|
||||
func restGetReport(w http.ResponseWriter, m *model.Model) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(reportData(m))
|
||||
}
|
||||
|
||||
func getQR(w http.ResponseWriter, params martini.Params) {
|
||||
code, err := qr.Encode(params["text"], qr.M)
|
||||
if err != nil {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"crypto/tls"
|
||||
"flag"
|
||||
"fmt"
|
||||
@@ -17,6 +18,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"runtime/pprof"
|
||||
@@ -36,6 +38,7 @@ import (
|
||||
|
||||
var (
|
||||
Version = "unknown-dev"
|
||||
BuildEnv = "default"
|
||||
BuildStamp = "0"
|
||||
BuildDate time.Time
|
||||
BuildHost = "unknown"
|
||||
@@ -46,11 +49,19 @@ var (
|
||||
var l = logger.DefaultLogger
|
||||
|
||||
func init() {
|
||||
if Version != "unknown-dev" {
|
||||
// If not a generic dev build, version string should come from git describe
|
||||
exp := regexp.MustCompile(`^v\d+\.\d+\.\d+(-\d+-g[0-9a-f]+)?(-dirty)?$`)
|
||||
if !exp.MatchString(Version) {
|
||||
l.Fatalf("Invalid version string %q;\n\tdoes not match regexp %v", Version, exp)
|
||||
}
|
||||
}
|
||||
|
||||
stamp, _ := strconv.Atoi(BuildStamp)
|
||||
BuildDate = time.Unix(int64(stamp), 0)
|
||||
|
||||
date := BuildDate.UTC().Format("2006-01-02 15:04:05 MST")
|
||||
LongVersion = fmt.Sprintf("syncthing %s (%s %s-%s) %s@%s %s", Version, runtime.Version(), runtime.GOOS, runtime.GOARCH, BuildUser, BuildHost, date)
|
||||
LongVersion = fmt.Sprintf("syncthing %s (%s %s-%s %s) %s@%s %s", Version, runtime.Version(), runtime.GOOS, runtime.GOARCH, BuildEnv, BuildUser, BuildHost, date)
|
||||
|
||||
if os.Getenv("STTRACE") != "" {
|
||||
logFlags = log.Ltime | log.Ldate | log.Lmicroseconds | log.Lshortfile
|
||||
@@ -104,9 +115,15 @@ The following enviroment variables are interpreted by syncthing:
|
||||
|
||||
STCPUPROFILE Write CPU profile to the specified file.
|
||||
|
||||
STGUIASSETS Directory to load GUI assets from. Overrides compiled in assets.`
|
||||
STGUIASSETS Directory to load GUI assets from. Overrides compiled in assets.
|
||||
|
||||
STDEADLOCKTIMEOUT Alter deadlock detection timeout (seconds; default 1200).`
|
||||
)
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
func main() {
|
||||
var reset bool
|
||||
var showVersion bool
|
||||
@@ -273,11 +290,28 @@ func main() {
|
||||
|
||||
m := model.NewModel(confDir, &cfg, "syncthing", Version)
|
||||
|
||||
for _, repo := range cfg.Repositories {
|
||||
nextRepo:
|
||||
for i, repo := range cfg.Repositories {
|
||||
if repo.Invalid != "" {
|
||||
continue
|
||||
}
|
||||
|
||||
repo.Directory = expandTilde(repo.Directory)
|
||||
|
||||
// Safety check. If the cached index contains files but the repository
|
||||
// doesn't exist, we have a problem. We would assume that all files
|
||||
// have been deleted which might not be the case, so abort instead.
|
||||
|
||||
id := fmt.Sprintf("%x", sha1.Sum([]byte(repo.Directory)))
|
||||
idxFile := filepath.Join(confDir, id+".idx.gz")
|
||||
if _, err := os.Stat(idxFile); err == nil {
|
||||
if fi, err := os.Stat(repo.Directory); err != nil || !fi.IsDir() {
|
||||
cfg.Repositories[i].Invalid = "repo directory missing"
|
||||
continue nextRepo
|
||||
}
|
||||
}
|
||||
|
||||
ensureDir(repo.Directory, -1)
|
||||
m.AddRepo(repo)
|
||||
}
|
||||
|
||||
@@ -321,41 +355,40 @@ func main() {
|
||||
|
||||
l.Infoln("Populating repository index")
|
||||
m.LoadIndexes(confDir)
|
||||
|
||||
for _, repo := range cfg.Repositories {
|
||||
if repo.Invalid != "" {
|
||||
continue
|
||||
}
|
||||
|
||||
dir := expandTilde(repo.Directory)
|
||||
|
||||
// Safety check. If the cached index contains files but the repository
|
||||
// doesn't exist, we have a problem. We would assume that all files
|
||||
// have been deleted which might not be the case, so abort instead.
|
||||
|
||||
if files, _, _ := m.LocalSize(repo.ID); files > 0 {
|
||||
if fi, err := os.Stat(dir); err != nil || !fi.IsDir() {
|
||||
l.Warnf("Configured repository %q has index but directory %q is missing; not starting.", repo.ID, repo.Directory)
|
||||
l.Fatalf("Ensure that directory is present or remove repository from configuration.")
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that repository directories exist for newly configured repositories.
|
||||
ensureDir(dir, -1)
|
||||
}
|
||||
|
||||
m.CleanRepos()
|
||||
m.ScanRepos()
|
||||
m.SaveIndexes(confDir)
|
||||
|
||||
// Remove all .idx* files that don't belong to an active repo.
|
||||
|
||||
validIndexes := make(map[string]bool)
|
||||
for _, repo := range cfg.Repositories {
|
||||
dir := expandTilde(repo.Directory)
|
||||
id := fmt.Sprintf("%x", sha1.Sum([]byte(dir)))
|
||||
validIndexes[id] = true
|
||||
}
|
||||
|
||||
allIndexes, err := filepath.Glob(filepath.Join(confDir, "*.idx*"))
|
||||
if err == nil {
|
||||
for _, idx := range allIndexes {
|
||||
bn := filepath.Base(idx)
|
||||
fs := strings.Split(bn, ".")
|
||||
if len(fs) > 1 {
|
||||
if _, ok := validIndexes[fs[0]]; !ok {
|
||||
l.Infoln("Removing old index", bn)
|
||||
os.Remove(idx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UPnP
|
||||
|
||||
var externalPort = 0
|
||||
if cfg.Options.UPnPEnabled {
|
||||
// We seed the random number generator with the node ID to get a
|
||||
// repeatable sequence of random external ports.
|
||||
rand.Seed(certSeed(cert.Certificate[0]))
|
||||
externalPort = setupUPnP()
|
||||
externalPort = setupUPnP(rand.NewSource(certSeed(cert.Certificate[0])))
|
||||
}
|
||||
|
||||
// Routine to connect out to configured nodes
|
||||
@@ -393,6 +426,21 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.Options.URAccepted > 0 && cfg.Options.URAccepted < usageReportVersion {
|
||||
l.Infoln("Anonymous usage report has changed; revoking acceptance")
|
||||
cfg.Options.URAccepted = 0
|
||||
}
|
||||
if cfg.Options.URAccepted >= usageReportVersion {
|
||||
go usageReportingLoop(m)
|
||||
go func() {
|
||||
time.Sleep(10 * time.Minute)
|
||||
err := sendUsageReport(m)
|
||||
if err != nil {
|
||||
l.Infoln("Usage report:", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
<-stop
|
||||
l.Okln("Exiting")
|
||||
}
|
||||
@@ -411,7 +459,7 @@ func waitForParentExit() {
|
||||
l.Okln("Continuing")
|
||||
}
|
||||
|
||||
func setupUPnP() int {
|
||||
func setupUPnP(r rand.Source) int {
|
||||
var externalPort = 0
|
||||
if len(cfg.Options.ListenAddress) == 1 {
|
||||
_, portStr, err := net.SplitHostPort(cfg.Options.ListenAddress[0])
|
||||
@@ -423,7 +471,7 @@ func setupUPnP() int {
|
||||
igd, err := upnp.Discover()
|
||||
if err == nil {
|
||||
for i := 0; i < 10; i++ {
|
||||
r := 1024 + rand.Intn(65535-1024)
|
||||
r := 1024 + int(r.Int63()%(65535-1024))
|
||||
err := igd.AddPortMapping(upnp.TCP, r, port, "syncthing", 0)
|
||||
if err == nil {
|
||||
externalPort = r
|
||||
@@ -435,7 +483,10 @@ func setupUPnP() int {
|
||||
l.Warnln("Failed to create UPnP port mapping")
|
||||
}
|
||||
} else {
|
||||
l.Infof("No UPnP IGD device found, no port mapping created (%v)", err)
|
||||
l.Infof("No UPnP gateway detected")
|
||||
if debugNet {
|
||||
l.Debugf("UPnP: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -569,6 +620,7 @@ func listenConnect(myID string, m *model.Model, tlsCfg *tls.Config) {
|
||||
|
||||
// Connect
|
||||
go func() {
|
||||
var delay time.Duration = 1 * time.Second
|
||||
for {
|
||||
nextNode:
|
||||
for _, nodeCfg := range cfg.Nodes {
|
||||
@@ -619,7 +671,11 @@ func listenConnect(myID string, m *model.Model, tlsCfg *tls.Config) {
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(time.Duration(cfg.Options.ReconnectIntervalS) * time.Second)
|
||||
time.Sleep(delay)
|
||||
delay *= 2
|
||||
if maxD := time.Duration(cfg.Options.ReconnectIntervalS) * time.Second; delay > maxD {
|
||||
delay = maxD
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -652,6 +708,9 @@ next:
|
||||
wr = &limitedWriter{conn, rateBucket}
|
||||
}
|
||||
protoConn := protocol.NewConnection(remoteID, conn, wr, m)
|
||||
|
||||
l.Infof("Connection to %s established at %v", remoteID, conn.RemoteAddr())
|
||||
|
||||
m.AddConnection(conn, protoConn)
|
||||
continue next
|
||||
}
|
||||
|
||||
25
cmd/syncthing/memsize_darwin.go
Normal file
25
cmd/syncthing/memsize_darwin.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func memorySize() (uint64, error) {
|
||||
cmd := exec.Command("sysctl", "hw.memsize")
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
fs := strings.Fields(string(out))
|
||||
if len(fs) != 2 {
|
||||
return 0, errors.New("sysctl parse error")
|
||||
}
|
||||
bytes, err := strconv.ParseUint(fs[1], 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return bytes, nil
|
||||
}
|
||||
33
cmd/syncthing/memsize_linux.go
Normal file
33
cmd/syncthing/memsize_linux.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func memorySize() (uint64, error) {
|
||||
f, err := os.Open("/proc/meminfo")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
s := bufio.NewScanner(f)
|
||||
if !s.Scan() {
|
||||
return 0, errors.New("/proc/meminfo parse error 1")
|
||||
}
|
||||
|
||||
l := s.Text()
|
||||
fs := strings.Fields(l)
|
||||
if len(fs) != 3 || fs[2] != "kB" {
|
||||
return 0, errors.New("/proc/meminfo parse error 2")
|
||||
}
|
||||
|
||||
kb, err := strconv.ParseUint(fs[1], 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return kb * 1024, nil
|
||||
}
|
||||
22
cmd/syncthing/memsize_solaris.go
Normal file
22
cmd/syncthing/memsize_solaris.go
Normal file
@@ -0,0 +1,22 @@
|
||||
// +build solaris
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func memorySize() (uint64, error) {
|
||||
cmd := exec.Command("prtconf", "-m")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
mb, err := strconv.ParseUint(string(out), 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return mb * 1024 * 1024, nil
|
||||
}
|
||||
9
cmd/syncthing/memsize_unimpl.go
Normal file
9
cmd/syncthing/memsize_unimpl.go
Normal file
@@ -0,0 +1,9 @@
|
||||
// +build freebsd
|
||||
|
||||
package main
|
||||
|
||||
import "errors"
|
||||
|
||||
func memorySize() (uint64, error) {
|
||||
return 0, errors.New("not implemented")
|
||||
}
|
||||
25
cmd/syncthing/memsize_windows.go
Normal file
25
cmd/syncthing/memsize_windows.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var (
|
||||
kernel32, _ = syscall.LoadLibrary("kernel32.dll")
|
||||
globalMemoryStatusEx, _ = syscall.GetProcAddress(kernel32, "GlobalMemoryStatusEx")
|
||||
)
|
||||
|
||||
func memorySize() (uint64, error) {
|
||||
var memoryStatusEx [64]byte
|
||||
binary.LittleEndian.PutUint32(memoryStatusEx[:], 64)
|
||||
p := uintptr(unsafe.Pointer(&memoryStatusEx[0]))
|
||||
|
||||
ret, _, callErr := syscall.Syscall(uintptr(globalMemoryStatusEx), 1, p, 0, 0)
|
||||
if ret == 0 {
|
||||
return 0, callErr
|
||||
}
|
||||
|
||||
return binary.LittleEndian.Uint64(memoryStatusEx[8:]), nil
|
||||
}
|
||||
@@ -2,6 +2,8 @@
|
||||
// Use of this source code is governed by an MIT-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build !solaris,!windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
9
cmd/syncthing/upgrade_unsupp.go
Normal file
9
cmd/syncthing/upgrade_unsupp.go
Normal file
@@ -0,0 +1,9 @@
|
||||
// +build windows solaris
|
||||
|
||||
package main
|
||||
|
||||
import "errors"
|
||||
|
||||
func upgrade() error {
|
||||
return errors.New("Upgrade currently unsupported on Windows")
|
||||
}
|
||||
133
cmd/syncthing/usage_report.go
Normal file
133
cmd/syncthing/usage_report.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/calmh/syncthing/model"
|
||||
)
|
||||
|
||||
// Current version number of the usage report, for acceptance purposes. If
|
||||
// fields are added or changed this integer must be incremented so that users
|
||||
// are prompted for acceptance of the new report.
|
||||
const usageReportVersion = 1
|
||||
|
||||
var stopUsageReportingCh = make(chan struct{})
|
||||
|
||||
func reportData(m *model.Model) map[string]interface{} {
|
||||
res := make(map[string]interface{})
|
||||
res["uniqueID"] = strings.ToLower(certID([]byte(myID)))[:6]
|
||||
res["version"] = Version
|
||||
res["longVersion"] = LongVersion
|
||||
res["platform"] = runtime.GOOS + "-" + runtime.GOARCH
|
||||
res["numRepos"] = len(cfg.Repositories)
|
||||
res["numNodes"] = len(cfg.Nodes)
|
||||
|
||||
var totFiles, maxFiles int
|
||||
var totBytes, maxBytes int64
|
||||
for _, repo := range cfg.Repositories {
|
||||
files, _, bytes := m.GlobalSize(repo.ID)
|
||||
totFiles += files
|
||||
totBytes += bytes
|
||||
if files > maxFiles {
|
||||
maxFiles = files
|
||||
}
|
||||
if bytes > maxBytes {
|
||||
maxBytes = bytes
|
||||
}
|
||||
}
|
||||
|
||||
res["totFiles"] = totFiles
|
||||
res["repoMaxFiles"] = maxFiles
|
||||
res["totMiB"] = totBytes / 1024 / 1024
|
||||
res["repoMaxMiB"] = maxBytes / 1024 / 1024
|
||||
|
||||
var mem runtime.MemStats
|
||||
runtime.ReadMemStats(&mem)
|
||||
res["memoryUsageMiB"] = mem.Sys / 1024 / 1024
|
||||
|
||||
var perf float64
|
||||
for i := 0; i < 5; i++ {
|
||||
p := cpuBench()
|
||||
if p > perf {
|
||||
perf = p
|
||||
}
|
||||
}
|
||||
res["sha256Perf"] = perf
|
||||
|
||||
bytes, err := memorySize()
|
||||
if err == nil {
|
||||
res["memorySize"] = bytes / 1024 / 1024
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func sendUsageReport(m *model.Model) error {
|
||||
d := reportData(m)
|
||||
var b bytes.Buffer
|
||||
json.NewEncoder(&b).Encode(d)
|
||||
|
||||
var client = http.DefaultClient
|
||||
if BuildEnv == "android" {
|
||||
// This works around the lack of DNS resolution on Android... :(
|
||||
tr := &http.Transport{
|
||||
Dial: func(network, addr string) (net.Conn, error) {
|
||||
return net.Dial(network, "194.126.249.13:443")
|
||||
},
|
||||
}
|
||||
client = &http.Client{Transport: tr}
|
||||
}
|
||||
_, err := client.Post("https://data.syncthing.net/newdata", "application/json", &b)
|
||||
return err
|
||||
}
|
||||
|
||||
func usageReportingLoop(m *model.Model) {
|
||||
l.Infoln("Starting usage reporting")
|
||||
t := time.NewTicker(86400 * time.Second)
|
||||
loop:
|
||||
for {
|
||||
select {
|
||||
case <-stopUsageReportingCh:
|
||||
break loop
|
||||
case <-t.C:
|
||||
err := sendUsageReport(m)
|
||||
if err != nil {
|
||||
l.Infoln("Usage report:", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
l.Infoln("Stopping usage reporting")
|
||||
}
|
||||
|
||||
func stopUsageReporting() {
|
||||
select {
|
||||
case stopUsageReportingCh <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// Returns CPU performance as a measure of single threaded SHA-256 MiB/s
|
||||
func cpuBench() float64 {
|
||||
chunkSize := 100 * 1 << 10
|
||||
h := sha256.New()
|
||||
bs := make([]byte, chunkSize)
|
||||
rand.Reader.Read(bs)
|
||||
|
||||
t0 := time.Now()
|
||||
b := 0
|
||||
for time.Since(t0) < 125*time.Millisecond {
|
||||
h.Write(bs)
|
||||
b += chunkSize
|
||||
}
|
||||
h.Sum(nil)
|
||||
d := time.Since(t0)
|
||||
return float64(int(float64(b)/d.Seconds()/(1<<20)*100)) / 100
|
||||
}
|
||||
@@ -16,9 +16,9 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/calmh/syncthing/scanner"
|
||||
"code.google.com/p/go.crypto/bcrypt"
|
||||
"github.com/calmh/syncthing/logger"
|
||||
"github.com/calmh/syncthing/scanner"
|
||||
)
|
||||
|
||||
var l = logger.DefaultLogger
|
||||
@@ -156,7 +156,10 @@ type OptionsConfiguration struct {
|
||||
MaxChangeKbps int `xml:"maxChangeKbps" default:"10000"`
|
||||
StartBrowser bool `xml:"startBrowser" default:"true"`
|
||||
UPnPEnabled bool `xml:"upnpEnabled" default:"true"`
|
||||
URAccepted int `xml:"urAccepted"` // Accepted usage reporting version; 0 for off (undecided), -1 for off (permanently)
|
||||
|
||||
Deprecated_UREnabled bool `xml:"urEnabled,omitempty" json:"-"`
|
||||
Deprecated_URDeclined bool `xml:"urDeclined,omitempty" json:"-"`
|
||||
Deprecated_ReadOnly bool `xml:"readOnly,omitempty" json:"-"`
|
||||
Deprecated_GUIEnabled bool `xml:"guiEnabled,omitempty" json:"-"`
|
||||
Deprecated_GUIAddress string `xml:"guiAddress,omitempty" json:"-"`
|
||||
@@ -341,6 +344,12 @@ func Load(rd io.Reader, myID string) (Configuration, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.Options.Deprecated_URDeclined {
|
||||
cfg.Options.URAccepted = -1
|
||||
}
|
||||
cfg.Options.Deprecated_URDeclined = false
|
||||
cfg.Options.Deprecated_UREnabled = false
|
||||
|
||||
// Upgrade to v2 configuration if appropriate
|
||||
if cfg.Version == 1 {
|
||||
convertV1V2(&cfg)
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/calmh/syncthing/beacon"
|
||||
"github.com/calmh/syncthing/buffers"
|
||||
)
|
||||
|
||||
type Discoverer struct {
|
||||
@@ -169,16 +168,21 @@ func (d *Discoverer) sendLocalAnnouncements() {
|
||||
}
|
||||
|
||||
func (d *Discoverer) sendExternalAnnouncements() {
|
||||
// this should go in the Discoverer struct
|
||||
errorRetryIntv := 60 * time.Second
|
||||
|
||||
remote, err := net.ResolveUDPAddr("udp", d.extServer)
|
||||
if err != nil {
|
||||
l.Warnf("Global discovery: %v; no external announcements", err)
|
||||
return
|
||||
for err != nil {
|
||||
l.Warnf("Global discovery: %v; trying again in %v", err, errorRetryIntv)
|
||||
time.Sleep(errorRetryIntv)
|
||||
remote, err = net.ResolveUDPAddr("udp", d.extServer)
|
||||
}
|
||||
|
||||
conn, err := net.ListenUDP("udp", nil)
|
||||
if err != nil {
|
||||
l.Warnf("Global discovery: %v; no external announcements", err)
|
||||
return
|
||||
for err != nil {
|
||||
l.Warnf("Global discovery: %v; trying again in %v", err, errorRetryIntv)
|
||||
time.Sleep(errorRetryIntv)
|
||||
conn, err = net.ListenUDP("udp", nil)
|
||||
}
|
||||
|
||||
var buf []byte
|
||||
@@ -199,7 +203,7 @@ func (d *Discoverer) sendExternalAnnouncements() {
|
||||
l.Debugf("discover: send announcement -> %v\n%s", remote, hex.Dump(buf))
|
||||
}
|
||||
|
||||
_, err = conn.WriteTo(buf, remote)
|
||||
_, err := conn.WriteTo(buf, remote)
|
||||
if err != nil {
|
||||
if debug {
|
||||
l.Debugln("discover: warning:", err)
|
||||
@@ -223,7 +227,7 @@ func (d *Discoverer) sendExternalAnnouncements() {
|
||||
if ok {
|
||||
time.Sleep(d.globalBcastIntv)
|
||||
} else {
|
||||
time.Sleep(60 * time.Second)
|
||||
time.Sleep(errorRetryIntv)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -329,11 +333,8 @@ func (d *Discoverer) externalLookup(node string) []string {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
buffers.Put(buf)
|
||||
|
||||
buf = buffers.Get(2048)
|
||||
defer buffers.Put(buf)
|
||||
|
||||
buf = make([]byte, 2048)
|
||||
n, err := conn.Read(buf)
|
||||
if err != nil {
|
||||
if err, ok := err.(net.Error); ok && err.Timeout() {
|
||||
|
||||
@@ -120,7 +120,12 @@ func (m *Set) Need(id uint) []scanner.File {
|
||||
continue
|
||||
}
|
||||
|
||||
if gk.newerThan(rkID[gk.Name]) {
|
||||
if rk, ok := rkID[gk.Name]; gk.newerThan(rk) {
|
||||
if protocol.IsDeleted(gf.File.Flags) && (!ok || protocol.IsDeleted(m.files[rk].File.Flags)) {
|
||||
// We don't need to delete files we don't have or that are already deleted
|
||||
continue
|
||||
}
|
||||
|
||||
fs = append(fs, gf.File)
|
||||
}
|
||||
}
|
||||
|
||||
BIN
files/testdata/index.db
vendored
Normal file
BIN
files/testdata/index.db
vendored
Normal file
Binary file not shown.
153
gui/app.js
153
gui/app.js
@@ -30,21 +30,37 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
|
||||
$scope.seenError = '';
|
||||
$scope.model = {};
|
||||
$scope.repos = {};
|
||||
$scope.reportData = {};
|
||||
$scope.reportPreview = false;
|
||||
|
||||
$scope.needActions = {
|
||||
'rm': 'Del',
|
||||
'rmdir': 'Del (dir)',
|
||||
'sync': 'Sync',
|
||||
'touch': 'Update',
|
||||
}
|
||||
$scope.needIcons = {
|
||||
'rm': 'remove',
|
||||
'rmdir': 'remove',
|
||||
'sync': 'download',
|
||||
'touch': 'asterisk',
|
||||
}
|
||||
|
||||
// Strings before bools look better
|
||||
$scope.settings = [
|
||||
{id: 'ListenStr', descr: 'Sync Protocol Listen Addresses', type: 'text', restart: true},
|
||||
{id: 'MaxSendKbps', descr: 'Outgoing Rate Limit (KiB/s)', type: 'number', restart: true},
|
||||
{id: 'RescanIntervalS', descr: 'Rescan Interval (s)', type: 'number', restart: true},
|
||||
{id: 'ReconnectIntervalS', descr: 'Reconnect Interval (s)', type: 'number', restart: true},
|
||||
{id: 'ParallelRequests', descr: 'Max Outstanding Requests', type: 'number', restart: true},
|
||||
{id: 'MaxChangeKbps', descr: 'Max File Change Rate (KiB/s)', type: 'number', restart: true},
|
||||
{id: 'ListenStr', descr: 'Sync Protocol Listen Addresses', type: 'text'},
|
||||
{id: 'MaxSendKbps', descr: 'Outgoing Rate Limit (KiB/s)', type: 'number'},
|
||||
{id: 'RescanIntervalS', descr: 'Rescan Interval (s)', type: 'number'},
|
||||
{id: 'ReconnectIntervalS', descr: 'Reconnect Interval (s)', type: 'number'},
|
||||
{id: 'ParallelRequests', descr: 'Max Outstanding Requests', type: 'number'},
|
||||
{id: 'MaxChangeKbps', descr: 'Max File Change Rate (KiB/s)', type: 'number'},
|
||||
|
||||
{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: 'LocalAnnPort', descr: 'Local Discovery Port', type: 'number'},
|
||||
{id: 'LocalAnnEnabled', descr: 'Local Discovery', type: 'bool'},
|
||||
{id: 'GlobalAnnEnabled', descr: 'Global Discovery', type: 'bool'},
|
||||
{id: 'StartBrowser', descr: 'Start Browser', type: 'bool'},
|
||||
{id: 'UPnPEnabled', descr: 'Enable UPnP', type: 'bool'},
|
||||
{id: 'UREnabled', descr: 'Anonymous Usage Reporting', type: 'bool'},
|
||||
];
|
||||
|
||||
$scope.guiSettings = [
|
||||
@@ -87,9 +103,20 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
|
||||
getFailed();
|
||||
});
|
||||
Object.keys($scope.repos).forEach(function (id) {
|
||||
$http.get(urlbase + '/model?repo=' + encodeURIComponent(id)).success(function (data) {
|
||||
$scope.model[id] = data;
|
||||
});
|
||||
if (typeof $scope.model[id] === 'undefined') {
|
||||
// Never fetched before
|
||||
$http.get(urlbase + '/model?repo=' + encodeURIComponent(id)).success(function (data) {
|
||||
$scope.model[id] = data;
|
||||
});
|
||||
} else {
|
||||
$http.get(urlbase + '/model/version?repo=' + encodeURIComponent(id)).success(function (data) {
|
||||
if (data.version > $scope.model[id].version) {
|
||||
$http.get(urlbase + '/model?repo=' + encodeURIComponent(id)).success(function (data) {
|
||||
$scope.model[id] = data;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
$http.get(urlbase + '/connections').success(function (data) {
|
||||
var now = Date.now(),
|
||||
@@ -263,8 +290,9 @@ 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);
|
||||
$scope.tmpOptions = angular.copy($scope.config.Options);
|
||||
$scope.tmpOptions.UREnabled = ($scope.tmpOptions.URAccepted > 0);
|
||||
$scope.tmpGUI = angular.copy($scope.config.GUI);
|
||||
$('#settings').modal({backdrop: 'static', keyboard: true});
|
||||
};
|
||||
|
||||
@@ -280,17 +308,24 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
|
||||
|
||||
$scope.saveSettings = function () {
|
||||
// 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){
|
||||
var changed = !angular.equals($scope.config.Options, $scope.tmpOptions) ||
|
||||
!angular.equals($scope.config.GUI, $scope.tmpGUI);
|
||||
if (changed) {
|
||||
// Check if usage reporting has been enabled or disabled
|
||||
if ($scope.tmpOptions.UREnabled && $scope.tmpOptions.URAccepted <= 0) {
|
||||
$scope.tmpOptions.URAccepted = 1000;
|
||||
} else if (!$scope.tmpOptions.UREnabled && $scope.tmpOptions.URAccepted > 0){
|
||||
$scope.tmpOptions.URAccepted = -1;
|
||||
}
|
||||
|
||||
// Check if protocol will need to be changed on restart
|
||||
if($scope.config.GUI.UseTLS !== $scope.tmpGUI.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.config.Options = angular.copy($scope.tmpOptions);
|
||||
$scope.config.GUI = angular.copy($scope.tmpGUI);
|
||||
$scope.config.Options.ListenAddress = $scope.config.Options.ListenStr.split(',').map(function (x) { return x.trim(); });
|
||||
|
||||
$scope.saveConfig();
|
||||
@@ -544,11 +579,75 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
|
||||
$scope.repos = repoMap($scope.config.Repositories);
|
||||
|
||||
$scope.refresh();
|
||||
|
||||
if ($scope.config.Options.URAccepted == 0) {
|
||||
// If usage reporting has been neither accepted nor declined,
|
||||
// we want to ask the user to make a choice. But we don't want
|
||||
// to bug them during initial setup, so we set a cookie with
|
||||
// the time of the first visit. When that cookie is present
|
||||
// and the time is more than four hours ago, we ask the
|
||||
// question.
|
||||
|
||||
var firstVisit = document.cookie.replace(/(?:(?:^|.*;\s*)firstVisit\s*\=\s*([^;]*).*$)|^.*$/, "$1");
|
||||
if (!firstVisit) {
|
||||
document.cookie = "firstVisit=" + Date.now() + ";max-age=" + 30*24*3600;
|
||||
} else {
|
||||
if (+firstVisit < Date.now() - 4*3600*1000){
|
||||
$('#ur').modal({backdrop: 'static', keyboard: false});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$http.get(urlbase + '/config/sync').success(function (data) {
|
||||
$scope.configInSync = data.configInSync;
|
||||
});
|
||||
|
||||
$http.get(urlbase + '/report').success(function (data) {
|
||||
$scope.reportData = data;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.acceptUR = function () {
|
||||
$scope.config.Options.URAccepted = 1000; // Larger than the largest existing report version
|
||||
$scope.saveConfig();
|
||||
$('#ur').modal('hide');
|
||||
};
|
||||
|
||||
$scope.declineUR = function () {
|
||||
$scope.config.Options.URAccepted = -1;
|
||||
$scope.saveConfig();
|
||||
$('#ur').modal('hide');
|
||||
};
|
||||
|
||||
$scope.showNeed = function (repo) {
|
||||
$scope.neededLoaded = false;
|
||||
$('#needed').modal({backdrop: 'static', keyboard: true});
|
||||
$http.get(urlbase + "/need?repo=" + encodeURIComponent(repo)).success(function (data) {
|
||||
$scope.needed = data;
|
||||
$scope.neededLoaded = true;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.needAction = function (file) {
|
||||
var fDelete = 4096;
|
||||
var fDirectory = 16384;
|
||||
|
||||
if ((file.Flags & (fDelete+fDirectory)) === fDelete+fDirectory) {
|
||||
return 'rmdir';
|
||||
} else if ((file.Flags & fDelete) === fDelete) {
|
||||
return 'rm';
|
||||
} else if ((file.Flags & fDirectory) === fDirectory) {
|
||||
return 'touch';
|
||||
} else {
|
||||
return 'sync';
|
||||
}
|
||||
};
|
||||
|
||||
$scope.override = function (repo) {
|
||||
$http.post(urlbase + "/model/override?repo=" + encodeURIComponent(repo)).success(function () {
|
||||
$scope.refresh();
|
||||
});
|
||||
};
|
||||
|
||||
$scope.init();
|
||||
@@ -701,6 +800,18 @@ syncthing.filter('shortPath', function () {
|
||||
};
|
||||
});
|
||||
|
||||
syncthing.filter('basename', function () {
|
||||
return function (input) {
|
||||
if (input === undefined)
|
||||
return "";
|
||||
var parts = input.split(/[\/\\]/);
|
||||
if (!parts || parts.length < 1) {
|
||||
return input;
|
||||
}
|
||||
return parts[parts.length-1];
|
||||
};
|
||||
});
|
||||
|
||||
syncthing.filter('clean', function () {
|
||||
return function (input) {
|
||||
return encodeURIComponent(input).replace(/%/g, '');
|
||||
|
||||
@@ -65,7 +65,7 @@ found in the LICENSE file.
|
||||
}
|
||||
|
||||
.table th {
|
||||
white-space:nowrap;
|
||||
white-space: nowrap;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@@ -73,6 +73,10 @@ found in the LICENSE file.
|
||||
padding-left: 20px !important;
|
||||
}
|
||||
|
||||
.table td.small-data {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (max-width:767px) {
|
||||
.table-responsive>.table>tbody>tr>td {
|
||||
/* revert a bootstrap setting e.g.:
|
||||
@@ -168,15 +172,18 @@ found in the LICENSE file.
|
||||
</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>
|
||||
<td class="text-right">{{model[repo.ID].globalFiles | alwaysNumber}} items, {{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>
|
||||
<td class="text-right">{{model[repo.ID].localFiles | alwaysNumber}} items, {{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>
|
||||
<td class="text-right">
|
||||
<a ng-if="model[repo.ID].needFiles > 0" ng-click="showNeed(repo.ID)" href="">{{model[repo.ID].needFiles | alwaysNumber}} items, {{model[repo.ID].needBytes | binary}}B</a>
|
||||
<span ng-if="model[repo.ID].needFiles == 0">0 items, 0 B</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-lock"></span> Master Repository</th>
|
||||
@@ -199,7 +206,10 @@ found in the LICENSE file.
|
||||
</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>
|
||||
<span class="pull-right">
|
||||
<a class="btn btn-sm btn-primary" href="" ng-click="editRepo(repo)"><span class="glyphicon glyphicon-pencil"></span> Edit</a>
|
||||
<a class="btn btn-sm btn-danger" ng-if="repo.ReadOnly && model[repo.ID].needFiles > 0" ng-click="override(repo.ID)" href=""><span class="glyphicon glyphicon-upload"></span> Override Changes</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -442,7 +452,7 @@ found in the LICENSE file.
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="name">Name</label>
|
||||
<label for="name">Node Name</label>
|
||||
<input placeholder="Home Server" id="name" class="form-control" type="text" ng-model="currentNode.Name"></input>
|
||||
<p class="help-block">Shown instead of Node ID in the cluster status.</p>
|
||||
</div>
|
||||
@@ -564,7 +574,7 @@ found in the LICENSE file.
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title"> Settings</h4>
|
||||
<h4 class="modal-title">Settings</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form role="form">
|
||||
@@ -573,11 +583,11 @@ found in the LICENSE file.
|
||||
<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.workingOptions[setting.id]"></input>
|
||||
<input id="{{setting.id}}" class="form-control" type="{{setting.type}}" ng-model="tmpOptions[setting.id]"></input>
|
||||
</div>
|
||||
<div class="checkbox" ng-if="setting.type == 'bool'">
|
||||
<label>
|
||||
{{setting.descr}} <input id="{{setting.id}}" type="checkbox" ng-model="config.workingOptions[setting.id]"></input>
|
||||
{{setting.descr}} <input id="{{setting.id}}" type="checkbox" ng-model="tmpOptions[setting.id]"></input>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -586,17 +596,17 @@ found in the LICENSE file.
|
||||
<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.workingGUI[setting.id]"></input>
|
||||
<input id="{{setting.id}}" class="form-control" type="{{setting.type}}" ng-model="tmpGUI[setting.id]"></input>
|
||||
</div>
|
||||
<div class="checkbox" ng-if="setting.type == 'bool'">
|
||||
<label>
|
||||
{{setting.descr}} <input id="{{setting.id}}" type="checkbox" ng-model="config.workingGUI[setting.id]"></input>
|
||||
{{setting.descr}} <input id="{{setting.id}}" type="checkbox" ng-model="tmpGUI[setting.id]"></input>
|
||||
</label>
|
||||
</div>
|
||||
<div ng-if="setting.type == 'apikey'">
|
||||
<label>{{setting.descr}} (<a href="http://discourse.syncthing.net/t/v0-8-14-api-keys/335">Usage</a>)</label>
|
||||
<div class="well well-sm text-monospace">{{config.workingGUI[setting.id] || "-"}}</div>
|
||||
<button type="button" class="btn btn-sm btn-default" ng-click="setAPIKey(config.workingGUI)">Generate</button>
|
||||
<div class="well well-sm text-monospace">{{tmpGUI[setting.id] || "-"}}</div>
|
||||
<button type="button" class="btn btn-sm btn-default" ng-click="setAPIKey(tmpGUI)">Generate</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -611,6 +621,56 @@ found in the LICENSE file.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Usage report modal -->
|
||||
|
||||
<div id="ur" class="modal fade">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header alert alert-success">
|
||||
<h4 class="modal-title">Allow Anonymous Usage Reporting?</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.
|
||||
</p>
|
||||
<p>
|
||||
The aggregated statistics are publicly available at <a href="https://data.syncthing.net/">https://data.syncthing.net/</a>.
|
||||
</p>
|
||||
<button type="button" class="btn btn-default" ng-show="!reportPreview" ng-click="reportPreview = true">Preview Usage Report</button>
|
||||
<pre ng-if="reportPreview"><small>{{reportData | json}}</small></pre>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-success" ng-click="acceptUR()"><span class="glyphicon glyphicon-ok"></span> Yes</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="declineUR()"><span class="glyphicon glyphicon-remove"></span> No</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Needed files modal -->
|
||||
|
||||
<div id="needed" class="modal fade">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header alert alert-info">
|
||||
<h4 class="modal-title">Out of Sync Items</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<table class="table table-striped table-condensed">
|
||||
<tr ng-repeat="f in needed" ng-init="a = needAction(f)">
|
||||
<td class="small-data"><span class="glyphicon glyphicon-{{needIcons[a]}}"></span> {{needActions[a]}}</td>
|
||||
<td title="{{f.Name}}">{{f.Name | basename}}</td>
|
||||
<td class="text-right small-data"><span ng-if="f.Size > 0">{{f.Size | binary}}B</span></td>
|
||||
</tr>
|
||||
</table>
|
||||
</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>
|
||||
|
||||
|
||||
<script src="angular.min.js"></script>
|
||||
<script src="jquery-2.0.3.min.js"></script>
|
||||
|
||||
1
integration/.gitignore
vendored
1
integration/.gitignore
vendored
@@ -14,3 +14,4 @@ dirs-*
|
||||
*.out
|
||||
csrftokens.txt
|
||||
s4d
|
||||
http
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
<gui enabled="true" tls="false">
|
||||
<address>127.0.0.1:8081</address>
|
||||
<apikey>abc123</apikey>
|
||||
<user>testuser</user>
|
||||
<password>testpass</password>
|
||||
</gui>
|
||||
<options>
|
||||
<listenAddress>127.0.0.1:22001</listenAddress>
|
||||
|
||||
232
integration/http.go
Normal file
232
integration/http.go
Normal file
@@ -0,0 +1,232 @@
|
||||
// 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
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"flag"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var (
|
||||
target string
|
||||
authUser string
|
||||
authPass string
|
||||
csrfToken string
|
||||
csrfFile string
|
||||
apiKey string
|
||||
)
|
||||
|
||||
var jsonEndpoints = []string{
|
||||
"/rest/model?repo=default",
|
||||
"/rest/model/version?repo=default",
|
||||
"/rest/need",
|
||||
"/rest/connections",
|
||||
"/rest/config",
|
||||
"/rest/config/sync",
|
||||
"/rest/system",
|
||||
"/rest/errors",
|
||||
// "/rest/discovery",
|
||||
"/rest/report",
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.StringVar(&target, "target", "localhost:8080", "Test target")
|
||||
flag.StringVar(&authUser, "user", "", "Username")
|
||||
flag.StringVar(&authPass, "pass", "", "Password")
|
||||
flag.StringVar(&csrfFile, "csrf", "", "CSRF token file")
|
||||
flag.StringVar(&apiKey, "api", "", "API key")
|
||||
flag.Parse()
|
||||
|
||||
if len(csrfFile) > 0 {
|
||||
fd, err := os.Open(csrfFile)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
s := bufio.NewScanner(fd)
|
||||
for s.Scan() {
|
||||
csrfToken = s.Text()
|
||||
}
|
||||
fd.Close()
|
||||
}
|
||||
|
||||
var tests []testing.InternalTest
|
||||
tests = append(tests, testing.InternalTest{"TestGetIndex", TestGetIndex})
|
||||
tests = append(tests, testing.InternalTest{"TestGetVersion", TestGetVersion})
|
||||
tests = append(tests, testing.InternalTest{"TestGetVersionNoCSRF", TestGetVersion})
|
||||
tests = append(tests, testing.InternalTest{"TestJSONEndpoints", TestJSONEndpoints})
|
||||
if len(authUser) > 0 || len(apiKey) > 0 {
|
||||
tests = append(tests, testing.InternalTest{"TestJSONEndpointsNoAuth", TestJSONEndpointsNoAuth})
|
||||
tests = append(tests, testing.InternalTest{"TestJSONEndpointsIncorrectAuth", TestJSONEndpointsIncorrectAuth})
|
||||
}
|
||||
if len(csrfToken) > 0 {
|
||||
tests = append(tests, testing.InternalTest{"TestJSONEndpointsNoCSRF", TestJSONEndpointsNoCSRF})
|
||||
}
|
||||
|
||||
testing.Main(matcher, tests, nil, nil)
|
||||
}
|
||||
|
||||
func matcher(s0, s1 string) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func TestGetIndex(t *testing.T) {
|
||||
res, err := get("/index.html")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
t.Errorf("Status %d != 200", res.StatusCode)
|
||||
}
|
||||
if res.ContentLength < 1024 {
|
||||
t.Errorf("Length %d < 1024", res.ContentLength)
|
||||
}
|
||||
res.Body.Close()
|
||||
|
||||
res, err = get("/")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
t.Errorf("Status %d != 200", res.StatusCode)
|
||||
}
|
||||
if res.ContentLength < 1024 {
|
||||
t.Errorf("Length %d < 1024", res.ContentLength)
|
||||
}
|
||||
res.Body.Close()
|
||||
}
|
||||
|
||||
func TestGetVersion(t *testing.T) {
|
||||
res, err := get("/rest/version")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
t.Fatalf("Status %d != 200", res.StatusCode)
|
||||
}
|
||||
ver, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
res.Body.Close()
|
||||
|
||||
if !regexp.MustCompile(`v\d+\.\d+\.\d+`).Match(ver) {
|
||||
t.Errorf("Invalid version %q", ver)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetVersionNoCSRF(t *testing.T) {
|
||||
r, err := http.NewRequest("GET", "http://"+target+"/rest/version", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(authUser) > 0 {
|
||||
r.SetBasicAuth(authUser, authPass)
|
||||
}
|
||||
res, err := http.DefaultClient.Do(r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if res.StatusCode != 403 {
|
||||
t.Fatalf("Status %d != 403", res.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONEndpoints(t *testing.T) {
|
||||
for _, p := range jsonEndpoints {
|
||||
res, err := get(p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
t.Errorf("Status %d != 200 for %q", res.StatusCode, p)
|
||||
}
|
||||
if ct := res.Header.Get("Content-Type"); ct != "application/json; charset=utf-8" {
|
||||
t.Errorf("Content-Type %q != \"application/json\" for %q", ct, p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONEndpointsNoCSRF(t *testing.T) {
|
||||
for _, p := range jsonEndpoints {
|
||||
r, err := http.NewRequest("GET", "http://"+target+p, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(authUser) > 0 {
|
||||
r.SetBasicAuth(authUser, authPass)
|
||||
}
|
||||
res, err := http.DefaultClient.Do(r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if res.StatusCode != 403 && res.StatusCode != 401 {
|
||||
t.Fatalf("Status %d != 403/401 for %q", res.StatusCode, p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONEndpointsNoAuth(t *testing.T) {
|
||||
for _, p := range jsonEndpoints {
|
||||
r, err := http.NewRequest("GET", "http://"+target+p, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(csrfToken) > 0 {
|
||||
r.Header.Set("X-CSRF-Token", csrfToken)
|
||||
}
|
||||
res, err := http.DefaultClient.Do(r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if res.StatusCode != 403 && res.StatusCode != 401 {
|
||||
t.Fatalf("Status %d != 403/401 for %q", res.StatusCode, p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONEndpointsIncorrectAuth(t *testing.T) {
|
||||
for _, p := range jsonEndpoints {
|
||||
r, err := http.NewRequest("GET", "http://"+target+p, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(csrfToken) > 0 {
|
||||
r.Header.Set("X-CSRF-Token", csrfToken)
|
||||
}
|
||||
r.SetBasicAuth("wronguser", "wrongpass")
|
||||
res, err := http.DefaultClient.Do(r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if res.StatusCode != 403 && res.StatusCode != 401 {
|
||||
t.Fatalf("Status %d != 403/401 for %q", res.StatusCode, p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func get(path string) (*http.Response, error) {
|
||||
r, err := http.NewRequest("GET", "http://"+target+path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(authUser) > 0 {
|
||||
r.SetBasicAuth(authUser, authPass)
|
||||
}
|
||||
if len(csrfToken) > 0 {
|
||||
r.Header.Set("X-CSRF-Token", csrfToken)
|
||||
}
|
||||
if len(apiKey) > 0 {
|
||||
r.Header.Set("X-API-Key", apiKey)
|
||||
}
|
||||
return http.DefaultClient.Do(r)
|
||||
}
|
||||
@@ -13,14 +13,41 @@ id3=373HSRPQLPNLIJYKZVQFP4PKZ6R2ZE6K3YD442UJHBGBQGWWXAHA
|
||||
go build genfiles.go
|
||||
go build md5r.go
|
||||
go build json.go
|
||||
go build http.go
|
||||
|
||||
start() {
|
||||
echo "Starting..."
|
||||
for i in 1 2 3 4 ; do
|
||||
STPROFILER=":909$i" syncthing -home "h$i" > "$i.out" 2>&1 &
|
||||
done
|
||||
|
||||
# Test REST API
|
||||
sleep 2
|
||||
curl -s -o /dev/null http://testuser:testpass@localhost:8081/index.html
|
||||
curl -s -o /dev/null http://localhost:8082/index.html
|
||||
sleep 1
|
||||
./http -target localhost:8081 -user testuser -pass testpass -csrf h1/csrftokens.txt || stop 1
|
||||
./http -target localhost:8081 -api abc123 || stop 1
|
||||
./http -target localhost:8082 -csrf h2/csrftokens.txt || stop 1
|
||||
./http -target localhost:8082 -api abc123 || stop 1
|
||||
}
|
||||
|
||||
stop() {
|
||||
for i in 1 2 3 4 ; do
|
||||
curl -HX-API-Key:abc123 -X POST "http://localhost:808$i/rest/shutdown"
|
||||
done
|
||||
exit $1
|
||||
}
|
||||
|
||||
clean() {
|
||||
if [[ $(uname -s) == "Linux" ]] ; then
|
||||
grep -v utf8-nfd
|
||||
else
|
||||
cat
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
testConvergence() {
|
||||
while true ; do
|
||||
sleep 5
|
||||
@@ -38,13 +65,13 @@ testConvergence() {
|
||||
done
|
||||
|
||||
echo "Verifying..."
|
||||
cat md5-? | sort | uniq > md5-tot
|
||||
cat md5-12-? | sort | uniq > md5-12-tot
|
||||
cat md5-23-? | sort | uniq > md5-23-tot
|
||||
cat md5-? | sort | clean | uniq > md5-tot
|
||||
cat md5-12-? | sort | clean | uniq > md5-12-tot
|
||||
cat md5-23-? | sort | clean | uniq > md5-23-tot
|
||||
|
||||
for i in 1 2 3 12-1 12-2 23-2 23-3; do
|
||||
pushd "s$i" >/dev/null
|
||||
../md5r -l | sort > ../md5-$i
|
||||
../md5r -l | sort | clean > ../md5-$i
|
||||
popd >/dev/null
|
||||
done
|
||||
|
||||
@@ -74,8 +101,7 @@ testConvergence() {
|
||||
fi
|
||||
done
|
||||
if [[ $ok != 7 ]] ; then
|
||||
pkill syncthing
|
||||
exit 1
|
||||
stop 1
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -148,6 +174,4 @@ for ((t = 1; t <= $iterations; t++)) ; do
|
||||
testConvergence
|
||||
done
|
||||
|
||||
for i in 1 2 3 4 ; do
|
||||
curl -HX-API-Key:abc123 -X POST "http://localhost:808$i/rest/shutdown"
|
||||
done
|
||||
stop 0
|
||||
|
||||
@@ -13,10 +13,10 @@ import (
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/calmh/syncthing/buffers"
|
||||
"github.com/calmh/syncthing/cid"
|
||||
"github.com/calmh/syncthing/config"
|
||||
"github.com/calmh/syncthing/files"
|
||||
@@ -98,6 +98,16 @@ func NewModel(indexDir string, cfg *config.Configuration, clientName, clientVers
|
||||
sup: suppressor{threshold: int64(cfg.Options.MaxChangeKbps)},
|
||||
}
|
||||
|
||||
var timeout = 20 * 60 // seconds
|
||||
if t := os.Getenv("STDEADLOCKTIMEOUT"); len(t) > 0 {
|
||||
it, err := strconv.Atoi(t)
|
||||
if err == nil {
|
||||
timeout = it
|
||||
}
|
||||
}
|
||||
deadlockDetect(&m.rmut, time.Duration(timeout)*time.Second)
|
||||
deadlockDetect(&m.smut, time.Duration(timeout)*time.Second)
|
||||
deadlockDetect(&m.pmut, time.Duration(timeout)*time.Second)
|
||||
go m.broadcastIndexLoop()
|
||||
return m
|
||||
}
|
||||
@@ -359,20 +369,14 @@ func (m *Model) ClusterConfig(nodeID string, config protocol.ClusterConfigMessag
|
||||
m.nodeVer[nodeID] = config.ClientName + " " + config.ClientVersion
|
||||
}
|
||||
m.pmut.Unlock()
|
||||
|
||||
l.Infof(`Node %s client is "%s %s"`, nodeID, config.ClientName, config.ClientVersion)
|
||||
}
|
||||
|
||||
// Close removes the peer from the model and closes the underlying connection if possible.
|
||||
// Implements the protocol.Model interface.
|
||||
func (m *Model) Close(node string, err error) {
|
||||
if debug {
|
||||
l.Debugf("%s: %v", node, err)
|
||||
}
|
||||
|
||||
if err != io.EOF {
|
||||
l.Warnf("Connection to %s closed: %v", node, err)
|
||||
} else if _, ok := err.(ClusterConfigMismatch); ok {
|
||||
l.Warnf("Connection to %s closed: %v", node, err)
|
||||
}
|
||||
l.Infof("Connection to %s closed: %v", node, err)
|
||||
|
||||
cid := m.cm.Get(node)
|
||||
m.rmut.RLock()
|
||||
@@ -433,7 +437,7 @@ func (m *Model) Request(nodeID, repo, name string, offset int64, size int) ([]by
|
||||
}
|
||||
defer fd.Close()
|
||||
|
||||
buf := buffers.Get(int(size))
|
||||
buf := make([]byte, size)
|
||||
_, err = fd.ReadAt(buf, offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -449,19 +453,6 @@ func (m *Model) ReplaceLocal(repo string, fs []scanner.File) {
|
||||
m.rmut.RUnlock()
|
||||
}
|
||||
|
||||
func (m *Model) SeedLocal(repo string, fs []protocol.FileInfo) {
|
||||
var sfs = make([]scanner.File, len(fs))
|
||||
for i := 0; i < len(fs); i++ {
|
||||
lamport.Default.Tick(fs[i].Version)
|
||||
sfs[i] = fileFromFileInfo(fs[i])
|
||||
sfs[i].Suppressed = false // we might have saved an index with files that were suppressed; the should not be on startup
|
||||
}
|
||||
|
||||
m.rmut.RLock()
|
||||
m.repoFiles[repo].Replace(cid.LocalID, sfs)
|
||||
m.rmut.RUnlock()
|
||||
}
|
||||
|
||||
func (m *Model) CurrentRepoFile(repo string, file string) scanner.File {
|
||||
m.rmut.RLock()
|
||||
f := m.repoFiles[repo].Get(cid.LocalID, file)
|
||||
@@ -526,7 +517,14 @@ func (m *Model) AddConnection(rawConn io.Closer, protoConn protocol.Connection)
|
||||
if debug {
|
||||
l.Debugf("IDX(out/initial): %s: %q: %d files", nodeID, repo, len(idx))
|
||||
}
|
||||
protoConn.Index(repo, idx)
|
||||
const batchSize = 1000
|
||||
for i := 0; i < len(idx); i += batchSize {
|
||||
if len(idx[i:]) < batchSize {
|
||||
protoConn.Index(repo, idx[i:])
|
||||
} else {
|
||||
protoConn.Index(repo, idx[i:i+batchSize])
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -731,7 +729,15 @@ func (m *Model) LoadIndexes(dir string) {
|
||||
m.rmut.RLock()
|
||||
for repo := range m.repoCfgs {
|
||||
fs := m.loadIndex(repo, dir)
|
||||
m.SeedLocal(repo, fs)
|
||||
|
||||
var sfs = make([]scanner.File, len(fs))
|
||||
for i := 0; i < len(fs); i++ {
|
||||
lamport.Default.Tick(fs[i].Version)
|
||||
sfs[i] = fileFromFileInfo(fs[i])
|
||||
sfs[i].Suppressed = false // we might have saved an index with files that were suppressed; the should not be on startup
|
||||
}
|
||||
|
||||
m.repoFiles[repo].Replace(cid.LocalID, sfs)
|
||||
}
|
||||
m.rmut.RUnlock()
|
||||
}
|
||||
@@ -851,3 +857,42 @@ func (m *Model) State(repo string) string {
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) Override(repo string) {
|
||||
fs := m.NeedFilesRepo(repo)
|
||||
|
||||
m.rmut.RLock()
|
||||
r := m.repoFiles[repo]
|
||||
m.rmut.RUnlock()
|
||||
|
||||
for i := range fs {
|
||||
f := &fs[i]
|
||||
h := r.Get(cid.LocalID, f.Name)
|
||||
if h.Name != f.Name {
|
||||
// We are missing the file
|
||||
f.Flags |= protocol.FlagDeleted
|
||||
f.Blocks = nil
|
||||
} else {
|
||||
// We have the file, replace with our version
|
||||
*f = h
|
||||
}
|
||||
f.Version = lamport.Default.Tick(f.Version)
|
||||
}
|
||||
|
||||
r.Update(cid.LocalID, fs)
|
||||
}
|
||||
|
||||
// Version returns the change version for the given repository. This is
|
||||
// guaranteed to increment if the contents of the local or global repository
|
||||
// has changed.
|
||||
func (m *Model) Version(repo string) uint64 {
|
||||
var ver uint64
|
||||
|
||||
m.rmut.Lock()
|
||||
for _, n := range m.repoNodes[repo] {
|
||||
ver += m.repoFiles[repo].Changes(m.cm.Get(n))
|
||||
}
|
||||
m.rmut.Unlock()
|
||||
|
||||
return ver
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/calmh/syncthing/buffers"
|
||||
"github.com/calmh/syncthing/cid"
|
||||
"github.com/calmh/syncthing/config"
|
||||
"github.com/calmh/syncthing/osutil"
|
||||
@@ -137,6 +136,7 @@ func (p *puller) run() {
|
||||
walkTicker := time.Tick(time.Duration(p.cfg.Options.RescanIntervalS) * time.Second)
|
||||
timeout := time.Tick(5 * time.Second)
|
||||
changed := true
|
||||
var prevVer uint64
|
||||
|
||||
for {
|
||||
// Run the pulling loop as long as there are blocks to fetch
|
||||
@@ -199,8 +199,11 @@ func (p *puller) run() {
|
||||
default:
|
||||
}
|
||||
|
||||
// Queue more blocks to fetch, if any
|
||||
p.queueNeededBlocks()
|
||||
if v := p.model.Version(p.repoCfg.ID); v > prevVer {
|
||||
// Queue more blocks to fetch, if any
|
||||
p.queueNeededBlocks()
|
||||
prevVer = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,7 +342,6 @@ func (p *puller) handleRequestResult(res requestResult) {
|
||||
}
|
||||
|
||||
_, of.err = of.file.WriteAt(res.data, res.offset)
|
||||
buffers.Put(res.data)
|
||||
|
||||
of.outstanding--
|
||||
p.openFiles[f.Name] = of
|
||||
@@ -490,12 +492,11 @@ func (p *puller) handleCopyBlock(b bqBlock) {
|
||||
defer exfd.Close()
|
||||
|
||||
for _, b := range b.copy {
|
||||
bs := buffers.Get(int(b.Size))
|
||||
bs := make([]byte, b.Size)
|
||||
_, of.err = exfd.ReadAt(bs, b.Offset)
|
||||
if of.err == nil {
|
||||
_, of.err = of.file.WriteAt(bs, b.Offset)
|
||||
}
|
||||
buffers.Put(bs)
|
||||
if of.err != nil {
|
||||
if debug {
|
||||
l.Debugf("pull: error: %q / %q: %v", p.repoCfg.ID, f.Name, of.err)
|
||||
|
||||
@@ -7,6 +7,8 @@ package model
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/calmh/syncthing/protocol"
|
||||
"github.com/calmh/syncthing/scanner"
|
||||
@@ -90,3 +92,27 @@ func compareClusterConfig(local, remote protocol.ClusterConfigMessage) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func deadlockDetect(mut sync.Locker, timeout time.Duration) {
|
||||
go func() {
|
||||
for {
|
||||
time.Sleep(timeout / 4)
|
||||
ok := make(chan bool, 2)
|
||||
|
||||
go func() {
|
||||
mut.Lock()
|
||||
mut.Unlock()
|
||||
ok <- true
|
||||
}()
|
||||
|
||||
go func() {
|
||||
time.Sleep(timeout)
|
||||
ok <- false
|
||||
}()
|
||||
|
||||
if r := <-ok; !r {
|
||||
panic("deadlock detected")
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -59,10 +59,11 @@ or certificate pinning combined with some out of band first
|
||||
verification. The reference implementation uses preshared certificate
|
||||
fingerprints (SHA-256) referred to as "Node IDs".
|
||||
|
||||
There is no required order or synchronization among BEP messages - any
|
||||
message type may be sent at any time and the sender need not await a
|
||||
response to one message before sending another. Responses MUST however
|
||||
be sent in the same order as the requests are received.
|
||||
There is no required order or synchronization among BEP messages except
|
||||
as noted per message type - any message type may be sent at any time and
|
||||
the sender need not await a response to one message before sending
|
||||
another. Responses MUST however be sent in the same order as the
|
||||
requests are received.
|
||||
|
||||
The underlying transport protocol MUST be TCP.
|
||||
|
||||
@@ -70,12 +71,13 @@ Messages
|
||||
--------
|
||||
|
||||
Every message starts with one 32 bit word indicating the message
|
||||
version, type and ID.
|
||||
version, type and ID. The header is in network byte order, i.e. big
|
||||
endian.
|
||||
|
||||
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
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Ver | Type | Message ID | Reply To |
|
||||
| Ver | Message ID | Type | Reserved |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
|
||||
For BEP v1 the Version field is set to zero. Future versions with
|
||||
@@ -84,19 +86,19 @@ with an unknown version is a protocol error and MUST result in the
|
||||
connection being terminated. A client supporting multiple versions MAY
|
||||
retry with a different protocol version upon disconnection.
|
||||
|
||||
The Message ID is set to a unique value for each transmitted request
|
||||
message. In response messages it is set to the Message ID of the
|
||||
corresponding request message. The uniqueness requirement implies that
|
||||
no more than 4096 messages may be outstanding at any given moment. The
|
||||
ordering requirement implies that a response to a given message ID also
|
||||
means that all preceding messages have been received, specifically those
|
||||
which do not otherwise demand a response. Hence their message ID:s may
|
||||
be reused.
|
||||
|
||||
The Type field indicates the type of data following the message header
|
||||
and is one of the integers defined below. A message of an unknown type
|
||||
is a protocol error and MUST result in the connection being terminated.
|
||||
|
||||
The Message ID is set to a unique value for each transmitted message. In
|
||||
request messages the Reply To is set to zero. In response messages it is
|
||||
set to the message ID of the corresponding request. The uniqueness
|
||||
requirement implies that no more than 4096 messages may be outstanding
|
||||
at any given moment. The ordering requirement implies that a response to
|
||||
a given message ID also means that all preceding messages have been
|
||||
received, specifically those which do not otherwise demand a response.
|
||||
Hence their message ID:s may be reused.
|
||||
|
||||
All data following the message header MUST be in XDR (RFC 1014)
|
||||
encoding. All fields shorter than 32 bits and all variable length data
|
||||
MUST be padded to a multiple of 32 bits. The actual data types in use by
|
||||
@@ -117,8 +119,9 @@ normalization form C.
|
||||
### Cluster Config (Type = 0)
|
||||
|
||||
This informational message provides information about the cluster
|
||||
configuration, as it pertains to the current connection. It is sent by
|
||||
both sides after connection establishment.
|
||||
configuration as it pertains to the current connection. A Cluster Config
|
||||
message MUST be the first message sent on a BEP connection. Additional
|
||||
Cluster Config messages MUST NOT be sent after the initial exchange.
|
||||
|
||||
#### Graphical Representation
|
||||
|
||||
@@ -294,11 +297,12 @@ peers acting in a specific manner as a result of sent options.
|
||||
### Index (Type = 1)
|
||||
|
||||
The Index message defines the contents of the senders repository. An
|
||||
Index message MUST be sent by each node immediately upon connection. A
|
||||
node with no data to advertise MUST send an empty Index message (a file
|
||||
list of zero length). If the repository contents change from non-empty
|
||||
to empty, an empty Index message MUST be sent. There is no response to
|
||||
the Index message.
|
||||
Index message MUST be sent for each repository mentioned in the Cluster
|
||||
Config message. An Index message for a repository MUST be sent before
|
||||
any other message referring to that repository. A node with no data to
|
||||
advertise MUST send an empty Index message (a file list of zero length).
|
||||
If the repository contents change from non-empty to empty, an empty
|
||||
Index message MUST be sent. There is no response to the Index message.
|
||||
|
||||
#### Graphical Representation
|
||||
|
||||
|
||||
@@ -28,6 +28,12 @@ const (
|
||||
messageTypeIndexUpdate = 6
|
||||
)
|
||||
|
||||
const (
|
||||
stateInitial = iota
|
||||
stateCCRcvd
|
||||
stateIdxRcvd
|
||||
)
|
||||
|
||||
const (
|
||||
FlagDeleted uint32 = 1 << 12
|
||||
FlagInvalid = 1 << 13
|
||||
@@ -70,24 +76,36 @@ type Connection interface {
|
||||
type rawConnection struct {
|
||||
id string
|
||||
receiver Model
|
||||
state int
|
||||
|
||||
reader io.ReadCloser
|
||||
cr *countingReader
|
||||
xr *xdr.Reader
|
||||
|
||||
writer io.WriteCloser
|
||||
cw *countingWriter
|
||||
wb *bufio.Writer
|
||||
xw *xdr.Writer
|
||||
|
||||
cw *countingWriter
|
||||
wb *bufio.Writer
|
||||
xw *xdr.Writer
|
||||
wmut sync.Mutex
|
||||
awaiting []chan asyncResult
|
||||
awaitingMut sync.Mutex
|
||||
|
||||
indexSent map[string]map[string][2]int64
|
||||
awaiting []chan asyncResult
|
||||
imut sync.Mutex
|
||||
idxSent map[string]map[string]uint64
|
||||
idxMut sync.Mutex // ensures serialization of Index calls
|
||||
|
||||
nextID chan int
|
||||
outbox chan []encodable
|
||||
closed chan struct{}
|
||||
once sync.Once
|
||||
|
||||
incomingIndexes chan incomingIndex
|
||||
}
|
||||
|
||||
type incomingIndex struct {
|
||||
update bool
|
||||
id string
|
||||
repo string
|
||||
files []FileInfo
|
||||
}
|
||||
|
||||
type asyncResult struct {
|
||||
@@ -96,8 +114,8 @@ type asyncResult struct {
|
||||
}
|
||||
|
||||
const (
|
||||
pingTimeout = 300 * time.Second
|
||||
pingIdleTime = 600 * time.Second
|
||||
pingTimeout = 30 * time.Second
|
||||
pingIdleTime = 60 * time.Second
|
||||
)
|
||||
|
||||
func NewConnection(nodeID string, reader io.Reader, writer io.Writer, receiver Model) Connection {
|
||||
@@ -112,22 +130,25 @@ func NewConnection(nodeID string, reader io.Reader, writer io.Writer, receiver M
|
||||
wb := bufio.NewWriter(flwr)
|
||||
|
||||
c := rawConnection{
|
||||
id: nodeID,
|
||||
receiver: nativeModel{receiver},
|
||||
reader: flrd,
|
||||
cr: cr,
|
||||
xr: xdr.NewReader(flrd),
|
||||
writer: flwr,
|
||||
cw: cw,
|
||||
wb: wb,
|
||||
xw: xdr.NewWriter(wb),
|
||||
awaiting: make([]chan asyncResult, 0x1000),
|
||||
indexSent: make(map[string]map[string][2]int64),
|
||||
outbox: make(chan []encodable),
|
||||
nextID: make(chan int),
|
||||
closed: make(chan struct{}),
|
||||
id: nodeID,
|
||||
receiver: nativeModel{receiver},
|
||||
state: stateInitial,
|
||||
reader: flrd,
|
||||
cr: cr,
|
||||
xr: xdr.NewReader(flrd),
|
||||
writer: flwr,
|
||||
cw: cw,
|
||||
wb: wb,
|
||||
xw: xdr.NewWriter(wb),
|
||||
awaiting: make([]chan asyncResult, 0x1000),
|
||||
idxSent: make(map[string]map[string]uint64),
|
||||
outbox: make(chan []encodable),
|
||||
nextID: make(chan int),
|
||||
closed: make(chan struct{}),
|
||||
incomingIndexes: make(chan incomingIndex, 100), // should be enough for anyone, right?
|
||||
}
|
||||
|
||||
go c.indexSerializerLoop()
|
||||
go c.readerLoop()
|
||||
go c.writerLoop()
|
||||
go c.pingerLoop()
|
||||
@@ -142,31 +163,34 @@ func (c *rawConnection) ID() string {
|
||||
|
||||
// Index writes the list of file information to the connected peer node
|
||||
func (c *rawConnection) Index(repo string, idx []FileInfo) {
|
||||
c.imut.Lock()
|
||||
c.idxMut.Lock()
|
||||
defer c.idxMut.Unlock()
|
||||
|
||||
var msgType int
|
||||
if c.indexSent[repo] == nil {
|
||||
if c.idxSent[repo] == nil {
|
||||
// This is the first time we send an index.
|
||||
msgType = messageTypeIndex
|
||||
|
||||
c.indexSent[repo] = make(map[string][2]int64)
|
||||
c.idxSent[repo] = make(map[string]uint64)
|
||||
for _, f := range idx {
|
||||
c.indexSent[repo][f.Name] = [2]int64{f.Modified, int64(f.Version)}
|
||||
c.idxSent[repo][f.Name] = f.Version
|
||||
}
|
||||
} else {
|
||||
// We have sent one full index. Only send updates now.
|
||||
msgType = messageTypeIndexUpdate
|
||||
var diff []FileInfo
|
||||
for _, f := range idx {
|
||||
if vs, ok := c.indexSent[repo][f.Name]; !ok || f.Modified != vs[0] || int64(f.Version) != vs[1] {
|
||||
if vs, ok := c.idxSent[repo][f.Name]; !ok || f.Version != vs {
|
||||
diff = append(diff, f)
|
||||
c.indexSent[repo][f.Name] = [2]int64{f.Modified, int64(f.Version)}
|
||||
c.idxSent[repo][f.Name] = f.Version
|
||||
}
|
||||
}
|
||||
idx = diff
|
||||
}
|
||||
c.imut.Unlock()
|
||||
|
||||
c.send(header{0, -1, msgType}, IndexMessage{repo, idx})
|
||||
if msgType == messageTypeIndex || len(idx) > 0 {
|
||||
c.send(header{0, -1, msgType}, IndexMessage{repo, idx})
|
||||
}
|
||||
}
|
||||
|
||||
// Request returns the bytes for the specified block after fetching them from the connected peer.
|
||||
@@ -178,13 +202,13 @@ func (c *rawConnection) Request(repo string, name string, offset int64, size int
|
||||
return nil, ErrClosed
|
||||
}
|
||||
|
||||
c.imut.Lock()
|
||||
c.awaitingMut.Lock()
|
||||
if ch := c.awaiting[id]; ch != nil {
|
||||
panic("id taken")
|
||||
}
|
||||
rc := make(chan asyncResult)
|
||||
rc := make(chan asyncResult, 1)
|
||||
c.awaiting[id] = rc
|
||||
c.imut.Unlock()
|
||||
c.awaitingMut.Unlock()
|
||||
|
||||
ok := c.send(header{0, id, messageTypeRequest},
|
||||
RequestMessage{repo, name, uint64(offset), uint32(size)})
|
||||
@@ -213,9 +237,9 @@ func (c *rawConnection) ping() bool {
|
||||
}
|
||||
|
||||
rc := make(chan asyncResult, 1)
|
||||
c.imut.Lock()
|
||||
c.awaitingMut.Lock()
|
||||
c.awaiting[id] = rc
|
||||
c.imut.Unlock()
|
||||
c.awaitingMut.Unlock()
|
||||
|
||||
ok := c.send(header{0, id, messageTypePing})
|
||||
if !ok {
|
||||
@@ -249,21 +273,34 @@ func (c *rawConnection) readerLoop() (err error) {
|
||||
|
||||
switch hdr.msgType {
|
||||
case messageTypeIndex:
|
||||
if c.state < stateCCRcvd {
|
||||
return fmt.Errorf("protocol error: index message in state %d", c.state)
|
||||
}
|
||||
if err := c.handleIndex(); err != nil {
|
||||
return err
|
||||
}
|
||||
c.state = stateIdxRcvd
|
||||
|
||||
case messageTypeIndexUpdate:
|
||||
if c.state < stateIdxRcvd {
|
||||
return fmt.Errorf("protocol error: index update message in state %d", c.state)
|
||||
}
|
||||
if err := c.handleIndexUpdate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case messageTypeRequest:
|
||||
if c.state < stateIdxRcvd {
|
||||
return fmt.Errorf("protocol error: request message in state %d", c.state)
|
||||
}
|
||||
if err := c.handleRequest(hdr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case messageTypeResponse:
|
||||
if c.state < stateIdxRcvd {
|
||||
return fmt.Errorf("protocol error: response message in state %d", c.state)
|
||||
}
|
||||
if err := c.handleResponse(hdr); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -275,9 +312,13 @@ func (c *rawConnection) readerLoop() (err error) {
|
||||
c.handlePong(hdr)
|
||||
|
||||
case messageTypeClusterConfig:
|
||||
if c.state != stateInitial {
|
||||
return fmt.Errorf("protocol error: cluster config message in state %d", c.state)
|
||||
}
|
||||
if err := c.handleClusterConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
c.state = stateCCRcvd
|
||||
|
||||
default:
|
||||
return fmt.Errorf("protocol error: %s: unknown message type %#x", c.id, hdr.msgType)
|
||||
@@ -285,6 +326,27 @@ func (c *rawConnection) readerLoop() (err error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *rawConnection) indexSerializerLoop() {
|
||||
// We must avoid blocking the reader loop when processing large indexes.
|
||||
// There is otherwise a potential deadlock where both sides has the model
|
||||
// locked because it's sending a large index update and can't receive the
|
||||
// large index update from the other side. But we must also ensure to
|
||||
// process the indexes in the order they are received, hence the separate
|
||||
// routine and buffered channel.
|
||||
for {
|
||||
select {
|
||||
case ii := <-c.incomingIndexes:
|
||||
if ii.update {
|
||||
c.receiver.IndexUpdate(ii.id, ii.repo, ii.files)
|
||||
} else {
|
||||
c.receiver.Index(ii.id, ii.repo, ii.files)
|
||||
}
|
||||
case <-c.closed:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *rawConnection) handleIndex() error {
|
||||
var im IndexMessage
|
||||
im.decodeXDR(c.xr)
|
||||
@@ -299,7 +361,7 @@ func (c *rawConnection) handleIndex() error {
|
||||
// update and can't receive the large index update from the
|
||||
// other side.
|
||||
|
||||
go c.receiver.Index(c.id, im.Repository, im.Files)
|
||||
c.incomingIndexes <- incomingIndex{false, c.id, im.Repository, im.Files}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -310,7 +372,7 @@ func (c *rawConnection) handleIndexUpdate() error {
|
||||
if err := c.xr.Error(); err != nil {
|
||||
return err
|
||||
} else {
|
||||
go c.receiver.IndexUpdate(c.id, im.Repository, im.Files)
|
||||
c.incomingIndexes <- incomingIndex{true, c.id, im.Repository, im.Files}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -332,32 +394,25 @@ func (c *rawConnection) handleResponse(hdr header) error {
|
||||
return err
|
||||
}
|
||||
|
||||
go func(hdr header, err error) {
|
||||
c.imut.Lock()
|
||||
rc := c.awaiting[hdr.msgID]
|
||||
c.awaitingMut.Lock()
|
||||
if rc := c.awaiting[hdr.msgID]; rc != nil {
|
||||
c.awaiting[hdr.msgID] = nil
|
||||
c.imut.Unlock()
|
||||
|
||||
if rc != nil {
|
||||
rc <- asyncResult{data, err}
|
||||
close(rc)
|
||||
}
|
||||
}(hdr, c.xr.Error())
|
||||
rc <- asyncResult{data, nil}
|
||||
close(rc)
|
||||
}
|
||||
c.awaitingMut.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *rawConnection) handlePong(hdr header) {
|
||||
c.imut.Lock()
|
||||
c.awaitingMut.Lock()
|
||||
if rc := c.awaiting[hdr.msgID]; rc != nil {
|
||||
go func() {
|
||||
rc <- asyncResult{}
|
||||
close(rc)
|
||||
}()
|
||||
|
||||
c.awaiting[hdr.msgID] = nil
|
||||
rc <- asyncResult{}
|
||||
close(rc)
|
||||
}
|
||||
c.imut.Unlock()
|
||||
c.awaitingMut.Unlock()
|
||||
}
|
||||
|
||||
func (c *rawConnection) handleClusterConfig() error {
|
||||
@@ -401,18 +456,20 @@ func (c *rawConnection) send(h header, es ...encodable) bool {
|
||||
|
||||
func (c *rawConnection) writerLoop() {
|
||||
var err error
|
||||
for es := range c.outbox {
|
||||
c.wmut.Lock()
|
||||
for _, e := range es {
|
||||
e.encodeXDR(c.xw)
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case es := <-c.outbox:
|
||||
for _, e := range es {
|
||||
e.encodeXDR(c.xw)
|
||||
}
|
||||
|
||||
if err = c.flush(); err != nil {
|
||||
c.wmut.Unlock()
|
||||
c.close(err)
|
||||
if err = c.flush(); err != nil {
|
||||
c.close(err)
|
||||
return
|
||||
}
|
||||
case <-c.closed:
|
||||
return
|
||||
}
|
||||
c.wmut.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -437,29 +494,20 @@ func (c *rawConnection) flush() error {
|
||||
}
|
||||
|
||||
func (c *rawConnection) close(err error) {
|
||||
c.imut.Lock()
|
||||
c.wmut.Lock()
|
||||
defer c.imut.Unlock()
|
||||
defer c.wmut.Unlock()
|
||||
|
||||
select {
|
||||
case <-c.closed:
|
||||
return
|
||||
default:
|
||||
c.once.Do(func() {
|
||||
close(c.closed)
|
||||
|
||||
c.awaitingMut.Lock()
|
||||
for i, ch := range c.awaiting {
|
||||
if ch != nil {
|
||||
close(ch)
|
||||
c.awaiting[i] = nil
|
||||
}
|
||||
}
|
||||
|
||||
c.writer.Close()
|
||||
c.reader.Close()
|
||||
c.awaitingMut.Unlock()
|
||||
|
||||
go c.receiver.Close(c.id, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (c *rawConnection) idGenerator() {
|
||||
@@ -521,8 +569,7 @@ func (c *rawConnection) pingerLoop() {
|
||||
func (c *rawConnection) processRequest(msgID int, req RequestMessage) {
|
||||
data, _ := c.receiver.Request(c.id, req.Repository, req.Name, int64(req.Offset), int(req.Size))
|
||||
|
||||
c.send(header{0, msgID, messageTypeResponse},
|
||||
encodableBytes(data))
|
||||
c.send(header{0, msgID, messageTypeResponse}, encodableBytes(data))
|
||||
}
|
||||
|
||||
type Statistics struct {
|
||||
|
||||
@@ -25,6 +25,31 @@ func TestHeaderFunctions(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeaderLayout(t *testing.T) {
|
||||
var e, a uint32
|
||||
|
||||
// Version are the first four bits
|
||||
e = 0xf0000000
|
||||
a = encodeHeader(header{0xf, 0, 0})
|
||||
if a != e {
|
||||
t.Errorf("Header layout incorrect; %08x != %08x", a, e)
|
||||
}
|
||||
|
||||
// Message ID are the following 12 bits
|
||||
e = 0x0fff0000
|
||||
a = encodeHeader(header{0, 0xfff, 0})
|
||||
if a != e {
|
||||
t.Errorf("Header layout incorrect; %08x != %08x", a, e)
|
||||
}
|
||||
|
||||
// Type are the last 8 bits before reserved
|
||||
e = 0x0000ff00
|
||||
a = encodeHeader(header{0, 0, 0xff})
|
||||
if a != e {
|
||||
t.Errorf("Header layout incorrect; %08x != %08x", a, e)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPing(t *testing.T) {
|
||||
ar, aw := io.Pipe()
|
||||
br, bw := io.Pipe()
|
||||
|
||||
@@ -7,12 +7,14 @@ package scanner
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
"code.google.com/p/go.text/unicode/norm"
|
||||
|
||||
"github.com/calmh/syncthing/lamport"
|
||||
"github.com/calmh/syncthing/protocol"
|
||||
@@ -103,13 +105,14 @@ func (w *Walker) loadIgnoreFiles(dir string, ign map[string][]string) filepath.W
|
||||
}
|
||||
|
||||
if pn, sn := filepath.Split(rn); sn == w.IgnoreFile {
|
||||
pn := strings.Trim(pn, "/")
|
||||
pn := filepath.Clean(pn)
|
||||
bs, _ := ioutil.ReadFile(p)
|
||||
lines := bytes.Split(bs, []byte("\n"))
|
||||
var patterns []string
|
||||
for _, line := range lines {
|
||||
if len(line) > 0 {
|
||||
patterns = append(patterns, string(line))
|
||||
lineStr := strings.TrimSpace(string(line))
|
||||
if len(lineStr) > 0 {
|
||||
patterns = append(patterns, lineStr)
|
||||
}
|
||||
}
|
||||
ign[pn] = patterns
|
||||
@@ -159,6 +162,11 @@ func (w *Walker) walkAndHashFiles(res *[]File, ign map[string][]string) filepath
|
||||
return nil
|
||||
}
|
||||
|
||||
if (runtime.GOOS == "linux" || runtime.GOOS == "windows") && !norm.NFC.IsNormalString(rn) {
|
||||
l.Warnf("File %q contains non-NFC UTF-8 sequences and cannot be synced. Consider renaming.", rn)
|
||||
return nil
|
||||
}
|
||||
|
||||
if info.Mode().IsDir() {
|
||||
if w.CurrentFiler != nil {
|
||||
cf := w.CurrentFiler.CurrentFile(rn)
|
||||
@@ -276,7 +284,7 @@ func (w *Walker) cleanTempFile(path string, info os.FileInfo, err error) error {
|
||||
func (w *Walker) ignoreFile(patterns map[string][]string, file string) bool {
|
||||
first, last := filepath.Split(file)
|
||||
for prefix, pats := range patterns {
|
||||
if len(prefix) == 0 || prefix == first || strings.HasPrefix(first, prefix+"/") {
|
||||
if prefix == "." || prefix == first || strings.HasPrefix(first, fmt.Sprintf("%s%c", prefix, os.PathSeparator)) {
|
||||
for _, pattern := range pats {
|
||||
if match, _ := filepath.Match(pattern, last); match {
|
||||
return true
|
||||
|
||||
@@ -22,7 +22,7 @@ var testdata = []struct {
|
||||
}
|
||||
|
||||
var correctIgnores = map[string][]string{
|
||||
"": {".*", "quux"},
|
||||
".": {".*", "quux"},
|
||||
}
|
||||
|
||||
func TestWalk(t *testing.T) {
|
||||
@@ -88,7 +88,7 @@ func TestWalkError(t *testing.T) {
|
||||
|
||||
func TestIgnore(t *testing.T) {
|
||||
var patterns = map[string][]string{
|
||||
"": {"t2"},
|
||||
".": {"t2"},
|
||||
"foo": {"bar", "z*"},
|
||||
"foo/baz": {"quux", ".*"},
|
||||
}
|
||||
@@ -97,6 +97,7 @@ func TestIgnore(t *testing.T) {
|
||||
r bool
|
||||
}{
|
||||
{"foo/bar", true},
|
||||
{"foofoo", false},
|
||||
{"foo/quux", false},
|
||||
{"foo/zuux", true},
|
||||
{"foo/qzuux", false},
|
||||
|
||||
22
upnp/upnp.go
22
upnp/upnp.go
@@ -203,14 +203,26 @@ func getServiceURL(rootURL string) (string, error) {
|
||||
}
|
||||
|
||||
u, _ := url.Parse(rootURL)
|
||||
if svc.ControlURL[0] == '/' {
|
||||
u.Path = svc.ControlURL
|
||||
} else {
|
||||
u.Path += svc.ControlURL
|
||||
}
|
||||
replaceRawPath(u, svc.ControlURL)
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
func replaceRawPath(u *url.URL, rp string) {
|
||||
var p, q string
|
||||
fs := strings.Split(rp, "?")
|
||||
p = fs[0]
|
||||
if len(fs) > 1 {
|
||||
q = fs[1]
|
||||
}
|
||||
|
||||
if p[0] == '/' {
|
||||
u.Path = p
|
||||
} else {
|
||||
u.Path += p
|
||||
}
|
||||
u.RawQuery = q
|
||||
}
|
||||
|
||||
func soapRequest(url, function, message string) error {
|
||||
tpl := `<?xml version="1.0" ?>
|
||||
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
|
||||
|
||||
Reference in New Issue
Block a user