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 @@
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 @@
- {{label}}
+
-
-
+
+
@@ -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.
+
Unshared Devices
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)