mirror of
https://github.com/octoleo/syncthing.git
synced 2024-11-09 14:50:56 +00:00
Merge branch 'pr-1094'
* pr-1094: GUI tweaks for last file synced Display last received file and time (fixes #292, fixes #801)
This commit is contained in:
commit
d452b7593f
@ -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())
|
||||
|
@ -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.",
|
||||
|
@ -153,6 +153,14 @@
|
||||
<th><span class="glyphicon glyphicon-share-alt"></span> <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> <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> <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> <span translate>Folders</span></th>
|
||||
|
@ -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();
|
||||
|
File diff suppressed because one or more lines are too long
@ -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()
|
||||
|
@ -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
internal/stats/folder.go
Normal file
133
internal/stats/folder.go
Normal file
@ -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
|
||||
}
|
@ -18,4 +18,5 @@ package stats
|
||||
// Same key space as files/leveldb.go keyType* constants
|
||||
const (
|
||||
keyTypeDeviceStatistic = iota + 30
|
||||
keyTypeFolderStatistic
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user