all: Allow dismissing pending devices / folders without ignoring (fixes #7700) (#7712)

This commit is contained in:
André Colomb 2021-06-07 10:29:24 +02:00 committed by GitHub
parent ea0a408849
commit 45edad867c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 294 additions and 23 deletions

View File

@ -94,6 +94,8 @@
"Discovered": "Discovered",
"Discovery": "Discovery",
"Discovery Failures": "Discovery Failures",
"Dismiss": "Dismiss",
"Do not add it to the ignore list, so this notification may recurr.": "Do not add it to the ignore list, so this notification may recurr.",
"Do not restore": "Do not restore",
"Do not restore all": "Do not restore all",
"Do you want to enable watching for changes for all your folders?": "Do you want to enable watching for changes for all your folders?",
@ -227,6 +229,7 @@
"Periodic scanning at given interval and disabled watching for changes": "Periodic scanning at given interval and disabled watching for changes",
"Periodic scanning at given interval and enabled watching for changes": "Periodic scanning at given interval and enabled watching for changes",
"Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:": "Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:",
"Permanently add it to the ignore list, suppressing further notifications.": "Permanently add it to the ignore list, suppressing further notifications.",
"Permissions": "Permissions",
"Please consult the release notes before performing a major upgrade.": "Please consult the release notes before performing a major upgrade.",
"Please set a GUI Authentication User and Password in the Settings dialog.": "Please set a GUI Authentication User and Password in the Settings dialog.",

View File

@ -209,10 +209,13 @@
</div>
<div class="panel-footer clearfix">
<div class="pull-right">
<button type="button" class="btn btn-sm btn-default" ng-click="dismissPendingDevice(deviceID)" tooltip data-original-title="{{'Do not add it to the ignore list, so this notification may recurr.' | translate}}">
<span class="far fa-clock"></span>&nbsp;<span translate>Dismiss</span>
</button>
<button type="button" class="btn btn-sm btn-success" ng-click="addDevice(deviceID, pendingDevice.name)">
<span class="fas fa-plus"></span>&nbsp;<span translate>Add Device</span>
</button>
<button type="button" class="btn btn-sm btn-danger" ng-click="ignoreDevice(deviceID, pendingDevice)">
<button type="button" class="btn btn-sm btn-danger" ng-click="ignoreDevice(deviceID, pendingDevice)" tooltip data-original-title="{{'Permanently add it to the ignore list, suppressing further notifications.' | translate}}">
<span class="fas fa-times"></span>&nbsp;<span translate>Ignore</span>
</button>
</div>
@ -250,13 +253,16 @@
</div>
<div class="panel-footer clearfix">
<div class="pull-right">
<button type="button" class="btn btn-sm btn-default" ng-click="dismissPendingFolder(folderID, deviceID)" tooltip data-original-title="{{'Do not add it to the ignore list, so this notification may recurr.' | translate}}">
<span class="far fa-clock"></span>&nbsp;<span translate>Dismiss</span>
</button>
<button type="button" class="btn btn-sm btn-success" ng-click="addFolderAndShare(folderID, pendingFolder, deviceID)" ng-if="!folders[folderID]">
<span class="fas fa-check"></span>&nbsp;<span translate>Add</span>
</button>
<button type="button" class="btn btn-sm btn-success" ng-click="shareFolderWithDevice(folderID, deviceID)" ng-if="folders[folderID]">
<span class="fas fa-check"></span>&nbsp;<span translate>Share</span>
</button>
<button type="button" class="btn btn-sm btn-danger" ng-click="ignoreFolder(deviceID, folderID, offeringDevice)">
<button type="button" class="btn btn-sm btn-danger" ng-click="ignoreFolder(deviceID, folderID, offeringDevice)" tooltip data-original-title="{{'Permanently add it to the ignore list, suppressing further notifications.' | translate}}">
<span class="fas fa-times"></span>&nbsp;<span translate>Ignore</span>
</button>
</div>

View File

@ -1749,6 +1749,10 @@ angular.module('syncthing.core')
$scope.saveConfig();
};
$scope.dismissPendingDevice = function (deviceID) {
$http.delete(urlbase + '/cluster/pending/devices?device=' + encodeURIComponent(deviceID));
};
$scope.unignoreDeviceFromTemporaryConfig = function (ignoredDevice) {
$scope.tmpRemoteIgnoredDevices = $scope.tmpRemoteIgnoredDevices.filter(function (existingIgnoredDevice) {
return ignoredDevice.deviceID !== existingIgnoredDevice.deviceID;
@ -2220,6 +2224,11 @@ angular.module('syncthing.core')
}
};
$scope.dismissPendingFolder = function (folderID, deviceID) {
$http.delete(urlbase + '/cluster/pending/folders?folder=' + encodeURIComponent(folderID)
+ '&device=' + encodeURIComponent(deviceID));
};
$scope.sharesFolder = function (folderCfg) {
var names = [];
folderCfg.devices.forEach(function (device) {

View File

@ -291,6 +291,10 @@ func (s *service) Serve(ctx context.Context) error {
restMux.HandlerFunc(http.MethodPost, "/rest/system/resume", s.makeDevicePauseHandler(false)) // [device]
restMux.HandlerFunc(http.MethodPost, "/rest/system/debug", s.postSystemDebug) // [enable] [disable]
// The DELETE handlers
restMux.HandlerFunc(http.MethodDelete, "/rest/cluster/pending/devices", s.deletePendingDevices) // device
restMux.HandlerFunc(http.MethodDelete, "/rest/cluster/pending/folders", s.deletePendingFolders) // folder [device]
// Config endpoints
configBuilder := &configMuxBuilder{
@ -632,6 +636,21 @@ func (s *service) getPendingDevices(w http.ResponseWriter, r *http.Request) {
sendJSON(w, devices)
}
func (s *service) deletePendingDevices(w http.ResponseWriter, r *http.Request) {
qs := r.URL.Query()
device := qs.Get("device")
deviceID, err := protocol.DeviceIDFromString(device)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := s.model.DismissPendingDevice(deviceID); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func (s *service) getPendingFolders(w http.ResponseWriter, r *http.Request) {
qs := r.URL.Query()
@ -650,6 +669,22 @@ func (s *service) getPendingFolders(w http.ResponseWriter, r *http.Request) {
sendJSON(w, folders)
}
func (s *service) deletePendingFolders(w http.ResponseWriter, r *http.Request) {
qs := r.URL.Query()
device := qs.Get("device")
deviceID, err := protocol.DeviceIDFromString(device)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
folderID := qs.Get("folder")
if err := s.model.DismissPendingFolder(deviceID, folderID); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func (s *service) restPing(w http.ResponseWriter, r *http.Request) {
sendJSON(w, map[string]string{"ping": "pong"})
}

View File

@ -26,11 +26,13 @@ func (db *Lowlevel) AddOrUpdatePendingDevice(device protocol.DeviceID, name, add
return db.Put(key, bs)
}
func (db *Lowlevel) RemovePendingDevice(device protocol.DeviceID) {
func (db *Lowlevel) RemovePendingDevice(device protocol.DeviceID) error {
key := db.keyer.GeneratePendingDeviceKey(nil, device[:])
if err := db.Delete(key); err != nil {
l.Warnf("Failed to remove pending device entry: %v", err)
return err
}
return nil
}
// PendingDevices enumerates all entries. Invalid ones are dropped from the database
@ -79,32 +81,35 @@ func (db *Lowlevel) AddOrUpdatePendingFolder(id string, of ObservedFolder, devic
}
// RemovePendingFolderForDevice removes entries for specific folder / device combinations.
func (db *Lowlevel) RemovePendingFolderForDevice(id string, device protocol.DeviceID) {
func (db *Lowlevel) RemovePendingFolderForDevice(id string, device protocol.DeviceID) error {
key, err := db.keyer.GeneratePendingFolderKey(nil, device[:], []byte(id))
if err != nil {
return
return err
}
if err := db.Delete(key); err != nil {
l.Warnf("Failed to remove pending folder entry: %v", err)
return err
}
return nil
}
// RemovePendingFolder removes all entries matching a specific folder ID.
func (db *Lowlevel) RemovePendingFolder(id string) {
func (db *Lowlevel) RemovePendingFolder(id string) error {
iter, err := db.NewPrefixIterator([]byte{KeyTypePendingFolder})
if err != nil {
l.Infof("Could not iterate through pending folder entries: %v", err)
return
return err
}
defer iter.Release()
for iter.Next() {
if id != string(db.keyer.FolderFromPendingFolderKey(iter.Key())) {
continue
}
if err := db.Delete(iter.Key()); err != nil {
if err = db.Delete(iter.Key()); err != nil {
l.Warnf("Failed to remove pending folder entry: %v", err)
}
}
return err
}
// Consolidated information about a pending folder

View File

@ -177,6 +177,29 @@ type Model struct {
result1 map[protocol.DeviceID]stats.DeviceStatistics
result2 error
}
DismissPendingDeviceStub func(protocol.DeviceID) error
dismissPendingDeviceMutex sync.RWMutex
dismissPendingDeviceArgsForCall []struct {
arg1 protocol.DeviceID
}
dismissPendingDeviceReturns struct {
result1 error
}
dismissPendingDeviceReturnsOnCall map[int]struct {
result1 error
}
DismissPendingFolderStub func(protocol.DeviceID, string) error
dismissPendingFolderMutex sync.RWMutex
dismissPendingFolderArgsForCall []struct {
arg1 protocol.DeviceID
arg2 string
}
dismissPendingFolderReturns struct {
result1 error
}
dismissPendingFolderReturnsOnCall map[int]struct {
result1 error
}
DownloadProgressStub func(protocol.DeviceID, string, []protocol.FileDownloadProgressUpdate) error
downloadProgressMutex sync.RWMutex
downloadProgressArgsForCall []struct {
@ -1332,6 +1355,129 @@ func (fake *Model) DeviceStatisticsReturnsOnCall(i int, result1 map[protocol.Dev
}{result1, result2}
}
func (fake *Model) DismissPendingDevice(arg1 protocol.DeviceID) error {
fake.dismissPendingDeviceMutex.Lock()
ret, specificReturn := fake.dismissPendingDeviceReturnsOnCall[len(fake.dismissPendingDeviceArgsForCall)]
fake.dismissPendingDeviceArgsForCall = append(fake.dismissPendingDeviceArgsForCall, struct {
arg1 protocol.DeviceID
}{arg1})
stub := fake.DismissPendingDeviceStub
fakeReturns := fake.dismissPendingDeviceReturns
fake.recordInvocation("DismissPendingDevice", []interface{}{arg1})
fake.dismissPendingDeviceMutex.Unlock()
if stub != nil {
return stub(arg1)
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *Model) DismissPendingDeviceCallCount() int {
fake.dismissPendingDeviceMutex.RLock()
defer fake.dismissPendingDeviceMutex.RUnlock()
return len(fake.dismissPendingDeviceArgsForCall)
}
func (fake *Model) DismissPendingDeviceCalls(stub func(protocol.DeviceID) error) {
fake.dismissPendingDeviceMutex.Lock()
defer fake.dismissPendingDeviceMutex.Unlock()
fake.DismissPendingDeviceStub = stub
}
func (fake *Model) DismissPendingDeviceArgsForCall(i int) protocol.DeviceID {
fake.dismissPendingDeviceMutex.RLock()
defer fake.dismissPendingDeviceMutex.RUnlock()
argsForCall := fake.dismissPendingDeviceArgsForCall[i]
return argsForCall.arg1
}
func (fake *Model) DismissPendingDeviceReturns(result1 error) {
fake.dismissPendingDeviceMutex.Lock()
defer fake.dismissPendingDeviceMutex.Unlock()
fake.DismissPendingDeviceStub = nil
fake.dismissPendingDeviceReturns = struct {
result1 error
}{result1}
}
func (fake *Model) DismissPendingDeviceReturnsOnCall(i int, result1 error) {
fake.dismissPendingDeviceMutex.Lock()
defer fake.dismissPendingDeviceMutex.Unlock()
fake.DismissPendingDeviceStub = nil
if fake.dismissPendingDeviceReturnsOnCall == nil {
fake.dismissPendingDeviceReturnsOnCall = make(map[int]struct {
result1 error
})
}
fake.dismissPendingDeviceReturnsOnCall[i] = struct {
result1 error
}{result1}
}
func (fake *Model) DismissPendingFolder(arg1 protocol.DeviceID, arg2 string) error {
fake.dismissPendingFolderMutex.Lock()
ret, specificReturn := fake.dismissPendingFolderReturnsOnCall[len(fake.dismissPendingFolderArgsForCall)]
fake.dismissPendingFolderArgsForCall = append(fake.dismissPendingFolderArgsForCall, struct {
arg1 protocol.DeviceID
arg2 string
}{arg1, arg2})
stub := fake.DismissPendingFolderStub
fakeReturns := fake.dismissPendingFolderReturns
fake.recordInvocation("DismissPendingFolder", []interface{}{arg1, arg2})
fake.dismissPendingFolderMutex.Unlock()
if stub != nil {
return stub(arg1, arg2)
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *Model) DismissPendingFolderCallCount() int {
fake.dismissPendingFolderMutex.RLock()
defer fake.dismissPendingFolderMutex.RUnlock()
return len(fake.dismissPendingFolderArgsForCall)
}
func (fake *Model) DismissPendingFolderCalls(stub func(protocol.DeviceID, string) error) {
fake.dismissPendingFolderMutex.Lock()
defer fake.dismissPendingFolderMutex.Unlock()
fake.DismissPendingFolderStub = stub
}
func (fake *Model) DismissPendingFolderArgsForCall(i int) (protocol.DeviceID, string) {
fake.dismissPendingFolderMutex.RLock()
defer fake.dismissPendingFolderMutex.RUnlock()
argsForCall := fake.dismissPendingFolderArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2
}
func (fake *Model) DismissPendingFolderReturns(result1 error) {
fake.dismissPendingFolderMutex.Lock()
defer fake.dismissPendingFolderMutex.Unlock()
fake.DismissPendingFolderStub = nil
fake.dismissPendingFolderReturns = struct {
result1 error
}{result1}
}
func (fake *Model) DismissPendingFolderReturnsOnCall(i int, result1 error) {
fake.dismissPendingFolderMutex.Lock()
defer fake.dismissPendingFolderMutex.Unlock()
fake.DismissPendingFolderStub = nil
if fake.dismissPendingFolderReturnsOnCall == nil {
fake.dismissPendingFolderReturnsOnCall = make(map[int]struct {
result1 error
})
}
fake.dismissPendingFolderReturnsOnCall[i] = struct {
result1 error
}{result1}
}
func (fake *Model) DownloadProgress(arg1 protocol.DeviceID, arg2 string, arg3 []protocol.FileDownloadProgressUpdate) error {
var arg3Copy []protocol.FileDownloadProgressUpdate
if arg3 != nil {
@ -3254,6 +3400,10 @@ func (fake *Model) Invocations() map[string][][]interface{} {
defer fake.delayScanMutex.RUnlock()
fake.deviceStatisticsMutex.RLock()
defer fake.deviceStatisticsMutex.RUnlock()
fake.dismissPendingDeviceMutex.RLock()
defer fake.dismissPendingDeviceMutex.RUnlock()
fake.dismissPendingFolderMutex.RLock()
defer fake.dismissPendingFolderMutex.RUnlock()
fake.downloadProgressMutex.RLock()
defer fake.downloadProgressMutex.RUnlock()
fake.folderErrorsMutex.RLock()

View File

@ -107,6 +107,8 @@ type Model interface {
PendingDevices() (map[protocol.DeviceID]db.ObservedDevice, error)
PendingFolders(device protocol.DeviceID) (map[string]db.PendingFolder, error)
DismissPendingDevice(device protocol.DeviceID) error
DismissPendingFolder(device protocol.DeviceID, folder string) error
StartDeadlockDetector(timeout time.Duration)
GlobalDirectoryTree(folder, prefix string, levels int, dirsOnly bool) ([]*TreeEntry, error)
@ -1374,17 +1376,18 @@ func (m *model) ccHandleFolders(folders []protocol.Folder, deviceCfg config.Devi
}
indexHandlers.RemoveAllExcept(seenFolders)
expiredPendingList := make([]map[string]string, 0, len(expiredPending))
for folder := range expiredPending {
m.db.RemovePendingFolderForDevice(folder, deviceID)
}
if len(updatedPending) > 0 || len(expiredPending) > 0 {
expiredPendingList := make([]map[string]string, 0, len(expiredPending))
for folderID := range expiredPending {
expiredPendingList = append(expiredPendingList, map[string]string{
"folderID": folderID,
"deviceID": deviceID.String(),
})
if err = m.db.RemovePendingFolderForDevice(folder, deviceID); err != nil {
// Nothing we can fix; logged from DB already
continue
}
expiredPendingList = append(expiredPendingList, map[string]string{
"folderID": folder,
"deviceID": deviceID.String(),
})
}
if len(updatedPending) > 0 || len(expiredPendingList) > 0 {
m.evLogger.Log(events.PendingFoldersChanged, map[string]interface{}{
"added": updatedPending,
"removed": expiredPendingList,
@ -2947,10 +2950,13 @@ func (m *model) cleanPending(existingDevices map[protocol.DeviceID]config.Device
// folders as well, assuming the folder is no longer of interest
// at all (but might become pending again).
l.Debugf("Discarding pending removed folder %v from all devices", folderID)
m.db.RemovePendingFolder(folderID)
removedPendingFolders = append(removedPendingFolders, map[string]string{
"folderID": folderID,
})
if err := m.db.RemovePendingFolder(folderID); err != nil {
// Nothing we can fix; logged from DB already
} else {
removedPendingFolders = append(removedPendingFolders, map[string]string{
"folderID": folderID,
})
}
continue
}
for deviceID := range pf.OfferedBy {
@ -2969,7 +2975,10 @@ func (m *model) cleanPending(existingDevices map[protocol.DeviceID]config.Device
}
continue
removeFolderForDevice:
m.db.RemovePendingFolderForDevice(folderID, deviceID)
if err := m.db.RemovePendingFolderForDevice(folderID, deviceID); err != nil {
// Nothing we can fix; logged from DB already
continue
}
removedPendingFolders = append(removedPendingFolders, map[string]string{
"folderID": folderID,
"deviceID": deviceID.String(),
@ -2999,7 +3008,10 @@ func (m *model) cleanPending(existingDevices map[protocol.DeviceID]config.Device
}
continue
removeDevice:
m.db.RemovePendingDevice(deviceID)
if err := m.db.RemovePendingDevice(deviceID); err != nil {
// Nothing we can fix; logged from DB already
continue
}
removedPendingDevices = append(removedPendingDevices, map[string]string{
"deviceID": deviceID.String(),
})
@ -3041,6 +3053,57 @@ func (m *model) PendingFolders(device protocol.DeviceID) (map[string]db.PendingF
return m.db.PendingFoldersForDevice(device)
}
// DismissPendingDevices removes the record of a specific pending device.
func (m *model) DismissPendingDevice(device protocol.DeviceID) error {
l.Debugf("Discarding pending device %v", device)
err := m.db.RemovePendingDevice(device)
if err != nil {
return err
}
removedPendingDevices := []map[string]string{
{"deviceID": device.String()},
}
m.evLogger.Log(events.PendingDevicesChanged, map[string]interface{}{
"removed": removedPendingDevices,
})
return nil
}
// DismissPendingFolders removes records of pending folders. Either a specific folder /
// device combination, or all matching a specific folder ID if the device argument is
// specified as EmptyDeviceID.
func (m *model) DismissPendingFolder(device protocol.DeviceID, folder string) error {
var removedPendingFolders []map[string]string
if device == protocol.EmptyDeviceID {
l.Debugf("Discarding pending removed folder %s from all devices", folder)
err := m.db.RemovePendingFolder(folder)
if err != nil {
return err
}
removedPendingFolders = []map[string]string{
{"folderID": folder},
}
} else {
l.Debugf("Discarding pending folder %s from device %v", folder, device)
err := m.db.RemovePendingFolderForDevice(folder, device)
if err != nil {
return err
}
removedPendingFolders = []map[string]string{
{
"folderID": folder,
"deviceID": device.String(),
},
}
}
if len(removedPendingFolders) > 0 {
m.evLogger.Log(events.PendingFoldersChanged, map[string]interface{}{
"removed": removedPendingFolders,
})
}
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 {