From 0c46e0a9cc9f1ac971f9c585ccf49cbb99da10c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Colomb?= Date: Sun, 10 Apr 2022 22:47:57 +0200 Subject: [PATCH] gui, lib/model: Mark folders unaccepted by remote device (fixes #8202) (#8201) --- gui/default/assets/lang/lang-en.json | 1 + gui/default/index.html | 16 +++--- .../syncthing/core/editShareTemplate.html | 10 ++-- .../syncthing/core/syncthingController.js | 52 ++++++++++++++++++- .../syncthing/device/editDeviceModalView.html | 5 +- .../syncthing/folder/editFolderModalView.html | 5 +- lib/model/folderstate.go | 8 +++ lib/model/indexhandler.go | 2 +- lib/model/model.go | 44 ++++++++++------ lib/model/model_test.go | 10 ++-- lib/syncthing/verboseservice.go | 2 +- 11 files changed, 118 insertions(+), 37 deletions(-) diff --git a/gui/default/assets/lang/lang-en.json b/gui/default/assets/lang/lang-en.json index 843b41612..b1e2909bd 100644 --- a/gui/default/assets/lang/lang-en.json +++ b/gui/default/assets/lang/lang-en.json @@ -378,6 +378,7 @@ "The number of versions must be a number and cannot be blank.": "The number of versions must be a number and cannot be blank.", "The path cannot be blank.": "The path cannot be blank.", "The rate limit must be a non-negative number (0: no limit)": "The rate limit must be a non-negative number (0: no limit)", + "The remote device has not accepted sharing this folder.": "The remote device has not accepted sharing this folder.", "The rescan interval must be a non-negative number of seconds.": "The rescan interval must be a non-negative number of seconds.", "There are no devices to share this folder with.": "There are no devices to share this folder with.", "There are no file versions to restore.": "There are no file versions to restore.", diff --git a/gui/default/index.html b/gui/default/index.html index efab72190..cbcfb47c9 100644 --- a/gui/default/index.html +++ b/gui/default/index.html @@ -532,7 +532,9 @@  Shared With - {{sharesFolder(folder)}} + + +  Last Scan @@ -668,8 +670,8 @@ - {{listenersTotal-listenersFailed.length}}/{{listenersTotal}} - + {{listenersTotal-listenersFailed.length}}/{{listenersTotal}} + @@ -678,8 +680,8 @@ - {{discoveryTotal-discoveryFailed.length}}/{{discoveryTotal}} - + {{discoveryTotal-discoveryFailed.length}}/{{discoveryTotal}} + @@ -833,7 +835,9 @@  Folders - {{deviceFolders(deviceCfg).map(folderLabel).join(", ")}} + + +  Remote GUI diff --git a/gui/default/syncthing/core/editShareTemplate.html b/gui/default/syncthing/core/editShareTemplate.html index 4d8de1430..c03bba16e 100644 --- a/gui/default/syncthing/core/editShareTemplate.html +++ b/gui/default/syncthing/core/editShareTemplate.html @@ -1,14 +1,14 @@
- - + + @@ -30,10 +30,10 @@ - + - +
diff --git a/gui/default/syncthing/core/syncthingController.js b/gui/default/syncthing/core/syncthingController.js index e528f0f0e..7e72a67b6 100755 --- a/gui/default/syncthing/core/syncthingController.js +++ b/gui/default/syncthing/core/syncthingController.js @@ -2365,17 +2365,37 @@ angular.module('syncthing.core') + '&device=' + encodeURIComponent(deviceID)); }; + $scope.deviceNameMarkUnaccepted = function (deviceID, folderID) { + var name = $scope.deviceName($scope.devices[deviceID]); + // Add footnote if sharing was not accepted on the remote device + if (deviceID in $scope.completion && folderID in $scope.completion[deviceID] && !$scope.completion[deviceID][folderID].accepted) { + name += '1'; + } + return name; + }; + $scope.sharesFolder = function (folderCfg) { var names = []; folderCfg.devices.forEach(function (device) { if (device.deviceID !== $scope.myID) { - names.push($scope.deviceName($scope.devices[device.deviceID])); + names.push($scope.deviceNameMarkUnaccepted(device.deviceID, folderCfg.id)); } }); names.sort(); return names.join(", "); }; + $scope.folderHasUnacceptedDevices = function (folderCfg) { + for (var deviceID in $scope.completion) { + if (deviceID in $scope.devices + && folderCfg.id in $scope.completion[deviceID] + && !$scope.completion[deviceID][folderCfg.id].accepted) { + return true; + } + } + return false; + }; + $scope.deviceFolders = function (deviceCfg) { var folders = []; $scope.folderList().forEach(function (folder) { @@ -2397,6 +2417,36 @@ angular.module('syncthing.core') return label && label.length > 0 ? label : folderID; }; + $scope.folderLabelMarkUnaccepted = function (folderID, deviceID) { + var label = $scope.folderLabel(folderID); + // Add footnote if sharing was not accepted on the remote device + if (deviceID in $scope.completion && folderID in $scope.completion[deviceID] && !$scope.completion[deviceID][folderID].accepted) { + label += '1'; + } + return label; + }; + + $scope.sharedFolders = function (deviceCfg) { + var labels = []; + $scope.deviceFolders(deviceCfg).forEach(function (folderID) { + labels.push($scope.folderLabelMarkUnaccepted(folderID, deviceCfg.deviceID)); + }); + return labels.join(', '); + }; + + $scope.deviceHasUnacceptedFolders = function (deviceCfg) { + if (!(deviceCfg.deviceID in $scope.completion)) { + return false; + } + for (var folderID in $scope.completion[deviceCfg.deviceID]) { + if (folderID in $scope.folders + && !$scope.completion[deviceCfg.deviceID][folderID].accepted) { + return true; + } + } + return false; + }; + $scope.deleteFolder = function (id) { hideFolderModal(); if ($scope.currentFolder._editing != "existing") { diff --git a/gui/default/syncthing/device/editDeviceModalView.html b/gui/default/syncthing/device/editDeviceModalView.html index 4a0cb9da6..ad7653dc9 100644 --- a/gui/default/syncthing/device/editDeviceModalView.html +++ b/gui/default/syncthing/device/editDeviceModalView.html @@ -83,8 +83,11 @@ Deselect All

- +
+

+ 1 The remote device has not accepted sharing this folder. +

diff --git a/gui/default/syncthing/folder/editFolderModalView.html b/gui/default/syncthing/folder/editFolderModalView.html index 4905c912c..183bd8b86 100644 --- a/gui/default/syncthing/folder/editFolderModalView.html +++ b/gui/default/syncthing/folder/editFolderModalView.html @@ -56,8 +56,11 @@ Deselect All

- +
+

+ 1 The remote device has not accepted sharing this folder. +

diff --git a/lib/model/folderstate.go b/lib/model/folderstate.go index 43b09119b..c9a8e5060 100644 --- a/lib/model/folderstate.go +++ b/lib/model/folderstate.go @@ -52,6 +52,14 @@ func (s folderState) String() string { } } +type remoteFolderState int + +const ( + remoteNotSharing remoteFolderState = iota + remotePaused + remoteValid +) + type stateTracker struct { folderID string evLogger events.Logger diff --git a/lib/model/indexhandler.go b/lib/model/indexhandler.go index bfd22c864..1d53cbaa0 100644 --- a/lib/model/indexhandler.go +++ b/lib/model/indexhandler.go @@ -471,7 +471,7 @@ func (r *indexHandlerRegistry) Remove(folder string) { // RemoveAllExcept stops all running index handlers and removes those pending to be started, // except mentioned ones. // It is a noop if the folder isn't known. -func (r *indexHandlerRegistry) RemoveAllExcept(except map[string]struct{}) { +func (r *indexHandlerRegistry) RemoveAllExcept(except map[string]remoteFolderState) { r.mut.Lock() defer r.mut.Unlock() diff --git a/lib/model/model.go b/lib/model/model.go index 67eb793a1..5391109e7 100644 --- a/lib/model/model.go +++ b/lib/model/model.go @@ -161,7 +161,7 @@ type model struct { closed map[protocol.DeviceID]chan struct{} helloMessages map[protocol.DeviceID]protocol.Hello deviceDownloads map[protocol.DeviceID]*deviceDownloadState - remotePausedFolders map[protocol.DeviceID]map[string]struct{} // deviceID -> folders + remoteFolderStates map[protocol.DeviceID]map[string]remoteFolderState // deviceID -> folders indexHandlers map[protocol.DeviceID]*indexHandlerRegistry // for testing only @@ -246,7 +246,7 @@ func NewModel(cfg config.Wrapper, id protocol.DeviceID, clientName, clientVersio closed: make(map[protocol.DeviceID]chan struct{}), helloMessages: make(map[protocol.DeviceID]protocol.Hello), deviceDownloads: make(map[protocol.DeviceID]*deviceDownloadState), - remotePausedFolders: make(map[protocol.DeviceID]map[string]struct{}), + remoteFolderStates: make(map[protocol.DeviceID]map[string]remoteFolderState), indexHandlers: make(map[protocol.DeviceID]*indexHandlerRegistry), } for devID := range cfg.Devices() { @@ -800,9 +800,10 @@ type FolderCompletion struct { NeedItems int NeedDeletes int Sequence int64 + Accepted bool } -func newFolderCompletion(global, need db.Counts, sequence int64) FolderCompletion { +func newFolderCompletion(global, need db.Counts, sequence int64, accepted bool) FolderCompletion { comp := FolderCompletion{ GlobalBytes: global.Bytes, NeedBytes: need.Bytes, @@ -810,6 +811,7 @@ func newFolderCompletion(global, need db.Counts, sequence int64) FolderCompletio NeedItems: need.Files + need.Directories + need.Symlinks, NeedDeletes: need.Deleted, Sequence: sequence, + Accepted: accepted, } comp.setComplectionPct() return comp @@ -851,6 +853,7 @@ func (comp FolderCompletion) Map() map[string]interface{} { "needItems": comp.NeedItems, "needDeletes": comp.NeedDeletes, "sequence": comp.Sequence, + "accepted": comp.Accepted, } } @@ -901,6 +904,7 @@ func (m *model) folderCompletion(device protocol.DeviceID, folder string) (Folde defer snap.Release() m.pmut.RLock() + accepted := m.remoteFolderStates[device][folder] != remoteNotSharing downloaded := m.deviceDownloads[device].BytesDownloaded(folder) m.pmut.RUnlock() @@ -911,7 +915,7 @@ func (m *model) folderCompletion(device protocol.DeviceID, folder string) (Folde need.Bytes = 0 } - comp := newFolderCompletion(snap.GlobalSize(), need, snap.Sequence(device)) + comp := newFolderCompletion(snap.GlobalSize(), need, snap.Sequence(device), accepted) l.Debugf("%v Completion(%s, %q): %v", m, device, folder, comp.Map()) return comp, nil @@ -1221,13 +1225,13 @@ func (m *model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon w.Wait() } - tempIndexFolders, paused, err := m.ccHandleFolders(cm.Folders, deviceCfg, ccDeviceInfos, indexHandlerRegistry) + tempIndexFolders, states, err := m.ccHandleFolders(cm.Folders, deviceCfg, ccDeviceInfos, indexHandlerRegistry) if err != nil { return err } m.pmut.Lock() - m.remotePausedFolders[deviceID] = paused + m.remoteFolderStates[deviceID] = states m.pmut.Unlock() if len(tempIndexFolders) > 0 { @@ -1262,11 +1266,10 @@ func (m *model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon return nil } -func (m *model) ccHandleFolders(folders []protocol.Folder, deviceCfg config.DeviceConfiguration, ccDeviceInfos map[string]*clusterConfigDeviceInfo, indexHandlers *indexHandlerRegistry) ([]string, map[string]struct{}, error) { +func (m *model) ccHandleFolders(folders []protocol.Folder, deviceCfg config.DeviceConfiguration, ccDeviceInfos map[string]*clusterConfigDeviceInfo, indexHandlers *indexHandlerRegistry) ([]string, map[string]remoteFolderState, error) { var folderDevice config.FolderDeviceConfiguration tempIndexFolders := make([]string, 0, len(folders)) - paused := make(map[string]struct{}, len(folders)) - seenFolders := make(map[string]struct{}, len(folders)) + seenFolders := make(map[string]remoteFolderState, len(folders)) updatedPending := make([]updatedPendingFolder, 0, len(folders)) deviceID := deviceCfg.DeviceID expiredPending, err := m.db.PendingFoldersForDevice(deviceID) @@ -1275,7 +1278,7 @@ func (m *model) ccHandleFolders(folders []protocol.Folder, deviceCfg config.Devi } of := db.ObservedFolder{Time: time.Now().Truncate(time.Second)} for _, folder := range folders { - seenFolders[folder.ID] = struct{}{} + seenFolders[folder.ID] = remoteValid cfg, ok := m.cfg.Folder(folder.ID) if ok { @@ -1316,7 +1319,7 @@ func (m *model) ccHandleFolders(folders []protocol.Folder, deviceCfg config.Devi if folder.Paused { indexHandlers.Remove(folder.ID) - paused[cfg.ID] = struct{}{} + seenFolders[cfg.ID] = remotePaused continue } @@ -1345,7 +1348,7 @@ func (m *model) ccHandleFolders(folders []protocol.Folder, deviceCfg config.Devi m.evLogger.Log(events.Failure, err.Error()) l.Warnln(msg) } - return tempIndexFolders, paused, err + return tempIndexFolders, seenFolders, err } m.fmut.Lock() if devErrs, ok := m.folderEncryptionFailures[folder.ID]; ok { @@ -1367,6 +1370,15 @@ func (m *model) ccHandleFolders(folders []protocol.Folder, deviceCfg config.Devi } indexHandlers.RemoveAllExcept(seenFolders) + + // Explicitly mark folders we offer, but the remote has not accepted + for folderID, cfg := range m.cfg.Folders() { + if _, seen := seenFolders[folderID]; !seen && cfg.SharedWith(deviceID) { + l.Debugf("Remote device %v has not accepted folder %s", deviceID.Short(), cfg.Description()) + seenFolders[folderID] = remoteNotSharing + } + } + expiredPendingList := make([]map[string]string, 0, len(expiredPending)) for folder := range expiredPending { if err = m.db.RemovePendingFolderForDevice(folder, deviceID); err != nil { @@ -1387,7 +1399,7 @@ func (m *model) ccHandleFolders(folders []protocol.Folder, deviceCfg config.Devi }) } - return tempIndexFolders, paused, nil + return tempIndexFolders, seenFolders, nil } func (m *model) ccCheckEncryption(fcfg config.FolderConfiguration, folderDevice config.FolderDeviceConfiguration, ccDeviceInfos *clusterConfigDeviceInfo, deviceUntrusted bool) error { @@ -1726,7 +1738,7 @@ func (m *model) Closed(device protocol.DeviceID, err error) { delete(m.connRequestLimiters, device) delete(m.helloMessages, device) delete(m.deviceDownloads, device) - delete(m.remotePausedFolders, device) + delete(m.remoteFolderStates, device) closed := m.closed[device] delete(m.closed, device) delete(m.indexHandlers, device) @@ -2697,10 +2709,10 @@ func (m *model) availabilityInSnapshot(cfg config.FolderConfiguration, snap *db. func (m *model) availabilityInSnapshotPRlocked(cfg config.FolderConfiguration, snap *db.Snapshot, file protocol.FileInfo, block protocol.BlockInfo) []Availability { var availabilities []Availability for _, device := range snap.Availability(file.Name) { - if _, ok := m.remotePausedFolders[device]; !ok { + if _, ok := m.remoteFolderStates[device]; !ok { continue } - if _, ok := m.remotePausedFolders[device][cfg.ID]; ok { + if state, ok := m.remoteFolderStates[device][cfg.ID]; !ok || state == remotePaused { continue } _, ok := m.conn[device] diff --git a/lib/model/model_test.go b/lib/model/model_test.go index e6f8ee49e..ac1df08a9 100644 --- a/lib/model/model_test.go +++ b/lib/model/model_test.go @@ -3766,16 +3766,16 @@ func TestClusterConfigOnFolderUnpause(t *testing.T) { func TestAddFolderCompletion(t *testing.T) { // Empty folders are always 100% complete. - comp := newFolderCompletion(db.Counts{}, db.Counts{}, 0) - comp.add(newFolderCompletion(db.Counts{}, db.Counts{}, 0)) + comp := newFolderCompletion(db.Counts{}, db.Counts{}, 0, true) + comp.add(newFolderCompletion(db.Counts{}, db.Counts{}, 0, false)) if comp.CompletionPct != 100 { t.Error(comp.CompletionPct) } // Completion is of the whole - comp = newFolderCompletion(db.Counts{Bytes: 100}, db.Counts{}, 0) // 100% complete - comp.add(newFolderCompletion(db.Counts{Bytes: 400}, db.Counts{Bytes: 50}, 0)) // 82.5% complete - if comp.CompletionPct != 90 { // 100 * (1 - 50/500) + comp = newFolderCompletion(db.Counts{Bytes: 100}, db.Counts{}, 0, true) // 100% complete + comp.add(newFolderCompletion(db.Counts{Bytes: 400}, db.Counts{Bytes: 50}, 0, true)) // 82.5% complete + if comp.CompletionPct != 90 { // 100 * (1 - 50/500) t.Error(comp.CompletionPct) } } diff --git a/lib/syncthing/verboseservice.go b/lib/syncthing/verboseservice.go index a78c2ff20..8d7084ad0 100644 --- a/lib/syncthing/verboseservice.go +++ b/lib/syncthing/verboseservice.go @@ -117,7 +117,7 @@ func (s *verboseService) formatEvent(ev events.Event) string { case events.FolderCompletion: data := ev.Data.(map[string]interface{}) - return fmt.Sprintf("Completion for folder %q on device %v is %v%%", data["folder"], data["device"], data["completion"]) + return fmt.Sprintf("Completion for folder %q on device %v is %v%% (accepted: %v)", data["folder"], data["device"], data["completion"], data["accepted"]) case events.FolderSummary: data := ev.Data.(model.FolderSummaryEventData)