1
0
Эх сурвалжийг харах

Merge branch 'pr-1094'

* pr-1094:
  GUI tweaks for last file synced
  Display last received file and time (fixes #292, fixes #801)
Jakob Borg 11 жил өмнө
parent
commit
d452b7593f

+ 7 - 0
cmd/syncthing/gui.go

@@ -129,6 +129,7 @@ func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) erro
 	getRestMux.HandleFunc("/rest/upgrade", restGetUpgrade)
 	getRestMux.HandleFunc("/rest/version", restGetVersion)
 	getRestMux.HandleFunc("/rest/stats/device", withModel(m, restGetDeviceStats))
+	getRestMux.HandleFunc("/rest/stats/folder", withModel(m, restGetFolderStats))
 
 	// Debug endpoints, not for general use
 	getRestMux.HandleFunc("/rest/debug/peerCompletion", withModel(m, restGetPeerCompletion))
@@ -343,6 +344,12 @@ func restGetDeviceStats(m *model.Model, w http.ResponseWriter, r *http.Request)
 	json.NewEncoder(w).Encode(res)
 }
 
+func restGetFolderStats(m *model.Model, w http.ResponseWriter, r *http.Request) {
+	var res = m.FolderStatistics()
+	w.Header().Set("Content-Type", "application/json; charset=utf-8")
+	json.NewEncoder(w).Encode(res)
+}
+
 func restGetConfig(w http.ResponseWriter, r *http.Request) {
 	w.Header().Set("Content-Type", "application/json; charset=utf-8")
 	json.NewEncoder(w).Encode(cfg.Raw())

+ 3 - 0
gui/assets/lang/lang-en.json

@@ -57,6 +57,7 @@
    "Introducer": "Introducer",
    "Inversion of the given condition (i.e. do not exclude)": "Inversion of the given condition (i.e. do not exclude)",
    "Keep Versions": "Keep Versions",
+   "Last File Synced": "Last File Synced",
    "Last seen": "Last seen",
    "Latest Release": "Latest Release",
    "Legend:": "Legend:",
@@ -90,7 +91,9 @@
    "Save": "Save",
    "Scanning": "Scanning",
    "Select the devices to share this folder with.": "Select the devices to share this folder with.",
+   "Select the folders to share with this device.": "Select the folders to share with this device.",
    "Settings": "Settings",
+   "Share Folders With Device": "Share Folders With Device",
    "Share With Devices": "Share With Devices",
    "Shared With": "Shared With",
    "Short identifier for the folder. Must be the same on all cluster devices.": "Short identifier for the folder. Must be the same on all cluster devices.",

+ 10 - 2
gui/index.html

@@ -153,6 +153,14 @@
                       <th><span class="glyphicon glyphicon-share-alt"></span>&emsp;<span translate>Shared With</span></th>
                       <td class="text-right">{{sharesFolder(folder)}}</td>
                     </tr>
+                    <tr ng-if="folderStats[folder.ID].LastFile">
+                      <th><span class="glyphicon glyphicon-transfer"></span>&emsp;<span translate>Last File Synced</span></th>
+                      <td class="text-right">
+                        <span title="{{folderStats[folder.ID].LastFile.Filename}} @ {{folderStats[folder.ID].LastFile.At | date:'yyyy-MM-dd HH:mm'}}">
+                          {{folderStats[folder.ID].LastFile.Filename | basename}}
+                        </span>
+                      </td>
+                    </tr>
                   </tbody>
                 </table>
               </div>
@@ -276,8 +284,8 @@
                     </tr>
                     <tr ng-if="!connections[deviceCfg.DeviceID]">
                       <th><span class="glyphicon glyphicon-eye-open"></span>&emsp;<span translate>Last seen</span></th>
-                      <td translate ng-if="!stats[deviceCfg.DeviceID].LastSeenDays || stats[deviceCfg.DeviceID].LastSeenDays >= 365" class="text-right">Never</td>
-                      <td ng-if="stats[deviceCfg.DeviceID].LastSeenDays < 365" class="text-right">{{stats[deviceCfg.DeviceID].LastSeen | date:"yyyy-MM-dd HH:mm"}}</td>
+                      <td translate ng-if="!deviceStats[deviceCfg.DeviceID].LastSeenDays || deviceStats[deviceCfg.DeviceID].LastSeenDays >= 365" class="text-right">Never</td>
+                      <td ng-if="deviceStats[deviceCfg.DeviceID].LastSeenDays < 365" class="text-right">{{deviceStats[deviceCfg.DeviceID].LastSeen | date:"yyyy-MM-dd HH:mm"}}</td>
                     </tr>
                     <tr ng-if="deviceFolders(deviceCfg).length > 0">
                       <th><span class="glyphicon glyphicon-hdd"></span>&emsp;<span translate>Folders</span></th>

+ 20 - 5
gui/scripts/syncthing/core/controllers/syncthingController.js

@@ -17,6 +17,7 @@ angular.module('syncthing.core')
             refreshConfig();
             refreshConnectionStats();
             refreshDeviceStats();
+            refreshFolderStats();
 
             $http.get(urlbase + '/version').success(function (data) {
                 $scope.version = data.version;
@@ -52,7 +53,8 @@ angular.module('syncthing.core')
         $scope.folders = {};
         $scope.seenError = '';
         $scope.upgradeInfo = null;
-        $scope.stats = {};
+        $scope.deviceStats = {};
+        $scope.folderStats = {};
         $scope.progress = {};
 
         $(window).bind('beforeunload', function () {
@@ -112,6 +114,7 @@ angular.module('syncthing.core')
         $scope.$on('LocalIndexUpdated', function (event, arg) {
             var data = arg.data;
             refreshFolder(data.folder);
+            refreshFolderStats();
 
             // Update completion status for all devices that we share this folder with.
             $scope.folders[data.folder].Devices.forEach(function (deviceCfg) {
@@ -364,15 +367,27 @@ angular.module('syncthing.core')
 
         var refreshDeviceStats = debounce(function () {
             $http.get(urlbase + "/stats/device").success(function (data) {
-                $scope.stats = data;
-                for (var device in $scope.stats) {
-                    $scope.stats[device].LastSeen = new Date($scope.stats[device].LastSeen);
-                    $scope.stats[device].LastSeenDays = (new Date() - $scope.stats[device].LastSeen) / 1000 / 86400;
+                $scope.deviceStats = data;
+                for (var device in $scope.deviceStats) {
+                    $scope.deviceStats[device].LastSeen = new Date($scope.deviceStats[device].LastSeen);
+                    $scope.deviceStats[device].LastSeenDays = (new Date() - $scope.deviceStats[device].LastSeen) / 1000 / 86400;
                 }
                 console.log("refreshDeviceStats", data);
             });
         }, 500);
 
+        var refreshFolderStats = debounce(function () {
+            $http.get(urlbase + "/stats/folder").success(function (data) {
+                $scope.folderStats = data;
+                for (var folder in $scope.folderStats) {
+                    if ($scope.folderStats[folder].LastFile) {
+                        $scope.folderStats[folder].LastFile.At = new Date($scope.folderStats[folder].LastFile.At);
+                    }
+                }
+                console.log("refreshfolderStats", data);
+            });
+        }, 500);
+
         $scope.refresh = function () {
             refreshSystem();
             refreshConnectionStats();

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
internal/auto/gui.files.go


+ 28 - 0
internal/model/model.go

@@ -98,6 +98,7 @@ type Model struct {
 	deviceStatRefs map[protocol.DeviceID]*stats.DeviceStatisticsReference // deviceID -> statsRef
 	folderIgnores  map[string]*ignore.Matcher                             // folder -> matcher object
 	folderRunners  map[string]service                                     // folder -> puller or scanner
+	folderStatRefs map[string]*stats.FolderStatisticsReference            // folder -> statsRef
 	fmut           sync.RWMutex                                           // protects the above
 
 	folderState        map[string]folderState // folder -> state
@@ -137,6 +138,7 @@ func NewModel(cfg *config.Wrapper, deviceName, clientName, clientVersion string,
 		deviceStatRefs:     make(map[protocol.DeviceID]*stats.DeviceStatisticsReference),
 		folderIgnores:      make(map[string]*ignore.Matcher),
 		folderRunners:      make(map[string]service),
+		folderStatRefs:     make(map[string]*stats.FolderStatisticsReference),
 		folderState:        make(map[string]folderState),
 		folderStateChanged: make(map[string]time.Time),
 		protoConn:          make(map[protocol.DeviceID]protocol.Connection),
@@ -283,6 +285,15 @@ func (m *Model) DeviceStatistics() map[string]stats.DeviceStatistics {
 	return res
 }
 
+// Returns statistics about each folder
+func (m *Model) FolderStatistics() map[string]stats.FolderStatistics {
+	var res = make(map[string]stats.FolderStatistics)
+	for id := range m.cfg.Folders() {
+		res[id] = m.folderStatRef(id).GetStatistics()
+	}
+	return res
+}
+
 // Returns the completion status, in percent, for the given device and folder.
 func (m *Model) Completion(device protocol.DeviceID, folder string) float64 {
 	defer m.leveldbPanicWorkaround()
@@ -873,6 +884,23 @@ func (m *Model) deviceWasSeen(deviceID protocol.DeviceID) {
 	m.deviceStatRef(deviceID).WasSeen()
 }
 
+func (m *Model) folderStatRef(folder string) *stats.FolderStatisticsReference {
+	m.fmut.Lock()
+	defer m.fmut.Unlock()
+
+	if sr, ok := m.folderStatRefs[folder]; ok {
+		return sr
+	} else {
+		sr = stats.NewFolderStatisticsReference(m.db, folder)
+		m.folderStatRefs[folder] = sr
+		return sr
+	}
+}
+
+func (m *Model) receivedFile(folder, filename string) {
+	m.folderStatRef(folder).ReceivedFile(filename)
+}
+
 func sendIndexes(conn protocol.Connection, folder string, fs *files.Set, ignores *ignore.Matcher) {
 	deviceID := conn.ID()
 	name := conn.Name()

+ 1 - 0
internal/model/puller.go

@@ -830,6 +830,7 @@ func (p *Puller) finisherRoutine(in <-chan *sharedPullerState) {
 			}
 
 			p.performFinish(state)
+			p.model.receivedFile(p.folder, state.file.Name)
 			if p.progressEmitter != nil {
 				p.progressEmitter.Deregister(state)
 			}

+ 133 - 0
internal/stats/folder.go

@@ -0,0 +1,133 @@
+// Copyright (C) 2014 The Syncthing Authors.
+//
+// This program is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation, either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program. If not, see <http://www.gnu.org/licenses/>.
+
+package stats
+
+import (
+	"encoding/binary"
+	"time"
+
+	"github.com/syndtr/goleveldb/leveldb"
+)
+
+const (
+	folderStatisticTypeLastFile = iota
+)
+
+var folderStatisticsTypes = []byte{
+	folderStatisticTypeLastFile,
+}
+
+type FolderStatistics struct {
+	LastFile *LastFile
+}
+
+type FolderStatisticsReference struct {
+	db     *leveldb.DB
+	folder string
+}
+
+func NewFolderStatisticsReference(db *leveldb.DB, folder string) *FolderStatisticsReference {
+	return &FolderStatisticsReference{
+		db:     db,
+		folder: folder,
+	}
+}
+
+func (s *FolderStatisticsReference) key(stat byte) []byte {
+	k := make([]byte, 1+1+64)
+	k[0] = keyTypeFolderStatistic
+	k[1] = stat
+	copy(k[1+1:], s.folder[:])
+	return k
+}
+
+func (s *FolderStatisticsReference) GetLastFile() *LastFile {
+	value, err := s.db.Get(s.key(folderStatisticTypeLastFile), nil)
+	if err != nil {
+		if err != leveldb.ErrNotFound {
+			l.Warnln("FolderStatisticsReference: Failed loading last file filename value for", s.folder, ":", err)
+		}
+		return nil
+	}
+
+	file := LastFile{}
+	err = file.UnmarshalBinary(value)
+	if err != nil {
+		l.Warnln("FolderStatisticsReference: Failed loading last file value for", s.folder, ":", err)
+		return nil
+	}
+	return &file
+}
+
+func (s *FolderStatisticsReference) ReceivedFile(filename string) {
+	f := LastFile{
+		Filename: filename,
+		At:       time.Now(),
+	}
+	if debug {
+		l.Debugln("stats.FolderStatisticsReference.ReceivedFile:", s.folder)
+	}
+
+	value, err := f.MarshalBinary()
+	if err != nil {
+		l.Warnln("FolderStatisticsReference: Failed serializing last file value for", s.folder, ":", err)
+		return
+	}
+
+	err = s.db.Put(s.key(folderStatisticTypeLastFile), value, nil)
+	if err != nil {
+		l.Warnln("Failed update last file value for", s.folder, ":", err)
+	}
+}
+
+// Never called, maybe because it's worth while to keep the data
+// or maybe because we have no easy way of knowing that a folder has been removed.
+func (s *FolderStatisticsReference) Delete() error {
+	for _, stype := range folderStatisticsTypes {
+		err := s.db.Delete(s.key(stype), nil)
+		if debug && err == nil {
+			l.Debugln("stats.FolderStatisticsReference.Delete:", s.folder, stype)
+		}
+		if err != nil && err != leveldb.ErrNotFound {
+			return err
+		}
+	}
+	return nil
+}
+
+func (s *FolderStatisticsReference) GetStatistics() FolderStatistics {
+	return FolderStatistics{
+		LastFile: s.GetLastFile(),
+	}
+}
+
+type LastFile struct {
+	At       time.Time
+	Filename string
+}
+
+func (f *LastFile) MarshalBinary() ([]byte, error) {
+	buf := make([]byte, 8+len(f.Filename))
+	binary.BigEndian.PutUint64(buf[:8], uint64(f.At.Unix()))
+	copy(buf[8:], []byte(f.Filename))
+	return buf, nil
+}
+
+func (f *LastFile) UnmarshalBinary(buf []byte) error {
+	f.At = time.Unix(int64(binary.BigEndian.Uint64(buf[:8])), 0)
+	f.Filename = string(buf[8:])
+	return nil
+}

+ 1 - 0
internal/stats/leveldb.go

@@ -18,4 +18,5 @@ package stats
 // Same key space as files/leveldb.go keyType* constants
 const (
 	keyTypeDeviceStatistic = iota + 30
+	keyTypeFolderStatistic
 )

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно