all: Display list of locally changed items in UI (fixes #5336) (#5337)

This commit is contained in:
Simon Frei 2018-12-11 09:59:04 +01:00 committed by Jakob Borg
parent 1b59960ff9
commit a09079ed25
9 changed files with 144 additions and 36 deletions

View File

@ -90,6 +90,7 @@ type modelIntf interface {
Revert(folder string)
NeedFolderFiles(folder string, page, perpage int) ([]db.FileInfoTruncated, []db.FileInfoTruncated, []db.FileInfoTruncated)
RemoteNeedFolderFiles(device protocol.DeviceID, folder string, page, perpage int) ([]db.FileInfoTruncated, error)
LocalChangedFiles(folder string, page, perpage int) []db.FileInfoTruncated
NeedSize(folder string) db.Counts
ConnectionStats() map[string]interface{}
DeviceStatistics() map[string]stats.DeviceStatistics
@ -258,6 +259,7 @@ func (s *apiService) Serve() {
getRestMux.HandleFunc("/rest/db/ignores", s.getDBIgnores) // folder
getRestMux.HandleFunc("/rest/db/need", s.getDBNeed) // folder [perpage] [page]
getRestMux.HandleFunc("/rest/db/remoteneed", s.getDBRemoteNeed) // device folder [perpage] [page]
getRestMux.HandleFunc("/rest/db/localchanged", s.getDBLocalChanged) // folder
getRestMux.HandleFunc("/rest/db/status", s.getDBStatus) // folder
getRestMux.HandleFunc("/rest/db/browse", s.getDBBrowse) // folder [prefix] [dirsonly] [levels]
getRestMux.HandleFunc("/rest/folder/versions", s.getFolderVersions) // folder
@ -707,13 +709,13 @@ func folderSummary(cfg configIntf, m modelIntf, folder string) (map[string]inter
res["invalid"] = "" // Deprecated, retains external API for now
global := m.GlobalSize(folder)
res["globalFiles"], res["globalDirectories"], res["globalSymlinks"], res["globalDeleted"], res["globalBytes"] = global.Files, global.Directories, global.Symlinks, global.Deleted, global.Bytes
res["globalFiles"], res["globalDirectories"], res["globalSymlinks"], res["globalDeleted"], res["globalBytes"], res["globalTotalItems"] = global.Files, global.Directories, global.Symlinks, global.Deleted, global.Bytes, global.TotalItems()
local := m.LocalSize(folder)
res["localFiles"], res["localDirectories"], res["localSymlinks"], res["localDeleted"], res["localBytes"] = local.Files, local.Directories, local.Symlinks, local.Deleted, local.Bytes
res["localFiles"], res["localDirectories"], res["localSymlinks"], res["localDeleted"], res["localBytes"], res["localTotalItems"] = local.Files, local.Directories, local.Symlinks, local.Deleted, local.Bytes, local.TotalItems()
need := m.NeedSize(folder)
res["needFiles"], res["needDirectories"], res["needSymlinks"], res["needDeletes"], res["needBytes"] = need.Files, need.Directories, need.Symlinks, need.Deleted, need.Bytes
res["needFiles"], res["needDirectories"], res["needSymlinks"], res["needDeletes"], res["needBytes"], res["needTotalItems"] = need.Files, need.Directories, need.Symlinks, need.Deleted, need.Bytes, need.TotalItems()
if cfg.Folders()[folder].Type == config.FolderTypeReceiveOnly {
// Add statistics for things that have changed locally in a receive
@ -724,6 +726,7 @@ func folderSummary(cfg configIntf, m modelIntf, folder string) (map[string]inter
res["receiveOnlyChangedSymlinks"] = ro.Symlinks
res["receiveOnlyChangedDeletes"] = ro.Deleted
res["receiveOnlyChangedBytes"] = ro.Bytes
res["receiveOnlyTotalItems"] = ro.TotalItems()
}
res["inSyncFiles"], res["inSyncBytes"] = global.Files-need.Files, global.Bytes-need.Bytes
@ -791,9 +794,9 @@ func (s *apiService) getDBNeed(w http.ResponseWriter, r *http.Request) {
// Convert the struct to a more loose structure, and inject the size.
sendJSON(w, map[string]interface{}{
"progress": toNeedSlice(progress),
"queued": toNeedSlice(queued),
"rest": toNeedSlice(rest),
"progress": toJsonFileInfoSlice(progress),
"queued": toJsonFileInfoSlice(queued),
"rest": toJsonFileInfoSlice(rest),
"page": page,
"perpage": perpage,
})
@ -816,13 +819,29 @@ func (s *apiService) getDBRemoteNeed(w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), http.StatusNotFound)
} else {
sendJSON(w, map[string]interface{}{
"files": toNeedSlice(files),
"files": toJsonFileInfoSlice(files),
"page": page,
"perpage": perpage,
})
}
}
func (s *apiService) getDBLocalChanged(w http.ResponseWriter, r *http.Request) {
qs := r.URL.Query()
folder := qs.Get("folder")
page, perpage := getPagingParams(qs)
files := s.model.LocalChangedFiles(folder, page, perpage)
sendJSON(w, map[string]interface{}{
"files": toJsonFileInfoSlice(files),
"page": page,
"perpage": perpage,
})
}
func (s *apiService) getSystemConnections(w http.ResponseWriter, r *http.Request) {
sendJSON(w, s.model.ConnectionStats())
}
@ -1638,7 +1657,7 @@ func (s *apiService) getHeapProf(w http.ResponseWriter, r *http.Request) {
pprof.WriteHeapProfile(w)
}
func toNeedSlice(fs []db.FileInfoTruncated) []jsonDBFileInfo {
func toJsonFileInfoSlice(fs []db.FileInfoTruncated) []jsonDBFileInfo {
res := make([]jsonDBFileInfo, len(fs))
for i, f := range fs {
res[i] = jsonDBFileInfo(f)

View File

@ -146,3 +146,7 @@ func (m *mockedModel) FolderErrors(folder string) ([]model.FileError, error) {
func (m *mockedModel) WatchError(folder string) error {
return nil
}
func (m *mockedModel) LocalChangedFiles(folder string, page, perpage int) []db.FileInfoTruncated {
return nil
}

View File

@ -373,10 +373,10 @@
</span>
</td>
</tr>
<tr ng-if="neededItems(folder.id) > 0">
<tr ng-if="model[folder.id].needTotalItems > 0">
<th><span class="fas fa-fw fa-cloud-download-alt"></span>&nbsp;<span translate>Out of Sync Items</span></th>
<td class="text-right">
<a href="" ng-click="showNeed(folder.id)">{{neededItems(folder.id) | alwaysNumber}} <span translate>items</span>, ~{{model[folder.id].needBytes | binary}}B</a>
<a href="" ng-click="showNeed(folder.id)">{{model[folder.id].needTotalItems | alwaysNumber}} <span translate>items</span>, ~{{model[folder.id].needBytes | binary}}B</a>
</td>
</tr>
<tr ng-if="folderStatus(folder) === 'scanning' && scanRate(folder.id) > 0">
@ -392,6 +392,12 @@
<a href="" ng-click="showFailed(folder.id)">{{model[folder.id].pullErrors | alwaysNumber | localeNumber}}&nbsp;<span translate>items</span></a>
</td>
</tr>
<tr ng-if="folder.type == 'receiveonly' && canRevert(folder.id)">
<th><span class="fas fa-fw fa-exclamation-circle"></span>&nbsp;<span translate>Locally Changed Items</span></th>
<td class="text-right">
<a href="" ng-click="showLocalChanged(folder.id)">{{model[folder.id].receiveOnlyTotalItems | alwaysNumber}} <span translate>items</span>, ~{{model[folder.id].receiveOnlyBytes | binary}}B</a>
</td>
</tr>
<tr ng-if="folder.type != 'sendreceive'">
<th><span class="fas fa-fw fa-folder"></span>&nbsp;<span translate>Folder Type</span></th>
<td class="text-right">
@ -822,6 +828,7 @@
<ng-include src="'syncthing/transfer/neededFilesModalView.html'"></ng-include>
<ng-include src="'syncthing/transfer/failedFilesModalView.html'"></ng-include>
<ng-include src="'syncthing/transfer/remoteNeededFilesModalView.html'"></ng-include>
<ng-include src="'syncthing/transfer/localChangedFilesModalView.html'"></ng-include>
<ng-include src="'syncthing/core/majorUpgradeModalView.html'"></ng-include>
<ng-include src="'syncthing/core/aboutModalView.html'"></ng-include>
<ng-include src="'syncthing/core/discoveryFailuresModalView.html'"></ng-include>

View File

@ -46,6 +46,7 @@ angular.module('syncthing.core')
$scope.neededCurrentPage = 1;
$scope.neededPageSize = 10;
$scope.failed = {};
$scope.localChanged = {};
$scope.scanProgress = {};
$scope.themes = [];
$scope.globalChangeEvents = {};
@ -672,6 +673,15 @@ angular.module('syncthing.core')
});
};
$scope.refreshLocalChanged = function (page, perpage) {
var url = urlbase + '/db/localchanged?folder=';
url += encodeURIComponent($scope.localChanged.folder);
url += "&page=" + page + "&perpage=" + perpage;
$http.get(url).success(function (data) {
$scope.localChanged = data;
}).error($scope.emitHTTPError);
};
var refreshDeviceStats = debounce(function () {
$http.get(urlbase + "/stats/device").success(function (data) {
$scope.deviceStats = data;
@ -737,7 +747,7 @@ angular.module('syncthing.core')
if (state === 'error') {
return 'stopped'; // legacy, the state is called "stopped" in the GUI
}
if (state === 'idle' && $scope.neededItems(folderCfg.id) > 0) {
if (state === 'idle' && $scope.model[folderCfg.id].needTotalItems > 0) {
return 'outofsync';
}
if (state === 'scanning') {
@ -776,15 +786,6 @@ angular.module('syncthing.core')
return 'info';
};
$scope.neededItems = function (folderID) {
if (!$scope.model[folderID]) {
return 0
}
return $scope.model[folderID].needFiles + $scope.model[folderID].needDirectories +
$scope.model[folderID].needSymlinks + $scope.model[folderID].needDeletes;
};
$scope.syncPercentage = function (folder) {
if (typeof $scope.model[folder] === 'undefined') {
return 100;
@ -2194,6 +2195,14 @@ angular.module('syncthing.core')
$http.post(urlbase + "/db/override?folder=" + encodeURIComponent(folder));
};
$scope.showLocalChanged = function (folder) {
$scope.localChanged.folder = folder;
$scope.localChanged = $scope.refreshLocalChanged(1, 10);
$('#localChanged').modal().one('hidden.bs.modal', function () {
$scope.localChanged = {};
});
};
$scope.revert = function (folder) {
$http.post(urlbase + "/db/revert?folder=" + encodeURIComponent(folder));
};
@ -2203,11 +2212,7 @@ angular.module('syncthing.core')
if (!f) {
return false;
}
return f.receiveOnlyChangedBytes > 0 ||
f.receiveOnlyChangedDeletes > 0 ||
f.receiveOnlyChangedDirectories > 0 ||
f.receiveOnlyChangedFiles > 0 ||
f.receiveOnlyChangedSymlinks > 0;
return $scope.model[folder].receiveOnlyTotalItems > 0;
};
$scope.advanced = function () {

View File

@ -0,0 +1,31 @@
<modal id="localChanged" status="info" icon="fas fa-exclamation-circle" heading="{{'Locally Changed Items' | translate}}" large="yes" closeable="yes">
<div class="modal-body">
<p translate>
The following items were changed locally.
</p>
<table class="table table-striped">
<thead>
<tr>
<th translate>Path</th>
<th translate>Size</th>
</tr>
</thead>
<tr dir-paginate="file in localChanged.files | itemsPerPage: localChanged.perpage" current-page="localChanged.page" total-items="model[localChanged.folder].receiveOnlyTotalItems" pagination-id="localChanged">
<td>{{file.name}}</td>
<td><span ng-hide="file.type == 'DIRECTORY'">{{file.size | binary}}B</span></td>
</tr>
</table>
<dir-pagination-controls on-page-change="refreshLocalChanged(newPageNumber, localChanged.perpage)" pagination-id="localChanged"></dir-pagination-controls>
<ul class="pagination pull-right">
<li ng-repeat="option in [10, 25, 50]" ng-class="{ active: localChanged.page == option }">
<a href="#" ng-click="refreshLocalChanged(localChanged.page, option)">{{option}}</a>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
<span class="fas fa-times"></span>&nbsp;<span translate>Close</span>
</button>
</div>
</modal>

View File

@ -14,7 +14,7 @@
<table class="table table-striped table-condensed">
<tr dir-paginate="f in needed | itemsPerPage: neededPageSize" current-page="neededCurrentPage" total-items="neededItems(neededFolder)" pagination-id="needed">
<tr dir-paginate="f in needed | itemsPerPage: neededPageSize" current-page="neededCurrentPage" total-items="model[neededFolder].needTotalItems" pagination-id="needed">
<!-- Icon -->
<td class="small-data col-xs-2">

View File

@ -45,6 +45,7 @@ type FileIntf interface {
IsIgnored() bool
IsUnsupported() bool
MustRescan() bool
IsReceiveOnlyChanged() bool
IsDirectory() bool
IsSymlink() bool
ShouldConflict() bool

View File

@ -142,6 +142,10 @@ func (c Counts) Add(other Counts) Counts {
}
}
func (c Counts) TotalItems() int32 {
return c.Files + c.Directories + c.Symlinks + c.Deleted
}
func (vl VersionList) String() string {
var b bytes.Buffer
var id protocol.DeviceID

View File

@ -797,12 +797,10 @@ func (m *Model) NeedFolderFiles(folder string, page, perpage int) ([]db.FileInfo
skip--
return true
}
if get > 0 {
ft := f.(db.FileInfoTruncated)
if _, ok := seen[ft.Name]; !ok {
rest = append(rest, ft)
get--
}
ft := f.(db.FileInfoTruncated)
if _, ok := seen[ft.Name]; !ok {
rest = append(rest, ft)
get--
}
return get > 0
})
@ -810,6 +808,47 @@ func (m *Model) NeedFolderFiles(folder string, page, perpage int) ([]db.FileInfo
return progress, queued, rest
}
// LocalChangedFiles returns a paginated list of currently needed files in
// progress, queued, and to be queued on next puller iteration, as well as the
// total number of files currently needed.
func (m *Model) LocalChangedFiles(folder string, page, perpage int) []db.FileInfoTruncated {
m.fmut.RLock()
defer m.fmut.RUnlock()
rf, ok := m.folderFiles[folder]
if !ok {
return nil
}
fcfg := m.folderCfgs[folder]
if fcfg.Type != config.FolderTypeReceiveOnly {
return nil
}
if rf.ReceiveOnlyChangedSize().TotalItems() == 0 {
return nil
}
files := make([]db.FileInfoTruncated, 0, perpage)
skip := (page - 1) * perpage
get := perpage
rf.WithHaveTruncated(protocol.LocalDeviceID, func(f db.FileIntf) bool {
if !f.IsReceiveOnlyChanged() {
return true
}
if skip > 0 {
skip--
return true
}
ft := f.(db.FileInfoTruncated)
files = append(files, ft)
get--
return get > 0
})
return files
}
// RemoteNeedFolderFiles returns paginated list of currently needed files in
// progress, queued, and to be queued on next puller iteration, as well as the
// total number of files currently needed.
@ -833,10 +872,8 @@ func (m *Model) RemoteNeedFolderFiles(device protocol.DeviceID, folder string, p
skip--
return true
}
if get > 0 {
files = append(files, f.(db.FileInfoTruncated))
get--
}
files = append(files, f.(db.FileInfoTruncated))
get--
return get > 0
})