mirror of
https://github.com/octoleo/syncthing.git
synced 2024-11-08 22:31:04 +00:00
Edit configuration in GUI; use XML configuration
This commit is contained in:
parent
71def3a970
commit
a1d575894a
File diff suppressed because one or more lines are too long
158
config.go
158
config.go
@ -1,33 +1,48 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"encoding/xml"
|
||||
"io"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
Listen string `ini:"listen-address" default:":22000" description:"ip:port to for incoming sync connections"`
|
||||
ReadOnly bool `ini:"read-only" description:"Allow changes to the local repository"`
|
||||
Delete bool `ini:"allow-delete" default:"true" description:"Allow deletes of files in the local repository"`
|
||||
Symlinks bool `ini:"follow-symlinks" default:"true" description:"Follow symbolic links at the top level of the repository"`
|
||||
GUI bool `ini:"gui-enabled" default:"true" description:"Enable the HTTP GUI"`
|
||||
GUIAddr string `ini:"gui-address" default:"127.0.0.1:8080" description:"ip:port for GUI connections"`
|
||||
ExternalServer string `ini:"global-announce-server" default:"syncthing.nym.se:22025" description:"Global server for announcements"`
|
||||
ExternalDiscovery bool `ini:"global-announce-enabled" default:"true" description:"Announce to the global announce server"`
|
||||
LocalDiscovery bool `ini:"local-announce-enabled" default:"true" description:"Announce to the local network"`
|
||||
ParallelRequests int `ini:"parallel-requests" default:"16" description:"Maximum number of blocks to request in parallel"`
|
||||
LimitRate int `ini:"max-send-kbps" description:"Limit outgoing data rate (kbyte/s)"`
|
||||
ScanInterval time.Duration `ini:"rescan-interval" default:"60s" description:"Scan repository for changes this often"`
|
||||
ConnInterval time.Duration `ini:"reconnection-interval" default:"60s" description:"Attempt to (re)connect to peers this often"`
|
||||
MaxChangeBW int `ini:"max-change-bw" default:"1000" description:"Suppress files changing more than this (kbyte/s)"`
|
||||
type Configuration struct {
|
||||
Version int `xml:"version,attr" default:"1"`
|
||||
Repositories []RepositoryConfiguration `xml:"repository"`
|
||||
Options OptionsConfiguration `xml:"options"`
|
||||
XMLName xml.Name `xml:"configuration" json:"-"`
|
||||
}
|
||||
|
||||
func loadConfig(m map[string]string, data interface{}) error {
|
||||
type RepositoryConfiguration struct {
|
||||
Directory string `xml:"directory,attr"`
|
||||
Nodes []NodeConfiguration `xml:"node"`
|
||||
}
|
||||
|
||||
type NodeConfiguration struct {
|
||||
NodeID string `xml:"id,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"`
|
||||
}
|
||||
|
||||
func setDefaults(data interface{}) error {
|
||||
s := reflect.ValueOf(data).Elem()
|
||||
t := s.Type()
|
||||
|
||||
@ -35,24 +50,9 @@ func loadConfig(m map[string]string, data interface{}) error {
|
||||
f := s.Field(i)
|
||||
tag := t.Field(i).Tag
|
||||
|
||||
name := tag.Get("ini")
|
||||
if len(name) == 0 {
|
||||
name = strings.ToLower(t.Field(i).Name)
|
||||
}
|
||||
|
||||
v, ok := m[name]
|
||||
if !ok {
|
||||
v = tag.Get("default")
|
||||
}
|
||||
v := tag.Get("default")
|
||||
if len(v) > 0 {
|
||||
switch f.Interface().(type) {
|
||||
case time.Duration:
|
||||
d, err := time.ParseDuration(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.SetInt(int64(d))
|
||||
|
||||
case string:
|
||||
f.SetString(v)
|
||||
|
||||
@ -74,56 +74,62 @@ func loadConfig(m map[string]string, data interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type cfg struct {
|
||||
Key string
|
||||
Value string
|
||||
Comment string
|
||||
}
|
||||
|
||||
func structToValues(data interface{}) []cfg {
|
||||
func readConfigINI(m map[string]string, data interface{}) error {
|
||||
s := reflect.ValueOf(data).Elem()
|
||||
t := s.Type()
|
||||
|
||||
var vals []cfg
|
||||
for i := 0; i < s.NumField(); i++ {
|
||||
f := s.Field(i)
|
||||
tag := t.Field(i).Tag
|
||||
|
||||
var c cfg
|
||||
c.Key = tag.Get("ini")
|
||||
if len(c.Key) == 0 {
|
||||
c.Key = strings.ToLower(t.Field(i).Name)
|
||||
name := tag.Get("ini")
|
||||
if len(name) == 0 {
|
||||
name = strings.ToLower(t.Field(i).Name)
|
||||
}
|
||||
|
||||
if v, ok := m[name]; ok {
|
||||
switch f.Interface().(type) {
|
||||
case string:
|
||||
f.SetString(v)
|
||||
|
||||
case int:
|
||||
i, err := strconv.ParseInt(v, 10, 64)
|
||||
if err == nil {
|
||||
f.SetInt(i)
|
||||
}
|
||||
|
||||
case bool:
|
||||
f.SetBool(v == "true")
|
||||
|
||||
default:
|
||||
panic(f.Type())
|
||||
}
|
||||
}
|
||||
c.Value = fmt.Sprint(f.Interface())
|
||||
c.Comment = tag.Get("description")
|
||||
vals = append(vals, c)
|
||||
}
|
||||
return vals
|
||||
return nil
|
||||
}
|
||||
|
||||
var configTemplateStr = `[repository]
|
||||
{{if .comments}}; The directory to synchronize. Will be created if it does not exist.
|
||||
{{end}}dir = {{.dir}}
|
||||
|
||||
[nodes]
|
||||
{{if .comments}}; Map of node ID to addresses, or "dynamic" for automatic discovery. Examples:
|
||||
; J3MZ4G5O4CLHJKB25WX47K5NUJUWDOLO2TTNY3TV3NRU4HVQRKEQ = 172.16.32.24:22000
|
||||
; ZNJZRXQKYHF56A2VVNESRZ6AY4ZOWGFJCV6FXDZJUTRVR3SNBT6Q = dynamic
|
||||
{{end}}{{range $n, $a := .nodes}}{{$n}} = {{$a}}
|
||||
{{end}}
|
||||
[settings]
|
||||
{{range $v := .settings}}; {{$v.Comment}}
|
||||
{{$v.Key}} = {{$v.Value}}
|
||||
{{end}}
|
||||
`
|
||||
|
||||
var configTemplate = template.Must(template.New("config").Parse(configTemplateStr))
|
||||
|
||||
func writeConfig(wr io.Writer, dir string, nodes map[string]string, opts Options, comments bool) {
|
||||
configTemplate.Execute(wr, map[string]interface{}{
|
||||
"dir": dir,
|
||||
"nodes": nodes,
|
||||
"settings": structToValues(&opts),
|
||||
"comments": comments,
|
||||
})
|
||||
func writeConfigXML(wr io.Writer, cfg Configuration) error {
|
||||
e := xml.NewEncoder(wr)
|
||||
e.Indent("", " ")
|
||||
err := e.Encode(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = wr.Write([]byte("\n"))
|
||||
return err
|
||||
}
|
||||
|
||||
func readConfigXML(rd io.Reader) (Configuration, error) {
|
||||
var cfg Configuration
|
||||
|
||||
setDefaults(&cfg)
|
||||
setDefaults(&cfg.Options)
|
||||
|
||||
var err error
|
||||
if rd != nil {
|
||||
err = xml.NewDecoder(rd).Decode(&cfg)
|
||||
}
|
||||
|
||||
return cfg, err
|
||||
}
|
||||
|
58
gui.go
58
gui.go
@ -2,19 +2,13 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"mime"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/calmh/syncthing/auto"
|
||||
"github.com/calmh/syncthing/model"
|
||||
"github.com/codegangsta/martini"
|
||||
"github.com/cratonica/embed"
|
||||
)
|
||||
|
||||
func startGUI(addr string, m *model.Model) {
|
||||
@ -27,14 +21,11 @@ func startGUI(addr string, m *model.Model) {
|
||||
router.Get("/rest/need", restGetNeed)
|
||||
router.Get("/rest/system", restGetSystem)
|
||||
|
||||
fs, err := embed.Unpack(auto.Resources)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
router.Post("/rest/config", restPostConfig)
|
||||
|
||||
go func() {
|
||||
mr := martini.New()
|
||||
mr.Use(embeddedStatic(fs))
|
||||
mr.Use(embeddedStatic())
|
||||
mr.Use(martini.Recovery())
|
||||
mr.Action(router.Handle)
|
||||
mr.Map(m)
|
||||
@ -43,7 +34,6 @@ func startGUI(addr string, m *model.Model) {
|
||||
warnln("GUI not possible:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
}
|
||||
|
||||
func getRoot(w http.ResponseWriter, r *http.Request) {
|
||||
@ -80,13 +70,16 @@ func restGetConnections(m *model.Model, w http.ResponseWriter) {
|
||||
}
|
||||
|
||||
func restGetConfig(w http.ResponseWriter) {
|
||||
var res = make(map[string]interface{})
|
||||
res["myID"] = myID
|
||||
res["repository"] = config.OptionMap("repository")
|
||||
res["nodes"] = config.OptionMap("nodes")
|
||||
res["nodes"].(map[string]string)[myID] = "self"
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(res)
|
||||
json.NewEncoder(w).Encode(cfg)
|
||||
}
|
||||
|
||||
func restPostConfig(req *http.Request) {
|
||||
err := json.NewDecoder(req.Body).Decode(&cfg)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
} else {
|
||||
saveConfig()
|
||||
}
|
||||
}
|
||||
|
||||
type guiFile model.File
|
||||
@ -120,6 +113,7 @@ func restGetSystem(w http.ResponseWriter) {
|
||||
runtime.ReadMemStats(&m)
|
||||
|
||||
res := make(map[string]interface{})
|
||||
res["myID"] = myID
|
||||
res["goroutines"] = runtime.NumGoroutine()
|
||||
res["alloc"] = m.Alloc
|
||||
res["sys"] = m.Sys
|
||||
@ -130,29 +124,3 @@ func restGetSystem(w http.ResponseWriter) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(res)
|
||||
}
|
||||
|
||||
func embeddedStatic(fs map[string][]byte) interface{} {
|
||||
var modt = time.Now().UTC().Format(http.TimeFormat)
|
||||
|
||||
return func(res http.ResponseWriter, req *http.Request, log *log.Logger) {
|
||||
file := req.URL.Path
|
||||
|
||||
if file[0] == '/' {
|
||||
file = file[1:]
|
||||
}
|
||||
|
||||
bs, ok := fs[file]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
mtype := mime.TypeByExtension(filepath.Ext(req.URL.Path))
|
||||
if len(mtype) != 0 {
|
||||
res.Header().Set("Content-Type", mtype)
|
||||
}
|
||||
res.Header().Set("Content-Size", fmt.Sprintf("%d", len(bs)))
|
||||
res.Header().Set("Last-Modified", modt)
|
||||
|
||||
res.Write(bs)
|
||||
}
|
||||
}
|
||||
|
175
gui/app.js
175
gui/app.js
@ -4,6 +4,28 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
|
||||
var prevDate = 0;
|
||||
var modelGetOK = true;
|
||||
|
||||
$scope.connections = {};
|
||||
$scope.config = {};
|
||||
$scope.myID = "";
|
||||
$scope.nodes = [];
|
||||
|
||||
// Strings before bools look better
|
||||
$scope.settings = [
|
||||
{id: 'ListenAddress', descr:"Sync Protocol Listen Address", 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: '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() {
|
||||
if (!modelGetOK) {
|
||||
$('#networkError').modal('hide');
|
||||
@ -21,8 +43,25 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
|
||||
$http.get("/rest/version").success(function (data) {
|
||||
$scope.version = data;
|
||||
});
|
||||
$http.get("/rest/config").success(function (data) {
|
||||
$scope.config = data;
|
||||
$http.get("/rest/system").success(function (data) {
|
||||
$scope.system = data;
|
||||
$scope.myID = data.myID;
|
||||
|
||||
$http.get("/rest/config").success(function (data) {
|
||||
$scope.config = data;
|
||||
|
||||
var nodes = $scope.config.Repositories[0].Nodes;
|
||||
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;
|
||||
})
|
||||
$scope.nodes = nodes;
|
||||
});
|
||||
});
|
||||
|
||||
$scope.refresh = function () {
|
||||
@ -70,6 +109,122 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
|
||||
});
|
||||
};
|
||||
|
||||
$scope.nodeIcon = function (nodeCfg) {
|
||||
if (nodeCfg.NodeID === $scope.myID) {
|
||||
return "ok";
|
||||
}
|
||||
|
||||
if ($scope.connections[nodeCfg.NodeID]) {
|
||||
return "ok";
|
||||
}
|
||||
|
||||
return "minus";
|
||||
};
|
||||
|
||||
$scope.nodeClass = function (nodeCfg) {
|
||||
if (nodeCfg.NodeID === $scope.myID) {
|
||||
return "default";
|
||||
}
|
||||
|
||||
var conn = $scope.connections[nodeCfg.NodeID];
|
||||
if (conn) {
|
||||
return "success";
|
||||
}
|
||||
|
||||
return "info";
|
||||
};
|
||||
|
||||
$scope.nodeAddr = function (nodeCfg) {
|
||||
if (nodeCfg.NodeID === $scope.myID) {
|
||||
return "this node";
|
||||
}
|
||||
var conn = $scope.connections[nodeCfg.NodeID];
|
||||
if (conn) {
|
||||
return conn.Address;
|
||||
}
|
||||
return nodeCfg.Addresses.join(", ");
|
||||
};
|
||||
|
||||
$scope.nodeVer = function (nodeCfg) {
|
||||
if (nodeCfg.NodeID === $scope.myID) {
|
||||
return $scope.version;
|
||||
}
|
||||
var conn = $scope.connections[nodeCfg.NodeID];
|
||||
if (conn) {
|
||||
return conn.ClientVersion;
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
$scope.saveSettings = function () {
|
||||
$http.post('/rest/config', JSON.stringify($scope.config), {headers: {'Content-Type': 'application/json'}});
|
||||
$('#settingsTable').collapse('hide');
|
||||
};
|
||||
|
||||
$scope.editNode = function (nodeCfg) {
|
||||
$scope.currentNode = nodeCfg;
|
||||
$scope.editingExisting = true;
|
||||
$scope.currentNode.AddressesStr = nodeCfg.Addresses.join(", ")
|
||||
$('#editNode').modal({backdrop: 'static', keyboard: false});
|
||||
};
|
||||
|
||||
$scope.addNode = function () {
|
||||
$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 = [];
|
||||
for (var 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.saveNode = function () {
|
||||
$('#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++) {
|
||||
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 == $scope.myID)
|
||||
return -1;
|
||||
if (b.NodeID == $scope.myID)
|
||||
return 1;
|
||||
if (a.NodeID < b.NodeID)
|
||||
return -1;
|
||||
return a.NodeID > b.NodeID;
|
||||
})
|
||||
|
||||
$scope.config.Repositories[0].Nodes = $scope.nodes;
|
||||
|
||||
$http.post('/rest/config', JSON.stringify($scope.config), {headers: {'Content-Type': 'application/json'}})
|
||||
};
|
||||
|
||||
$scope.refresh();
|
||||
setInterval($scope.refresh, 10000);
|
||||
});
|
||||
@ -90,7 +245,7 @@ syncthing.filter('natural', function() {
|
||||
syncthing.filter('binary', function() {
|
||||
return function(input) {
|
||||
if (input === undefined) {
|
||||
return '- '
|
||||
return '0 '
|
||||
}
|
||||
if (input > 1024 * 1024 * 1024) {
|
||||
input /= 1024 * 1024 * 1024;
|
||||
@ -111,7 +266,7 @@ syncthing.filter('binary', function() {
|
||||
syncthing.filter('metric', function() {
|
||||
return function(input) {
|
||||
if (input === undefined) {
|
||||
return '- '
|
||||
return '0 '
|
||||
}
|
||||
if (input > 1000 * 1000 * 1000) {
|
||||
input /= 1000 * 1000 * 1000;
|
||||
@ -143,3 +298,15 @@ syncthing.filter('alwaysNumber', function() {
|
||||
return input;
|
||||
}
|
||||
});
|
||||
|
||||
syncthing.directive('optionEditor', function() {
|
||||
return {
|
||||
restrict: 'C',
|
||||
replace: true,
|
||||
transclude: true,
|
||||
scope: {
|
||||
setting: '=setting',
|
||||
},
|
||||
template: '<input type="text" ng-model="config.Options[setting.id]"></input>',
|
||||
};
|
||||
})
|
||||
|
7
gui/bootstrap/css/bootstrap-theme.min.css
vendored
Executable file
7
gui/bootstrap/css/bootstrap-theme.min.css
vendored
Executable file
File diff suppressed because one or more lines are too long
12
gui/bootstrap/css/bootstrap.min.css
vendored
12
gui/bootstrap/css/bootstrap.min.css
vendored
File diff suppressed because one or more lines are too long
0
gui/bootstrap/fonts/glyphicons-halflings-regular.eot
Normal file → Executable file
0
gui/bootstrap/fonts/glyphicons-halflings-regular.eot
Normal file → Executable file
0
gui/bootstrap/fonts/glyphicons-halflings-regular.svg
Normal file → Executable file
0
gui/bootstrap/fonts/glyphicons-halflings-regular.svg
Normal file → Executable file
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
0
gui/bootstrap/fonts/glyphicons-halflings-regular.ttf
Normal file → Executable file
0
gui/bootstrap/fonts/glyphicons-halflings-regular.ttf
Normal file → Executable file
0
gui/bootstrap/fonts/glyphicons-halflings-regular.woff
Normal file → Executable file
0
gui/bootstrap/fonts/glyphicons-halflings-regular.woff
Normal file → Executable file
8
gui/bootstrap/js/bootstrap.min.js
vendored
Normal file → Executable file
8
gui/bootstrap/js/bootstrap.min.js
vendored
Normal file → Executable file
File diff suppressed because one or more lines are too long
168
gui/index.html
168
gui/index.html
@ -35,6 +35,11 @@ html, body {
|
||||
.text-monospace {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.table-condensed>thead>tr>th, .table-condensed>tbody>tr>th, .table-condensed>tfoot>tr>th, .table-condensed>thead>tr>td, .table-condensed>tbody>tr>td, .table-condensed>tfoot>tr>td {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
|
||||
@ -63,6 +68,60 @@ html, body {
|
||||
</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>
|
||||
<tr ng-repeat="nodeCfg in nodes" ng-class="{'text-muted': nodeCfg.NodeID == myID}">
|
||||
<td>
|
||||
<span class="label label-{{nodeClass(nodeCfg)}}">
|
||||
<span class="glyphicon glyphicon-{{nodeIcon(nodeCfg)}}"></span>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="text-monospace">{{nodeCfg.NodeID | short}}</span>
|
||||
</td>
|
||||
<td>
|
||||
{{nodeVer(nodeCfg)}}
|
||||
</td>
|
||||
<td>
|
||||
{{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>
|
||||
</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>
|
||||
</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>
|
||||
</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">
|
||||
@ -75,7 +134,9 @@ html, body {
|
||||
<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="panel-body">
|
||||
@ -83,57 +144,34 @@ html, body {
|
||||
<p>{{system.cpuPercent | alwaysNumber | natural:1}}% CPU, {{system.goroutines | alwaysNumber}} goroutines</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-show="model.needFiles > 0">
|
||||
<h2>Files to Synchronize</h2>
|
||||
<table class="table table-condensed table-striped">
|
||||
<tr ng-repeat="file in need track by $index">
|
||||
<td><abbr title="{{file.Name}}">{{file.ShortName}}</abbr></td>
|
||||
<td class="text-right">{{file.Size | binary}}B</td>
|
||||
</tr>
|
||||
</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">Cluster</h3></div>
|
||||
<table class="table table-condensed">
|
||||
<tbody>
|
||||
<tr ng-repeat="(node, address) in config.nodes" ng-class="{'text-primary': !!connections[node], 'text-muted': node == config.myID}">
|
||||
<td><span class="text-monospace">{{node | short}}</span></td>
|
||||
<td>
|
||||
<span ng-show="node != config.myID">{{connections[node].ClientVersion}}</span>
|
||||
<span ng-show="node == config.myID">{{version}}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span ng-show="node == config.myID">
|
||||
<span class="glyphicon glyphicon-ok"></span>
|
||||
(this node)
|
||||
</span>
|
||||
<span ng-show="node != config.myID && !!connections[node]">
|
||||
<span class="glyphicon glyphicon-link"></span>
|
||||
{{connections[node].Address}}
|
||||
</span>
|
||||
<span ng-show="node != config.myID && !connections[node]">
|
||||
<span class="glyphicon glyphicon-cog"></span>
|
||||
{{address}}
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<span ng-show="node != config.myID">
|
||||
<abbr title="{{connections[node].InBytesTotal | binary}}B">{{connections[node].inbps | metric}}b/s</abbr>
|
||||
<span class="text-muted glyphicon glyphicon-cloud-download"></span>
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<span ng-show="node != config.myID">
|
||||
<abbr title="{{connections[node].OutBytesTotal | binary}}B">{{connections[node].outbps | metric}}b/s</abbr>
|
||||
<span class="text-muted glyphicon glyphicon-cloud-upload"></span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<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>
|
||||
</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>
|
||||
@ -166,6 +204,40 @@ html, body {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="editNode" class="modal fade">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h4 class="modal-title">Edit Node</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form role="form">
|
||||
<div class="form-group">
|
||||
<label for="nodeID">Node ID</label>
|
||||
<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="addresses">Addresses</label>
|
||||
<input placeholder="dynamic" 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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" ng-click="saveNode()">Save</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
<button ng-if="editingExisting" type="button" class="btn btn-danger pull-left" ng-click="deleteNode()">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="angular.min.js"></script>
|
||||
<script src="jquery-2.0.3.min.js"></script>
|
||||
<script src="bootstrap/js/bootstrap.min.js"></script>
|
||||
|
9
gui_development.go
Normal file
9
gui_development.go
Normal file
@ -0,0 +1,9 @@
|
||||
//+build guidev
|
||||
|
||||
package main
|
||||
|
||||
import "github.com/codegangsta/martini"
|
||||
|
||||
func embeddedStatic() interface{} {
|
||||
return martini.Static("gui")
|
||||
}
|
46
gui_embedded.go
Normal file
46
gui_embedded.go
Normal file
@ -0,0 +1,46 @@
|
||||
//+build !guidev
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"mime"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/calmh/syncthing/auto"
|
||||
"github.com/cratonica/embed"
|
||||
)
|
||||
|
||||
func embeddedStatic() interface{} {
|
||||
fs, err := embed.Unpack(auto.Resources)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var modt = time.Now().UTC().Format(http.TimeFormat)
|
||||
|
||||
return func(res http.ResponseWriter, req *http.Request, log *log.Logger) {
|
||||
file := req.URL.Path
|
||||
|
||||
if file[0] == '/' {
|
||||
file = file[1:]
|
||||
}
|
||||
|
||||
bs, ok := fs[file]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
mtype := mime.TypeByExtension(filepath.Ext(req.URL.Path))
|
||||
if len(mtype) != 0 {
|
||||
res.Header().Set("Content-Type", mtype)
|
||||
}
|
||||
res.Header().Set("Content-Size", fmt.Sprintf("%d", len(bs)))
|
||||
res.Header().Set("Last-Modified", modt)
|
||||
|
||||
res.Write(bs)
|
||||
}
|
||||
}
|
195
main.go
195
main.go
@ -23,17 +23,12 @@ import (
|
||||
"github.com/calmh/syncthing/protocol"
|
||||
)
|
||||
|
||||
var opts Options
|
||||
var cfg Configuration
|
||||
var Version string = "unknown-dev"
|
||||
|
||||
const (
|
||||
confFileName = "syncthing.ini"
|
||||
)
|
||||
|
||||
var (
|
||||
myID string
|
||||
config ini.Config
|
||||
nodeAddrs = make(map[string][]string)
|
||||
myID string
|
||||
config ini.Config
|
||||
)
|
||||
|
||||
var (
|
||||
@ -89,46 +84,75 @@ func main() {
|
||||
log.SetPrefix("[" + myID[0:5] + "] ")
|
||||
logger.SetPrefix("[" + myID[0:5] + "] ")
|
||||
|
||||
// Prepare to be able to save configuration
|
||||
|
||||
cfgFile := path.Join(confDir, "config.xml")
|
||||
go saveConfigLoop(cfgFile)
|
||||
|
||||
// Load the configuration file, if it exists.
|
||||
// If it does not, create a template.
|
||||
|
||||
cfgFile := path.Join(confDir, confFileName)
|
||||
cf, err := os.Open(cfgFile)
|
||||
|
||||
if err != nil {
|
||||
infoln("My ID:", myID)
|
||||
|
||||
infoln("No config file; creating a template")
|
||||
|
||||
loadConfig(nil, &opts) //loads defaults
|
||||
fd, err := os.Create(cfgFile)
|
||||
if err == nil {
|
||||
// Read config.xml
|
||||
cfg, err = readConfigXML(cf)
|
||||
if err != nil {
|
||||
fatalln(err)
|
||||
}
|
||||
cf.Close()
|
||||
} else {
|
||||
// No config.xml, let's try the old syncthing.ini
|
||||
iniFile := path.Join(confDir, "syncthing.ini")
|
||||
cf, err := os.Open(iniFile)
|
||||
if err == nil {
|
||||
infoln("Migrating syncthing.ini to config.xml")
|
||||
iniCfg := ini.Parse(cf)
|
||||
cf.Close()
|
||||
os.Rename(iniFile, path.Join(confDir, "migrated_syncthing.ini"))
|
||||
|
||||
writeConfig(fd, "~/Sync", map[string]string{myID: "dynamic"}, opts, true)
|
||||
fd.Close()
|
||||
infof("Edit %s to suit and restart syncthing.", cfgFile)
|
||||
cfg, _ = readConfigXML(nil)
|
||||
cfg.Repositories = []RepositoryConfiguration{
|
||||
{Directory: iniCfg.Get("repository", "dir")},
|
||||
}
|
||||
readConfigINI(iniCfg.OptionMap("settings"), &cfg.Options)
|
||||
for name, addrs := range iniCfg.OptionMap("nodes") {
|
||||
n := NodeConfiguration{
|
||||
NodeID: name,
|
||||
Addresses: strings.Fields(addrs),
|
||||
}
|
||||
cfg.Repositories[0].Nodes = append(cfg.Repositories[0].Nodes, n)
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
saveConfig()
|
||||
}
|
||||
}
|
||||
|
||||
config = ini.Parse(cf)
|
||||
cf.Close()
|
||||
if len(cfg.Repositories) == 0 {
|
||||
infoln("No config file; starting with empty defaults")
|
||||
|
||||
loadConfig(config.OptionMap("settings"), &opts)
|
||||
cfg, err = readConfigXML(nil)
|
||||
cfg.Repositories = []RepositoryConfiguration{
|
||||
{
|
||||
Directory: "~/Sync",
|
||||
Nodes: []NodeConfiguration{
|
||||
{NodeID: myID, Addresses: []string{"dynamic"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
saveConfig()
|
||||
infof("Edit %s to taste or use the GUI\n", cfgFile)
|
||||
}
|
||||
|
||||
if showConfig {
|
||||
writeConfig(os.Stdout,
|
||||
config.Get("repository", "dir"),
|
||||
config.OptionMap("nodes"), opts, false)
|
||||
writeConfigXML(os.Stdout, cfg)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
infoln("Version", Version)
|
||||
infoln("My ID:", myID)
|
||||
|
||||
var dir = expandTilde(config.Get("repository", "dir"))
|
||||
var dir = expandTilde(cfg.Repositories[0].Directory)
|
||||
if len(dir) == 0 {
|
||||
fatalln("No repository directory. Set dir under [repository] in syncthing.ini.")
|
||||
}
|
||||
@ -145,7 +169,7 @@ func main() {
|
||||
// The TLS configuration is used for both the listening socket and outgoing
|
||||
// connections.
|
||||
|
||||
cfg := &tls.Config{
|
||||
tlsCfg := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
NextProtos: []string{"bep/1.0"},
|
||||
ServerName: myID,
|
||||
@ -155,35 +179,27 @@ func main() {
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
|
||||
// Create a map of desired node connections based on the configuration file
|
||||
// directives.
|
||||
|
||||
for nodeID, addrs := range config.OptionMap("nodes") {
|
||||
addrs := strings.Fields(addrs)
|
||||
nodeAddrs[nodeID] = addrs
|
||||
}
|
||||
|
||||
ensureDir(dir, -1)
|
||||
m := model.NewModel(dir, opts.MaxChangeBW*1000)
|
||||
m := model.NewModel(dir, cfg.Options.MaxChangeKbps*1000)
|
||||
for _, t := range strings.Split(trace, ",") {
|
||||
m.Trace(t)
|
||||
}
|
||||
if opts.LimitRate > 0 {
|
||||
m.LimitRate(opts.LimitRate)
|
||||
if cfg.Options.MaxSendKbps > 0 {
|
||||
m.LimitRate(cfg.Options.MaxSendKbps)
|
||||
}
|
||||
|
||||
// GUI
|
||||
if opts.GUI && opts.GUIAddr != "" {
|
||||
host, port, err := net.SplitHostPort(opts.GUIAddr)
|
||||
if cfg.Options.GUIEnabled && cfg.Options.GUIAddress != "" {
|
||||
host, port, err := net.SplitHostPort(cfg.Options.GUIAddress)
|
||||
if err != nil {
|
||||
warnf("Cannot start GUI on %q: %v", opts.GUIAddr, err)
|
||||
warnf("Cannot start GUI on %q: %v", cfg.Options.GUIAddress, err)
|
||||
} else {
|
||||
if len(host) > 0 {
|
||||
infof("Starting web GUI on http://%s", opts.GUIAddr)
|
||||
infof("Starting web GUI on http://%s", cfg.Options.GUIAddress)
|
||||
} else {
|
||||
infof("Starting web GUI on port %s", port)
|
||||
}
|
||||
startGUI(opts.GUIAddr, m)
|
||||
startGUI(cfg.Options.GUIAddress, m)
|
||||
}
|
||||
}
|
||||
|
||||
@ -196,22 +212,22 @@ func main() {
|
||||
|
||||
// Routine to listen for incoming connections
|
||||
infoln("Listening for incoming connections")
|
||||
go listen(myID, opts.Listen, m, cfg)
|
||||
go listen(myID, cfg.Options.ListenAddress, m, tlsCfg)
|
||||
|
||||
// Routine to connect out to configured nodes
|
||||
infoln("Attempting to connect to other nodes")
|
||||
go connect(myID, opts.Listen, nodeAddrs, m, cfg)
|
||||
go connect(myID, cfg.Options.ListenAddress, 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.
|
||||
if !opts.ReadOnly {
|
||||
if opts.Delete {
|
||||
if !cfg.Options.ReadOnly {
|
||||
if cfg.Options.AllowDelete {
|
||||
infoln("Deletes from peer nodes are allowed")
|
||||
} else {
|
||||
infoln("Deletes from peer nodes will be ignored")
|
||||
}
|
||||
okln("Ready to synchronize (read-write)")
|
||||
m.StartRW(opts.Delete, opts.ParallelRequests)
|
||||
m.StartRW(cfg.Options.AllowDelete, cfg.Options.ParallelRequests)
|
||||
} else {
|
||||
okln("Ready to synchronize (read only; no external updates accepted)")
|
||||
}
|
||||
@ -219,9 +235,10 @@ func main() {
|
||||
// Periodically scan the repository and update the local model.
|
||||
// XXX: Should use some fsnotify mechanism.
|
||||
go func() {
|
||||
td := time.Duration(cfg.Options.RescanIntervalS) * time.Second
|
||||
for {
|
||||
time.Sleep(opts.ScanInterval)
|
||||
if m.LocalAge() > opts.ScanInterval.Seconds()/2 {
|
||||
time.Sleep(td)
|
||||
if m.LocalAge() > (td / 2).Seconds() {
|
||||
updateLocalModel(m)
|
||||
}
|
||||
}
|
||||
@ -233,6 +250,40 @@ func main() {
|
||||
select {}
|
||||
}
|
||||
|
||||
var saveConfigCh = make(chan struct{})
|
||||
|
||||
func saveConfigLoop(cfgFile string) {
|
||||
for _ = range saveConfigCh {
|
||||
fd, err := os.Create(cfgFile + ".tmp")
|
||||
if err != nil {
|
||||
warnln(err)
|
||||
continue
|
||||
}
|
||||
|
||||
err = writeConfigXML(fd, cfg)
|
||||
if err != nil {
|
||||
warnln(err)
|
||||
fd.Close()
|
||||
continue
|
||||
}
|
||||
|
||||
err = fd.Close()
|
||||
if err != nil {
|
||||
warnln(err)
|
||||
continue
|
||||
}
|
||||
|
||||
err = os.Rename(cfgFile+".tmp", cfgFile)
|
||||
if err != nil {
|
||||
warnln(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func saveConfig() {
|
||||
saveConfigCh <- struct{}{}
|
||||
}
|
||||
|
||||
func printStatsLoop(m *model.Model) {
|
||||
var lastUpdated int64
|
||||
var lastStats = make(map[string]model.ConnectionInfo)
|
||||
@ -264,8 +315,8 @@ func printStatsLoop(m *model.Model) {
|
||||
}
|
||||
}
|
||||
|
||||
func listen(myID string, addr string, m *model.Model, cfg *tls.Config) {
|
||||
l, err := tls.Listen("tcp", addr, cfg)
|
||||
func listen(myID string, addr string, m *model.Model, tlsCfg *tls.Config) {
|
||||
l, err := tls.Listen("tcp", addr, tlsCfg)
|
||||
fatalErr(err)
|
||||
|
||||
connOpts := map[string]string{
|
||||
@ -305,8 +356,8 @@ listen:
|
||||
warnf("Connect from connected node (%s)", remoteID)
|
||||
}
|
||||
|
||||
for nodeID := range nodeAddrs {
|
||||
if nodeID == remoteID {
|
||||
for _, nodeCfg := range cfg.Repositories[0].Nodes {
|
||||
if nodeCfg.NodeID == remoteID {
|
||||
protoConn := protocol.NewConnection(remoteID, conn, conn, m, connOpts)
|
||||
m.AddConnection(conn, protoConn)
|
||||
continue listen
|
||||
@ -316,24 +367,24 @@ listen:
|
||||
}
|
||||
}
|
||||
|
||||
func connect(myID string, addr string, nodeAddrs map[string][]string, m *model.Model, cfg *tls.Config) {
|
||||
func connect(myID string, addr string, m *model.Model, tlsCfg *tls.Config) {
|
||||
_, portstr, err := net.SplitHostPort(addr)
|
||||
fatalErr(err)
|
||||
port, _ := strconv.Atoi(portstr)
|
||||
|
||||
if !opts.LocalDiscovery {
|
||||
if !cfg.Options.LocalAnnEnabled {
|
||||
port = -1
|
||||
} else {
|
||||
infoln("Sending local discovery announcements")
|
||||
}
|
||||
|
||||
if !opts.ExternalDiscovery {
|
||||
opts.ExternalServer = ""
|
||||
if !cfg.Options.GlobalAnnEnabled {
|
||||
cfg.Options.GlobalAnnServer = ""
|
||||
} else {
|
||||
infoln("Sending external discovery announcements")
|
||||
}
|
||||
|
||||
disc, err := discover.NewDiscoverer(myID, port, opts.ExternalServer)
|
||||
disc, err := discover.NewDiscoverer(myID, port, cfg.Options.GlobalAnnServer)
|
||||
|
||||
if err != nil {
|
||||
warnf("No discovery possible (%v)", err)
|
||||
@ -346,18 +397,18 @@ func connect(myID string, addr string, nodeAddrs map[string][]string, m *model.M
|
||||
|
||||
for {
|
||||
nextNode:
|
||||
for nodeID, addrs := range nodeAddrs {
|
||||
if nodeID == myID {
|
||||
for _, nodeCfg := range cfg.Repositories[0].Nodes {
|
||||
if nodeCfg.NodeID == myID {
|
||||
continue
|
||||
}
|
||||
if m.ConnectedTo(nodeID) {
|
||||
if m.ConnectedTo(nodeCfg.NodeID) {
|
||||
continue
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
for _, addr := range nodeCfg.Addresses {
|
||||
if addr == "dynamic" {
|
||||
var ok bool
|
||||
if disc != nil {
|
||||
addr, ok = disc.Lookup(nodeID)
|
||||
addr, ok = disc.Lookup(nodeCfg.NodeID)
|
||||
}
|
||||
if !ok {
|
||||
continue
|
||||
@ -365,9 +416,9 @@ func connect(myID string, addr string, nodeAddrs map[string][]string, m *model.M
|
||||
}
|
||||
|
||||
if strings.Contains(trace, "connect") {
|
||||
debugln("NET: Dial", nodeID, addr)
|
||||
debugln("NET: Dial", nodeCfg.NodeID, addr)
|
||||
}
|
||||
conn, err := tls.Dial("tcp", addr, cfg)
|
||||
conn, err := tls.Dial("tcp", addr, tlsCfg)
|
||||
if err != nil {
|
||||
if strings.Contains(trace, "connect") {
|
||||
debugln("NET:", err)
|
||||
@ -376,8 +427,8 @@ func connect(myID string, addr string, nodeAddrs map[string][]string, m *model.M
|
||||
}
|
||||
|
||||
remoteID := certId(conn.ConnectionState().PeerCertificates[0].Raw)
|
||||
if remoteID != nodeID {
|
||||
warnln("Unexpected nodeID", remoteID, "!=", nodeID)
|
||||
if remoteID != nodeCfg.NodeID {
|
||||
warnln("Unexpected nodeID", remoteID, "!=", nodeCfg.NodeID)
|
||||
conn.Close()
|
||||
continue
|
||||
}
|
||||
@ -388,12 +439,12 @@ func connect(myID string, addr string, nodeAddrs map[string][]string, m *model.M
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(opts.ConnInterval)
|
||||
time.Sleep(time.Duration(cfg.Options.ReconnectIntervalS) * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func updateLocalModel(m *model.Model) {
|
||||
files, _ := m.Walk(opts.Symlinks)
|
||||
files, _ := m.Walk(cfg.Options.FollowSymlinks)
|
||||
m.ReplaceLocal(files)
|
||||
saveIndex(m)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user