diff --git a/cmd/syncthing/gui.go b/cmd/syncthing/gui.go
index 5bf7f73a..ad39a8c2 100644
--- a/cmd/syncthing/gui.go
+++ b/cmd/syncthing/gui.go
@@ -13,6 +13,7 @@ import (
"io/ioutil"
"net"
"net/http"
+ "net/url"
"os"
"path/filepath"
"reflect"
@@ -83,7 +84,8 @@ type modelIntf interface {
GlobalDirectoryTree(folder, prefix string, levels int, dirsonly bool) map[string]interface{}
Completion(device protocol.DeviceID, folder string) model.FolderCompletion
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
ConnectionStats() map[string]interface{}
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/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/status", s.getDBStatus) // folder
getRestMux.HandleFunc("/rest/db/browse", s.getDBBrowse) // folder [prefix] [dirsonly] [levels]
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{}{
"completion": comp.CompletionPct,
"needBytes": comp.NeedBytes,
+ "needItems": comp.NeedItems,
"globalBytes": comp.GlobalBytes,
"needDeletes": comp.NeedDeletes,
})
@@ -718,11 +722,7 @@ func (s *apiService) postDBOverride(w http.ResponseWriter, r *http.Request) {
go s.model.Override(folder)
}
-func (s *apiService) getDBNeed(w http.ResponseWriter, r *http.Request) {
- qs := r.URL.Query()
-
- folder := qs.Get("folder")
-
+func getPagingParams(qs url.Values) (int, int) {
page, err := strconv.Atoi(qs.Get("page"))
if err != nil || page < 1 {
page = 1
@@ -731,20 +731,52 @@ func (s *apiService) getDBNeed(w http.ResponseWriter, r *http.Request) {
if err != nil || perpage < 1 {
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.
sendJSON(w, map[string]interface{}{
- "progress": s.toNeedSlice(progress),
- "queued": s.toNeedSlice(queued),
- "rest": s.toNeedSlice(rest),
- "total": total,
+ "progress": toNeedSlice(progress),
+ "queued": toNeedSlice(queued),
+ "rest": toNeedSlice(rest),
"page": page,
"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) {
sendJSON(w, s.model.ConnectionStats())
}
@@ -1351,7 +1383,7 @@ func (s *apiService) getHeapProf(w http.ResponseWriter, r *http.Request) {
pprof.WriteHeapProfile(w)
}
-func (s *apiService) toNeedSlice(fs []db.FileInfoTruncated) []jsonDBFileInfo {
+func toNeedSlice(fs []db.FileInfoTruncated) []jsonDBFileInfo {
res := make([]jsonDBFileInfo, len(fs))
for i, f := range fs {
res[i] = jsonDBFileInfo(f)
@@ -1373,6 +1405,7 @@ func (f jsonFileInfo) MarshalJSON() ([]byte, error) {
"invalid": f.Invalid,
"noPermissions": f.NoPermissions,
"modified": protocol.FileInfo(f).ModTime(),
+ "modifiedBy": f.ModifiedBy.String(),
"sequence": f.Sequence,
"numBlocks": len(f.Blocks),
"version": jsonVersionVector(f.Version),
@@ -1384,13 +1417,14 @@ type jsonDBFileInfo db.FileInfoTruncated
func (f jsonDBFileInfo) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]interface{}{
"name": f.Name,
- "type": f.Type,
+ "type": f.Type.String(),
"size": f.Size,
"permissions": fmt.Sprintf("%#o", f.Permissions),
"deleted": f.Deleted,
"invalid": f.Invalid,
"noPermissions": f.NoPermissions,
"modified": db.FileInfoTruncated(f).ModTime(),
+ "modifiedBy": f.ModifiedBy.String(),
"sequence": f.Sequence,
})
}
diff --git a/cmd/syncthing/mocked_model_test.go b/cmd/syncthing/mocked_model_test.go
index 096081c0..f6e36d03 100644
--- a/cmd/syncthing/mocked_model_test.go
+++ b/cmd/syncthing/mocked_model_test.go
@@ -28,8 +28,12 @@ func (m *mockedModel) Completion(device protocol.DeviceID, folder string) model.
func (m *mockedModel) Override(folder string) {}
-func (m *mockedModel) NeedFolderFiles(folder string, page, perpage int) ([]db.FileInfoTruncated, []db.FileInfoTruncated, []db.FileInfoTruncated, int) {
- return nil, nil, nil, 0
+func (m *mockedModel) NeedFolderFiles(folder string, page, perpage int) ([]db.FileInfoTruncated, []db.FileInfoTruncated, []db.FileInfoTruncated) {
+ 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 {
diff --git a/cmd/syncthing/summaryservice.go b/cmd/syncthing/summaryservice.go
index 58ef3db4..c437d8ef 100644
--- a/cmd/syncthing/summaryservice.go
+++ b/cmd/syncthing/summaryservice.go
@@ -211,6 +211,7 @@ func (c *folderSummaryService) sendSummary(folder string) {
"device": devCfg.DeviceID.String(),
"completion": comp.CompletionPct,
"needBytes": comp.NeedBytes,
+ "needItems": comp.NeedItems,
"globalBytes": comp.GlobalBytes,
})
}
diff --git a/gui/default/assets/lang/lang-en.json b/gui/default/assets/lang/lang-en.json
index b4673766..3efffb1e 100644
--- a/gui/default/assets/lang/lang-en.json
+++ b/gui/default/assets/lang/lang-en.json
@@ -59,6 +59,7 @@
"Device ID": "Device ID",
"Device Identification": "Device Identification",
"Device Name": "Device Name",
+ "Device that last modified the item": "Device that last modified the item",
"Devices": "Devices",
"Disabled": "Disabled",
"Disconnected": "Disconnected",
@@ -130,6 +131,7 @@
"Latest Change": "Latest Change",
"Learn more": "Learn more",
"Listeners": "Listeners",
+ "Loading data...": "Loading data...",
"Local Discovery": "Local Discovery",
"Local State": "Local State",
"Local State (Total)": "Local State (Total)",
@@ -138,6 +140,8 @@
"Maximum Age": "Maximum Age",
"Metadata Only": "Metadata Only",
"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",
"Multi level wildcard (matches multiple directory levels)": "Multi level wildcard (matches multiple directory levels)",
"Never": "Never",
@@ -221,6 +225,7 @@
"Shutdown Complete": "Shutdown Complete",
"Simple File Versioning": "Simple File Versioning",
"Single level wildcard (matches within a directory only)": "Single level wildcard (matches within a directory only)",
+ "Size": "Size",
"Smallest First": "Smallest First",
"Source Code": "Source Code",
"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 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 the item was last modified": "Time the item was last modified",
"Trash Can File Versioning": "Trash Can File Versioning",
"Type": "Type",
"Unavailable": "Unavailable",
diff --git a/gui/default/index.html b/gui/default/index.html
index c8705fd5..38969c13 100644
--- a/gui/default/index.html
+++ b/gui/default/index.html
@@ -603,6 +603,12 @@
+
+ | Out of Sync Items |
+
+ {{completion[deviceCfg.deviceID]._needItems | alwaysNumber}} items, ~{{completion[deviceCfg.deviceID]._needBytes | binary}}B
+ |
+
|  Address |
@@ -722,6 +728,7 @@
+
diff --git a/gui/default/syncthing/core/syncthingController.js b/gui/default/syncthing/core/syncthingController.js
index 5df09bd1..ad333b1f 100755
--- a/gui/default/syncthing/core/syncthingController.js
+++ b/gui/default/syncthing/core/syncthingController.js
@@ -45,7 +45,6 @@ angular.module('syncthing.core')
$scope.progress = {};
$scope.version = {};
$scope.needed = [];
- $scope.neededTotal = 0;
$scope.neededCurrentPage = 1;
$scope.neededPageSize = 10;
$scope.failed = {};
@@ -56,6 +55,7 @@ angular.module('syncthing.core')
$scope.globalChangeEvents = {};
$scope.metricRates = false;
$scope.folderPathErrors = {};
+ resetRemoteNeed();
try {
$scope.metricRates = (window.localStorage["metricRates"] == "true");
@@ -241,7 +241,8 @@ angular.module('syncthing.core')
};
$scope.completion[arg.data.id] = {
_total: 100,
- _needBytes: 0
+ _needBytes: 0,
+ _needItems: 0
};
}
});
@@ -389,7 +390,8 @@ angular.module('syncthing.core')
$scope.devices.forEach(function (deviceCfg) {
$scope.completion[deviceCfg.deviceID] = {
_total: 100,
- _needBytes: 0
+ _needBytes: 0,
+ _needItems: 0
};
});
$scope.devices.sort(deviceCompare);
@@ -431,7 +433,7 @@ angular.module('syncthing.core')
}
}
$scope.listenersFailed = listenersFailed;
- $scope.listenersTotal = Object.keys(data.connectionServiceStatus).length;
+ $scope.listenersTotal = $scope.sizeOf(data.connectionServiceStatus);
$scope.discoveryTotal = data.discoveryMethods;
var discoveryFailed = [];
@@ -476,21 +478,24 @@ angular.module('syncthing.core')
}
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]) {
- if (folder === "_total" || folder === '_needBytes') {
+ if (folder === "_total" || folder === '_needBytes' || folder === '_needItems') {
continue;
}
total += $scope.completion[device][folder].globalBytes;
needed += $scope.completion[device][folder].needBytes;
+ items += $scope.completion[device][folder].needItems;
deletes += $scope.completion[device][folder].needDeletes;
}
if (total == 0) {
$scope.completion[device]._total = 100;
$scope.completion[device]._needBytes = 0;
+ $scope.completion[device]._needItems = 0;
} else {
$scope.completion[device]._total = Math.floor(100 * (1 - needed / total));
$scope.completion[device]._needBytes = needed
+ $scope.completion[device]._needItems = items;
}
if (needed == 0 && deletes > 0) {
@@ -498,7 +503,6 @@ angular.module('syncthing.core')
// to do. Drop down the completion percentage to indicate
// that we have stuff to do.
$scope.completion[device]._total = 95;
- $scope.completion[device]._needBytes = 0;
}
console.log("recalcCompletion", device, $scope.completion[device]);
@@ -616,7 +620,6 @@ angular.module('syncthing.core')
merged.push(item);
});
$scope.needed = merged;
- $scope.neededTotal = data.total;
}
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
}
+ function resetRemoteNeed() {
+ $scope.remoteNeed = {};
+ $scope.remoteNeedFolders = [];
+ $scope.remoteNeedDevice = undefined;
+ }
+
$scope.neededPageChanged = function (page) {
$scope.neededCurrentPage = page;
refreshNeed($scope.neededFolder);
@@ -656,6 +665,20 @@ angular.module('syncthing.core')
$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 () {
$http.get(urlbase + "/stats/device").success(function (data) {
$scope.deviceStats = data;
@@ -965,7 +988,7 @@ angular.module('syncthing.core')
}
// 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++;
}
@@ -1623,17 +1646,14 @@ angular.module('syncthing.core')
$scope.deviceFolders = function (deviceCfg) {
var folders = [];
- for (var folderID in $scope.folders) {
- var devices = $scope.folders[folderID].devices;
- for (var i = 0; i < devices.length; i++) {
- if (devices[i].deviceID === deviceCfg.deviceID) {
- folders.push(folderID);
+ $scope.folderList().forEach(function (folder) {
+ for (var i = 0; i < folder.devices.length; i++) {
+ if (folder.devices[i].deviceID === deviceCfg.deviceID) {
+ folders.push(folder.id);
break;
}
}
- }
-
- folders.sort(folderCompare);
+ });
return folders;
};
@@ -1729,11 +1749,25 @@ angular.module('syncthing.core')
$('#needed').modal().on('hidden.bs.modal', function () {
$scope.neededFolder = undefined;
$scope.needed = undefined;
- $scope.neededTotal = 0;
$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.failedCurrent = $scope.failed[folder];
$scope.failedFolderPath = $scope.folders[folder].path;
@@ -1900,12 +1934,16 @@ angular.module('syncthing.core')
// pseudo main. called on all definitions assigned
initController();
}
- }
+ };
$scope.toggleUnits = function () {
$scope.metricRates = !$scope.metricRates;
try {
window.localStorage["metricRates"] = $scope.metricRates;
} catch (exception) { }
- }
+ };
+
+ $scope.sizeOf = function (dict) {
+ return Object.keys(dict).length;
+ };
});
diff --git a/gui/default/syncthing/transfer/neededFilesModalView.html b/gui/default/syncthing/transfer/neededFilesModalView.html
index e08a2d95..02401353 100644
--- a/gui/default/syncthing/transfer/neededFilesModalView.html
+++ b/gui/default/syncthing/transfer/neededFilesModalView.html
@@ -14,7 +14,7 @@
-
+
diff --git a/gui/default/syncthing/transfer/remoteNeededFilesModalView.html b/gui/default/syncthing/transfer/remoteNeededFilesModalView.html
new file mode 100644
index 00000000..c7a09308
--- /dev/null
+++ b/gui/default/syncthing/transfer/remoteNeededFilesModalView.html
@@ -0,0 +1,45 @@
+é
+
+
+ Loading data...
+
+
+
+
+
+
+
+
+
+ | Path |
+ Size |
+ Mod. Time |
+ Mod. Device |
+
+
+
+ | {{file.name}} |
+ {{file.size | binary}}B |
+ {{file.modified | date:"yyyy-MM-dd HH:mm:ss"}} |
+ {{friendlyNameFromShort(file.modifiedBy)}} |
+ Unknown |
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/lib/model/model.go b/lib/model/model.go
index aa70da86..f86676b4 100644
--- a/lib/model/model.go
+++ b/lib/model/model.go
@@ -110,6 +110,7 @@ var (
errDevicePaused = errors.New("device is paused")
errDeviceIgnored = errors.New("device is ignored")
errFolderPaused = errors.New("folder is paused")
+ errFolderNotRunning = errors.New("folder is not running")
errFolderMissing = errors.New("no such folder")
errNetworkNotAllowed = errors.New("network not allowed")
)
@@ -182,15 +183,13 @@ func (m *Model) StartFolder(folder string) {
}
func (m *Model) startFolderLocked(folder string) config.FolderType {
- cfg, ok := m.folderCfgs[folder]
- if !ok {
- panic("cannot start nonexistent folder " + cfg.Description())
+ if err := m.checkFolderRunningLocked(folder); err == errFolderMissing {
+ panic("cannot start nonexistent folder " + folder)
+ } else if err == nil {
+ panic("cannot start already running folder " + folder)
}
- _, ok = m.folderRunners[folder]
- if ok {
- panic("cannot start already running folder " + cfg.Description())
- }
+ cfg := m.folderCfgs[folder]
folderFactory, ok := folderFactories[cfg.Type]
if !ok {
@@ -585,6 +584,7 @@ func (m *Model) FolderStatistics() map[string]stats.FolderStatistics {
type FolderCompletion struct {
CompletionPct float64
NeedBytes int64
+ NeedItems int64
GlobalBytes int64
NeedDeletes int64
}
@@ -611,7 +611,7 @@ func (m *Model) Completion(device protocol.DeviceID, folder string) FolderComple
counts := m.deviceDownloads[device].GetBlockCounts(folder)
m.pmut.RUnlock()
- var need, fileNeed, downloaded, deletes int64
+ var need, items, fileNeed, downloaded, deletes int64
rf.WithNeedTruncated(device, func(f db.FileIntf) bool {
ft := f.(db.FileInfoTruncated)
@@ -630,6 +630,8 @@ func (m *Model) Completion(device protocol.DeviceID, folder string) FolderComple
}
need += fileNeed
+ items++
+
return true
})
@@ -649,6 +651,7 @@ func (m *Model) Completion(device protocol.DeviceID, folder string) FolderComple
return FolderCompletion{
CompletionPct: completionPct,
NeedBytes: need,
+ NeedItems: items,
GlobalBytes: tot,
NeedDeletes: deletes,
}
@@ -715,15 +718,13 @@ func (m *Model) NeedSize(folder string) db.Counts {
// NeedFolderFiles 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) 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()
defer m.fmut.RUnlock()
- total := 0
-
rf, ok := m.folderFiles[folder]
if !ok {
- return nil, nil, nil, 0
+ return nil, nil, nil
}
var progress, queued, rest []db.FileInfoTruncated
@@ -766,7 +767,6 @@ func (m *Model) NeedFolderFiles(folder string, page, perpage int) ([]db.FileInfo
return true
}
- total++
if skip > 0 {
skip--
return true
@@ -778,10 +778,43 @@ func (m *Model) NeedFolderFiles(folder string, page, perpage int) ([]db.FileInfo
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.
@@ -1865,22 +1898,30 @@ func (m *Model) ScanFolder(folder string) error {
}
func (m *Model) ScanFolderSubdirs(folder string, subs []string) error {
- m.fmut.Lock()
- runner, okRunner := m.folderRunners[folder]
- cfg, okCfg := m.folderCfgs[folder]
- m.fmut.Unlock()
-
- if !okRunner {
- if okCfg && cfg.Paused {
- return errFolderPaused
- }
- return errFolderMissing
+ m.fmut.RLock()
+ if err := m.checkFolderRunningLocked(folder); err != nil {
+ m.fmut.RUnlock()
+ return err
}
+ runner := m.folderRunners[folder]
+ m.fmut.RUnlock()
return runner.Scan(subs)
}
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++ {
sub := osutil.NativeFilename(subDirs[i])
@@ -1899,14 +1940,6 @@ func (m *Model) internalScanFolderSubdirs(ctx context.Context, folder string, su
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.
// 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.
@@ -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 {
return err
}
@@ -2495,6 +2521,49 @@ func (m *Model) CommitConfiguration(from, to config.Configuration) bool {
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
// slice of folder configurations.
func mapFolders(folders []config.FolderConfiguration) map[string]config.FolderConfiguration {
diff --git a/lib/model/model_test.go b/lib/model/model_test.go
index 50d13180..d67d310e 100644
--- a/lib/model/model_test.go
+++ b/lib/model/model_test.go
@@ -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 {
fc := &fakeConnection{id: dev, model: m}
m.AddConnection(fc, protocol.HelloResult{})
| |