mirror of
https://github.com/octoleo/syncthing.git
synced 2024-11-10 23:30:58 +00:00
gui, lib/model: Display list of files needed by remote (fixes #4369)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/4559 LGTM: AudriusButkevicius, calmh
This commit is contained in:
parent
c7522063b3
commit
8fa2b7765a
@ -13,6 +13,7 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
@ -83,7 +84,8 @@ type modelIntf interface {
|
|||||||
GlobalDirectoryTree(folder, prefix string, levels int, dirsonly bool) map[string]interface{}
|
GlobalDirectoryTree(folder, prefix string, levels int, dirsonly bool) map[string]interface{}
|
||||||
Completion(device protocol.DeviceID, folder string) model.FolderCompletion
|
Completion(device protocol.DeviceID, folder string) model.FolderCompletion
|
||||||
Override(folder string)
|
Override(folder string)
|
||||||
NeedFolderFiles(folder string, page, perpage int) ([]db.FileInfoTruncated, []db.FileInfoTruncated, []db.FileInfoTruncated, int)
|
NeedFolderFiles(folder string, page, perpage int) ([]db.FileInfoTruncated, []db.FileInfoTruncated, []db.FileInfoTruncated)
|
||||||
|
RemoteNeedFolderFiles(device protocol.DeviceID, folder string, page, perpage int) ([]db.FileInfoTruncated, error)
|
||||||
NeedSize(folder string) db.Counts
|
NeedSize(folder string) db.Counts
|
||||||
ConnectionStats() map[string]interface{}
|
ConnectionStats() map[string]interface{}
|
||||||
DeviceStatistics() map[string]stats.DeviceStatistics
|
DeviceStatistics() map[string]stats.DeviceStatistics
|
||||||
@ -254,6 +256,7 @@ func (s *apiService) Serve() {
|
|||||||
getRestMux.HandleFunc("/rest/db/file", s.getDBFile) // folder file
|
getRestMux.HandleFunc("/rest/db/file", s.getDBFile) // folder file
|
||||||
getRestMux.HandleFunc("/rest/db/ignores", s.getDBIgnores) // folder
|
getRestMux.HandleFunc("/rest/db/ignores", s.getDBIgnores) // folder
|
||||||
getRestMux.HandleFunc("/rest/db/need", s.getDBNeed) // folder [perpage] [page]
|
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/status", s.getDBStatus) // folder
|
getRestMux.HandleFunc("/rest/db/status", s.getDBStatus) // folder
|
||||||
getRestMux.HandleFunc("/rest/db/browse", s.getDBBrowse) // folder [prefix] [dirsonly] [levels]
|
getRestMux.HandleFunc("/rest/db/browse", s.getDBBrowse) // folder [prefix] [dirsonly] [levels]
|
||||||
getRestMux.HandleFunc("/rest/events", s.getIndexEvents) // [since] [limit] [timeout] [events]
|
getRestMux.HandleFunc("/rest/events", s.getIndexEvents) // [since] [limit] [timeout] [events]
|
||||||
@ -661,6 +664,7 @@ func (s *apiService) getDBCompletion(w http.ResponseWriter, r *http.Request) {
|
|||||||
sendJSON(w, map[string]interface{}{
|
sendJSON(w, map[string]interface{}{
|
||||||
"completion": comp.CompletionPct,
|
"completion": comp.CompletionPct,
|
||||||
"needBytes": comp.NeedBytes,
|
"needBytes": comp.NeedBytes,
|
||||||
|
"needItems": comp.NeedItems,
|
||||||
"globalBytes": comp.GlobalBytes,
|
"globalBytes": comp.GlobalBytes,
|
||||||
"needDeletes": comp.NeedDeletes,
|
"needDeletes": comp.NeedDeletes,
|
||||||
})
|
})
|
||||||
@ -718,11 +722,7 @@ func (s *apiService) postDBOverride(w http.ResponseWriter, r *http.Request) {
|
|||||||
go s.model.Override(folder)
|
go s.model.Override(folder)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *apiService) getDBNeed(w http.ResponseWriter, r *http.Request) {
|
func getPagingParams(qs url.Values) (int, int) {
|
||||||
qs := r.URL.Query()
|
|
||||||
|
|
||||||
folder := qs.Get("folder")
|
|
||||||
|
|
||||||
page, err := strconv.Atoi(qs.Get("page"))
|
page, err := strconv.Atoi(qs.Get("page"))
|
||||||
if err != nil || page < 1 {
|
if err != nil || page < 1 {
|
||||||
page = 1
|
page = 1
|
||||||
@ -731,20 +731,52 @@ func (s *apiService) getDBNeed(w http.ResponseWriter, r *http.Request) {
|
|||||||
if err != nil || perpage < 1 {
|
if err != nil || perpage < 1 {
|
||||||
perpage = 1 << 16
|
perpage = 1 << 16
|
||||||
}
|
}
|
||||||
|
return page, perpage
|
||||||
|
}
|
||||||
|
|
||||||
progress, queued, rest, total := s.model.NeedFolderFiles(folder, page, perpage)
|
func (s *apiService) getDBNeed(w http.ResponseWriter, r *http.Request) {
|
||||||
|
qs := r.URL.Query()
|
||||||
|
|
||||||
|
folder := qs.Get("folder")
|
||||||
|
|
||||||
|
page, perpage := getPagingParams(qs)
|
||||||
|
|
||||||
|
progress, queued, rest := s.model.NeedFolderFiles(folder, page, perpage)
|
||||||
|
|
||||||
// Convert the struct to a more loose structure, and inject the size.
|
// Convert the struct to a more loose structure, and inject the size.
|
||||||
sendJSON(w, map[string]interface{}{
|
sendJSON(w, map[string]interface{}{
|
||||||
"progress": s.toNeedSlice(progress),
|
"progress": toNeedSlice(progress),
|
||||||
"queued": s.toNeedSlice(queued),
|
"queued": toNeedSlice(queued),
|
||||||
"rest": s.toNeedSlice(rest),
|
"rest": toNeedSlice(rest),
|
||||||
"total": total,
|
|
||||||
"page": page,
|
"page": page,
|
||||||
"perpage": perpage,
|
"perpage": perpage,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *apiService) getDBRemoteNeed(w http.ResponseWriter, r *http.Request) {
|
||||||
|
qs := r.URL.Query()
|
||||||
|
|
||||||
|
folder := qs.Get("folder")
|
||||||
|
device := qs.Get("device")
|
||||||
|
deviceID, err := protocol.DeviceIDFromString(device)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
page, perpage := getPagingParams(qs)
|
||||||
|
|
||||||
|
if files, err := s.model.RemoteNeedFolderFiles(deviceID, folder, page, perpage); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusNotFound)
|
||||||
|
} else {
|
||||||
|
sendJSON(w, map[string]interface{}{
|
||||||
|
"files": toNeedSlice(files),
|
||||||
|
"page": page,
|
||||||
|
"perpage": perpage,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *apiService) getSystemConnections(w http.ResponseWriter, r *http.Request) {
|
func (s *apiService) getSystemConnections(w http.ResponseWriter, r *http.Request) {
|
||||||
sendJSON(w, s.model.ConnectionStats())
|
sendJSON(w, s.model.ConnectionStats())
|
||||||
}
|
}
|
||||||
@ -1351,7 +1383,7 @@ func (s *apiService) getHeapProf(w http.ResponseWriter, r *http.Request) {
|
|||||||
pprof.WriteHeapProfile(w)
|
pprof.WriteHeapProfile(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *apiService) toNeedSlice(fs []db.FileInfoTruncated) []jsonDBFileInfo {
|
func toNeedSlice(fs []db.FileInfoTruncated) []jsonDBFileInfo {
|
||||||
res := make([]jsonDBFileInfo, len(fs))
|
res := make([]jsonDBFileInfo, len(fs))
|
||||||
for i, f := range fs {
|
for i, f := range fs {
|
||||||
res[i] = jsonDBFileInfo(f)
|
res[i] = jsonDBFileInfo(f)
|
||||||
@ -1373,6 +1405,7 @@ func (f jsonFileInfo) MarshalJSON() ([]byte, error) {
|
|||||||
"invalid": f.Invalid,
|
"invalid": f.Invalid,
|
||||||
"noPermissions": f.NoPermissions,
|
"noPermissions": f.NoPermissions,
|
||||||
"modified": protocol.FileInfo(f).ModTime(),
|
"modified": protocol.FileInfo(f).ModTime(),
|
||||||
|
"modifiedBy": f.ModifiedBy.String(),
|
||||||
"sequence": f.Sequence,
|
"sequence": f.Sequence,
|
||||||
"numBlocks": len(f.Blocks),
|
"numBlocks": len(f.Blocks),
|
||||||
"version": jsonVersionVector(f.Version),
|
"version": jsonVersionVector(f.Version),
|
||||||
@ -1384,13 +1417,14 @@ type jsonDBFileInfo db.FileInfoTruncated
|
|||||||
func (f jsonDBFileInfo) MarshalJSON() ([]byte, error) {
|
func (f jsonDBFileInfo) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(map[string]interface{}{
|
return json.Marshal(map[string]interface{}{
|
||||||
"name": f.Name,
|
"name": f.Name,
|
||||||
"type": f.Type,
|
"type": f.Type.String(),
|
||||||
"size": f.Size,
|
"size": f.Size,
|
||||||
"permissions": fmt.Sprintf("%#o", f.Permissions),
|
"permissions": fmt.Sprintf("%#o", f.Permissions),
|
||||||
"deleted": f.Deleted,
|
"deleted": f.Deleted,
|
||||||
"invalid": f.Invalid,
|
"invalid": f.Invalid,
|
||||||
"noPermissions": f.NoPermissions,
|
"noPermissions": f.NoPermissions,
|
||||||
"modified": db.FileInfoTruncated(f).ModTime(),
|
"modified": db.FileInfoTruncated(f).ModTime(),
|
||||||
|
"modifiedBy": f.ModifiedBy.String(),
|
||||||
"sequence": f.Sequence,
|
"sequence": f.Sequence,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -28,8 +28,12 @@ func (m *mockedModel) Completion(device protocol.DeviceID, folder string) model.
|
|||||||
|
|
||||||
func (m *mockedModel) Override(folder string) {}
|
func (m *mockedModel) Override(folder string) {}
|
||||||
|
|
||||||
func (m *mockedModel) NeedFolderFiles(folder string, page, perpage int) ([]db.FileInfoTruncated, []db.FileInfoTruncated, []db.FileInfoTruncated, int) {
|
func (m *mockedModel) NeedFolderFiles(folder string, page, perpage int) ([]db.FileInfoTruncated, []db.FileInfoTruncated, []db.FileInfoTruncated) {
|
||||||
return nil, nil, nil, 0
|
return nil, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockedModel) RemoteNeedFolderFiles(device protocol.DeviceID, folder string, page, perpage int) ([]db.FileInfoTruncated, error) {
|
||||||
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockedModel) NeedSize(folder string) db.Counts {
|
func (m *mockedModel) NeedSize(folder string) db.Counts {
|
||||||
|
@ -211,6 +211,7 @@ func (c *folderSummaryService) sendSummary(folder string) {
|
|||||||
"device": devCfg.DeviceID.String(),
|
"device": devCfg.DeviceID.String(),
|
||||||
"completion": comp.CompletionPct,
|
"completion": comp.CompletionPct,
|
||||||
"needBytes": comp.NeedBytes,
|
"needBytes": comp.NeedBytes,
|
||||||
|
"needItems": comp.NeedItems,
|
||||||
"globalBytes": comp.GlobalBytes,
|
"globalBytes": comp.GlobalBytes,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -59,6 +59,7 @@
|
|||||||
"Device ID": "Device ID",
|
"Device ID": "Device ID",
|
||||||
"Device Identification": "Device Identification",
|
"Device Identification": "Device Identification",
|
||||||
"Device Name": "Device Name",
|
"Device Name": "Device Name",
|
||||||
|
"Device that last modified the item": "Device that last modified the item",
|
||||||
"Devices": "Devices",
|
"Devices": "Devices",
|
||||||
"Disabled": "Disabled",
|
"Disabled": "Disabled",
|
||||||
"Disconnected": "Disconnected",
|
"Disconnected": "Disconnected",
|
||||||
@ -130,6 +131,7 @@
|
|||||||
"Latest Change": "Latest Change",
|
"Latest Change": "Latest Change",
|
||||||
"Learn more": "Learn more",
|
"Learn more": "Learn more",
|
||||||
"Listeners": "Listeners",
|
"Listeners": "Listeners",
|
||||||
|
"Loading data...": "Loading data...",
|
||||||
"Local Discovery": "Local Discovery",
|
"Local Discovery": "Local Discovery",
|
||||||
"Local State": "Local State",
|
"Local State": "Local State",
|
||||||
"Local State (Total)": "Local State (Total)",
|
"Local State (Total)": "Local State (Total)",
|
||||||
@ -138,6 +140,8 @@
|
|||||||
"Maximum Age": "Maximum Age",
|
"Maximum Age": "Maximum Age",
|
||||||
"Metadata Only": "Metadata Only",
|
"Metadata Only": "Metadata Only",
|
||||||
"Minimum Free Disk Space": "Minimum Free Disk Space",
|
"Minimum Free Disk Space": "Minimum Free Disk Space",
|
||||||
|
"Mod. Device": "Mod. Device",
|
||||||
|
"Mod. Time": "Mod. Time",
|
||||||
"Move to top of queue": "Move to top of queue",
|
"Move to top of queue": "Move to top of queue",
|
||||||
"Multi level wildcard (matches multiple directory levels)": "Multi level wildcard (matches multiple directory levels)",
|
"Multi level wildcard (matches multiple directory levels)": "Multi level wildcard (matches multiple directory levels)",
|
||||||
"Never": "Never",
|
"Never": "Never",
|
||||||
@ -221,6 +225,7 @@
|
|||||||
"Shutdown Complete": "Shutdown Complete",
|
"Shutdown Complete": "Shutdown Complete",
|
||||||
"Simple File Versioning": "Simple File Versioning",
|
"Simple File Versioning": "Simple File Versioning",
|
||||||
"Single level wildcard (matches within a directory only)": "Single level wildcard (matches within a directory only)",
|
"Single level wildcard (matches within a directory only)": "Single level wildcard (matches within a directory only)",
|
||||||
|
"Size": "Size",
|
||||||
"Smallest First": "Smallest First",
|
"Smallest First": "Smallest First",
|
||||||
"Source Code": "Source Code",
|
"Source Code": "Source Code",
|
||||||
"Stable releases and release candidates": "Stable releases and release candidates",
|
"Stable releases and release candidates": "Stable releases and release candidates",
|
||||||
@ -268,6 +273,7 @@
|
|||||||
"This is a major version upgrade.": "This is a major version upgrade.",
|
"This is a major version upgrade.": "This is a major version upgrade.",
|
||||||
"This setting controls the free space required on the home (i.e., index database) disk.": "This setting controls the free space required on the home (i.e., index database) disk.",
|
"This setting controls the free space required on the home (i.e., index database) disk.": "This setting controls the free space required on the home (i.e., index database) disk.",
|
||||||
"Time": "Time",
|
"Time": "Time",
|
||||||
|
"Time the item was last modified": "Time the item was last modified",
|
||||||
"Trash Can File Versioning": "Trash Can File Versioning",
|
"Trash Can File Versioning": "Trash Can File Versioning",
|
||||||
"Type": "Type",
|
"Type": "Type",
|
||||||
"Unavailable": "Unavailable",
|
"Unavailable": "Unavailable",
|
||||||
|
@ -603,6 +603,12 @@
|
|||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr ng-if="deviceStatus(deviceCfg) == 'syncing'">
|
||||||
|
<th><span class="fa fa-fw fa-exchange"></span> <span translate>Out of Sync Items</span></th>
|
||||||
|
<td class="text-right">
|
||||||
|
<a href="" ng-click="showRemoteNeed(deviceCfg)">{{completion[deviceCfg.deviceID]._needItems | alwaysNumber}} <span translate>items</span>, ~{{completion[deviceCfg.deviceID]._needBytes | binary}}B</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th><span class="fa fa-fw fa-link"></span> <span translate>Address</span></th>
|
<th><span class="fa fa-fw fa-link"></span> <span translate>Address</span></th>
|
||||||
<td ng-if="connections[deviceCfg.deviceID].connected" class="text-right">
|
<td ng-if="connections[deviceCfg.deviceID].connected" class="text-right">
|
||||||
@ -722,6 +728,7 @@
|
|||||||
<ng-include src="'syncthing/usagereport/usageReportPreviewModalView.html'"></ng-include>
|
<ng-include src="'syncthing/usagereport/usageReportPreviewModalView.html'"></ng-include>
|
||||||
<ng-include src="'syncthing/transfer/neededFilesModalView.html'"></ng-include>
|
<ng-include src="'syncthing/transfer/neededFilesModalView.html'"></ng-include>
|
||||||
<ng-include src="'syncthing/transfer/failedFilesModalView.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/core/majorUpgradeModalView.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/aboutModalView.html'"></ng-include>
|
||||||
<ng-include src="'syncthing/core/discoveryFailuresModalView.html'"></ng-include>
|
<ng-include src="'syncthing/core/discoveryFailuresModalView.html'"></ng-include>
|
||||||
|
@ -45,7 +45,6 @@ angular.module('syncthing.core')
|
|||||||
$scope.progress = {};
|
$scope.progress = {};
|
||||||
$scope.version = {};
|
$scope.version = {};
|
||||||
$scope.needed = [];
|
$scope.needed = [];
|
||||||
$scope.neededTotal = 0;
|
|
||||||
$scope.neededCurrentPage = 1;
|
$scope.neededCurrentPage = 1;
|
||||||
$scope.neededPageSize = 10;
|
$scope.neededPageSize = 10;
|
||||||
$scope.failed = {};
|
$scope.failed = {};
|
||||||
@ -56,6 +55,7 @@ angular.module('syncthing.core')
|
|||||||
$scope.globalChangeEvents = {};
|
$scope.globalChangeEvents = {};
|
||||||
$scope.metricRates = false;
|
$scope.metricRates = false;
|
||||||
$scope.folderPathErrors = {};
|
$scope.folderPathErrors = {};
|
||||||
|
resetRemoteNeed();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$scope.metricRates = (window.localStorage["metricRates"] == "true");
|
$scope.metricRates = (window.localStorage["metricRates"] == "true");
|
||||||
@ -241,7 +241,8 @@ angular.module('syncthing.core')
|
|||||||
};
|
};
|
||||||
$scope.completion[arg.data.id] = {
|
$scope.completion[arg.data.id] = {
|
||||||
_total: 100,
|
_total: 100,
|
||||||
_needBytes: 0
|
_needBytes: 0,
|
||||||
|
_needItems: 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -389,7 +390,8 @@ angular.module('syncthing.core')
|
|||||||
$scope.devices.forEach(function (deviceCfg) {
|
$scope.devices.forEach(function (deviceCfg) {
|
||||||
$scope.completion[deviceCfg.deviceID] = {
|
$scope.completion[deviceCfg.deviceID] = {
|
||||||
_total: 100,
|
_total: 100,
|
||||||
_needBytes: 0
|
_needBytes: 0,
|
||||||
|
_needItems: 0
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
$scope.devices.sort(deviceCompare);
|
$scope.devices.sort(deviceCompare);
|
||||||
@ -431,7 +433,7 @@ angular.module('syncthing.core')
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
$scope.listenersFailed = listenersFailed;
|
$scope.listenersFailed = listenersFailed;
|
||||||
$scope.listenersTotal = Object.keys(data.connectionServiceStatus).length;
|
$scope.listenersTotal = $scope.sizeOf(data.connectionServiceStatus);
|
||||||
|
|
||||||
$scope.discoveryTotal = data.discoveryMethods;
|
$scope.discoveryTotal = data.discoveryMethods;
|
||||||
var discoveryFailed = [];
|
var discoveryFailed = [];
|
||||||
@ -476,21 +478,24 @@ angular.module('syncthing.core')
|
|||||||
}
|
}
|
||||||
|
|
||||||
function recalcCompletion(device) {
|
function recalcCompletion(device) {
|
||||||
var total = 0, needed = 0, deletes = 0;
|
var total = 0, needed = 0, deletes = 0, items = 0;
|
||||||
for (var folder in $scope.completion[device]) {
|
for (var folder in $scope.completion[device]) {
|
||||||
if (folder === "_total" || folder === '_needBytes') {
|
if (folder === "_total" || folder === '_needBytes' || folder === '_needItems') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
total += $scope.completion[device][folder].globalBytes;
|
total += $scope.completion[device][folder].globalBytes;
|
||||||
needed += $scope.completion[device][folder].needBytes;
|
needed += $scope.completion[device][folder].needBytes;
|
||||||
|
items += $scope.completion[device][folder].needItems;
|
||||||
deletes += $scope.completion[device][folder].needDeletes;
|
deletes += $scope.completion[device][folder].needDeletes;
|
||||||
}
|
}
|
||||||
if (total == 0) {
|
if (total == 0) {
|
||||||
$scope.completion[device]._total = 100;
|
$scope.completion[device]._total = 100;
|
||||||
$scope.completion[device]._needBytes = 0;
|
$scope.completion[device]._needBytes = 0;
|
||||||
|
$scope.completion[device]._needItems = 0;
|
||||||
} else {
|
} else {
|
||||||
$scope.completion[device]._total = Math.floor(100 * (1 - needed / total));
|
$scope.completion[device]._total = Math.floor(100 * (1 - needed / total));
|
||||||
$scope.completion[device]._needBytes = needed
|
$scope.completion[device]._needBytes = needed
|
||||||
|
$scope.completion[device]._needItems = items;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (needed == 0 && deletes > 0) {
|
if (needed == 0 && deletes > 0) {
|
||||||
@ -498,7 +503,6 @@ angular.module('syncthing.core')
|
|||||||
// to do. Drop down the completion percentage to indicate
|
// to do. Drop down the completion percentage to indicate
|
||||||
// that we have stuff to do.
|
// that we have stuff to do.
|
||||||
$scope.completion[device]._total = 95;
|
$scope.completion[device]._total = 95;
|
||||||
$scope.completion[device]._needBytes = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("recalcCompletion", device, $scope.completion[device]);
|
console.log("recalcCompletion", device, $scope.completion[device]);
|
||||||
@ -616,7 +620,6 @@ angular.module('syncthing.core')
|
|||||||
merged.push(item);
|
merged.push(item);
|
||||||
});
|
});
|
||||||
$scope.needed = merged;
|
$scope.needed = merged;
|
||||||
$scope.neededTotal = data.total;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function pathJoin(base, name) {
|
function pathJoin(base, name) {
|
||||||
@ -638,6 +641,12 @@ angular.module('syncthing.core')
|
|||||||
return $scope.config.options && $scope.config.options.defaultFolderPath && !$scope.editingExisting && $scope.folderEditor.folderPath.$pristine
|
return $scope.config.options && $scope.config.options.defaultFolderPath && !$scope.editingExisting && $scope.folderEditor.folderPath.$pristine
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resetRemoteNeed() {
|
||||||
|
$scope.remoteNeed = {};
|
||||||
|
$scope.remoteNeedFolders = [];
|
||||||
|
$scope.remoteNeedDevice = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
$scope.neededPageChanged = function (page) {
|
$scope.neededPageChanged = function (page) {
|
||||||
$scope.neededCurrentPage = page;
|
$scope.neededCurrentPage = page;
|
||||||
refreshNeed($scope.neededFolder);
|
refreshNeed($scope.neededFolder);
|
||||||
@ -656,6 +665,20 @@ angular.module('syncthing.core')
|
|||||||
$scope.failedPageSize = perpage;
|
$scope.failedPageSize = perpage;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.refreshRemoteNeed = function (folder, page, perpage) {
|
||||||
|
var url = urlbase + '/db/remoteneed?device=' + $scope.remoteNeedDevice.deviceID;
|
||||||
|
url += '&folder=' + encodeURIComponent(folder);
|
||||||
|
url += "&page=" + page + "&perpage=" + perpage;
|
||||||
|
$http.get(url).success(function (data) {
|
||||||
|
if ($scope.remoteNeedDevice !== '') {
|
||||||
|
$scope.remoteNeed[folder] = data;
|
||||||
|
}
|
||||||
|
}).error(function (err) {
|
||||||
|
$scope.remoteNeed[folder] = undefined;
|
||||||
|
$scope.emitHTTPError(err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
var refreshDeviceStats = debounce(function () {
|
var refreshDeviceStats = debounce(function () {
|
||||||
$http.get(urlbase + "/stats/device").success(function (data) {
|
$http.get(urlbase + "/stats/device").success(function (data) {
|
||||||
$scope.deviceStats = data;
|
$scope.deviceStats = data;
|
||||||
@ -965,7 +988,7 @@ angular.module('syncthing.core')
|
|||||||
}
|
}
|
||||||
|
|
||||||
// enumerate notifications
|
// enumerate notifications
|
||||||
if ($scope.openNoAuth || !$scope.configInSync || Object.keys($scope.deviceRejections).length > 0 || Object.keys($scope.folderRejections).length > 0 || $scope.errorList().length > 0 || !online) {
|
if ($scope.openNoAuth || !$scope.configInSync || $scope.sizeOf($scope.deviceRejections) > 0 || $scope.sizeOf($scope.folderRejections) > 0 || $scope.errorList().length > 0 || !online) {
|
||||||
notifyCount++;
|
notifyCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1623,17 +1646,14 @@ angular.module('syncthing.core')
|
|||||||
|
|
||||||
$scope.deviceFolders = function (deviceCfg) {
|
$scope.deviceFolders = function (deviceCfg) {
|
||||||
var folders = [];
|
var folders = [];
|
||||||
for (var folderID in $scope.folders) {
|
$scope.folderList().forEach(function (folder) {
|
||||||
var devices = $scope.folders[folderID].devices;
|
for (var i = 0; i < folder.devices.length; i++) {
|
||||||
for (var i = 0; i < devices.length; i++) {
|
if (folder.devices[i].deviceID === deviceCfg.deviceID) {
|
||||||
if (devices[i].deviceID === deviceCfg.deviceID) {
|
folders.push(folder.id);
|
||||||
folders.push(folderID);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
folders.sort(folderCompare);
|
|
||||||
return folders;
|
return folders;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1729,11 +1749,25 @@ angular.module('syncthing.core')
|
|||||||
$('#needed').modal().on('hidden.bs.modal', function () {
|
$('#needed').modal().on('hidden.bs.modal', function () {
|
||||||
$scope.neededFolder = undefined;
|
$scope.neededFolder = undefined;
|
||||||
$scope.needed = undefined;
|
$scope.needed = undefined;
|
||||||
$scope.neededTotal = 0;
|
|
||||||
$scope.neededCurrentPage = 1;
|
$scope.neededCurrentPage = 1;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.showRemoteNeed = function (device) {
|
||||||
|
resetRemoteNeed();
|
||||||
|
$scope.remoteNeedDevice = device;
|
||||||
|
$scope.deviceFolders(device).forEach(function(folder) {
|
||||||
|
if ($scope.completion[device.deviceID][folder].needItems === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$scope.remoteNeedFolders.push(folder);
|
||||||
|
$scope.refreshRemoteNeed(folder, 1, 10);
|
||||||
|
});
|
||||||
|
$('#remoteNeed').modal().on('hidden.bs.modal', function () {
|
||||||
|
resetRemoteNeed();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
$scope.showFailed = function (folder) {
|
$scope.showFailed = function (folder) {
|
||||||
$scope.failedCurrent = $scope.failed[folder];
|
$scope.failedCurrent = $scope.failed[folder];
|
||||||
$scope.failedFolderPath = $scope.folders[folder].path;
|
$scope.failedFolderPath = $scope.folders[folder].path;
|
||||||
@ -1900,12 +1934,16 @@ angular.module('syncthing.core')
|
|||||||
// pseudo main. called on all definitions assigned
|
// pseudo main. called on all definitions assigned
|
||||||
initController();
|
initController();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
$scope.toggleUnits = function () {
|
$scope.toggleUnits = function () {
|
||||||
$scope.metricRates = !$scope.metricRates;
|
$scope.metricRates = !$scope.metricRates;
|
||||||
try {
|
try {
|
||||||
window.localStorage["metricRates"] = $scope.metricRates;
|
window.localStorage["metricRates"] = $scope.metricRates;
|
||||||
} catch (exception) { }
|
} catch (exception) { }
|
||||||
}
|
};
|
||||||
|
|
||||||
|
$scope.sizeOf = function (dict) {
|
||||||
|
return Object.keys(dict).length;
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
<table class="table table-striped table-condensed">
|
<table class="table table-striped table-condensed">
|
||||||
|
|
||||||
<tr dir-paginate="f in needed | itemsPerPage: neededPageSize" current-page="neededCurrentPage" total-items="neededTotal" pagination-id="needed">
|
<tr dir-paginate="f in needed | itemsPerPage: neededPageSize" current-page="neededCurrentPage" total-items="neededItems(neededFolder)" pagination-id="needed">
|
||||||
|
|
||||||
<!-- Icon -->
|
<!-- Icon -->
|
||||||
<td class="small-data col-xs-2">
|
<td class="small-data col-xs-2">
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
é<modal id="remoteNeed" status="info" icon="exchange" heading="{{'Out of Sync Items' | translate}} - {{deviceName(remoteNeedDevice)}}" large="yes" closeable="yes">
|
||||||
|
<div class="modal-body">
|
||||||
|
<div ng-if="sizeOf(remoteNeed) == 0">
|
||||||
|
<span translate>Loading data...</span>
|
||||||
|
</div>
|
||||||
|
<div ng-if="sizeOf(remoteNeed) > 0">
|
||||||
|
<div class="panel panel-default" ng-repeat="folder in remoteNeedFolders" ng-if="remoteNeed[folder] && remoteNeed[folder].files.length > 0">
|
||||||
|
<button class="btn panel-heading" data-toggle="collapse" data-target="#remoteNeed-{{folder}}" aria-expanded="false">
|
||||||
|
<h4 class="panel-title">
|
||||||
|
<span>{{folderLabel(folder)}}</span>
|
||||||
|
</h4>
|
||||||
|
</button>
|
||||||
|
<div id="remoteNeed-{{folder}}" class="panel-collapse collapse">
|
||||||
|
<div class="panel-body">
|
||||||
|
<table class="table table-striped table-dynamic">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th translate>Path</th>
|
||||||
|
<th translate>Size</th>
|
||||||
|
<th><span tooltip data-original-title="{{'Time the item was last modified' | translate}}" translate>Mod. Time</span></th>
|
||||||
|
<th><span tooltip data-original-title="{{'Device that last modified the item' | translate}}" translate>Mod. Device</span></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tr dir-paginate="file in remoteNeed[folder].files | itemsPerPage: remoteNeed[folder].perpage" current-page="remoteNeed[folder].page" total-items="completion[remoteNeedDevice.deviceID][folder].needItems" pagination-id="'remoteNeed-' + folder">
|
||||||
|
<td>{{file.name}}</td>
|
||||||
|
<td><span ng-hide="file.type == 'DIRECTORY'">{{file.size | binary}}B</span></td>
|
||||||
|
<td>{{file.modified | date:"yyyy-MM-dd HH:mm:ss"}}</td>
|
||||||
|
<td ng-if="file.modifiedBy">{{friendlyNameFromShort(file.modifiedBy)}}</td>
|
||||||
|
<td ng-if="!file.modifiedBy"><span translate>Unknown</span></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<dir-pagination-controls on-page-change="refreshRemoteNeed(folder, newPageNumber, remoteNeed[folder].perpage)" pagination-id="'remoteNeed-' + folder"></dir-pagination-controls>
|
||||||
|
<ul class="pagination pull-right">
|
||||||
|
<li ng-repeat="option in [10, 25, 50]" ng-class="{ active: remoteNeed[folder].perpage == option }">
|
||||||
|
<a href="#" ng-click="refreshRemoteNeed(folder, remoteNeed[folder].page, option)">{{option}}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div class="clearfix"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</modal>
|
@ -110,6 +110,7 @@ var (
|
|||||||
errDevicePaused = errors.New("device is paused")
|
errDevicePaused = errors.New("device is paused")
|
||||||
errDeviceIgnored = errors.New("device is ignored")
|
errDeviceIgnored = errors.New("device is ignored")
|
||||||
errFolderPaused = errors.New("folder is paused")
|
errFolderPaused = errors.New("folder is paused")
|
||||||
|
errFolderNotRunning = errors.New("folder is not running")
|
||||||
errFolderMissing = errors.New("no such folder")
|
errFolderMissing = errors.New("no such folder")
|
||||||
errNetworkNotAllowed = errors.New("network not allowed")
|
errNetworkNotAllowed = errors.New("network not allowed")
|
||||||
)
|
)
|
||||||
@ -182,15 +183,13 @@ func (m *Model) StartFolder(folder string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) startFolderLocked(folder string) config.FolderType {
|
func (m *Model) startFolderLocked(folder string) config.FolderType {
|
||||||
cfg, ok := m.folderCfgs[folder]
|
if err := m.checkFolderRunningLocked(folder); err == errFolderMissing {
|
||||||
if !ok {
|
panic("cannot start nonexistent folder " + folder)
|
||||||
panic("cannot start nonexistent folder " + cfg.Description())
|
} else if err == nil {
|
||||||
|
panic("cannot start already running folder " + folder)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, ok = m.folderRunners[folder]
|
cfg := m.folderCfgs[folder]
|
||||||
if ok {
|
|
||||||
panic("cannot start already running folder " + cfg.Description())
|
|
||||||
}
|
|
||||||
|
|
||||||
folderFactory, ok := folderFactories[cfg.Type]
|
folderFactory, ok := folderFactories[cfg.Type]
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -585,6 +584,7 @@ func (m *Model) FolderStatistics() map[string]stats.FolderStatistics {
|
|||||||
type FolderCompletion struct {
|
type FolderCompletion struct {
|
||||||
CompletionPct float64
|
CompletionPct float64
|
||||||
NeedBytes int64
|
NeedBytes int64
|
||||||
|
NeedItems int64
|
||||||
GlobalBytes int64
|
GlobalBytes int64
|
||||||
NeedDeletes int64
|
NeedDeletes int64
|
||||||
}
|
}
|
||||||
@ -611,7 +611,7 @@ func (m *Model) Completion(device protocol.DeviceID, folder string) FolderComple
|
|||||||
counts := m.deviceDownloads[device].GetBlockCounts(folder)
|
counts := m.deviceDownloads[device].GetBlockCounts(folder)
|
||||||
m.pmut.RUnlock()
|
m.pmut.RUnlock()
|
||||||
|
|
||||||
var need, fileNeed, downloaded, deletes int64
|
var need, items, fileNeed, downloaded, deletes int64
|
||||||
rf.WithNeedTruncated(device, func(f db.FileIntf) bool {
|
rf.WithNeedTruncated(device, func(f db.FileIntf) bool {
|
||||||
ft := f.(db.FileInfoTruncated)
|
ft := f.(db.FileInfoTruncated)
|
||||||
|
|
||||||
@ -630,6 +630,8 @@ func (m *Model) Completion(device protocol.DeviceID, folder string) FolderComple
|
|||||||
}
|
}
|
||||||
|
|
||||||
need += fileNeed
|
need += fileNeed
|
||||||
|
items++
|
||||||
|
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -649,6 +651,7 @@ func (m *Model) Completion(device protocol.DeviceID, folder string) FolderComple
|
|||||||
return FolderCompletion{
|
return FolderCompletion{
|
||||||
CompletionPct: completionPct,
|
CompletionPct: completionPct,
|
||||||
NeedBytes: need,
|
NeedBytes: need,
|
||||||
|
NeedItems: items,
|
||||||
GlobalBytes: tot,
|
GlobalBytes: tot,
|
||||||
NeedDeletes: deletes,
|
NeedDeletes: deletes,
|
||||||
}
|
}
|
||||||
@ -715,15 +718,13 @@ func (m *Model) NeedSize(folder string) db.Counts {
|
|||||||
// NeedFolderFiles returns paginated list of currently needed files in
|
// NeedFolderFiles returns paginated list of currently needed files in
|
||||||
// progress, queued, and to be queued on next puller iteration, as well as the
|
// progress, queued, and to be queued on next puller iteration, as well as the
|
||||||
// total number of files currently needed.
|
// total number of files currently needed.
|
||||||
func (m *Model) NeedFolderFiles(folder string, page, perpage int) ([]db.FileInfoTruncated, []db.FileInfoTruncated, []db.FileInfoTruncated, int) {
|
func (m *Model) NeedFolderFiles(folder string, page, perpage int) ([]db.FileInfoTruncated, []db.FileInfoTruncated, []db.FileInfoTruncated) {
|
||||||
m.fmut.RLock()
|
m.fmut.RLock()
|
||||||
defer m.fmut.RUnlock()
|
defer m.fmut.RUnlock()
|
||||||
|
|
||||||
total := 0
|
|
||||||
|
|
||||||
rf, ok := m.folderFiles[folder]
|
rf, ok := m.folderFiles[folder]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, nil, nil, 0
|
return nil, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var progress, queued, rest []db.FileInfoTruncated
|
var progress, queued, rest []db.FileInfoTruncated
|
||||||
@ -766,7 +767,6 @@ func (m *Model) NeedFolderFiles(folder string, page, perpage int) ([]db.FileInfo
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
total++
|
|
||||||
if skip > 0 {
|
if skip > 0 {
|
||||||
skip--
|
skip--
|
||||||
return true
|
return true
|
||||||
@ -778,10 +778,43 @@ func (m *Model) NeedFolderFiles(folder string, page, perpage int) ([]db.FileInfo
|
|||||||
get--
|
get--
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return get > 0
|
||||||
})
|
})
|
||||||
|
|
||||||
return progress, queued, rest, total
|
return progress, queued, rest
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
func (m *Model) RemoteNeedFolderFiles(device protocol.DeviceID, folder string, page, perpage int) ([]db.FileInfoTruncated, error) {
|
||||||
|
m.fmut.RLock()
|
||||||
|
m.pmut.RLock()
|
||||||
|
if err := m.checkDeviceFolderConnectedLocked(device, folder); err != nil {
|
||||||
|
m.pmut.RUnlock()
|
||||||
|
m.fmut.RUnlock()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rf := m.folderFiles[folder]
|
||||||
|
m.pmut.RUnlock()
|
||||||
|
m.fmut.RUnlock()
|
||||||
|
|
||||||
|
files := make([]db.FileInfoTruncated, 0, perpage)
|
||||||
|
skip := (page - 1) * perpage
|
||||||
|
get := perpage
|
||||||
|
rf.WithNeedTruncated(device, func(f db.FileIntf) bool {
|
||||||
|
if skip > 0 {
|
||||||
|
skip--
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if get > 0 {
|
||||||
|
files = append(files, f.(db.FileInfoTruncated))
|
||||||
|
get--
|
||||||
|
}
|
||||||
|
return get > 0
|
||||||
|
})
|
||||||
|
|
||||||
|
return files, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Index is called when a new device is connected and we receive their full index.
|
// Index is called when a new device is connected and we receive their full index.
|
||||||
@ -1865,22 +1898,30 @@ func (m *Model) ScanFolder(folder string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) ScanFolderSubdirs(folder string, subs []string) error {
|
func (m *Model) ScanFolderSubdirs(folder string, subs []string) error {
|
||||||
m.fmut.Lock()
|
m.fmut.RLock()
|
||||||
runner, okRunner := m.folderRunners[folder]
|
if err := m.checkFolderRunningLocked(folder); err != nil {
|
||||||
cfg, okCfg := m.folderCfgs[folder]
|
m.fmut.RUnlock()
|
||||||
m.fmut.Unlock()
|
return err
|
||||||
|
|
||||||
if !okRunner {
|
|
||||||
if okCfg && cfg.Paused {
|
|
||||||
return errFolderPaused
|
|
||||||
}
|
|
||||||
return errFolderMissing
|
|
||||||
}
|
}
|
||||||
|
runner := m.folderRunners[folder]
|
||||||
|
m.fmut.RUnlock()
|
||||||
|
|
||||||
return runner.Scan(subs)
|
return runner.Scan(subs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) internalScanFolderSubdirs(ctx context.Context, folder string, subDirs []string) error {
|
func (m *Model) internalScanFolderSubdirs(ctx context.Context, folder string, subDirs []string) error {
|
||||||
|
m.fmut.RLock()
|
||||||
|
if err := m.checkFolderRunningLocked(folder); err != nil {
|
||||||
|
m.fmut.RUnlock()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fset := m.folderFiles[folder]
|
||||||
|
folderCfg := m.folderCfgs[folder]
|
||||||
|
ignores := m.folderIgnores[folder]
|
||||||
|
runner := m.folderRunners[folder]
|
||||||
|
m.fmut.RUnlock()
|
||||||
|
mtimefs := fset.MtimeFS()
|
||||||
|
|
||||||
for i := 0; i < len(subDirs); i++ {
|
for i := 0; i < len(subDirs); i++ {
|
||||||
sub := osutil.NativeFilename(subDirs[i])
|
sub := osutil.NativeFilename(subDirs[i])
|
||||||
|
|
||||||
@ -1899,14 +1940,6 @@ func (m *Model) internalScanFolderSubdirs(ctx context.Context, folder string, su
|
|||||||
subDirs[i] = sub
|
subDirs[i] = sub
|
||||||
}
|
}
|
||||||
|
|
||||||
m.fmut.Lock()
|
|
||||||
fset := m.folderFiles[folder]
|
|
||||||
folderCfg := m.folderCfgs[folder]
|
|
||||||
ignores := m.folderIgnores[folder]
|
|
||||||
runner, ok := m.folderRunners[folder]
|
|
||||||
m.fmut.Unlock()
|
|
||||||
mtimefs := fset.MtimeFS()
|
|
||||||
|
|
||||||
// Check if the ignore patterns changed as part of scanning this folder.
|
// Check if the ignore patterns changed as part of scanning this folder.
|
||||||
// If they did we should schedule a pull of the folder so that we
|
// If they did we should schedule a pull of the folder so that we
|
||||||
// request things we might have suddenly become unignored and so on.
|
// request things we might have suddenly become unignored and so on.
|
||||||
@ -1918,13 +1951,6 @@ func (m *Model) internalScanFolderSubdirs(ctx context.Context, folder string, su
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if !ok {
|
|
||||||
if folderCfg.Paused {
|
|
||||||
return errFolderPaused
|
|
||||||
}
|
|
||||||
return errFolderMissing
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := runner.CheckHealth(); err != nil {
|
if err := runner.CheckHealth(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -2495,6 +2521,49 @@ func (m *Model) CommitConfiguration(from, to config.Configuration) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checkFolderRunningLocked returns nil if the folder is up and running and a
|
||||||
|
// descriptive error if not.
|
||||||
|
// Need to hold (read) lock on m.fmut when calling this.
|
||||||
|
func (m *Model) checkFolderRunningLocked(folder string) error {
|
||||||
|
_, ok := m.folderRunners[folder]
|
||||||
|
if ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg, ok := m.cfg.Folder(folder); !ok {
|
||||||
|
return errFolderMissing
|
||||||
|
} else if cfg.Paused {
|
||||||
|
return errFolderPaused
|
||||||
|
}
|
||||||
|
|
||||||
|
return errFolderNotRunning
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkFolderDeviceStatusLocked first checks the folder and then whether the
|
||||||
|
// given device is connected and shares this folder.
|
||||||
|
// Need to hold (read) lock on both m.fmut and m.pmut when calling this.
|
||||||
|
func (m *Model) checkDeviceFolderConnectedLocked(device protocol.DeviceID, folder string) error {
|
||||||
|
if err := m.checkFolderRunningLocked(folder); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg, ok := m.cfg.Device(device); !ok {
|
||||||
|
return errDeviceUnknown
|
||||||
|
} else if cfg.Paused {
|
||||||
|
return errDevicePaused
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := m.conn[device]; !ok {
|
||||||
|
return errors.New("device is not connected")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !m.folderDevices.has(device, folder) {
|
||||||
|
return errors.New("folder is not shared with device")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// mapFolders returns a map of folder ID to folder configuration for the given
|
// mapFolders returns a map of folder ID to folder configuration for the given
|
||||||
// slice of folder configurations.
|
// slice of folder configurations.
|
||||||
func mapFolders(folders []config.FolderConfiguration) map[string]config.FolderConfiguration {
|
func mapFolders(folders []config.FolderConfiguration) map[string]config.FolderConfiguration {
|
||||||
|
@ -2870,6 +2870,39 @@ func TestIssue4475(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPausedFolders(t *testing.T) {
|
||||||
|
// Create a separate wrapper not to pollute other tests.
|
||||||
|
cfg := defaultConfig.RawCopy()
|
||||||
|
wrapper := config.Wrap("/tmp/test", cfg)
|
||||||
|
|
||||||
|
db := db.OpenMemory()
|
||||||
|
m := NewModel(wrapper, protocol.LocalDeviceID, "syncthing", "dev", db, nil)
|
||||||
|
m.AddFolder(defaultFolderConfig)
|
||||||
|
m.StartFolder("default")
|
||||||
|
m.ServeBackground()
|
||||||
|
defer m.Stop()
|
||||||
|
|
||||||
|
if err := m.ScanFolder("default"); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pausedConfig := wrapper.RawCopy()
|
||||||
|
pausedConfig.Folders[0].Paused = true
|
||||||
|
w, err := m.cfg.Replace(pausedConfig)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
w.Wait()
|
||||||
|
|
||||||
|
if err := m.ScanFolder("default"); err != errFolderPaused {
|
||||||
|
t.Errorf("Expected folder paused error, received: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.ScanFolder("nonexistent"); err != errFolderMissing {
|
||||||
|
t.Errorf("Expected missing folder error, received: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func addFakeConn(m *Model, dev protocol.DeviceID) *fakeConnection {
|
func addFakeConn(m *Model, dev protocol.DeviceID) *fakeConnection {
|
||||||
fc := &fakeConnection{id: dev, model: m}
|
fc := &fakeConnection{id: dev, model: m}
|
||||||
m.AddConnection(fc, protocol.HelloResult{})
|
m.AddConnection(fc, protocol.HelloResult{})
|
||||||
|
Loading…
Reference in New Issue
Block a user