mirror of
https://github.com/octoleo/syncthing.git
synced 2024-11-08 22:31:04 +00:00
Use event interface for GUI (fixes #383)
This commit is contained in:
parent
9c99d65716
commit
e27d42935c
File diff suppressed because one or more lines are too long
@ -47,7 +47,7 @@ var (
|
|||||||
static func(http.ResponseWriter, *http.Request, *log.Logger)
|
static func(http.ResponseWriter, *http.Request, *log.Logger)
|
||||||
apiKey string
|
apiKey string
|
||||||
modt = time.Now().UTC().Format(http.TimeFormat)
|
modt = time.Now().UTC().Format(http.TimeFormat)
|
||||||
eventSub = events.NewBufferedSubscription(events.Default.Subscribe(events.AllEvents), 1000)
|
eventSub *events.BufferedSubscription
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -56,6 +56,8 @@ const (
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
l.AddHandler(logger.LevelWarn, showGuiError)
|
l.AddHandler(logger.LevelWarn, showGuiError)
|
||||||
|
sub := events.Default.Subscribe(^events.EventType(events.ItemStarted | events.ItemCompleted))
|
||||||
|
eventSub = events.NewBufferedSubscription(sub, 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) error {
|
func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) error {
|
||||||
@ -92,32 +94,33 @@ func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) erro
|
|||||||
|
|
||||||
// The GET handlers
|
// The GET handlers
|
||||||
getRestMux := http.NewServeMux()
|
getRestMux := http.NewServeMux()
|
||||||
getRestMux.HandleFunc("/rest/version", restGetVersion)
|
getRestMux.HandleFunc("/rest/completion", withModel(m, restGetCompletion))
|
||||||
|
getRestMux.HandleFunc("/rest/config", restGetConfig)
|
||||||
|
getRestMux.HandleFunc("/rest/config/sync", restGetConfigInSync)
|
||||||
|
getRestMux.HandleFunc("/rest/connections", withModel(m, restGetConnections))
|
||||||
|
getRestMux.HandleFunc("/rest/discovery", restGetDiscovery)
|
||||||
|
getRestMux.HandleFunc("/rest/errors", restGetErrors)
|
||||||
|
getRestMux.HandleFunc("/rest/events", restGetEvents)
|
||||||
|
getRestMux.HandleFunc("/rest/lang", restGetLang)
|
||||||
getRestMux.HandleFunc("/rest/model", withModel(m, restGetModel))
|
getRestMux.HandleFunc("/rest/model", withModel(m, restGetModel))
|
||||||
getRestMux.HandleFunc("/rest/model/version", withModel(m, restGetModelVersion))
|
getRestMux.HandleFunc("/rest/model/version", withModel(m, restGetModelVersion))
|
||||||
getRestMux.HandleFunc("/rest/need", withModel(m, restGetNeed))
|
getRestMux.HandleFunc("/rest/need", withModel(m, restGetNeed))
|
||||||
getRestMux.HandleFunc("/rest/connections", withModel(m, restGetConnections))
|
|
||||||
getRestMux.HandleFunc("/rest/config", restGetConfig)
|
|
||||||
getRestMux.HandleFunc("/rest/config/sync", restGetConfigInSync)
|
|
||||||
getRestMux.HandleFunc("/rest/system", restGetSystem)
|
|
||||||
getRestMux.HandleFunc("/rest/errors", restGetErrors)
|
|
||||||
getRestMux.HandleFunc("/rest/discovery", restGetDiscovery)
|
|
||||||
getRestMux.HandleFunc("/rest/report", withModel(m, restGetReport))
|
|
||||||
getRestMux.HandleFunc("/rest/events", restGetEvents)
|
|
||||||
getRestMux.HandleFunc("/rest/upgrade", restGetUpgrade)
|
|
||||||
getRestMux.HandleFunc("/rest/nodeid", restGetNodeID)
|
getRestMux.HandleFunc("/rest/nodeid", restGetNodeID)
|
||||||
getRestMux.HandleFunc("/rest/lang", restGetLang)
|
getRestMux.HandleFunc("/rest/report", withModel(m, restGetReport))
|
||||||
|
getRestMux.HandleFunc("/rest/system", restGetSystem)
|
||||||
|
getRestMux.HandleFunc("/rest/upgrade", restGetUpgrade)
|
||||||
|
getRestMux.HandleFunc("/rest/version", restGetVersion)
|
||||||
|
|
||||||
// The POST handlers
|
// The POST handlers
|
||||||
postRestMux := http.NewServeMux()
|
postRestMux := http.NewServeMux()
|
||||||
postRestMux.HandleFunc("/rest/config", withModel(m, restPostConfig))
|
postRestMux.HandleFunc("/rest/config", withModel(m, restPostConfig))
|
||||||
postRestMux.HandleFunc("/rest/restart", restPostRestart)
|
postRestMux.HandleFunc("/rest/discovery/hint", restPostDiscoveryHint)
|
||||||
postRestMux.HandleFunc("/rest/reset", restPostReset)
|
|
||||||
postRestMux.HandleFunc("/rest/shutdown", restPostShutdown)
|
|
||||||
postRestMux.HandleFunc("/rest/error", restPostError)
|
postRestMux.HandleFunc("/rest/error", restPostError)
|
||||||
postRestMux.HandleFunc("/rest/error/clear", restClearErrors)
|
postRestMux.HandleFunc("/rest/error/clear", restClearErrors)
|
||||||
postRestMux.HandleFunc("/rest/discovery/hint", restPostDiscoveryHint)
|
|
||||||
postRestMux.HandleFunc("/rest/model/override", withModel(m, restPostOverride))
|
postRestMux.HandleFunc("/rest/model/override", withModel(m, restPostOverride))
|
||||||
|
postRestMux.HandleFunc("/rest/reset", restPostReset)
|
||||||
|
postRestMux.HandleFunc("/rest/restart", restPostRestart)
|
||||||
|
postRestMux.HandleFunc("/rest/shutdown", restPostShutdown)
|
||||||
postRestMux.HandleFunc("/rest/upgrade", restPostUpgrade)
|
postRestMux.HandleFunc("/rest/upgrade", restPostUpgrade)
|
||||||
|
|
||||||
// A handler that splits requests between the two above and disables
|
// A handler that splits requests between the two above and disables
|
||||||
@ -175,6 +178,25 @@ func restGetVersion(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Write([]byte(Version))
|
w.Write([]byte(Version))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func restGetCompletion(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||||
|
var qs = r.URL.Query()
|
||||||
|
var repo = qs.Get("repo")
|
||||||
|
var nodeStr = qs.Get("node")
|
||||||
|
|
||||||
|
node, err := protocol.NodeIDFromString(nodeStr)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res := map[string]float64{
|
||||||
|
"completion": m.Completion(node, repo),
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
json.NewEncoder(w).Encode(res)
|
||||||
|
}
|
||||||
|
|
||||||
func restGetModelVersion(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
func restGetModelVersion(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||||
var qs = r.URL.Query()
|
var qs = r.URL.Query()
|
||||||
var repo = qs.Get("repo")
|
var repo = qs.Get("repo")
|
||||||
@ -423,11 +445,18 @@ func restGetReport(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
func restGetEvents(w http.ResponseWriter, r *http.Request) {
|
func restGetEvents(w http.ResponseWriter, r *http.Request) {
|
||||||
qs := r.URL.Query()
|
qs := r.URL.Query()
|
||||||
ts := qs.Get("since")
|
sinceStr := qs.Get("since")
|
||||||
since, _ := strconv.Atoi(ts)
|
limitStr := qs.Get("limit")
|
||||||
|
since, _ := strconv.Atoi(sinceStr)
|
||||||
|
limit, _ := strconv.Atoi(limitStr)
|
||||||
|
|
||||||
|
evs := eventSub.Since(since, nil)
|
||||||
|
if 0 < limit && limit < len(evs) {
|
||||||
|
evs = evs[len(evs)-limit:]
|
||||||
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
json.NewEncoder(w).Encode(eventSub.Since(since, nil))
|
json.NewEncoder(w).Encode(evs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func restGetUpgrade(w http.ResponseWriter, r *http.Request) {
|
func restGetUpgrade(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -18,6 +18,12 @@ type translation struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
log.SetFlags(log.Lshortfile)
|
||||||
|
|
||||||
|
if u, p := userPass(); u == "" || p == "" {
|
||||||
|
log.Fatal("Need environment variables TRANSIFEX_USER and TRANSIFEX_PASS")
|
||||||
|
}
|
||||||
|
|
||||||
resp := req("https://www.transifex.com/api/2/project/syncthing/resource/gui/stats")
|
resp := req("https://www.transifex.com/api/2/project/syncthing/resource/gui/stats")
|
||||||
|
|
||||||
var stats map[string]stat
|
var stats map[string]stat
|
||||||
@ -63,9 +69,14 @@ func main() {
|
|||||||
json.NewEncoder(os.Stdout).Encode(langs)
|
json.NewEncoder(os.Stdout).Encode(langs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func req(url string) *http.Response {
|
func userPass() (string, string) {
|
||||||
user := os.Getenv("TRANSIFEX_USER")
|
user := os.Getenv("TRANSIFEX_USER")
|
||||||
pass := os.Getenv("TRANSIFEX_PASS")
|
pass := os.Getenv("TRANSIFEX_PASS")
|
||||||
|
return user, pass
|
||||||
|
}
|
||||||
|
|
||||||
|
func req(url string) *http.Response {
|
||||||
|
user, pass := userPass()
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", url, nil)
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -632,9 +632,6 @@ func ldbWithNeed(db *leveldb.DB, repo, node []byte, fn fileIterator) {
|
|||||||
|
|
||||||
if need || !have {
|
if need || !have {
|
||||||
name := globalKeyName(dbi.Key())
|
name := globalKeyName(dbi.Key())
|
||||||
if debug {
|
|
||||||
l.Debugf("need repo=%q node=%v name=%q need=%v have=%v haveV=%d globalV=%d", repo, protocol.NodeIDFromBytes(node), name, need, have, haveVersion, vl.versions[0].version)
|
|
||||||
}
|
|
||||||
fk := nodeKey(repo, vl.versions[0].node, name)
|
fk := nodeKey(repo, vl.versions[0].node, name)
|
||||||
bs, err := snap.Get(fk, nil)
|
bs, err := snap.Get(fk, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -652,6 +649,10 @@ func ldbWithNeed(db *leveldb.DB, repo, node []byte, fn fileIterator) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if debug {
|
||||||
|
l.Debugf("need repo=%q node=%v name=%q need=%v have=%v haveV=%d globalV=%d", repo, protocol.NodeIDFromBytes(node), name, need, have, haveVersion, vl.versions[0].version)
|
||||||
|
}
|
||||||
|
|
||||||
if cont := fn(gf); !cont {
|
if cont := fn(gf); !cont {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -85,7 +85,7 @@ func (s *Set) Update(node protocol.NodeID, fs []protocol.FileInfo) {
|
|||||||
|
|
||||||
func (s *Set) WithNeed(node protocol.NodeID, fn fileIterator) {
|
func (s *Set) WithNeed(node protocol.NodeID, fn fileIterator) {
|
||||||
if debug {
|
if debug {
|
||||||
l.Debugf("%s Need(%v)", s.repo, node)
|
l.Debugf("%s WithNeed(%v)", s.repo, node)
|
||||||
}
|
}
|
||||||
ldbWithNeed(s.db, []byte(s.repo), node[:], fn)
|
ldbWithNeed(s.db, []byte(s.repo), node[:], fn)
|
||||||
}
|
}
|
||||||
|
357
gui/app.js
357
gui/app.js
@ -21,23 +21,68 @@ syncthing.config(function ($httpProvider, $translateProvider) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
syncthing.controller('EventCtrl', function ($scope, $http) {
|
||||||
|
$scope.lastEvent = null;
|
||||||
|
var online = false;
|
||||||
|
var lastID = 0;
|
||||||
|
|
||||||
|
var successFn = function (data) {
|
||||||
|
if (!online) {
|
||||||
|
$scope.$emit('UIOnline');
|
||||||
|
online = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastID > 0) {
|
||||||
|
data.forEach(function (event) {
|
||||||
|
$scope.$emit(event.type, event);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.lastEvent = data[data.length - 1];
|
||||||
|
lastID = $scope.lastEvent.id;
|
||||||
|
|
||||||
|
setTimeout(function () {
|
||||||
|
$http.get(urlbase + '/events?since=' + lastID)
|
||||||
|
.success(successFn)
|
||||||
|
.error(errorFn);
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
var errorFn = function (data) {
|
||||||
|
if (online) {
|
||||||
|
$scope.$emit('UIOffline');
|
||||||
|
online = false;
|
||||||
|
}
|
||||||
|
setTimeout(function () {
|
||||||
|
$http.get(urlbase + '/events?since=' + lastID)
|
||||||
|
.success(successFn)
|
||||||
|
.error(errorFn);
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
$http.get(urlbase + '/events?limit=1')
|
||||||
|
.success(successFn)
|
||||||
|
.error(errorFn);
|
||||||
|
});
|
||||||
|
|
||||||
syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $location) {
|
syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $location) {
|
||||||
var prevDate = 0;
|
var prevDate = 0;
|
||||||
var getOK = true;
|
var getOK = true;
|
||||||
var restarting = false;
|
var restarting = false;
|
||||||
|
|
||||||
$scope.connections = {};
|
$scope.completion = {};
|
||||||
$scope.config = {};
|
$scope.config = {};
|
||||||
|
$scope.configInSync = true;
|
||||||
|
$scope.connections = {};
|
||||||
|
$scope.errors = [];
|
||||||
|
$scope.model = {};
|
||||||
$scope.myID = '';
|
$scope.myID = '';
|
||||||
$scope.nodes = [];
|
$scope.nodes = [];
|
||||||
$scope.configInSync = true;
|
|
||||||
$scope.protocolChanged = false;
|
$scope.protocolChanged = false;
|
||||||
$scope.errors = [];
|
|
||||||
$scope.seenError = '';
|
|
||||||
$scope.model = {};
|
|
||||||
$scope.repos = {};
|
|
||||||
$scope.reportData = {};
|
$scope.reportData = {};
|
||||||
$scope.reportPreview = false;
|
$scope.reportPreview = false;
|
||||||
|
$scope.repos = {};
|
||||||
|
$scope.seenError = '';
|
||||||
$scope.upgradeInfo = {};
|
$scope.upgradeInfo = {};
|
||||||
|
|
||||||
$http.get(urlbase+"/lang").success(function (langs) {
|
$http.get(urlbase+"/lang").success(function (langs) {
|
||||||
@ -71,53 +116,118 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
|||||||
'touch': 'asterisk',
|
'touch': 'asterisk',
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSucceeded() {
|
$scope.$on('UIOnline', function (event, arg) {
|
||||||
if (!getOK) {
|
$scope.init();
|
||||||
$scope.init();
|
$('#networkError').modal('hide');
|
||||||
$('#networkError').modal('hide');
|
$('#restarting').modal('hide');
|
||||||
getOK = true;
|
$('#shutdown').modal('hide');
|
||||||
}
|
});
|
||||||
if (restarting) {
|
|
||||||
$scope.init();
|
|
||||||
$('#restarting').modal('hide');
|
|
||||||
$('#shutdown').modal('hide');
|
|
||||||
restarting = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFailed() {
|
$scope.$on('UIOffline', function (event, arg) {
|
||||||
if (restarting) {
|
if (!restarting) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (getOK) {
|
|
||||||
$('#networkError').modal({backdrop: 'static', keyboard: false});
|
$('#networkError').modal({backdrop: 'static', keyboard: false});
|
||||||
getOK = false;
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.$on('StateChanged', function (event, arg) {
|
||||||
|
var data = arg.data;
|
||||||
|
if ($scope.model[data.repo]) {
|
||||||
|
$scope.model[data.repo].state = data.to;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.$on('LocalIndexUpdated', function (event, arg) {
|
||||||
|
var data = arg.data;
|
||||||
|
refreshRepo(data.repo);
|
||||||
|
|
||||||
|
// Update completion status for all nodes that we share this repo with.
|
||||||
|
$scope.repos[data.repo].Nodes.forEach(function (nodeCfg) {
|
||||||
|
debouncedRefreshCompletion(nodeCfg.NodeID, data.repo);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.$on('RemoteIndexUpdated', function (event, arg) {
|
||||||
|
var data = arg.data;
|
||||||
|
refreshRepo(data.repo);
|
||||||
|
refreshCompletion(data.node, data.repo);
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.$on('NodeDisconnected', function (event, arg) {
|
||||||
|
delete $scope.connections[arg.data.id];
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.$on('NodeConnected', function (event, arg) {
|
||||||
|
if (!$scope.connections[arg.data.id]) {
|
||||||
|
$scope.connections[arg.data.id] = {
|
||||||
|
inbps: 0,
|
||||||
|
outbps: 0,
|
||||||
|
InBytesTotal: 0,
|
||||||
|
OutBytesTotal: 0,
|
||||||
|
Address: arg.data.addr,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.$on('ConfigLoaded', function (event) {
|
||||||
|
if ($scope.config.Options.URAccepted == 0) {
|
||||||
|
// If usage reporting has been neither accepted nor declined,
|
||||||
|
// we want to ask the user to make a choice. But we don't want
|
||||||
|
// to bug them during initial setup, so we set a cookie with
|
||||||
|
// the time of the first visit. When that cookie is present
|
||||||
|
// and the time is more than four hours ago, we ask the
|
||||||
|
// question.
|
||||||
|
|
||||||
|
var firstVisit = document.cookie.replace(/(?:(?:^|.*;\s*)firstVisit\s*\=\s*([^;]*).*$)|^.*$/, "$1");
|
||||||
|
if (!firstVisit) {
|
||||||
|
document.cookie = "firstVisit=" + Date.now() + ";max-age=" + 30*24*3600;
|
||||||
|
} else {
|
||||||
|
if (+firstVisit < Date.now() - 4*3600*1000){
|
||||||
|
$('#ur').modal({backdrop: 'static', keyboard: false});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function refreshRepo(repo) {
|
||||||
|
$http.get(urlbase + '/model?repo=' + encodeURIComponent(repo)).success(function (data) {
|
||||||
|
$scope.model[repo] = data;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.refresh = function () {
|
function refreshSystem() {
|
||||||
$http.get(urlbase + '/system').success(function (data) {
|
$http.get(urlbase + '/system').success(function (data) {
|
||||||
getSucceeded();
|
$scope.myID = data.myID;
|
||||||
$scope.system = data;
|
$scope.system = data;
|
||||||
}).error(function () {
|
|
||||||
getFailed();
|
|
||||||
});
|
});
|
||||||
Object.keys($scope.repos).forEach(function (id) {
|
}
|
||||||
if (typeof $scope.model[id] === 'undefined') {
|
|
||||||
// Never fetched before
|
var completionFuncs = {};
|
||||||
$http.get(urlbase + '/model?repo=' + encodeURIComponent(id)).success(function (data) {
|
function refreshCompletion(node, repo) {
|
||||||
$scope.model[id] = data;
|
if (node === $scope.myID) {
|
||||||
});
|
return
|
||||||
} else {
|
}
|
||||||
$http.get(urlbase + '/model/version?repo=' + encodeURIComponent(id)).success(function (data) {
|
|
||||||
if (data.version > $scope.model[id].version) {
|
if (!completionFuncs[node+repo]) {
|
||||||
$http.get(urlbase + '/model?repo=' + encodeURIComponent(id)).success(function (data) {
|
completionFuncs[node+repo] = debounce(function () {
|
||||||
$scope.model[id] = data;
|
$http.get(urlbase + '/completion?node=' + node + '&repo=' + encodeURIComponent(repo)).success(function (data) {
|
||||||
});
|
if (!$scope.completion[node]) {
|
||||||
|
$scope.completion[node] = {};
|
||||||
}
|
}
|
||||||
|
$scope.completion[node][repo] = data.completion;
|
||||||
|
|
||||||
|
var tot = 0, cnt = 0;
|
||||||
|
for (var cmp in $scope.completion[node]) {
|
||||||
|
tot += $scope.completion[node][cmp];
|
||||||
|
cnt += 1;
|
||||||
|
}
|
||||||
|
$scope.completion[node]._total = tot / cnt;
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
});
|
}
|
||||||
|
completionFuncs[node+repo]();
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshConnectionStats() {
|
||||||
$http.get(urlbase + '/connections').success(function (data) {
|
$http.get(urlbase + '/connections').success(function (data) {
|
||||||
var now = Date.now(),
|
var now = Date.now(),
|
||||||
td = (now - prevDate) / 1000,
|
td = (now - prevDate) / 1000,
|
||||||
@ -138,9 +248,66 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
|||||||
}
|
}
|
||||||
$scope.connections = data;
|
$scope.connections = data;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshErrors() {
|
||||||
$http.get(urlbase + '/errors').success(function (data) {
|
$http.get(urlbase + '/errors').success(function (data) {
|
||||||
$scope.errors = data;
|
$scope.errors = data;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshConfig() {
|
||||||
|
$http.get(urlbase + '/config').success(function (data) {
|
||||||
|
var hasConfig = !isEmptyObject($scope.config);
|
||||||
|
|
||||||
|
$scope.config = data;
|
||||||
|
$scope.config.Options.ListenStr = $scope.config.Options.ListenAddress.join(', ');
|
||||||
|
|
||||||
|
$scope.nodes = $scope.config.Nodes;
|
||||||
|
$scope.nodes.sort(nodeCompare);
|
||||||
|
|
||||||
|
$scope.repos = repoMap($scope.config.Repositories);
|
||||||
|
Object.keys($scope.repos).forEach(function (repo) {
|
||||||
|
refreshRepo(repo);
|
||||||
|
$scope.repos[repo].Nodes.forEach(function (nodeCfg) {
|
||||||
|
refreshCompletion(nodeCfg.NodeID, repo);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!hasConfig) {
|
||||||
|
$scope.$emit('ConfigLoaded');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$http.get(urlbase + '/config/sync').success(function (data) {
|
||||||
|
$scope.configInSync = data.configInSync;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.init = function() {
|
||||||
|
refreshSystem();
|
||||||
|
refreshConfig();
|
||||||
|
refreshConnectionStats();
|
||||||
|
|
||||||
|
$http.get(urlbase + '/version').success(function (data) {
|
||||||
|
$scope.version = data;
|
||||||
|
});
|
||||||
|
|
||||||
|
$http.get(urlbase + '/report').success(function (data) {
|
||||||
|
$scope.reportData = data;
|
||||||
|
});
|
||||||
|
|
||||||
|
$http.get(urlbase + '/upgrade').success(function (data) {
|
||||||
|
$scope.upgradeInfo = data;
|
||||||
|
}).error(function () {
|
||||||
|
$scope.upgradeInfo = {};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.refresh = function () {
|
||||||
|
refreshSystem();
|
||||||
|
refreshConnectionStats();
|
||||||
|
refreshErrors();
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.repoStatus = function (repo) {
|
$scope.repoStatus = function (repo) {
|
||||||
@ -187,9 +354,8 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
|||||||
};
|
};
|
||||||
|
|
||||||
$scope.nodeIcon = function (nodeCfg) {
|
$scope.nodeIcon = function (nodeCfg) {
|
||||||
var conn = $scope.connections[nodeCfg.NodeID];
|
if ($scope.connections[nodeCfg.NodeID]) {
|
||||||
if (conn) {
|
if ($scope.completion[nodeCfg.NodeID] && $scope.completion[nodeCfg.NodeID]._total === 100) {
|
||||||
if (conn.Completion === 100) {
|
|
||||||
return 'ok';
|
return 'ok';
|
||||||
} else {
|
} else {
|
||||||
return 'refresh';
|
return 'refresh';
|
||||||
@ -200,9 +366,8 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
|||||||
};
|
};
|
||||||
|
|
||||||
$scope.nodeClass = function (nodeCfg) {
|
$scope.nodeClass = function (nodeCfg) {
|
||||||
var conn = $scope.connections[nodeCfg.NodeID];
|
if ($scope.connections[nodeCfg.NodeID]) {
|
||||||
if (conn) {
|
if ($scope.completion[nodeCfg.NodeID] && $scope.completion[nodeCfg.NodeID]._total === 100) {
|
||||||
if (conn.Completion === 100) {
|
|
||||||
return 'success';
|
return 'success';
|
||||||
} else {
|
} else {
|
||||||
return 'primary';
|
return 'primary';
|
||||||
@ -552,60 +717,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
|||||||
cfg.APIKey = randomString(30, 32);
|
cfg.APIKey = randomString(30, 32);
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.init = function() {
|
|
||||||
$http.get(urlbase + '/version').success(function (data) {
|
|
||||||
$scope.version = data;
|
|
||||||
});
|
|
||||||
|
|
||||||
$http.get(urlbase + '/system').success(function (data) {
|
|
||||||
$scope.system = data;
|
|
||||||
$scope.myID = data.myID;
|
|
||||||
});
|
|
||||||
|
|
||||||
$http.get(urlbase + '/config').success(function (data) {
|
|
||||||
$scope.config = data;
|
|
||||||
$scope.config.Options.ListenStr = $scope.config.Options.ListenAddress.join(', ');
|
|
||||||
|
|
||||||
$scope.nodes = $scope.config.Nodes;
|
|
||||||
$scope.nodes.sort(nodeCompare);
|
|
||||||
|
|
||||||
$scope.repos = repoMap($scope.config.Repositories);
|
|
||||||
|
|
||||||
$scope.refresh();
|
|
||||||
|
|
||||||
if ($scope.config.Options.URAccepted == 0) {
|
|
||||||
// If usage reporting has been neither accepted nor declined,
|
|
||||||
// we want to ask the user to make a choice. But we don't want
|
|
||||||
// to bug them during initial setup, so we set a cookie with
|
|
||||||
// the time of the first visit. When that cookie is present
|
|
||||||
// and the time is more than four hours ago, we ask the
|
|
||||||
// question.
|
|
||||||
|
|
||||||
var firstVisit = document.cookie.replace(/(?:(?:^|.*;\s*)firstVisit\s*\=\s*([^;]*).*$)|^.*$/, "$1");
|
|
||||||
if (!firstVisit) {
|
|
||||||
document.cookie = "firstVisit=" + Date.now() + ";max-age=" + 30*24*3600;
|
|
||||||
} else {
|
|
||||||
if (+firstVisit < Date.now() - 4*3600*1000){
|
|
||||||
$('#ur').modal({backdrop: 'static', keyboard: false});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$http.get(urlbase + '/config/sync').success(function (data) {
|
|
||||||
$scope.configInSync = data.configInSync;
|
|
||||||
});
|
|
||||||
|
|
||||||
$http.get(urlbase + '/report').success(function (data) {
|
|
||||||
$scope.reportData = data;
|
|
||||||
});
|
|
||||||
|
|
||||||
$http.get(urlbase + '/upgrade').success(function (data) {
|
|
||||||
$scope.upgradeInfo = data;
|
|
||||||
}).error(function () {
|
|
||||||
$scope.upgradeInfo = {};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.acceptUR = function () {
|
$scope.acceptUR = function () {
|
||||||
$scope.config.Options.URAccepted = 1000; // Larger than the largest existing report version
|
$scope.config.Options.URAccepted = 1000; // Larger than the largest existing report version
|
||||||
@ -717,6 +829,47 @@ function randomString(len, bits)
|
|||||||
return outStr.toLowerCase();
|
return outStr.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isEmptyObject(obj) {
|
||||||
|
var name;
|
||||||
|
for (name in obj) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function debounce(func, wait, immediate) {
|
||||||
|
var timeout, args, context, timestamp, result;
|
||||||
|
|
||||||
|
var later = function() {
|
||||||
|
var last = Date.now() - timestamp;
|
||||||
|
if (last < wait) {
|
||||||
|
timeout = setTimeout(later, wait - last);
|
||||||
|
} else {
|
||||||
|
timeout = null;
|
||||||
|
if (!immediate) {
|
||||||
|
result = func.apply(context, args);
|
||||||
|
context = args = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return function() {
|
||||||
|
context = this;
|
||||||
|
args = arguments;
|
||||||
|
timestamp = Date.now();
|
||||||
|
var callNow = immediate && !timeout;
|
||||||
|
if (!timeout) {
|
||||||
|
timeout = setTimeout(later, wait);
|
||||||
|
}
|
||||||
|
if (callNow) {
|
||||||
|
result = func.apply(context, args);
|
||||||
|
context = args = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
syncthing.filter('natural', function () {
|
syncthing.filter('natural', function () {
|
||||||
return function (input, valid) {
|
return function (input, valid) {
|
||||||
return input.toFixed(decimals(input, valid));
|
return input.toFixed(decimals(input, valid));
|
||||||
|
@ -89,6 +89,7 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
<div ng-controller="EventCtrl"></div>
|
||||||
|
|
||||||
<!-- Top bar -->
|
<!-- Top bar -->
|
||||||
|
|
||||||
@ -289,10 +290,10 @@
|
|||||||
<span class="glyphicon glyphicon-retweet"></span>
|
<span class="glyphicon glyphicon-retweet"></span>
|
||||||
{{nodeName(nodeCfg)}}
|
{{nodeName(nodeCfg)}}
|
||||||
<span class="pull-right hidden-xs">
|
<span class="pull-right hidden-xs">
|
||||||
<span ng-if="connections[nodeCfg.NodeID].Completion == 100">
|
<span ng-if="connections[nodeCfg.NodeID] && completion[nodeCfg.NodeID]._total == 100">
|
||||||
<span translate>Up to Date</span> (100%)
|
<span translate>Up to Date</span> (100%)
|
||||||
</span>
|
</span>
|
||||||
<span ng-if="connections[nodeCfg.NodeID].Completion < 100">
|
<span ng-if="connections[nodeCfg.NodeID] && completion[nodeCfg.NodeID]._total < 100">
|
||||||
<span translate>Syncing</span> ({{connections[nodeCfg.NodeID].Completion}}%)
|
<span translate>Syncing</span> ({{connections[nodeCfg.NodeID].Completion}}%)
|
||||||
</span>
|
</span>
|
||||||
<span translate ng-if="!connections[nodeCfg.NodeID]">Disconnected</span>
|
<span translate ng-if="!connections[nodeCfg.NodeID]">Disconnected</span>
|
||||||
@ -311,7 +312,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th><span class="glyphicon glyphicon-comment"></span> <span translate>Synchronization</span></th>
|
<th><span class="glyphicon glyphicon-comment"></span> <span translate>Synchronization</span></th>
|
||||||
<td class="text-right">{{connections[nodeCfg.NodeID].Completion | alwaysNumber}}%</td>
|
<td class="text-right">{{completion[nodeCfg.NodeID]._total | alwaysNumber}}%</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th><span class="glyphicon glyphicon-compressed"></span> <span translate>Use Compression</span></th>
|
<th><span class="glyphicon glyphicon-compressed"></span> <span translate>Use Compression</span></th>
|
||||||
|
@ -101,7 +101,7 @@
|
|||||||
"Upgrade To {%version%}": "Atualizar para {{version}}",
|
"Upgrade To {%version%}": "Atualizar para {{version}}",
|
||||||
"Upload Rate": "Taxa de envio",
|
"Upload Rate": "Taxa de envio",
|
||||||
"Usage": "Utilização",
|
"Usage": "Utilização",
|
||||||
"Use Compression": "Use Compression",
|
"Use Compression": "Usar Compressão",
|
||||||
"Use HTTPS for GUI": "Utilizar HTTPS para GUI",
|
"Use HTTPS for GUI": "Utilizar HTTPS para GUI",
|
||||||
"Version": "Versão",
|
"Version": "Versão",
|
||||||
"When adding a new node, keep in mind that this node must be added on the other side too.": "Quando adicionar um novo nó, lembre-se que este nó tem que ser adicionado do outro lado também.",
|
"When adding a new node, keep in mind that this node must be added on the other side too.": "Quando adicionar um novo nó, lembre-se que este nó tem que ser adicionado do outro lado também.",
|
||||||
|
@ -157,7 +157,6 @@ type ConnectionInfo struct {
|
|||||||
protocol.Statistics
|
protocol.Statistics
|
||||||
Address string
|
Address string
|
||||||
ClientVersion string
|
ClientVersion string
|
||||||
Completion int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConnectionStats returns a map with connection statistics for each connected node.
|
// ConnectionStats returns a map with connection statistics for each connected node.
|
||||||
@ -179,43 +178,6 @@ func (m *Model) ConnectionStats() map[string]ConnectionInfo {
|
|||||||
ci.Address = nc.RemoteAddr().String()
|
ci.Address = nc.RemoteAddr().String()
|
||||||
}
|
}
|
||||||
|
|
||||||
var tot int64
|
|
||||||
var have int64
|
|
||||||
|
|
||||||
for _, repo := range m.nodeRepos[node] {
|
|
||||||
m.repoFiles[repo].WithGlobal(func(f protocol.FileInfo) bool {
|
|
||||||
if !protocol.IsDeleted(f.Flags) {
|
|
||||||
var size int64
|
|
||||||
if protocol.IsDirectory(f.Flags) {
|
|
||||||
size = zeroEntrySize
|
|
||||||
} else {
|
|
||||||
size = f.Size()
|
|
||||||
}
|
|
||||||
tot += size
|
|
||||||
have += size
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
m.repoFiles[repo].WithNeed(node, func(f protocol.FileInfo) bool {
|
|
||||||
if !protocol.IsDeleted(f.Flags) {
|
|
||||||
var size int64
|
|
||||||
if protocol.IsDirectory(f.Flags) {
|
|
||||||
size = zeroEntrySize
|
|
||||||
} else {
|
|
||||||
size = f.Size()
|
|
||||||
}
|
|
||||||
have -= size
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
ci.Completion = 100
|
|
||||||
if tot != 0 {
|
|
||||||
ci.Completion = int(100 * have / tot)
|
|
||||||
}
|
|
||||||
|
|
||||||
res[node.String()] = ci
|
res[node.String()] = ci
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -234,6 +196,39 @@ func (m *Model) ConnectionStats() map[string]ConnectionInfo {
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns the completion status, in percent, for the given node and repo.
|
||||||
|
func (m *Model) Completion(node protocol.NodeID, repo string) float64 {
|
||||||
|
var tot int64
|
||||||
|
m.repoFiles[repo].WithGlobal(func(f protocol.FileInfo) bool {
|
||||||
|
if !protocol.IsDeleted(f.Flags) {
|
||||||
|
var size int64
|
||||||
|
if protocol.IsDirectory(f.Flags) {
|
||||||
|
size = zeroEntrySize
|
||||||
|
} else {
|
||||||
|
size = f.Size()
|
||||||
|
}
|
||||||
|
tot += size
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
var need int64
|
||||||
|
m.repoFiles[repo].WithNeed(node, func(f protocol.FileInfo) bool {
|
||||||
|
if !protocol.IsDeleted(f.Flags) {
|
||||||
|
var size int64
|
||||||
|
if protocol.IsDirectory(f.Flags) {
|
||||||
|
size = zeroEntrySize
|
||||||
|
} else {
|
||||||
|
size = f.Size()
|
||||||
|
}
|
||||||
|
need += size
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
return 100 * (1 - float64(need)/float64(tot))
|
||||||
|
}
|
||||||
|
|
||||||
func sizeOf(fs []protocol.FileInfo) (files, deleted int, bytes int64) {
|
func sizeOf(fs []protocol.FileInfo) (files, deleted int, bytes int64) {
|
||||||
for _, f := range fs {
|
for _, f := range fs {
|
||||||
fs, de, by := sizeOfFile(f)
|
fs, de, by := sizeOfFile(f)
|
||||||
|
Loading…
Reference in New Issue
Block a user