Kaynağa Gözat

gui, lib/model: Mark folders unaccepted by remote device (fixes #8202) (#8201)

André Colomb 3 yıl önce
ebeveyn
işleme
0c46e0a9cc

+ 1 - 0
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.",

+ 10 - 6
gui/default/index.html

@@ -532,7 +532,9 @@
                       </tr>
                       <tr>
                         <th><span class="fas fa-fw fa-share-alt"></span>&nbsp;<span translate>Shared With</span></th>
-                        <td class="text-right" ng-attr-title="{{sharesFolder(folder)}}">{{sharesFolder(folder)}}</td>
+                        <td class="text-right">
+                          <span tooltip data-original-title="{{sharesFolder(folder)}} {{folderHasUnacceptedDevices(folder) ? '<br/>(<sup>1</sup>' + ('The remote device has not accepted sharing this folder.' | translate) + ')' : ''}}" ng-bind-html="sharesFolder(folder)"></span>
+                        </td>
                       </tr>
                       <tr ng-if="folderStats[folder.id].lastScan">
                         <th><span class="far fa-fw fa-clock"></span>&nbsp;<span translate>Last Scan</span></th>
@@ -668,8 +670,8 @@
                       <td class="text-right">
                         <span class="data" tooltip data-original-title="{{'Show detailed listener status' | translate}}.">
                           <a href="" ng-class="{'text-success': listenersFailed.length == 0, 'text-danger': listenersFailed.length == listenersTotal}" ng-click="showListenerStatus()">
-			    {{listenersTotal-listenersFailed.length}}/{{listenersTotal}}
-			  </a>
+                            {{listenersTotal-listenersFailed.length}}/{{listenersTotal}}
+                          </a>
                         </span>
                       </td>
                     </tr>
@@ -678,8 +680,8 @@
                       <td class="text-right">
                         <span class="data" tooltip data-original-title="{{'Show detailed discovery status' | translate}}.">
                           <a href="" ng-class="{'text-success': discoveryFailed.length == 0, 'text-danger': discoveryFailed.length == discoveryTotal}" ng-click="showDiscoveryStatus()">
-			    {{discoveryTotal-discoveryFailed.length}}/{{discoveryTotal}}
-			  </a>
+                            {{discoveryTotal-discoveryFailed.length}}/{{discoveryTotal}}
+                          </a>
                         </span>
                       </td>
                     </tr>
@@ -833,7 +835,9 @@
                       </tr>
                       <tr ng-if="deviceFolders(deviceCfg).length > 0">
                         <th><span class="fas fa-fw fa-folder"></span>&nbsp;<span translate>Folders</span></th>
-                        <td class="text-right" ng-attr-title="{{deviceFolders(deviceCfg).map(folderLabel).join(', ')}}">{{deviceFolders(deviceCfg).map(folderLabel).join(", ")}}</td>
+                        <td class="text-right">
+                          <span tooltip data-original-title="{{sharedFolders(deviceCfg)}} {{deviceHasUnacceptedFolders(deviceCfg) ? '<br/>(<sup>1</sup>' + ('The remote device has not accepted sharing this folder.' | translate) + ')' : '' }}" ng-bind-html="sharedFolders(deviceCfg)"></span>
+                        </td>
                       </tr>
                       <tr ng-if="deviceCfg.remoteGUIPort > 0">
                         <th><span class="fas fa-fw fa-desktop"></span>&nbsp;<span translate>Remote GUI</span></th>

+ 5 - 5
gui/default/syncthing/core/editShareTemplate.html

@@ -1,14 +1,14 @@
 <div class="col-md-6 checkbox">
   <label for="sharedwith-{{id}}">
     <input id="sharedwith-{{id}}" ng-model="selected[id]" type="checkbox" />
-    <span tooltip data-original-title="{{id}}">{{label}}</span>
+    <span tooltip data-original-title="{{id}}" ng-bind-html="label"></span>
   </label>
 </div>
 <div class="col-md-6">
   <div class="input-group">
     <span class="input-group-addon" ng-switch="folderType !== 'receiveencrypted' && !encryptionPasswords[id]">
-      <span ng-switch-when='true' class="fas fa-fw fa-unlock" />
-      <span ng-switch-default class="fas fa-fw fa-lock" />
+      <span ng-switch-when='true' class="fas fa-fw fa-unlock"></span>
+      <span ng-switch-default class="fas fa-fw fa-lock"></span>
     </span>
     <span ng-switch="folderType === 'receiveencrypted'">
       <span ng-switch-when='true'>
@@ -30,10 +30,10 @@
     </span>
     <span ng-switch="selected[id] && folderType !== 'receiveencrypted'" class="input-group-addon">
       <span ng-switch-when='true'>
-        <span class="button fas fa-fw fa-eye" ng-click="togglePasswordVisibility()" />
+        <span class="button fas fa-fw fa-eye" ng-click="togglePasswordVisibility()"></span>
       </span>
       <span ng-switch-default>
-        <span class="button fas fa-fw fa-eye" disabled />
+        <span class="button fas fa-fw fa-eye" disabled></span>
       </span>
     </span>
   </div>

+ 51 - 1
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 += '<sup>1</sup>';
+            }
+            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 += '<sup>1</sup>';
+            }
+            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") {

+ 4 - 1
gui/default/syncthing/device/editDeviceModalView.html

@@ -83,8 +83,11 @@
                   <a href="#" ng-click="selectAllSharedFolders(false)" translate>Deselect All</a></small>
               </p>
               <div class="form-group" ng-repeat="folder in currentSharing.shared">
-                <share-template selected="currentSharing.selected" encryption-passwords="currentSharing.encryptionPasswords" id="{{folder.id}}" label="{{folderLabel(folder.id)}}" folder-type="{{folder.type}}" untrusted="currentDevice.untrusted" />
+                <share-template selected="currentSharing.selected" encryption-passwords="currentSharing.encryptionPasswords" id="{{folder.id}}" label="{{folderLabelMarkUnaccepted(folder.id, currentDevice.deviceID)}}" folder-type="{{folder.type}}" untrusted="currentDevice.untrusted" />
               </div>
+              <p class="help-block" ng-if="deviceHasUnacceptedFolders(currentDevice)">
+                <sup>1</sup> <span translate>The remote device has not accepted sharing this folder.</span>
+              </p>
             </div>
             <div class="form-horizontal" ng-if="currentSharing.unrelated.length">
               <label translate for="folders">Unshared Folders</label>

+ 4 - 1
gui/default/syncthing/folder/editFolderModalView.html

@@ -56,8 +56,11 @@
                 <a href="#" ng-click="selectAllSharedDevices(false)" translate>Deselect All</a></small>
             </p>
             <div class="form-group" ng-repeat="device in currentSharing.shared">
-              <share-template selected="currentSharing.selected" encryption-passwords="currentSharing.encryptionPasswords" id="{{device.deviceID}}" label="{{deviceName(device)}}" folder-type="{{currentFolder.type}}" untrusted="device.untrusted || pendingIsRemoteEncrypted(currentFolder.id, device.deviceID)" />
+              <share-template selected="currentSharing.selected" encryption-passwords="currentSharing.encryptionPasswords" id="{{device.deviceID}}" label="{{deviceNameMarkUnaccepted(device.deviceID, currentFolder.id)}}" folder-type="{{currentFolder.type}}" untrusted="device.untrusted || pendingIsRemoteEncrypted(currentFolder.id, device.deviceID)" />
             </div>
+            <p class="help-block" ng-if="folderHasUnacceptedDevices(currentFolder)">
+              <sup>1</sup> <span translate>The remote device has not accepted sharing this folder.</span>
+            </p>
           </div>
           <div class="form-horizontal" ng-if="currentSharing.unrelated.length || otherDevices().length <= 0">
             <label translate>Unshared Devices</label>

+ 8 - 0
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

+ 1 - 1
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()
 

+ 28 - 16
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]

+ 5 - 5
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)
 	}
 }

+ 1 - 1
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)