Compare commits

...

13 Commits

Author SHA1 Message Date
Jakob Borg
725f748b17 Find syncthing binary in $PATH when restarting (fixes #68) 2014-02-17 08:50:55 +01:00
Jakob Borg
f3a793ce91 Add peer node sync status in GUI (fixes #46) 2014-02-16 08:30:32 +01:00
Jakob Borg
c171780c0d Reorder locking to avoid deadlock (fixes #64) 2014-02-13 12:51:51 +01:00
Jakob Borg
5daf6ecf70 Actually embed GUI changes from 91d5c4a... 2014-02-13 09:27:06 +01:00
Jakob Borg
6c8135126d Initialize logging earlier (fix panic in tests) 2014-02-13 08:59:27 +01:00
Jakob Borg
91d5c4a1ae Show warnings in GUI (fixes #66) 2014-02-12 23:18:41 +01:00
Jakob Borg
2cbe81f1c7 Restart from web gui (fixes #50) 2014-02-12 12:10:44 +01:00
Jakob Borg
a26ce61d92 (Re)Fix locking around deleteFile (fixes #64) 2014-02-11 16:04:55 +01:00
Jakob Borg
478300f6d8 Only show address when connected (fixes #58)
The configured address is visible in the config dialog.
2014-02-11 14:34:47 +01:00
Jakob Borg
3a5b816125 Allow setting a friendly name for the local node (fixes #65) 2014-02-10 20:54:57 +01:00
Jakob Borg
b6814241cc Fix locking around deleteFile (fixes #64) 2014-02-09 23:24:55 +01:00
Jakob Borg
fc6eabea28 Enforce identical member configuration among nodes (fixes #63) 2014-02-09 23:13:06 +01:00
Jakob Borg
14b3791b2b Don't panic for legitimate file errors (fixes #55) 2014-02-09 22:41:30 +01:00
24 changed files with 622 additions and 330 deletions

View File

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,4 @@
package model
package main
import (
"bytes"

View File

@@ -1,4 +1,4 @@
package model
package main
import (
"bytes"

View File

@@ -1,9 +1,12 @@
package main
import (
"crypto/sha256"
"encoding/xml"
"fmt"
"io"
"reflect"
"sort"
"strconv"
"strings"
)
@@ -154,3 +157,46 @@ func readConfigXML(rd io.Reader) (Configuration, error) {
cfg.Options.ListenAddress = uniqueStrings(cfg.Options.ListenAddress)
return cfg, err
}
type NodeConfigurationList []NodeConfiguration
func (l NodeConfigurationList) Less(a, b int) bool {
return l[a].NodeID < l[b].NodeID
}
func (l NodeConfigurationList) Swap(a, b int) {
l[a], l[b] = l[b], l[a]
}
func (l NodeConfigurationList) Len() int {
return len(l)
}
func clusterHash(nodes []NodeConfiguration) string {
sort.Sort(NodeConfigurationList(nodes))
h := sha256.New()
for _, n := range nodes {
h.Write([]byte(n.NodeID))
}
return fmt.Sprintf("%x", h.Sum(nil))
}
func cleanNodeList(nodes []NodeConfiguration, myID string) []NodeConfiguration {
var myIDExists bool
for _, node := range nodes {
if node.NodeID == myID {
myIDExists = true
break
}
}
if !myIDExists {
nodes = append(nodes, NodeConfiguration{
NodeID: myID,
Addresses: []string{"dynamic"},
Name: "",
})
}
sort.Sort(NodeConfigurationList(nodes))
return nodes
}

View File

@@ -1,4 +1,4 @@
package model
package main
import (
"bytes"

View File

@@ -1,4 +1,4 @@
package model
package main
import (
"log"
@@ -184,7 +184,8 @@ func (q *FileQueue) Done(file string, offset int64, data []byte) {
return
}
}
panic("unreachable")
// We found nothing, might have errored out already
}
func (q *FileQueue) QueuedFiles() (files []string) {
@@ -198,7 +199,7 @@ func (q *FileQueue) QueuedFiles() (files []string) {
}
func (q *FileQueue) deleteAt(i int) {
q.files = q.files[:i+copy(q.files[i:], q.files[i+1:])]
q.files = append(q.files[:i], q.files[i+1:]...)
}
func (q *FileQueue) deleteFile(n string) {
@@ -219,8 +220,10 @@ func (q *FileQueue) SetAvailable(file string, nodes []string) {
}
func (q *FileQueue) RemoveAvailable(toRemove string) {
q.fmut.Lock()
q.amut.Lock()
defer q.amut.Unlock()
defer q.fmut.Unlock()
for file, nodes := range q.availability {
for i, node := range nodes {

View File

@@ -1,4 +1,4 @@
package model
package main
import (
"reflect"
@@ -275,3 +275,21 @@ func TestFileQueueThreadHandling(t *testing.T) {
t.Error("Total mismatch; %d != %d", gotTot, total)
}
}
func TestDeleteAt(t *testing.T) {
q := FileQueue{}
for i := 0; i < 4; i++ {
q.files = queuedFileList{{name: "a"}, {name: "b"}, {name: "c"}, {name: "d"}}
q.deleteAt(i)
if l := len(q.files); l != 3 {
t.Fatal("deleteAt(%d) failed; %d != 3", i, l)
}
}
q.files = queuedFileList{{name: "a"}}
q.deleteAt(0)
if l := len(q.files); l != 0 {
t.Fatal("deleteAt(only) failed; %d != 0", l)
}
}

60
gui.go
View File

@@ -2,26 +2,42 @@ package main
import (
"encoding/json"
"io/ioutil"
"log"
"net/http"
"runtime"
"sync"
"time"
"github.com/calmh/syncthing/model"
"github.com/codegangsta/martini"
)
func startGUI(addr string, m *model.Model) {
type guiError struct {
Time time.Time
Error string
}
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)
router.Get("/rest/model", restGetModel)
router.Get("/rest/connections", restGetConnections)
router.Get("/rest/config", restGetConfig)
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()
@@ -44,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()
@@ -63,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)
@@ -79,10 +95,19 @@ func restPostConfig(req *http.Request) {
log.Println(err)
} else {
saveConfig()
configInSync = false
}
}
type guiFile model.File
func restGetConfigInSync(w http.ResponseWriter) {
json.NewEncoder(w).Encode(map[string]bool{"configInSync": configInSync})
}
func restPostRestart(req *http.Request) {
restart()
}
type guiFile File
func (f guiFile) MarshalJSON() ([]byte, error) {
type t struct {
@@ -91,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 {
@@ -124,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()
}

View File

@@ -1,29 +1,37 @@
/*jslint browser: true, continue: true, plusplus: true */
/*global $: false, angular: false */
'use strict';
var syncthing = angular.module('syncthing', []);
syncthing.controller('SyncthingCtrl', function ($scope, $http) {
var prevDate = 0;
var modelGetOK = true;
var prevDate = 0,
modelGetOK = true;
$scope.connections = {};
$scope.config = {};
$scope.myID = "";
$scope.myID = '';
$scope.nodes = [];
$scope.configInSync = true;
$scope.errors = [];
$scope.seenError = '';
// Strings before bools look better
$scope.settings = [
{id: 'ListenStr', descr:"Sync Protocol Listen Addresses", type: 'string', restart: true},
{id: 'GUIAddress', descr: "GUI Listen Address", type: 'string', restart: true},
{id: 'MaxSendKbps', descr: "Outgoing Rate Limit (KBps)", type: 'string', restart: true},
{id: 'RescanIntervalS', descr: "Rescan Interval (s)", type: 'string', restart: true},
{id: 'ReconnectIntervalS', descr: "Reconnect Interval (s)", type: 'string', restart: true},
{id: 'ParallelRequests', descr: "Max Outstanding Requests", type: 'string', restart: true},
{id: 'MaxChangeKbps', descr: "Max File Change Rate (KBps)", type: 'string', restart: true},
{id: 'ListenStr', descr: 'Sync Protocol Listen Addresses', type: 'text', restart: true},
{id: 'GUIAddress', descr: 'GUI Listen Address', type: 'text', restart: true},
{id: 'MaxSendKbps', descr: 'Outgoing Rate Limit (KBps)', 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 (KBps)', type: 'number', restart: true},
{id: 'ReadOnly', descr: "Read Only", type: 'bool', restart: true},
{id: 'AllowDelete', descr: "Allow Delete", type: 'bool', restart: true},
{id: 'FollowSymlinks', descr: "Follow Symlinks", type: 'bool', restart: true},
{id: 'GlobalAnnEnabled', descr: "Global Announce", type: 'bool', restart: true},
{id: 'LocalAnnEnabled', descr: "Local Announce", type: 'bool', restart: true},
{id: 'ReadOnly', descr: 'Read Only', type: 'bool', restart: true},
{id: 'AllowDelete', descr: 'Allow Delete', type: 'bool', restart: true},
{id: 'FollowSymlinks', descr: 'Follow Symlinks', type: 'bool', restart: true},
{id: 'GlobalAnnEnabled', descr: 'Global Announce', type: 'bool', restart: true},
{id: 'LocalAnnEnabled', descr: 'Local Announce', type: 'bool', restart: true},
];
function modelGetSucceeded() {
@@ -40,46 +48,62 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
}
}
$http.get("/rest/version").success(function (data) {
function nodeCompare(a, b) {
if (a.NodeID === $scope.myID) {
return -1;
}
if (b.NodeID === $scope.myID) {
return 1;
}
if (a.NodeID < b.NodeID) {
return -1;
}
return a.NodeID > b.NodeID;
}
$http.get('/rest/version').success(function (data) {
$scope.version = data;
});
$http.get("/rest/system").success(function (data) {
$http.get('/rest/system').success(function (data) {
$scope.system = data;
$scope.myID = data.myID;
$http.get("/rest/config").success(function (data) {
$http.get('/rest/config').success(function (data) {
$scope.config = data;
$scope.config.Options.ListenStr = $scope.config.Options.ListenAddress.join(", ")
$scope.config.Options.ListenStr = $scope.config.Options.ListenAddress.join(', ');
var nodes = $scope.config.Repositories[0].Nodes;
nodes = nodes.filter(function (x) { return x.NodeID != $scope.myID; });
nodes.sort(function (a, b) {
if (a.NodeID < b.NodeID)
return -1;
return a.NodeID > b.NodeID;
})
nodes.sort(nodeCompare);
$scope.nodes = nodes;
});
$http.get('/rest/config/sync').success(function (data) {
$scope.configInSync = data.configInSync;
});
});
$scope.refresh = function () {
$http.get("/rest/system").success(function (data) {
$http.get('/rest/system').success(function (data) {
$scope.system = data;
});
$http.get("/rest/model").success(function (data) {
$http.get('/rest/model').success(function (data) {
$scope.model = data;
modelGetSucceeded();
}).error(function () {
modelGetFailed();
});
$http.get("/rest/connections").success(function (data) {
var now = Date.now();
var td = (now - prevDate) / 1000;
prevDate = now;
$http.get('/rest/connections').success(function (data) {
var now = Date.now(),
td = (now - prevDate) / 1000,
id;
$scope.inbps = 0
$scope.outbps = 0
for (var id in data) {
prevDate = now;
$scope.inbps = 0;
$scope.outbps = 0;
for (id in data) {
if (!data.hasOwnProperty(id)) {
continue;
}
try {
data[id].inbps = Math.max(0, 8 * (data[id].InBytesTotal - $scope.connections[id].InBytesTotal) / td);
data[id].outbps = Math.max(0, 8 * (data[id].OutBytesTotal - $scope.connections[id].OutBytesTotal) / td);
@@ -87,16 +111,16 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
data[id].inbps = 0;
data[id].outbps = 0;
}
$scope.inbps += data[id].outbps;
$scope.outbps += data[id].inbps;
$scope.inbps += data[id].inbps;
$scope.outbps += data[id].outbps;
}
$scope.connections = data;
});
$http.get("/rest/need").success(function (data) {
$http.get('/rest/need').success(function (data) {
var i, name;
for (i = 0; i < data.length; i++) {
name = data[i].Name.split("/");
data[i].ShortName = name[name.length-1];
name = data[i].Name.split('/');
data[i].ShortName = name[name.length - 1];
}
data.sort(function (a, b) {
if (a.ShortName < b.ShortName) {
@@ -109,39 +133,48 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
});
$scope.need = data;
});
};
$scope.nodeIcon = function (nodeCfg) {
if ($scope.connections[nodeCfg.NodeID]) {
return "ok";
}
return "minus";
$http.get('/rest/errors').success(function (data) {
$scope.errors = data;
});
};
$scope.nodeStatus = function (nodeCfg) {
if ($scope.connections[nodeCfg.NodeID]) {
return "Connected";
var conn = $scope.connections[nodeCfg.NodeID];
if (conn) {
if (conn.Completion === 100) {
return 'In Sync';
} else {
return 'Syncing (' + conn.Completion + '%)';
}
}
return "Disconnected";
return 'Disconnected';
};
$scope.nodeIcon = function (nodeCfg) {
if ($scope.connections[nodeCfg.NodeID]) {
return "ok";
var conn = $scope.connections[nodeCfg.NodeID];
if (conn) {
if (conn.Completion === 100) {
return 'ok';
} else {
return 'refresh';
}
}
return "minus";
return 'minus';
};
$scope.nodeClass = function (nodeCfg) {
var conn = $scope.connections[nodeCfg.NodeID];
if (conn) {
return "success";
if (conn.Completion === 100) {
return 'success';
} else {
return 'primary';
}
}
return "info";
return 'info';
};
$scope.nodeAddr = function (nodeCfg) {
@@ -149,7 +182,15 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
if (conn) {
return conn.Address;
}
return nodeCfg.Addresses.join(", ");
return '(unknown address)';
};
$scope.nodeCompletion = function (nodeCfg) {
var conn = $scope.connections[nodeCfg.NodeID];
if (conn) {
return conn.Completion + '%';
}
return '';
};
$scope.nodeVer = function (nodeCfg) {
@@ -160,7 +201,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
if (conn) {
return conn.ClientVersion;
}
return "";
return '(unknown version)';
};
$scope.nodeName = function (nodeCfg) {
@@ -171,69 +212,122 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
};
$scope.saveSettings = function () {
$scope.configInSync = false;
$scope.config.Options.ListenAddress = $scope.config.Options.ListenStr.split(',').map(function (x) { return x.trim(); });
$http.post('/rest/config', JSON.stringify($scope.config), {headers: {'Content-Type': 'application/json'}});
$('#settingsTable').collapse('hide');
};
$scope.restart = function () {
$http.post('/rest/restart');
$scope.configInSync = true;
};
$scope.editNode = function (nodeCfg) {
$scope.currentNode = nodeCfg;
$scope.editingExisting = true;
$scope.currentNode.AddressesStr = nodeCfg.Addresses.join(", ")
$scope.currentNode.AddressesStr = nodeCfg.Addresses.join(', ');
$('#editNode').modal({backdrop: 'static', keyboard: false});
};
$scope.addNode = function () {
$scope.currentNode = {NodeID: "", AddressesStr: "dynamic"};
$scope.currentNode = {NodeID: '', AddressesStr: 'dynamic'};
$scope.editingExisting = false;
$('#editNode').modal({backdrop: 'static', keyboard: false});
};
$scope.deleteNode = function () {
$('#editNode').modal('hide');
if (!$scope.editingExisting)
return;
var newNodes = [], i;
var newNodes = [];
for (var i = 0; i < $scope.nodes.length; i++) {
$('#editNode').modal('hide');
if (!$scope.editingExisting) {
return;
}
for (i = 0; i < $scope.nodes.length; i++) {
if ($scope.nodes[i].NodeID !== $scope.currentNode.NodeID) {
newNodes.push($scope.nodes[i]);
}
}
}
$scope.nodes = newNodes;
$scope.config.Repositories[0].Nodes = newNodes;
$http.post('/rest/config', JSON.stringify($scope.config), {headers: {'Content-Type': 'application/json'}})
}
$scope.configInSync = false;
$http.post('/rest/config', JSON.stringify($scope.config), {headers: {'Content-Type': 'application/json'}});
};
$scope.saveNode = function () {
var nodeCfg, done, i;
$scope.configInSync = false;
$('#editNode').modal('hide');
nodeCfg = $scope.currentNode;
nodeCfg.Addresses = nodeCfg.AddressesStr.split(',').map(function (x) { return x.trim(); });
var done = false;
for (var i = 0; i < $scope.nodes.length; i++) {
done = false;
for (i = 0; i < $scope.nodes.length; i++) {
if ($scope.nodes[i].NodeID === nodeCfg.NodeID) {
$scope.nodes[i] = nodeCfg;
done = true;
break;
}
}
}
if (!done) {
$scope.nodes.push(nodeCfg);
}
$scope.nodes.sort(function (a, b) {
if (a.NodeID < b.NodeID)
return -1;
return a.NodeID > b.NodeID;
})
$scope.nodes.sort(nodeCompare);
$scope.config.Repositories[0].Nodes = $scope.nodes;
$http.post('/rest/config', JSON.stringify($scope.config), {headers: {'Content-Type': 'application/json'}})
$http.post('/rest/config', JSON.stringify($scope.config), {headers: {'Content-Type': 'application/json'}});
};
$scope.otherNodes = function () {
var nodes = [], i, n;
for (i = 0; i < $scope.nodes.length; i++) {
n = $scope.nodes[i];
if (n.NodeID !== $scope.myID) {
nodes.push(n);
}
}
return nodes;
};
$scope.thisNode = function () {
var i, n;
for (i = 0; i < $scope.nodes.length; i++) {
n = $scope.nodes[i];
if (n.NodeID === $scope.myID) {
return [n];
}
}
};
$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();
@@ -241,22 +335,27 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
});
function decimals(val, num) {
if (val === 0) { return 0; }
var digits = Math.floor(Math.log(Math.abs(val))/Math.log(10));
var decimals = Math.max(0, num - digits);
return decimals;
var digits, decs;
if (val === 0) {
return 0;
}
digits = Math.floor(Math.log(Math.abs(val)) / Math.log(10));
decs = Math.max(0, num - digits);
return decs;
}
syncthing.filter('natural', function() {
return function(input, valid) {
syncthing.filter('natural', function () {
return function (input, valid) {
return input.toFixed(decimals(input, valid));
}
};
});
syncthing.filter('binary', function() {
return function(input) {
syncthing.filter('binary', function () {
return function (input) {
if (input === undefined) {
return '0 '
return '0 ';
}
if (input > 1024 * 1024 * 1024) {
input /= 1024 * 1024 * 1024;
@@ -271,13 +370,13 @@ syncthing.filter('binary', function() {
return input.toFixed(decimals(input, 2)) + ' Ki';
}
return Math.round(input) + ' ';
}
};
});
syncthing.filter('metric', function() {
return function(input) {
syncthing.filter('metric', function () {
return function (input) {
if (input === undefined) {
return '0 '
return '0 ';
}
if (input > 1000 * 1000 * 1000) {
input /= 1000 * 1000 * 1000;
@@ -292,25 +391,25 @@ syncthing.filter('metric', function() {
return input.toFixed(decimals(input, 2)) + ' k';
}
return Math.round(input) + ' ';
}
};
});
syncthing.filter('short', function() {
return function(input) {
syncthing.filter('short', function () {
return function (input) {
return input.substr(0, 6);
}
};
});
syncthing.filter('alwaysNumber', function() {
return function(input) {
syncthing.filter('alwaysNumber', function () {
return function (input) {
if (input === undefined) {
return 0;
}
return input;
}
};
});
syncthing.directive('optionEditor', function() {
syncthing.directive('optionEditor', function () {
return {
restrict: 'C',
replace: true,
@@ -320,4 +419,4 @@ syncthing.directive('optionEditor', function() {
},
template: '<input type="text" ng-model="config.Options[setting.id]"></input>',
};
})
});

View File

@@ -11,25 +11,10 @@
<title>syncthing</title>
<link href="bootstrap/css/bootstrap.min.css" rel="stylesheet">
<style type="text/css">
html, body {
height: 100%;
}
#wrap{
padding-top: 20px;
min-height: 100%;
height: auto;
margin: 0 auto -50px;
padding: 20px 0 50px 0;
}
#footer {
height: 50px;
padding: 12px;
background-color: #f5f5f5;
}
.header {
border-bottom: 1px solid #e5e5e5;
padding-bottom: 10px;
body {
padding-top: 70px;
padding-bottom: 70px;
}
.text-monospace {
@@ -40,128 +25,146 @@ html, body {
border-top: none;
}
thead tr th {
text-align: center;
}
.logo {
margin: 0;
padding: 0;
top: -5px;
position: relative;
}
</style>
</head>
<body ng-controller="SyncthingCtrl">
<div id="wrap">
<div class="navbar navbar-fixed-top navbar-default">
<div class="container">
<div class="page-header">
<h1 class="text-muted"><img width="64" height="64" src="st-logo-128.png"> syncthing</h1>
<a class="navbar-brand"><img class="logo" src="st-logo-128.png" width="32" height="32"> Syncthing</a>
<div ng-if="!configInSync">
<form class="navbar-form navbar-right">
<button type="button" class="btn btn-primary" ng-click="restart()">Restart Now</button>
</form>
<p class="navbar-text navbar-right">The configuration has been changed but not activated. Syncthing must restart to activate the new configuration.</p>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="panel" ng-class="{'panel-success': model.needBytes === 0, 'panel-primary': model.needBytes !== 0}">
<div class="panel-heading"><h3 class="panel-title">Synchronization</h3></div>
<div class="panel-body">
<div class="progress">
<div class="progress-bar" role="progressbar" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100"
ng-class="{'progress-bar-success': model.needBytes === 0, 'progress-bar-info': model.needBytes !== 0}"
style="width: {{100 * model.inSyncBytes / model.globalBytes | number:2}}%;">
{{100 * model.inSyncBytes / model.globalBytes | alwaysNumber | number:0}}%
</div>
<div class="container">
<div class="row">
<div class="col-md-12">
<div ng-if="errorList().length > 0" class="alert alert-warning">
<p ng-repeat="err in errorList()"><small>{{err.Time | date:"hh:mm:ss.sss"}}:</small> {{friendlyNodes(err.Error)}}</p>
<button type="button" class="pull-right btn btn-warning" ng-click="clearErrors()">OK</button>
<div class="clearfix"></div>
</div>
<div class="panel panel-info">
<div class="panel-heading"><h3 class="panel-title">Cluster</h3></div>
<table class="table table-condensed">
<tbody>
<!-- myself -->
<tr class="text-muted" ng-repeat="nodeCfg in thisNode()">
<td style="width:12%">
<span class="label label-default">
<span class="glyphicon glyphicon-ok"></span> This node
</span>
</td>
<td style="width:10%">
<span class="text-monospace">{{nodeName(nodeCfg)}}</span>
</td>
<td style="width:20%">{{version}}</td>
<td style="width:25%">(this node)</td>
<td style="width:9%" class="text-right">
{{inbps | metric}}bps
<span class="text-muted glyphicon glyphicon-chevron-down"></span>
</td>
<td style="width:9%" class="text-right">
{{outbps | metric}}bps
<span class="text-muted glyphicon glyphicon-chevron-up"></span>
</td>
<td style="width:7%" class="text-right">
<button type="button" ng-click="editNode(nodeCfg)" class="btn btn-default btn-xs"><span class="glyphicon glyphicon-pencil"></span> Edit</button>
</td>
</tr>
<!-- all other nodes -->
<tr ng-repeat="nodeCfg in otherNodes()">
<td>
<span class="label label-{{nodeClass(nodeCfg)}}">
<span class="glyphicon glyphicon-{{nodeIcon(nodeCfg)}}"></span> {{nodeStatus(nodeCfg)}}
</span>
</td>
<td>
<span class="text-monospace">{{nodeName(nodeCfg)}}</span>
</td>
<td>{{nodeVer(nodeCfg)}}</td>
<td>{{nodeAddr(nodeCfg)}}</td>
<td class="text-right">
<abbr title="{{connections[nodeCfg.NodeID].InBytesTotal | binary}}B">{{connections[nodeCfg.NodeID].inbps | metric}}bps</abbr>
<span class="text-muted glyphicon glyphicon-chevron-down"></span>
</td>
<td class="text-right">
<abbr title="{{connections[nodeCfg.NodeID].OutBytesTotal | binary}}B">{{connections[nodeCfg.NodeID].outbps | metric}}bps</abbr>
<span class="text-muted glyphicon glyphicon-chevron-up"></span>
</td>
<td class="text-right">
<button type="button" ng-click="editNode(nodeCfg)" class="btn btn-default btn-xs"><span class="glyphicon glyphicon-pencil"></span> Edit</button>
</td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td class="text-right">
<button type="button" class="btn btn-default btn-xs" ng-click="addNode()"><span class="glyphicon glyphicon-plus"></span> Add</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="panel panel-info">
<div class="panel-heading"><h3 class="panel-title">Repository</h3></div>
<div class="panel-body">
<p>Cluster contains {{model.globalFiles | alwaysNumber}} files, {{model.globalBytes | binary}}B
<span class="text-muted">(+{{model.globalDeleted | alwaysNumber}} delete records)</span></p>
<p>Local repository has {{model.localFiles | alwaysNumber}} files, {{model.localBytes | binary}}B
<span class="text-muted">(+{{model.localDeleted | alwaysNumber}} delete records)</span></p>
</div>
</div>
</div>
<div class="col-md-6">
<div class="panel" ng-class="{'panel-success': model.needBytes === 0, 'panel-primary': model.needBytes !== 0}">
<div class="panel-heading"><h3 class="panel-title">Synchronization</h3></div>
<div class="panel-body">
<div class="progress">
<div class="progress-bar" role="progressbar" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100"
ng-class="{'progress-bar-success': model.needBytes === 0, 'progress-bar-info': model.needBytes !== 0}"
style="width: {{100 * model.inSyncBytes / model.globalBytes | number:2}}%;">
{{100 * model.inSyncBytes / model.globalBytes | alwaysNumber | number:0}}%
</div>
<p ng-show="model.needBytes > 0">Need {{model.needFiles | alwaysNumber}} files, {{model.needBytes | binary}}B</p>
</div>
<p ng-show="model.needBytes > 0">Need {{model.needFiles | alwaysNumber}} files, {{model.needBytes | binary}}B</p>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="panel panel-info">
<div class="panel-heading"><h3 class="panel-title">Cluster</h3></div>
<table class="table table-condensed">
<tbody>
<!-- myself -->
<tr class="text-muted">
<td style="width:13%">
<span class="label label-default">
<span class="glyphicon glyphicon-ok"></span> This node
</span>
</td>
<td style="width:12%">
<span class="text-monospace">{{myID | short}}</span>
</td>
<td style="width:20%">{{version}}</td>
<td style="width:25%"></td>
<td style="width:10%" class="text-right">
<span ng-show="nodeCfg.NodeID != myID">
{{inbps | metric}}bps
<span class="text-muted glyphicon glyphicon-chevron-down"></span>
</span>
</td>
<td style="width:10%" class="text-right">
<span ng-show="nodeCfg.NodeID != myID">
{{outbps | metric}}bps
<span class="text-muted glyphicon glyphicon-chevron-up"></span>
</span>
</td>
<td style="width:10%" class="text-right"></td>
</tr>
<!-- all other nodes -->
<tr ng-repeat="nodeCfg in nodes">
<td>
<span class="label label-{{nodeClass(nodeCfg)}}">
<span class="glyphicon glyphicon-{{nodeIcon(nodeCfg)}}"></span> {{nodeStatus(nodeCfg)}}
</span>
</td>
<td>
<span class="text-monospace">{{nodeName(nodeCfg)}}</span>
</td>
<td>
{{nodeVer(nodeCfg)}}
</td>
<td>
{{nodeAddr(nodeCfg)}}
</td>
<td class="text-right">
<abbr title="{{connections[nodeCfg.NodeID].InBytesTotal | binary}}B">{{connections[nodeCfg.NodeID].inbps | metric}}bps</abbr>
<span class="text-muted glyphicon glyphicon-chevron-down"></span>
</td>
<td class="text-right">
<abbr title="{{connections[nodeCfg.NodeID].OutBytesTotal | binary}}B">{{connections[nodeCfg.NodeID].outbps | metric}}bps</abbr>
<span class="text-muted glyphicon glyphicon-chevron-up"></span>
</td>
<td class="text-right">
<button type="button" ng-click="editNode(nodeCfg)" class="btn btn-default btn-xs"><span class="glyphicon glyphicon-pencil"></span> Edit</button>
</td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td class="text-right">
<button type="button" class="btn btn-default btn-xs" ng-click="addNode()"><span class="glyphicon glyphicon-plus"></span> Add</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="panel panel-info">
<div class="panel-heading"><h3 class="panel-title">Repository</h3></div>
<div class="panel-body">
<p>Cluster contains {{model.globalFiles | alwaysNumber}} files, {{model.globalBytes | binary}}B
<span class="text-muted">(+{{model.globalDeleted | alwaysNumber}} delete records)</span></p>
<p>Local repository has {{model.localFiles | alwaysNumber}} files, {{model.localBytes | binary}}B
<span class="text-muted">(+{{model.localDeleted | alwaysNumber}} delete records)</span></p>
</div>
</div>
</div>
<div class="col-md-6">
<div class="panel panel-info">
<div class="panel-heading"><h3 class="panel-title">System</h3></div>
<div class="row">
<div class="col-md-6">
<div class="panel panel-info">
<div class="panel-heading"><h3 class="panel-title"><a href="" data-toggle="collapse" data-target="#system">System</a></h3></div>
<div id="system" class="panel-collapse collapse">
<div class="panel-body">
<p>{{system.sys | binary}}B RAM allocated, {{system.alloc | binary}}B in use</p>
<p>{{system.cpuPercent | alwaysNumber | natural:1}}% CPU, {{system.goroutines | alwaysNumber}} goroutines</p>
@@ -169,43 +172,46 @@ html, body {
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="panel panel-info">
<div class="panel-heading"><h3 class="panel-title"><a href="" data-toggle="collapse" data-target="#settingsTable">Settings</a></h3></div>
<div id="settingsTable" class="panel-collapse collapse">
<div class="panel-body">
<form role="form">
<div class="form-group" ng-repeat="setting in settings">
<div ng-if="setting.type == 'string'">
<label for="{{setting.id}}">{{setting.descr}}</label>
<input id="{{setting.id}}" class="form-control" type="text" ng-model="config.Options[setting.id]"></input>
</div>
<div class="checkbox" ng-if="setting.type == 'bool'">
<label>
{{setting.descr}} <input id="{{setting.id}}" type="checkbox" ng-model="config.Options[setting.id]"></input>
</label>
</div>
<div class="col-md-6">
<div class="panel panel-info">
<div class="panel-heading"><h3 class="panel-title"><a href="" data-toggle="collapse" data-target="#settingsTable">Settings</a></h3></div>
<div id="settingsTable" class="panel-collapse collapse">
<div class="panel-body">
<form role="form">
<div class="form-group" ng-repeat="setting in settings">
<div ng-if="setting.type == 'text' || setting.type == 'number'">
<label for="{{setting.id}}">{{setting.descr}}</label>
<input id="{{setting.id}}" class="form-control" type="{{setting.type}}" ng-model="config.Options[setting.id]"></input>
</div>
</form>
</div>
<div class="panel-footer">
<button type="button" class="btn btn-sm btn-default" ng-click="saveSettings()">Save</button>
<small><span class="text-muted">Changes take effect when restarting syncthing.</span></small>
</div>
<div class="checkbox" ng-if="setting.type == 'bool'">
<label>
{{setting.descr}} <input id="{{setting.id}}" type="checkbox" ng-model="config.Options[setting.id]"></input>
</label>
</div>
</div>
</form>
</div>
<div class="panel-footer">
<button type="button" class="btn btn-sm btn-default" ng-click="saveSettings()">Save</button>
<small><span class="text-muted">Changes take effect when restarting syncthing.</span></small>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="footer" class="text-center">
syncthing {{version}}
| <a href="https://github.com/calmh/syncthing/releases">Latest Release</a>
| <a href="https://github.com/calmh/syncthing/wiki">Documentation</a>
| <a href="https://github.com/calmh/syncthing/issues">Bugs</a>
| <a href="https://github.com/calmh/syncthing">Source Code</a>
<div class="navbar navbar-default navbar-fixed-bottom">
<div class="container">
<p class="navbar-text">{{version}}</p>
<ul class="nav navbar-nav navbar-right">
<li><a class="navbar-link" href="https://github.com/calmh/syncthing/releases">Latest Release</a></li>
<li><a class="navbar-link" href="https://github.com/calmh/syncthing/wiki">Documentation</a></li>
<li><a class="navbar-link" href="https://github.com/calmh/syncthing/issues">Bugs</a></li>
<li><a class="navbar-link" href="https://github.com/calmh/syncthing">Source Code</a></li>
</ul>
</p>
</div>
</div>
<div id="networkError" class="modal fade">
@@ -248,7 +254,7 @@ html, body {
</div>
<div class="form-group">
<label for="addresses">Addresses</label>
<input placeholder="dynamic" id="addresses" class="form-control" type="text" ng-model="currentNode.AddressesStr"></input>
<input placeholder="dynamic" ng-disabled="currentNode.NodeID == myID" id="addresses" class="form-control" type="text" ng-model="currentNode.AddressesStr"></input>
<p class="help-block">Enter comma separated <span class="text-monospace">ip:port</span> addresses or <span class="text-monospace">dynamic</span> to perform automatic discovery of the address.</p>
</div>
</form>

View File

@@ -6,9 +6,13 @@ import (
"os"
)
// set in main()
var logger *log.Logger
func init() {
log.SetOutput(os.Stderr)
logger = log.New(os.Stderr, "", log.Flags())
}
func debugln(vals ...interface{}) {
s := fmt.Sprintln(vals...)
logger.Output(2, "DEBUG: "+s)
@@ -41,11 +45,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)
}

91
main.go
View File

@@ -10,6 +10,7 @@ import (
"net/http"
_ "net/http/pprof"
"os"
"os/exec"
"path"
"runtime"
"runtime/debug"
@@ -19,7 +20,6 @@ import (
"github.com/calmh/ini"
"github.com/calmh/syncthing/discover"
"github.com/calmh/syncthing/model"
"github.com/calmh/syncthing/protocol"
)
@@ -32,25 +32,28 @@ var (
)
var (
showVersion bool
confDir string
trace string
profiler string
verbose bool
showVersion bool
confDir string
trace string
profiler string
verbose bool
startupDelay int
)
func main() {
log.SetOutput(os.Stderr)
logger = log.New(os.Stderr, "", log.Flags())
flag.StringVar(&confDir, "home", "~/.syncthing", "Set configuration directory")
flag.StringVar(&trace, "debug.trace", "", "(connect,net,idx,file,pull)")
flag.StringVar(&profiler, "debug.profiler", "", "(addr)")
flag.BoolVar(&showVersion, "version", false, "Show version")
flag.BoolVar(&verbose, "v", false, "Be more verbose")
flag.IntVar(&startupDelay, "delay", 0, "Startup delay (s)")
flag.Usage = usageFor(flag.CommandLine, "syncthing [options]")
flag.Parse()
if startupDelay > 0 {
time.Sleep(time.Duration(startupDelay) * time.Second)
}
if showVersion {
fmt.Println(Version)
os.Exit(0)
@@ -147,6 +150,9 @@ func main() {
infof("Edit %s to taste or use the GUI\n", cfgFile)
}
// Make sure the local node is in the node list.
cfg.Repositories[0].Nodes = cleanNodeList(cfg.Repositories[0].Nodes, myID)
var dir = expandTilde(cfg.Repositories[0].Directory)
if len(profiler) > 0 {
@@ -172,7 +178,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)
}
@@ -204,12 +210,18 @@ func main() {
loadIndex(m)
updateLocalModel(m)
connOpts := map[string]string{
"clientId": "syncthing",
"clientVersion": Version,
"clusterHash": clusterHash(cfg.Repositories[0].Nodes),
}
// Routine to listen for incoming connections
if verbose {
infoln("Listening for incoming connections")
}
for _, addr := range cfg.Options.ListenAddress {
go listen(myID, addr, m, tlsCfg)
go listen(myID, addr, m, tlsCfg, connOpts)
}
// Routine to connect out to configured nodes
@@ -217,7 +229,7 @@ func main() {
infoln("Attempting to connect to other nodes")
}
disc := discovery(cfg.Options.ListenAddress[0])
go connect(myID, disc, m, tlsCfg)
go connect(myID, disc, m, tlsCfg, connOpts)
// Routine to pull blocks from other nodes to synchronize the local
// repository. Does not run when we are in read only (publish only) mode.
@@ -235,7 +247,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
@@ -255,6 +267,35 @@ func main() {
select {}
}
func restart() {
infoln("Restarting")
args := os.Args
doAppend := true
for _, arg := range args {
if arg == "-delay" {
doAppend = false
break
}
}
if doAppend {
args = append(args, "-delay", "2")
}
pgm, err := exec.LookPath(os.Args[0])
if err != nil {
warnln(err)
return
}
proc, err := os.StartProcess(pgm, args, &os.ProcAttr{
Env: os.Environ(),
Files: []*os.File{os.Stdin, os.Stdout, os.Stderr},
})
if err != nil {
fatalln(err)
}
proc.Release()
os.Exit(0)
}
var saveConfigCh = make(chan struct{})
func saveConfigLoop(cfgFile string) {
@@ -289,9 +330,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)
@@ -320,18 +361,13 @@ func printStatsLoop(m *model.Model) {
}
}
func listen(myID string, addr string, m *model.Model, tlsCfg *tls.Config) {
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)
}
l, err := tls.Listen("tcp", addr, tlsCfg)
fatalErr(err)
connOpts := map[string]string{
"clientId": "syncthing",
"clientVersion": Version,
}
listen:
for {
conn, err := l.Accept()
@@ -401,12 +437,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{
"clientId": "syncthing",
"clientVersion": Version,
}
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 {
@@ -455,13 +486,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")
@@ -477,7 +508,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 {

View File

@@ -1,11 +1,10 @@
package model
package main
import (
"crypto/sha1"
"errors"
"fmt"
"io"
"log"
"net"
"os"
"path"
@@ -159,6 +158,7 @@ type ConnectionInfo struct {
Address string
ClientID string
ClientVersion string
Completion int
}
// ConnectionStats returns a map with connection statistics for each connected node.
@@ -167,7 +167,14 @@ func (m *Model) ConnectionStats() map[string]ConnectionInfo {
RemoteAddr() net.Addr
}
m.gmut.RLock()
m.pmut.RLock()
m.rmut.RLock()
var tot int
for _, f := range m.global {
tot += f.Size()
}
var res = make(map[string]ConnectionInfo)
for node, conn := range m.protoConn {
@@ -179,10 +186,22 @@ func (m *Model) ConnectionStats() map[string]ConnectionInfo {
if nc, ok := m.rawConn[node].(remoteAddrer); ok {
ci.Address = nc.RemoteAddr().String()
}
var have int
for _, f := range m.remote[node] {
if f.Equals(m.global[f.Name]) {
have += f.Size()
}
}
ci.Completion = 100 * have / tot
res[node] = ci
}
m.rmut.RUnlock()
m.pmut.RUnlock()
m.gmut.RUnlock()
return res
}
@@ -268,7 +287,7 @@ func (m *Model) Index(nodeID string, fs []protocol.FileInfo) {
defer m.imut.Unlock()
if m.trace["net"] {
log.Printf("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 +315,13 @@ func (m *Model) IndexUpdate(nodeID string, fs []protocol.FileInfo) {
defer m.imut.Unlock()
if m.trace["net"] {
log.Printf("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 +341,11 @@ func (m *Model) indexUpdate(repo map[string]File, f File) {
if f.Flags&protocol.FlagDeleted != 0 {
flagComment = " (deleted)"
}
log.Printf("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
}
@@ -336,6 +355,13 @@ func (m *Model) indexUpdate(repo map[string]File, f File) {
// 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 m.trace["net"] {
debugf("NET: %s: %v", node, err)
}
if err == protocol.ErrClusterHash {
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)
m.pmut.Lock()
@@ -370,7 +396,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 {
@@ -378,7 +404,7 @@ func (m *Model) Request(nodeID, name string, offset int64, size uint32, hash []b
}
if m.trace["net"] && nodeID != "<local>" {
log.Printf("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?
@@ -495,13 +521,13 @@ func (m *Model) AddConnection(rawConn io.Closer, protoConn Connection) {
i := i
go func() {
if m.trace["pull"] {
log.Println("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("PULL: Exiting", nodeID, i)
debugln("PULL: Exiting", nodeID, i)
}
m.pmut.RUnlock()
return
@@ -511,7 +537,7 @@ func (m *Model) AddConnection(rawConn io.Closer, protoConn Connection) {
qb, ok := m.fq.Get(nodeID)
if ok {
if m.trace["pull"] {
log.Println("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)
@@ -537,7 +563,7 @@ func (m *Model) ProtocolIndex() []protocol.FileInfo {
if mf.Flags&protocol.FlagDeleted != 0 {
flagComment = " (deleted)"
}
log.Printf("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)
}
@@ -556,7 +582,7 @@ func (m *Model) requestGlobal(nodeID, name string, offset int64, size uint32, ha
}
if m.trace["net"] {
log.Printf("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)
@@ -584,7 +610,7 @@ func (m *Model) broadcastIndexLoop() {
for _, node := range m.protoConn {
node := node
if m.trace["net"] {
log.Printf("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)
@@ -796,7 +822,7 @@ func (m *Model) recomputeNeedForFile(gf File, toAdd []addOrder, toDelete []File)
return toAdd, toDelete
}
if m.trace["need"] {
log.Printf("NEED: lf:%v gf:%v", lf, gf)
debugf("NEED: lf:%v gf:%v", lf, gf)
}
if gf.Flags&protocol.FlagDeleted != 0 {
@@ -838,12 +864,12 @@ func (m *Model) WhoHas(name string) []string {
func (m *Model) deleteLoop() {
for file := range m.dq {
if m.trace["file"] {
log.Println("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)

View File

@@ -1,4 +1,4 @@
package model
package main
import (
"bytes"

View File

@@ -27,6 +27,10 @@ const (
FlagInvalid = 1 << 13
)
var (
ErrClusterHash = fmt.Errorf("Configuration error: mismatched cluster hash")
)
type FileInfo struct {
Name string
Flags uint32
@@ -64,7 +68,8 @@ type Connection struct {
awaiting map[int]chan asyncResult
nextId int
indexSent map[string][2]int64
options map[string]string
peerOptions map[string]string
myOptions map[string]string
optionsLock sync.Mutex
hasSentIndex bool
@@ -106,6 +111,7 @@ func NewConnection(nodeID string, reader io.Reader, writer io.Writer, receiver M
go c.pingerLoop()
if options != nil {
c.myOptions = options
go func() {
c.Lock()
c.mwriter.writeHeader(header{0, c.nextId, messageTypeOptions})
@@ -348,9 +354,14 @@ loop:
case messageTypeOptions:
c.optionsLock.Lock()
c.options = c.mreader.readOptions()
c.peerOptions = c.mreader.readOptions()
c.optionsLock.Unlock()
if mh, rh := c.myOptions["clusterHash"], c.peerOptions["clusterHash"]; len(mh) > 0 && len(rh) > 0 && mh != rh {
c.close(ErrClusterHash)
break loop
}
default:
c.close(fmt.Errorf("Protocol error: %s: unknown message type %#x", c.ID, hdr.msgType))
break loop
@@ -423,5 +434,5 @@ func (c *Connection) Statistics() Statistics {
func (c *Connection) Option(key string) string {
c.optionsLock.Lock()
defer c.optionsLock.Unlock()
return c.options[key]
return c.peerOptions[key]
}

View File

@@ -1,4 +1,4 @@
package model
package main
import (
"sync"

View File

@@ -1,4 +1,4 @@
package model
package main
import (
"testing"

View File

View File

View File

View File

View File

View File

@@ -1,4 +1,4 @@
package model
package main
import (
"bytes"

View File

@@ -1,4 +1,4 @@
package model
package main
import (
"fmt"