Ver Fonte

Use event interface for GUI (fixes #383)

Jakob Borg há 11 anos atrás
pai
commit
e27d42935c
9 ficheiros alterados com 355 adições e 165 exclusões
  1. 0 0
      auto/gui.files.go
  2. 47 18
      cmd/syncthing/gui.go
  3. 12 1
      cmd/transifexdl/main.go
  4. 4 3
      files/leveldb.go
  5. 1 1
      files/set.go
  6. 253 100
      gui/app.js
  7. 4 3
      gui/index.html
  8. 1 1
      gui/lang-pt.json
  9. 33 38
      model/model.go

Diff do ficheiro suprimidas por serem muito extensas
+ 0 - 0
auto/gui.files.go


+ 47 - 18
cmd/syncthing/gui.go

@@ -47,7 +47,7 @@ var (
 	static       func(http.ResponseWriter, *http.Request, *log.Logger)
 	apiKey       string
 	modt         = time.Now().UTC().Format(http.TimeFormat)
-	eventSub     = events.NewBufferedSubscription(events.Default.Subscribe(events.AllEvents), 1000)
+	eventSub     *events.BufferedSubscription
 )
 
 const (
@@ -56,6 +56,8 @@ const (
 
 func init() {
 	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 {
@@ -92,32 +94,33 @@ func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) erro
 
 	// The GET handlers
 	getRestMux := http.NewServeMux()
-	getRestMux.HandleFunc("/rest/version", restGetVersion)
-	getRestMux.HandleFunc("/rest/model", withModel(m, restGetModel))
-	getRestMux.HandleFunc("/rest/model/version", withModel(m, restGetModelVersion))
-	getRestMux.HandleFunc("/rest/need", withModel(m, restGetNeed))
-	getRestMux.HandleFunc("/rest/connections", withModel(m, restGetConnections))
+	getRestMux.HandleFunc("/rest/completion", withModel(m, restGetCompletion))
 	getRestMux.HandleFunc("/rest/config", restGetConfig)
 	getRestMux.HandleFunc("/rest/config/sync", restGetConfigInSync)
-	getRestMux.HandleFunc("/rest/system", restGetSystem)
-	getRestMux.HandleFunc("/rest/errors", restGetErrors)
+	getRestMux.HandleFunc("/rest/connections", withModel(m, restGetConnections))
 	getRestMux.HandleFunc("/rest/discovery", restGetDiscovery)
-	getRestMux.HandleFunc("/rest/report", withModel(m, restGetReport))
+	getRestMux.HandleFunc("/rest/errors", restGetErrors)
 	getRestMux.HandleFunc("/rest/events", restGetEvents)
-	getRestMux.HandleFunc("/rest/upgrade", restGetUpgrade)
-	getRestMux.HandleFunc("/rest/nodeid", restGetNodeID)
 	getRestMux.HandleFunc("/rest/lang", restGetLang)
+	getRestMux.HandleFunc("/rest/model", withModel(m, restGetModel))
+	getRestMux.HandleFunc("/rest/model/version", withModel(m, restGetModelVersion))
+	getRestMux.HandleFunc("/rest/need", withModel(m, restGetNeed))
+	getRestMux.HandleFunc("/rest/nodeid", restGetNodeID)
+	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
 	postRestMux := http.NewServeMux()
 	postRestMux.HandleFunc("/rest/config", withModel(m, restPostConfig))
-	postRestMux.HandleFunc("/rest/restart", restPostRestart)
-	postRestMux.HandleFunc("/rest/reset", restPostReset)
-	postRestMux.HandleFunc("/rest/shutdown", restPostShutdown)
+	postRestMux.HandleFunc("/rest/discovery/hint", restPostDiscoveryHint)
 	postRestMux.HandleFunc("/rest/error", restPostError)
 	postRestMux.HandleFunc("/rest/error/clear", restClearErrors)
-	postRestMux.HandleFunc("/rest/discovery/hint", restPostDiscoveryHint)
 	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)
 
 	// 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))
 }
 
+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) {
 	var qs = r.URL.Query()
 	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) {
 	qs := r.URL.Query()
-	ts := qs.Get("since")
-	since, _ := strconv.Atoi(ts)
+	sinceStr := qs.Get("since")
+	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")
-	json.NewEncoder(w).Encode(eventSub.Since(since, nil))
+	json.NewEncoder(w).Encode(evs)
 }
 
 func restGetUpgrade(w http.ResponseWriter, r *http.Request) {

+ 12 - 1
cmd/transifexdl/main.go

@@ -18,6 +18,12 @@ type translation struct {
 }
 
 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")
 
 	var stats map[string]stat
@@ -63,9 +69,14 @@ func main() {
 	json.NewEncoder(os.Stdout).Encode(langs)
 }
 
-func req(url string) *http.Response {
+func userPass() (string, string) {
 	user := os.Getenv("TRANSIFEX_USER")
 	pass := os.Getenv("TRANSIFEX_PASS")
+	return user, pass
+}
+
+func req(url string) *http.Response {
+	user, pass := userPass()
 
 	req, err := http.NewRequest("GET", url, nil)
 	if err != nil {

+ 4 - 3
files/leveldb.go

@@ -632,9 +632,6 @@ func ldbWithNeed(db *leveldb.DB, repo, node []byte, fn fileIterator) {
 
 		if need || !have {
 			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)
 			bs, err := snap.Get(fk, nil)
 			if err != nil {
@@ -652,6 +649,10 @@ func ldbWithNeed(db *leveldb.DB, repo, node []byte, fn fileIterator) {
 				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 {
 				return
 			}

+ 1 - 1
files/set.go

@@ -85,7 +85,7 @@ func (s *Set) Update(node protocol.NodeID, fs []protocol.FileInfo) {
 
 func (s *Set) WithNeed(node protocol.NodeID, fn fileIterator) {
 	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)
 }

+ 253 - 100
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) {
     var prevDate = 0;
     var getOK = true;
     var restarting = false;
 
-    $scope.connections = {};
+    $scope.completion = {};
     $scope.config = {};
-    $scope.myID = '';
-    $scope.nodes = [];
     $scope.configInSync = true;
-    $scope.protocolChanged = false;
+    $scope.connections = {};
     $scope.errors = [];
-    $scope.seenError = '';
     $scope.model = {};
-    $scope.repos = {};
+    $scope.myID = '';
+    $scope.nodes = [];
+    $scope.protocolChanged = false;
     $scope.reportData = {};
     $scope.reportPreview = false;
+    $scope.repos = {};
+    $scope.seenError = '';
     $scope.upgradeInfo = {};
 
     $http.get(urlbase+"/lang").success(function (langs) {
@@ -71,53 +116,118 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
         'touch': 'asterisk',
     }
 
-    function getSucceeded() {
-        if (!getOK) {
-            $scope.init();
-            $('#networkError').modal('hide');
-            getOK = true;
+    $scope.$on('UIOnline', function (event, arg) {
+        $scope.init();
+        $('#networkError').modal('hide');
+        $('#restarting').modal('hide');
+        $('#shutdown').modal('hide');
+    });
+
+    $scope.$on('UIOffline', function (event, arg) {
+        if (!restarting) {
+            $('#networkError').modal({backdrop: 'static', keyboard: false});
         }
-        if (restarting) {
-            $scope.init();
-            $('#restarting').modal('hide');
-            $('#shutdown').modal('hide');
-            restarting = false;
+    });
+
+    $scope.$on('StateChanged', function (event, arg) {
+        var data = arg.data;
+        if ($scope.model[data.repo]) {
+            $scope.model[data.repo].state = data.to;
         }
-    }
+    });
 
-    function getFailed() {
-        if (restarting) {
-            return;
+    $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,
+            };
         }
-        if (getOK) {
-            $('#networkError').modal({backdrop: 'static', keyboard: false});
-            getOK = false;
+    });
+
+    $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) {
-            getSucceeded();
+            $scope.myID = data.myID;
             $scope.system = data;
-        }).error(function () {
-            getFailed();
         });
-        Object.keys($scope.repos).forEach(function (id) {
-            if (typeof $scope.model[id] === 'undefined') {
-                // Never fetched before
-                $http.get(urlbase + '/model?repo=' + encodeURIComponent(id)).success(function (data) {
-                    $scope.model[id] = data;
-                });
-            } else {
-                $http.get(urlbase + '/model/version?repo=' + encodeURIComponent(id)).success(function (data) {
-                    if (data.version > $scope.model[id].version) {
-                        $http.get(urlbase + '/model?repo=' + encodeURIComponent(id)).success(function (data) {
-                            $scope.model[id] = data;
-                        });
+    }
+
+    var completionFuncs = {};
+    function refreshCompletion(node, repo) {
+        if (node === $scope.myID) {
+            return
+        }
+
+        if (!completionFuncs[node+repo]) {
+            completionFuncs[node+repo] = debounce(function () {
+                $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) {
             var now = Date.now(),
             td = (now - prevDate) / 1000,
@@ -138,9 +248,66 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
             }
             $scope.connections = data;
         });
+    }
+
+    function refreshErrors() {
         $http.get(urlbase + '/errors').success(function (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) {
@@ -187,9 +354,8 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
     };
 
     $scope.nodeIcon = function (nodeCfg) {
-        var conn = $scope.connections[nodeCfg.NodeID];
-        if (conn) {
-            if (conn.Completion === 100) {
+        if ($scope.connections[nodeCfg.NodeID]) {
+            if ($scope.completion[nodeCfg.NodeID] && $scope.completion[nodeCfg.NodeID]._total === 100) {
                 return 'ok';
             } else {
                 return 'refresh';
@@ -200,9 +366,8 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
     };
 
     $scope.nodeClass = function (nodeCfg) {
-        var conn = $scope.connections[nodeCfg.NodeID];
-        if (conn) {
-            if (conn.Completion === 100) {
+        if ($scope.connections[nodeCfg.NodeID]) {
+            if ($scope.completion[nodeCfg.NodeID] && $scope.completion[nodeCfg.NodeID]._total === 100) {
                 return 'success';
             } else {
                 return 'primary';
@@ -552,60 +717,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
         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.config.Options.URAccepted = 1000; // Larger than the largest existing report version
@@ -717,6 +829,47 @@ function randomString(len, bits)
     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 () {
     return function (input, valid) {
         return input.toFixed(decimals(input, valid));

+ 4 - 3
gui/index.html

@@ -89,6 +89,7 @@
 </head>
 
 <body>
+  <div ng-controller="EventCtrl"></div>
 
   <!-- Top bar -->
 
@@ -289,10 +290,10 @@
                   <span class="glyphicon glyphicon-retweet"></span>
                   {{nodeName(nodeCfg)}}
                   <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>
-                    <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>
                     <span translate ng-if="!connections[nodeCfg.NodeID]">Disconnected</span>
@@ -311,7 +312,7 @@
                       </tr>
                       <tr>
                         <th><span class="glyphicon glyphicon-comment"></span>&emsp;<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>
                         <th><span class="glyphicon glyphicon-compressed"></span>&emsp;<span translate>Use Compression</span></th>

+ 1 - 1
gui/lang-pt.json

@@ -101,7 +101,7 @@
     "Upgrade To {%version%}": "Atualizar para {{version}}",
     "Upload Rate": "Taxa de envio",
     "Usage": "Utilização",
-    "Use Compression": "Use Compression",
+    "Use Compression": "Usar Compressão",
     "Use HTTPS for GUI": "Utilizar HTTPS para GUI",
     "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.",

+ 33 - 38
model/model.go

@@ -157,7 +157,6 @@ type ConnectionInfo struct {
 	protocol.Statistics
 	Address       string
 	ClientVersion string
-	Completion    int
 }
 
 // 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()
 		}
 
-		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
 	}
 
@@ -234,6 +196,39 @@ func (m *Model) ConnectionStats() map[string]ConnectionInfo {
 	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) {
 	for _, f := range fs {
 		fs, de, by := sizeOfFile(f)

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff