Compare commits

...

6 Commits

Author SHA1 Message Date
Jakob Borg
e6b29988e5 Logo 2014-02-07 22:33:58 +01:00
Jakob Borg
3cb7b8f22b Allow multiple listenAddresses (fixes #52) 2014-02-05 23:17:17 +01:00
Jakob Borg
2297e29502 Give friendly names to nodes (fixes #54) 2014-02-05 22:49:26 +01:00
Jakob Borg
ea41acfff5 Clarify status badges and fix column widths (fixes #53) 2014-02-05 22:42:23 +01:00
Jakob Borg
1aefc50e35 Always show local node, and summarize traffic stats (fixes #43) 2014-02-05 21:30:04 +01:00
Jakob Borg
9bd4fa5008 Make immediate write error only slightly less cryptic (fixes #51) 2014-02-05 20:58:39 +01:00
9 changed files with 123 additions and 53 deletions

BIN
assets/st-logo.pxm Normal file
View File

Binary file not shown.

View File

File diff suppressed because one or more lines are too long

View File

@@ -22,24 +22,25 @@ type RepositoryConfiguration struct {
type NodeConfiguration struct {
NodeID string `xml:"id,attr"`
Name string `xml:"name,attr"`
Addresses []string `xml:"address"`
}
type OptionsConfiguration struct {
ListenAddress string `xml:"listenAddress" default:":22000" ini:"listen-address"`
ReadOnly bool `xml:"readOnly" ini:"read-only"`
AllowDelete bool `xml:"allowDelete" default:"true" ini:"allow-delete"`
FollowSymlinks bool `xml:"followSymlinks" default:"true" ini:"follow-symlinks"`
GUIEnabled bool `xml:"guiEnabled" default:"true" ini:"gui-enabled"`
GUIAddress string `xml:"guiAddress" default:"127.0.0.1:8080" ini:"gui-address"`
GlobalAnnServer string `xml:"globalAnnounceServer" default:"syncthing.nym.se:22025" ini:"global-announce-server"`
GlobalAnnEnabled bool `xml:"globalAnnounceEnabled" default:"true" ini:"global-announce-enabled"`
LocalAnnEnabled bool `xml:"localAnnounceEnabled" default:"true" ini:"local-announce-enabled"`
ParallelRequests int `xml:"parallelRequests" default:"16" ini:"parallel-requests"`
MaxSendKbps int `xml:"maxSendKbps" ini:"max-send-kbps"`
RescanIntervalS int `xml:"rescanIntervalS" default:"60" ini:"rescan-interval"`
ReconnectIntervalS int `xml:"reconnectionIntervalS" default:"60" ini:"reconnection-interval"`
MaxChangeKbps int `xml:"maxChangeKbps" default:"1000" ini:"max-change-bw"`
ListenAddress []string `xml:"listenAddress" default:":22000" ini:"listen-address"`
ReadOnly bool `xml:"readOnly" ini:"read-only"`
AllowDelete bool `xml:"allowDelete" default:"true" ini:"allow-delete"`
FollowSymlinks bool `xml:"followSymlinks" default:"true" ini:"follow-symlinks"`
GUIEnabled bool `xml:"guiEnabled" default:"true" ini:"gui-enabled"`
GUIAddress string `xml:"guiAddress" default:"127.0.0.1:8080" ini:"gui-address"`
GlobalAnnServer string `xml:"globalAnnounceServer" default:"syncthing.nym.se:22025" ini:"global-announce-server"`
GlobalAnnEnabled bool `xml:"globalAnnounceEnabled" default:"true" ini:"global-announce-enabled"`
LocalAnnEnabled bool `xml:"localAnnounceEnabled" default:"true" ini:"local-announce-enabled"`
ParallelRequests int `xml:"parallelRequests" default:"16" ini:"parallel-requests"`
MaxSendKbps int `xml:"maxSendKbps" ini:"max-send-kbps"`
RescanIntervalS int `xml:"rescanIntervalS" default:"60" ini:"rescan-interval"`
ReconnectIntervalS int `xml:"reconnectionIntervalS" default:"60" ini:"reconnection-interval"`
MaxChangeKbps int `xml:"maxChangeKbps" default:"1000" ini:"max-change-bw"`
}
func setDefaults(data interface{}) error {
@@ -56,6 +57,11 @@ func setDefaults(data interface{}) error {
case string:
f.SetString(v)
case []string:
rv := reflect.MakeSlice(reflect.TypeOf([]string{}), 1, 1)
rv.Index(0).SetString(v)
f.Set(rv)
case int:
i, err := strconv.ParseInt(v, 10, 64)
if err != nil {
@@ -120,6 +126,20 @@ func writeConfigXML(wr io.Writer, cfg Configuration) error {
return err
}
func uniqueStrings(ss []string) []string {
var m = make(map[string]bool, len(ss))
for _, s := range ss {
m[s] = true
}
var us = make([]string, 0, len(m))
for k := range m {
us = append(us, k)
}
return us
}
func readConfigXML(rd io.Reader) (Configuration, error) {
var cfg Configuration
@@ -131,5 +151,6 @@ func readConfigXML(rd io.Reader) (Configuration, error) {
err = xml.NewDecoder(rd).Decode(&cfg)
}
cfg.Options.ListenAddress = uniqueStrings(cfg.Options.ListenAddress)
return cfg, err
}

View File

@@ -11,7 +11,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
// Strings before bools look better
$scope.settings = [
{id: 'ListenAddress', descr:"Sync Protocol Listen Address", type: 'string', restart: true},
{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},
@@ -49,13 +49,11 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
$http.get("/rest/config").success(function (data) {
$scope.config = data;
$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 == $scope.myID)
return -1;
if (b.NodeID == $scope.myID)
return 1;
if (a.NodeID < b.NodeID)
return -1;
return a.NodeID > b.NodeID;
@@ -79,6 +77,8 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
var td = (now - prevDate) / 1000;
prevDate = now;
$scope.inbps = 0
$scope.outbps = 0
for (var id in data) {
try {
data[id].inbps = Math.max(0, 8 * (data[id].InBytesTotal - $scope.connections[id].InBytesTotal) / td);
@@ -87,6 +87,8 @@ 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.connections = data;
});
@@ -110,10 +112,22 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
};
$scope.nodeIcon = function (nodeCfg) {
if (nodeCfg.NodeID === $scope.myID) {
if ($scope.connections[nodeCfg.NodeID]) {
return "ok";
}
return "minus";
};
$scope.nodeStatus = function (nodeCfg) {
if ($scope.connections[nodeCfg.NodeID]) {
return "Connected";
}
return "Disconnected";
};
$scope.nodeIcon = function (nodeCfg) {
if ($scope.connections[nodeCfg.NodeID]) {
return "ok";
}
@@ -122,10 +136,6 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
};
$scope.nodeClass = function (nodeCfg) {
if (nodeCfg.NodeID === $scope.myID) {
return "default";
}
var conn = $scope.connections[nodeCfg.NodeID];
if (conn) {
return "success";
@@ -135,9 +145,6 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
};
$scope.nodeAddr = function (nodeCfg) {
if (nodeCfg.NodeID === $scope.myID) {
return "this node";
}
var conn = $scope.connections[nodeCfg.NodeID];
if (conn) {
return conn.Address;
@@ -156,7 +163,15 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
return "";
};
$scope.nodeName = function (nodeCfg) {
if (nodeCfg.Name) {
return nodeCfg.Name;
}
return nodeCfg.NodeID.substr(0, 6);
};
$scope.saveSettings = function () {
$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');
};
@@ -211,10 +226,6 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
}
$scope.nodes.sort(function (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;

BIN
gui/favicon.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

@@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="">
<meta name="author" content="">
<link rel="shortcut icon" href="../../docs-assets/ico/favicon.png">
<link rel="shortcut icon" href="favicon.png">
<title>syncthing</title>
<link href="bootstrap/css/bootstrap.min.css" rel="stylesheet">
@@ -46,8 +46,8 @@ html, body {
<body ng-controller="SyncthingCtrl">
<div id="wrap">
<div class="container">
<div class="header">
<h3 class="text-muted">syncthing</h3>
<div class="page-header">
<h1 class="text-muted"><img width="64" height="64" src="st-logo-128.png"> syncthing</h1>
</div>
<div class="row">
@@ -74,14 +74,41 @@ html, body {
<div class="panel-heading"><h3 class="panel-title">Cluster</h3></div>
<table class="table table-condensed">
<tbody>
<tr ng-repeat="nodeCfg in nodes" ng-class="{'text-muted': nodeCfg.NodeID == myID}">
<!-- 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>
<span class="glyphicon glyphicon-{{nodeIcon(nodeCfg)}}"></span> {{nodeStatus(nodeCfg)}}
</span>
</td>
<td>
<span class="text-monospace">{{nodeCfg.NodeID | short}}</span>
<span class="text-monospace">{{nodeName(nodeCfg)}}</span>
</td>
<td>
{{nodeVer(nodeCfg)}}
@@ -90,19 +117,15 @@ html, body {
{{nodeAddr(nodeCfg)}}
</td>
<td class="text-right">
<span ng-show="nodeCfg.NodeID != myID">
<abbr title="{{connections[nodeCfg.NodeID].InBytesTotal | binary}}B">{{connections[nodeCfg.NodeID].inbps | metric}}bps</abbr>
<span class="text-muted glyphicon glyphicon-chevron-down"></span>
</span>
<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">
<span ng-show="nodeCfg.NodeID != myID">
<abbr title="{{connections[nodeCfg.NodeID].OutBytesTotal | binary}}B">{{connections[nodeCfg.NodeID].outbps | metric}}bps</abbr>
<span class="text-muted glyphicon glyphicon-chevron-up"></span>
</span>
<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 ng-show="nodeCfg.NodeID != myID" type="button" ng-click="editNode(nodeCfg)" class="btn btn-default btn-xs"><span class="glyphicon glyphicon-pencil"></span> Edit</button>
<button type="button" ng-click="editNode(nodeCfg)" class="btn btn-default btn-xs"><span class="glyphicon glyphicon-pencil"></span> Edit</button>
</td>
</tr>
<tr>
@@ -218,6 +241,11 @@ html, body {
<input placeholder="YUFJOUDPORCMA..." ng-disabled="editingExisting" id="nodeID" class="form-control" type="text" ng-model="currentNode.NodeID"></input>
<p class="help-block">The node ID can be found in the logs or in the "Add Node" dialog on the other node.</p>
</div>
<div class="form-group">
<label for="name">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>
<div class="form-group">
<label for="addresses">Addresses</label>
<input placeholder="dynamic" id="addresses" class="form-control" type="text" ng-model="currentNode.AddressesStr"></input>
@@ -225,8 +253,8 @@ html, body {
</div>
</form>
<div ng-show="!editingExisting">
When adding a new node, keep in mind that <em>this node</em> must be added on the other side too. The Node ID of this node is:
<pre>{{myID}}</pre>
When adding a new node, keep in mind that <em>this node</em> must be added on the other side too. The Node ID of this node is:
<pre>{{myID}}</pre>
</div>
</div>
<div class="modal-footer">

BIN
gui/st-logo-128.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

16
main.go
View File

@@ -208,13 +208,16 @@ func main() {
if verbose {
infoln("Listening for incoming connections")
}
go listen(myID, cfg.Options.ListenAddress, m, tlsCfg)
for _, addr := range cfg.Options.ListenAddress {
go listen(myID, addr, m, tlsCfg)
}
// Routine to connect out to configured nodes
if verbose {
infoln("Attempting to connect to other nodes")
}
go connect(myID, cfg.Options.ListenAddress, m, tlsCfg)
disc := discovery(cfg.Options.ListenAddress[0])
go connect(myID, disc, m, tlsCfg)
// Routine to pull blocks from other nodes to synchronize the local
// repository. Does not run when we are in read only (publish only) mode.
@@ -318,6 +321,9 @@ func printStatsLoop(m *model.Model) {
}
func listen(myID string, addr string, m *model.Model, tlsCfg *tls.Config) {
if strings.Contains(trace, "connect") {
debugln("NET: Listening on", addr)
}
l, err := tls.Listen("tcp", addr, tlsCfg)
fatalErr(err)
@@ -369,7 +375,7 @@ listen:
}
}
func connect(myID string, addr string, m *model.Model, tlsCfg *tls.Config) {
func discovery(addr string) *discover.Discoverer {
_, portstr, err := net.SplitHostPort(addr)
fatalErr(err)
port, _ := strconv.Atoi(portstr)
@@ -392,6 +398,10 @@ func connect(myID string, addr string, m *model.Model, tlsCfg *tls.Config) {
warnf("No discovery possible (%v)", err)
}
return disc
}
func connect(myID string, disc *discover.Discoverer, m *model.Model, tlsCfg *tls.Config) {
connOpts := map[string]string{
"clientId": "syncthing",
"clientVersion": Version,

View File

@@ -112,7 +112,7 @@ func NewConnection(nodeID string, reader io.Reader, writer io.Writer, receiver M
c.mwriter.writeOptions(options)
err := c.flush()
if err != nil {
log.Printf("Warning:", err)
log.Println("Warning: Write error during initial handshake:", err)
}
c.nextId++
c.Unlock()