diff --git a/model/blocks.go b/blocks.go
similarity index 99%
rename from model/blocks.go
rename to blocks.go
index 57b96e4f..7f2c9081 100644
--- a/model/blocks.go
+++ b/blocks.go
@@ -1,4 +1,4 @@
-package model
+package main
import (
"bytes"
diff --git a/model/blocks_test.go b/blocks_test.go
similarity index 99%
rename from model/blocks_test.go
rename to blocks_test.go
index a3fcd437..be791493 100644
--- a/model/blocks_test.go
+++ b/blocks_test.go
@@ -1,4 +1,4 @@
-package model
+package main
import (
"bytes"
diff --git a/model/filemonitor.go b/filemonitor.go
similarity index 99%
rename from model/filemonitor.go
rename to filemonitor.go
index 76ac2cab..2aa7df8e 100644
--- a/model/filemonitor.go
+++ b/filemonitor.go
@@ -1,4 +1,4 @@
-package model
+package main
import (
"bytes"
diff --git a/model/filequeue.go b/filequeue.go
similarity index 99%
rename from model/filequeue.go
rename to filequeue.go
index 77a217d5..5835de32 100644
--- a/model/filequeue.go
+++ b/filequeue.go
@@ -1,4 +1,4 @@
-package model
+package main
import (
"log"
diff --git a/model/filequeue_test.go b/filequeue_test.go
similarity index 99%
rename from model/filequeue_test.go
rename to filequeue_test.go
index 5f867029..289c69a9 100644
--- a/model/filequeue_test.go
+++ b/filequeue_test.go
@@ -1,4 +1,4 @@
-package model
+package main
import (
"reflect"
diff --git a/gui.go b/gui.go
index f7fc6751..a584f886 100644
--- a/gui.go
+++ b/gui.go
@@ -2,18 +2,28 @@ package main
import (
"encoding/json"
+ "io/ioutil"
"log"
"net/http"
"runtime"
"sync"
+ "time"
- "github.com/calmh/syncthing/model"
"github.com/codegangsta/martini"
)
-var configInSync = true
+type guiError struct {
+ Time time.Time
+ Error string
+}
-func startGUI(addr string, m *model.Model) {
+var (
+ configInSync = true
+ guiErrors = []guiError{}
+ guiErrorsMut sync.Mutex
+)
+
+func startGUI(addr string, m *Model) {
router := martini.NewRouter()
router.Get("/", getRoot)
router.Get("/rest/version", restGetVersion)
@@ -23,9 +33,11 @@ func startGUI(addr string, m *model.Model) {
router.Get("/rest/config/sync", restGetConfigInSync)
router.Get("/rest/need", restGetNeed)
router.Get("/rest/system", restGetSystem)
+ router.Get("/rest/errors", restGetErrors)
router.Post("/rest/config", restPostConfig)
router.Post("/rest/restart", restPostRestart)
+ router.Post("/rest/error", restPostError)
go func() {
mr := martini.New()
@@ -48,7 +60,7 @@ func restGetVersion() string {
return Version
}
-func restGetModel(m *model.Model, w http.ResponseWriter) {
+func restGetModel(m *Model, w http.ResponseWriter) {
var res = make(map[string]interface{})
globalFiles, globalDeleted, globalBytes := m.GlobalSize()
@@ -67,7 +79,7 @@ func restGetModel(m *model.Model, w http.ResponseWriter) {
json.NewEncoder(w).Encode(res)
}
-func restGetConnections(m *model.Model, w http.ResponseWriter) {
+func restGetConnections(m *Model, w http.ResponseWriter) {
var res = m.ConnectionStats()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(res)
@@ -95,7 +107,7 @@ func restPostRestart(req *http.Request) {
restart()
}
-type guiFile model.File
+type guiFile File
func (f guiFile) MarshalJSON() ([]byte, error) {
type t struct {
@@ -104,11 +116,11 @@ func (f guiFile) MarshalJSON() ([]byte, error) {
}
return json.Marshal(t{
Name: f.Name,
- Size: model.File(f).Size(),
+ Size: File(f).Size(),
})
}
-func restGetNeed(m *model.Model, w http.ResponseWriter) {
+func restGetNeed(m *Model, w http.ResponseWriter) {
files, _ := m.NeedFiles()
gfs := make([]guiFile, len(files))
for i, f := range files {
@@ -137,3 +149,24 @@ func restGetSystem(w http.ResponseWriter) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(res)
}
+
+func restGetErrors(w http.ResponseWriter) {
+ guiErrorsMut.Lock()
+ json.NewEncoder(w).Encode(guiErrors)
+ guiErrorsMut.Unlock()
+}
+
+func restPostError(req *http.Request) {
+ bs, _ := ioutil.ReadAll(req.Body)
+ req.Body.Close()
+ showGuiError(string(bs))
+}
+
+func showGuiError(err string) {
+ guiErrorsMut.Lock()
+ guiErrors = append(guiErrors, guiError{time.Now(), err})
+ if len(guiErrors) > 5 {
+ guiErrors = guiErrors[len(guiErrors)-5:]
+ }
+ guiErrorsMut.Unlock()
+}
diff --git a/gui/app.js b/gui/app.js
index 12795ac8..7cab3a08 100644
--- a/gui/app.js
+++ b/gui/app.js
@@ -14,6 +14,8 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
$scope.myID = '';
$scope.nodes = [];
$scope.configInSync = true;
+ $scope.errors = [];
+ $scope.seenError = '';
// Strings before bools look better
$scope.settings = [
@@ -131,6 +133,9 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
});
$scope.need = data;
});
+ $http.get('/rest/errors').success(function (data) {
+ $scope.errors = data;
+ });
};
$scope.nodeIcon = function (nodeCfg) {
@@ -234,6 +239,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
$scope.nodes = newNodes;
$scope.config.Repositories[0].Nodes = newNodes;
+ $scope.configInSync = false;
$http.post('/rest/config', JSON.stringify($scope.config), {headers: {'Content-Type': 'application/json'}});
};
@@ -287,6 +293,29 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
}
};
+ $scope.errorList = function () {
+ var errors = [];
+ for (var i = 0; i < $scope.errors.length; i++) {
+ var e = $scope.errors[i];
+ if (e.Time > $scope.seenError) {
+ errors.push(e);
+ }
+ }
+ return errors;
+ };
+
+ $scope.clearErrors = function () {
+ $scope.seenError = $scope.errors[$scope.errors.length - 1].Time;
+ };
+
+ $scope.friendlyNodes = function (str) {
+ for (var i = 0; i < $scope.nodes.length; i++) {
+ var cfg = $scope.nodes[i];
+ str = str.replace(cfg.NodeID, $scope.nodeName(cfg));
+ }
+ return str;
+ };
+
$scope.refresh();
setInterval($scope.refresh, 10000);
});
diff --git a/gui/index.html b/gui/index.html
index 4dac829b..350d8c45 100644
--- a/gui/index.html
+++ b/gui/index.html
@@ -55,6 +55,12 @@ thead tr th {
+
+
{{err.Time | date:"hh:mm:ss.sss"}}: {{friendlyNodes(err.Error)}}
+
+
+
+
Cluster
diff --git a/logger.go b/logger.go
index 1608b3bc..ee61bb5b 100644
--- a/logger.go
+++ b/logger.go
@@ -41,11 +41,13 @@ func okf(format string, vals ...interface{}) {
func warnln(vals ...interface{}) {
s := fmt.Sprintln(vals...)
+ showGuiError(s)
logger.Output(2, "WARNING: "+s)
}
func warnf(format string, vals ...interface{}) {
s := fmt.Sprintf(format, vals...)
+ showGuiError(s)
logger.Output(2, "WARNING: "+s)
}
diff --git a/main.go b/main.go
index fdf6de44..876d023f 100644
--- a/main.go
+++ b/main.go
@@ -19,7 +19,6 @@ import (
"github.com/calmh/ini"
"github.com/calmh/syncthing/discover"
- "github.com/calmh/syncthing/model"
"github.com/calmh/syncthing/protocol"
)
@@ -181,7 +180,7 @@ func main() {
}
ensureDir(dir, -1)
- m := model.NewModel(dir, cfg.Options.MaxChangeKbps*1000)
+ m := NewModel(dir, cfg.Options.MaxChangeKbps*1000)
for _, t := range strings.Split(trace, ",") {
m.Trace(t)
}
@@ -250,7 +249,7 @@ func main() {
okln("Ready to synchronize (read only; no external updates accepted)")
}
- // Periodically scan the repository and update the local model.
+ // Periodically scan the repository and update the local
// XXX: Should use some fsnotify mechanism.
go func() {
td := time.Duration(cfg.Options.RescanIntervalS) * time.Second
@@ -328,9 +327,9 @@ func saveConfig() {
saveConfigCh <- struct{}{}
}
-func printStatsLoop(m *model.Model) {
+func printStatsLoop(m *Model) {
var lastUpdated int64
- var lastStats = make(map[string]model.ConnectionInfo)
+ var lastStats = make(map[string]ConnectionInfo)
for {
time.Sleep(60 * time.Second)
@@ -359,7 +358,7 @@ func printStatsLoop(m *model.Model) {
}
}
-func listen(myID string, addr string, m *model.Model, tlsCfg *tls.Config, connOpts map[string]string) {
+func listen(myID string, addr string, m *Model, tlsCfg *tls.Config, connOpts map[string]string) {
if strings.Contains(trace, "connect") {
debugln("NET: Listening on", addr)
}
@@ -435,7 +434,7 @@ func discovery(addr string) *discover.Discoverer {
return disc
}
-func connect(myID string, disc *discover.Discoverer, m *model.Model, tlsCfg *tls.Config, connOpts map[string]string) {
+func connect(myID string, disc *discover.Discoverer, m *Model, tlsCfg *tls.Config, connOpts map[string]string) {
for {
nextNode:
for _, nodeCfg := range cfg.Repositories[0].Nodes {
@@ -484,13 +483,13 @@ func connect(myID string, disc *discover.Discoverer, m *model.Model, tlsCfg *tls
}
}
-func updateLocalModel(m *model.Model) {
+func updateLocalModel(m *Model) {
files, _ := m.Walk(cfg.Options.FollowSymlinks)
m.ReplaceLocal(files)
saveIndex(m)
}
-func saveIndex(m *model.Model) {
+func saveIndex(m *Model) {
name := m.RepoID() + ".idx.gz"
fullName := path.Join(confDir, name)
idxf, err := os.Create(fullName + ".tmp")
@@ -506,7 +505,7 @@ func saveIndex(m *model.Model) {
os.Rename(fullName+".tmp", fullName)
}
-func loadIndex(m *model.Model) {
+func loadIndex(m *Model) {
name := m.RepoID() + ".idx.gz"
idxf, err := os.Open(path.Join(confDir, name))
if err != nil {
diff --git a/model/model.go b/model.go
similarity index 92%
rename from model/model.go
rename to model.go
index 65bbb7c1..059200eb 100644
--- a/model/model.go
+++ b/model.go
@@ -1,11 +1,10 @@
-package model
+package main
import (
"crypto/sha1"
"errors"
"fmt"
"io"
- "log"
"net"
"os"
"path"
@@ -268,7 +267,7 @@ func (m *Model) Index(nodeID string, fs []protocol.FileInfo) {
defer m.imut.Unlock()
if m.trace["net"] {
- log.Printf("DEBUG: NET IDX(in): %s: %d files", nodeID, len(fs))
+ debugf("NET IDX(in): %s: %d files", nodeID, len(fs))
}
repo := make(map[string]File)
@@ -296,13 +295,13 @@ func (m *Model) IndexUpdate(nodeID string, fs []protocol.FileInfo) {
defer m.imut.Unlock()
if m.trace["net"] {
- log.Printf("DEBUG: NET IDXUP(in): %s: %d files", nodeID, len(files))
+ debugf("NET IDXUP(in): %s: %d files", nodeID, len(files))
}
m.rmut.Lock()
repo, ok := m.remote[nodeID]
if !ok {
- log.Printf("WARNING: Index update from node %s that does not have an index", nodeID)
+ warnf("Index update from node %s that does not have an index", nodeID)
m.rmut.Unlock()
return
}
@@ -322,11 +321,11 @@ func (m *Model) indexUpdate(repo map[string]File, f File) {
if f.Flags&protocol.FlagDeleted != 0 {
flagComment = " (deleted)"
}
- log.Printf("DEBUG: IDX(in): %q m=%d f=%o%s v=%d (%d blocks)", f.Name, f.Modified, f.Flags, flagComment, f.Version, len(f.Blocks))
+ debugf("IDX(in): %q m=%d f=%o%s v=%d (%d blocks)", f.Name, f.Modified, f.Flags, flagComment, f.Version, len(f.Blocks))
}
if extraFlags := f.Flags &^ (protocol.FlagInvalid | protocol.FlagDeleted | 0xfff); extraFlags != 0 {
- log.Printf("WARNING: IDX(in): Unknown flags 0x%x in index record %+v", extraFlags, f)
+ warnf("IDX(in): Unknown flags 0x%x in index record %+v", extraFlags, f)
return
}
@@ -337,10 +336,10 @@ func (m *Model) indexUpdate(repo map[string]File, f File) {
// Implements the protocol.Model interface.
func (m *Model) Close(node string, err error) {
if m.trace["net"] {
- log.Printf("DEBUG: NET: %s: %v", node, err)
+ debugf("NET: %s: %v", node, err)
}
if err == protocol.ErrClusterHash {
- log.Printf("WARNING: Connection to %s closed due to mismatched cluster hash. Ensure that the configured cluster members are identical on both nodes.", node)
+ warnf("Connection to %s closed due to mismatched cluster hash. Ensure that the configured cluster members are identical on both nodes.", node)
}
m.fq.RemoveAvailable(node)
@@ -377,7 +376,7 @@ func (m *Model) Request(nodeID, name string, offset int64, size uint32, hash []b
m.gmut.RUnlock()
if !localOk || !globalOk {
- log.Printf("SECURITY (nonexistent file) REQ(in): %s: %q o=%d s=%d h=%x", nodeID, name, offset, size, hash)
+ warnf("SECURITY (nonexistent file) REQ(in): %s: %q o=%d s=%d h=%x", nodeID, name, offset, size, hash)
return nil, ErrNoSuchFile
}
if lf.Flags&protocol.FlagInvalid != 0 {
@@ -385,7 +384,7 @@ func (m *Model) Request(nodeID, name string, offset int64, size uint32, hash []b
}
if m.trace["net"] && nodeID != "" {
- log.Printf("DEBUG: NET REQ(in): %s: %q o=%d s=%d h=%x", nodeID, name, offset, size, hash)
+ debugf("NET REQ(in): %s: %q o=%d s=%d h=%x", nodeID, name, offset, size, hash)
}
fn := path.Join(m.dir, name)
fd, err := os.Open(fn) // XXX: Inefficient, should cache fd?
@@ -502,13 +501,13 @@ func (m *Model) AddConnection(rawConn io.Closer, protoConn Connection) {
i := i
go func() {
if m.trace["pull"] {
- log.Println("DEBUG: PULL: Starting", nodeID, i)
+ debugln("PULL: Starting", nodeID, i)
}
for {
m.pmut.RLock()
if _, ok := m.protoConn[nodeID]; !ok {
if m.trace["pull"] {
- log.Println("DEBUG: PULL: Exiting", nodeID, i)
+ debugln("PULL: Exiting", nodeID, i)
}
m.pmut.RUnlock()
return
@@ -518,7 +517,7 @@ func (m *Model) AddConnection(rawConn io.Closer, protoConn Connection) {
qb, ok := m.fq.Get(nodeID)
if ok {
if m.trace["pull"] {
- log.Println("DEBUG: PULL: Request", nodeID, i, qb.name, qb.block.Offset)
+ debugln("PULL: Request", nodeID, i, qb.name, qb.block.Offset)
}
data, _ := protoConn.Request(qb.name, qb.block.Offset, qb.block.Size, qb.block.Hash)
m.fq.Done(qb.name, qb.block.Offset, data)
@@ -544,7 +543,7 @@ func (m *Model) ProtocolIndex() []protocol.FileInfo {
if mf.Flags&protocol.FlagDeleted != 0 {
flagComment = " (deleted)"
}
- log.Printf("DEBUG: IDX(out): %q m=%d f=%o%s v=%d (%d blocks)", mf.Name, mf.Modified, mf.Flags, flagComment, mf.Version, len(mf.Blocks))
+ debugf("IDX(out): %q m=%d f=%o%s v=%d (%d blocks)", mf.Name, mf.Modified, mf.Flags, flagComment, mf.Version, len(mf.Blocks))
}
index = append(index, mf)
}
@@ -563,7 +562,7 @@ func (m *Model) requestGlobal(nodeID, name string, offset int64, size uint32, ha
}
if m.trace["net"] {
- log.Printf("DEBUG: NET REQ(out): %s: %q o=%d s=%d h=%x", nodeID, name, offset, size, hash)
+ debugf("NET REQ(out): %s: %q o=%d s=%d h=%x", nodeID, name, offset, size, hash)
}
return nc.Request(name, offset, size, hash)
@@ -591,7 +590,7 @@ func (m *Model) broadcastIndexLoop() {
for _, node := range m.protoConn {
node := node
if m.trace["net"] {
- log.Printf("DEBUG: NET IDX(out/loop): %s: %d files", node.ID(), len(idx))
+ debugf("NET IDX(out/loop): %s: %d files", node.ID(), len(idx))
}
go func() {
node.Index(idx)
@@ -803,7 +802,7 @@ func (m *Model) recomputeNeedForFile(gf File, toAdd []addOrder, toDelete []File)
return toAdd, toDelete
}
if m.trace["need"] {
- log.Printf("DEBUG: NEED: lf:%v gf:%v", lf, gf)
+ debugf("NEED: lf:%v gf:%v", lf, gf)
}
if gf.Flags&protocol.FlagDeleted != 0 {
@@ -845,12 +844,12 @@ func (m *Model) WhoHas(name string) []string {
func (m *Model) deleteLoop() {
for file := range m.dq {
if m.trace["file"] {
- log.Println("DEBUG: FILE: Delete", file.Name)
+ debugln("FILE: Delete", file.Name)
}
path := path.Clean(path.Join(m.dir, file.Name))
err := os.Remove(path)
if err != nil {
- log.Printf("WARNING: %s: %v", file.Name, err)
+ warnf("%s: %v", file.Name, err)
}
m.updateLocal(file)
diff --git a/model/model_test.go b/model_test.go
similarity index 99%
rename from model/model_test.go
rename to model_test.go
index a61f3ec5..ef30b59d 100644
--- a/model/model_test.go
+++ b/model_test.go
@@ -1,4 +1,4 @@
-package model
+package main
import (
"bytes"
diff --git a/model/suppressor.go b/suppressor.go
similarity index 98%
rename from model/suppressor.go
rename to suppressor.go
index 0eb43963..5690207f 100644
--- a/model/suppressor.go
+++ b/suppressor.go
@@ -1,4 +1,4 @@
-package model
+package main
import (
"sync"
diff --git a/model/suppressor_test.go b/suppressor_test.go
similarity index 99%
rename from model/suppressor_test.go
rename to suppressor_test.go
index 58fc370f..26cc5e3b 100644
--- a/model/suppressor_test.go
+++ b/suppressor_test.go
@@ -1,4 +1,4 @@
-package model
+package main
import (
"testing"
diff --git a/model/testdata/.stignore b/testdata/.stignore
similarity index 100%
rename from model/testdata/.stignore
rename to testdata/.stignore
diff --git a/model/testdata/bar b/testdata/bar
similarity index 100%
rename from model/testdata/bar
rename to testdata/bar
diff --git a/model/testdata/baz/quux b/testdata/baz/quux
similarity index 100%
rename from model/testdata/baz/quux
rename to testdata/baz/quux
diff --git a/model/testdata/empty b/testdata/empty
similarity index 100%
rename from model/testdata/empty
rename to testdata/empty
diff --git a/model/testdata/foo b/testdata/foo
similarity index 100%
rename from model/testdata/foo
rename to testdata/foo
diff --git a/model/walk.go b/walk.go
similarity index 99%
rename from model/walk.go
rename to walk.go
index 1255bf40..23cd985c 100644
--- a/model/walk.go
+++ b/walk.go
@@ -1,4 +1,4 @@
-package model
+package main
import (
"bytes"
diff --git a/model/walk_test.go b/walk_test.go
similarity index 99%
rename from model/walk_test.go
rename to walk_test.go
index 26353946..43707e50 100644
--- a/model/walk_test.go
+++ b/walk_test.go
@@ -1,4 +1,4 @@
-package model
+package main
import (
"fmt"