diff --git a/gui/default/syncthing/core/eventService.js b/gui/default/syncthing/core/eventService.js index 7e528d5a3..cb37b6520 100644 --- a/gui/default/syncthing/core/eventService.js +++ b/gui/default/syncthing/core/eventService.js @@ -64,6 +64,7 @@ angular.module('syncthing.core') PENDING_DEVICES_CHANGED: 'PendingDevicesChanged', // Emitted when pending devices were added / updated (connection from unknown ID) or removed (device is ignored or added) DEVICE_PAUSED: 'DevicePaused', // Emitted when a device has been paused DEVICE_RESUMED: 'DeviceResumed', // Emitted when a device has been resumed + CLUSTER_CONFIG_RECEIVED: 'ClusterConfigReceived', // Emitted when receiving a remote device's cluster config DOWNLOAD_PROGRESS: 'DownloadProgress', // Emitted during file downloads for each folder for each file FAILURE: 'Failure', // Specific errors sent to the usage reporting server for diagnosis FOLDER_COMPLETION: 'FolderCompletion', //Emitted when the local or remote contents for a folder changes diff --git a/gui/default/syncthing/core/syncthingController.js b/gui/default/syncthing/core/syncthingController.js index 7e72a67b6..939ff5859 100755 --- a/gui/default/syncthing/core/syncthingController.js +++ b/gui/default/syncthing/core/syncthingController.js @@ -2368,7 +2368,7 @@ angular.module('syncthing.core') $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) { + if (deviceID in $scope.completion && folderID in $scope.completion[deviceID] && $scope.completion[deviceID][folderID].remoteState == 'notSharing') { name += '1'; } return name; @@ -2389,7 +2389,7 @@ angular.module('syncthing.core') for (var deviceID in $scope.completion) { if (deviceID in $scope.devices && folderCfg.id in $scope.completion[deviceID] - && !$scope.completion[deviceID][folderCfg.id].accepted) { + && $scope.completion[deviceID][folderCfg.id].remoteState == 'notSharing') { return true; } } @@ -2420,7 +2420,7 @@ angular.module('syncthing.core') $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) { + if (deviceID in $scope.completion && folderID in $scope.completion[deviceID] && $scope.completion[deviceID][folderID].remoteState == 'notSharing') { label += '1'; } return label; @@ -2440,7 +2440,7 @@ angular.module('syncthing.core') } for (var folderID in $scope.completion[deviceCfg.deviceID]) { if (folderID in $scope.folders - && !$scope.completion[deviceCfg.deviceID][folderID].accepted) { + && $scope.completion[deviceCfg.deviceID][folderID].remoteState == 'notSharing') { return true; } } diff --git a/lib/events/events.go b/lib/events/events.go index 53976f59a..20b347955 100644 --- a/lib/events/events.go +++ b/lib/events/events.go @@ -35,6 +35,7 @@ const ( PendingDevicesChanged DevicePaused DeviceResumed + ClusterConfigReceived LocalChangeDetected RemoteChangeDetected LocalIndexUpdated @@ -118,6 +119,8 @@ func (t EventType) String() string { return "DevicePaused" case DeviceResumed: return "DeviceResumed" + case ClusterConfigReceived: + return "ClusterConfigReceived" case FolderScanProgress: return "FolderScanProgress" case FolderPaused: @@ -203,6 +206,8 @@ func UnmarshalEventType(s string) EventType { return DevicePaused case "DeviceResumed": return DeviceResumed + case "ClusterConfigReceived": + return ClusterConfigReceived case "FolderScanProgress": return FolderScanProgress case "FolderPaused": diff --git a/lib/model/folder_summary.go b/lib/model/folder_summary.go index 405a8e54b..b3c924476 100644 --- a/lib/model/folder_summary.go +++ b/lib/model/folder_summary.go @@ -221,7 +221,7 @@ func (c *folderSummaryService) OnEventRequest() { // listenForUpdates subscribes to the event bus and makes note of folders that // need their data recalculated. func (c *folderSummaryService) listenForUpdates(ctx context.Context) error { - sub := c.evLogger.Subscribe(events.LocalIndexUpdated | events.RemoteIndexUpdated | events.StateChanged | events.RemoteDownloadProgress | events.DeviceConnected | events.FolderWatchStateChanged | events.DownloadProgress) + sub := c.evLogger.Subscribe(events.LocalIndexUpdated | events.RemoteIndexUpdated | events.StateChanged | events.RemoteDownloadProgress | events.DeviceConnected | events.ClusterConfigReceived | events.FolderWatchStateChanged | events.DownloadProgress) defer sub.Unsubscribe() for { @@ -244,12 +244,18 @@ func (c *folderSummaryService) processUpdate(ev events.Event) { var folder string switch ev.Type { - case events.DeviceConnected: + case events.DeviceConnected, events.ClusterConfigReceived: // When a device connects we schedule a refresh of all // folders shared with that device. - data := ev.Data.(map[string]string) - deviceID, _ := protocol.DeviceIDFromString(data["id"]) + var deviceID protocol.DeviceID + if ev.Type == events.DeviceConnected { + data := ev.Data.(map[string]string) + deviceID, _ = protocol.DeviceIDFromString(data["id"]) + } else { + data := ev.Data.(ClusterConfigReceivedEventData) + deviceID = data.Device + } c.foldersMut.Lock() nextFolder: diff --git a/lib/model/folderstate.go b/lib/model/folderstate.go index 897875c37..0437e259a 100644 --- a/lib/model/folderstate.go +++ b/lib/model/folderstate.go @@ -55,11 +55,31 @@ func (s folderState) String() string { type remoteFolderState int const ( - remoteValid remoteFolderState = iota - remoteNotSharing - remotePaused + remoteFolderUnknown remoteFolderState = iota + remoteFolderNotSharing + remoteFolderPaused + remoteFolderValid ) +func (s remoteFolderState) String() string { + switch s { + case remoteFolderUnknown: + return "unknown" + case remoteFolderNotSharing: + return "notSharing" + case remoteFolderPaused: + return "paused" + case remoteFolderValid: + return "valid" + default: + return "unknown" + } +} + +func (s remoteFolderState) MarshalText() ([]byte, error) { + return []byte(s.String()), nil +} + type stateTracker struct { folderID string evLogger events.Logger diff --git a/lib/model/model.go b/lib/model/model.go index 5391109e7..0aa529985 100644 --- a/lib/model/model.go +++ b/lib/model/model.go @@ -800,10 +800,10 @@ type FolderCompletion struct { NeedItems int NeedDeletes int Sequence int64 - Accepted bool + RemoteState remoteFolderState } -func newFolderCompletion(global, need db.Counts, sequence int64, accepted bool) FolderCompletion { +func newFolderCompletion(global, need db.Counts, sequence int64, state remoteFolderState) FolderCompletion { comp := FolderCompletion{ GlobalBytes: global.Bytes, NeedBytes: need.Bytes, @@ -811,7 +811,7 @@ func newFolderCompletion(global, need db.Counts, sequence int64, accepted bool) NeedItems: need.Files + need.Directories + need.Symlinks, NeedDeletes: need.Deleted, Sequence: sequence, - Accepted: accepted, + RemoteState: state, } comp.setComplectionPct() return comp @@ -843,7 +843,7 @@ func (comp *FolderCompletion) setComplectionPct() { } } -// Map returns the members as a map, e.g. used in api to serialize as Json. +// Map returns the members as a map, e.g. used in api to serialize as JSON. func (comp FolderCompletion) Map() map[string]interface{} { return map[string]interface{}{ "completion": comp.CompletionPct, @@ -853,7 +853,7 @@ func (comp FolderCompletion) Map() map[string]interface{} { "needItems": comp.NeedItems, "needDeletes": comp.NeedDeletes, "sequence": comp.Sequence, - "accepted": comp.Accepted, + "remoteState": comp.RemoteState, } } @@ -904,7 +904,7 @@ func (m *model) folderCompletion(device protocol.DeviceID, folder string) (Folde defer snap.Release() m.pmut.RLock() - accepted := m.remoteFolderStates[device][folder] != remoteNotSharing + state := m.remoteFolderStates[device][folder] downloaded := m.deviceDownloads[device].BytesDownloaded(folder) m.pmut.RUnlock() @@ -915,7 +915,7 @@ func (m *model) folderCompletion(device protocol.DeviceID, folder string) (Folde need.Bytes = 0 } - comp := newFolderCompletion(snap.GlobalSize(), need, snap.Sequence(device), accepted) + comp := newFolderCompletion(snap.GlobalSize(), need, snap.Sequence(device), state) l.Debugf("%v Completion(%s, %q): %v", m, device, folder, comp.Map()) return comp, nil @@ -1147,6 +1147,10 @@ type clusterConfigDeviceInfo struct { local, remote protocol.Device } +type ClusterConfigReceivedEventData struct { + Device protocol.DeviceID `json:"device"` +} + func (m *model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterConfig) error { // Check the peer device's announced folders against our own. Emits events // for folders that we don't expect (unknown or not shared). @@ -1234,6 +1238,10 @@ func (m *model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon m.remoteFolderStates[deviceID] = states m.pmut.Unlock() + m.evLogger.Log(events.ClusterConfigReceived, ClusterConfigReceivedEventData{ + Device: deviceID, + }) + if len(tempIndexFolders) > 0 { m.pmut.RLock() conn, ok := m.conn[deviceID] @@ -1278,7 +1286,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] = remoteValid + seenFolders[folder.ID] = remoteFolderValid cfg, ok := m.cfg.Folder(folder.ID) if ok { @@ -1319,7 +1327,7 @@ func (m *model) ccHandleFolders(folders []protocol.Folder, deviceCfg config.Devi if folder.Paused { indexHandlers.Remove(folder.ID) - seenFolders[cfg.ID] = remotePaused + seenFolders[cfg.ID] = remoteFolderPaused continue } @@ -1374,8 +1382,8 @@ func (m *model) ccHandleFolders(folders []protocol.Folder, deviceCfg config.Devi // 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 + l.Debugf("Remote device %v has not accepted sharing folder %s", deviceID.Short(), cfg.Description()) + seenFolders[folderID] = remoteFolderNotSharing } } @@ -2712,7 +2720,7 @@ func (m *model) availabilityInSnapshotPRlocked(cfg config.FolderConfiguration, s if _, ok := m.remoteFolderStates[device]; !ok { continue } - if state, ok := m.remoteFolderStates[device][cfg.ID]; !ok || state == remotePaused { + if state := m.remoteFolderStates[device][cfg.ID]; state != remoteFolderValid { continue } _, ok := m.conn[device] diff --git a/lib/model/model_test.go b/lib/model/model_test.go index 6d25a3f19..39e79dfc7 100644 --- a/lib/model/model_test.go +++ b/lib/model/model_test.go @@ -3764,16 +3764,16 @@ func TestClusterConfigOnFolderUnpause(t *testing.T) { func TestAddFolderCompletion(t *testing.T) { // Empty folders are always 100% complete. - comp := newFolderCompletion(db.Counts{}, db.Counts{}, 0, true) - comp.add(newFolderCompletion(db.Counts{}, db.Counts{}, 0, false)) + comp := newFolderCompletion(db.Counts{}, db.Counts{}, 0, remoteFolderValid) + comp.add(newFolderCompletion(db.Counts{}, db.Counts{}, 0, remoteFolderPaused)) if comp.CompletionPct != 100 { t.Error(comp.CompletionPct) } // Completion is of the whole - 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) + comp = newFolderCompletion(db.Counts{Bytes: 100}, db.Counts{}, 0, remoteFolderValid) // 100% complete + comp.add(newFolderCompletion(db.Counts{Bytes: 400}, db.Counts{Bytes: 50}, 0, remoteFolderValid)) // 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 8d7084ad0..cea602d3f 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%% (accepted: %v)", data["folder"], data["device"], data["completion"], data["accepted"]) + return fmt.Sprintf("Completion for folder %q on device %v is %v%% (state: %s)", data["folder"], data["device"], data["completion"], data["remoteState"]) case events.FolderSummary: data := ev.Data.(model.FolderSummaryEventData) @@ -145,6 +145,10 @@ func (s *verboseService) formatEvent(ev events.Event) string { device := data["device"] return fmt.Sprintf("Device %v was resumed", device) + case events.ClusterConfigReceived: + data := ev.Data.(model.ClusterConfigReceivedEventData) + return fmt.Sprintf("Received ClusterConfig from device %v", data.Device) + case events.FolderPaused: data := ev.Data.(map[string]string) id := data["id"]