1
0
Эх сурвалжийг харах

all: Store pending devices and folders in database (fixes #7178) (#6443)

André Colomb 4 жил өмнө
parent
commit
7502997e7e

+ 17 - 0
cmd/stindex/dump.go

@@ -131,6 +131,23 @@ func dump(ldb backend.Backend) {
 				fmt.Printf(" V:%v\n", v)
 			}
 
+		case db.KeyTypePendingFolder:
+			device := binary.BigEndian.Uint32(key[1:])
+			folder := string(key[5:])
+			var of db.ObservedFolder
+			of.Unmarshal(it.Value())
+			fmt.Printf("[pendingFolder] D:%d F:%s V:%v\n", device, folder, of)
+
+		case db.KeyTypePendingDevice:
+			device := "<invalid>"
+			dev, err := protocol.DeviceIDFromBytes(key[1:])
+			if err == nil {
+				device = dev.String()
+			}
+			var od db.ObservedDevice
+			od.Unmarshal(it.Value())
+			fmt.Printf("[pendingDevice] D:%v V:%v\n", device, od)
+
 		default:
 			fmt.Printf("[??? %d]\n  %x\n  %x\n", key[0], key, it.Value())
 		}

+ 16 - 16
gui/default/index.html

@@ -190,7 +190,7 @@
 
         <!-- Panel: New Device -->
 
-        <div ng-repeat="pendingDevice in config.pendingDevices" class="row">
+        <div ng-repeat="(deviceID, pendingDevice) in pendingDevices" class="row">
           <div class="col-md-12">
             <div class="panel panel-warning">
               <div class="panel-heading">
@@ -202,17 +202,17 @@
               </div>
               <div class="panel-body">
                 <p>
-                  <span translate translate-value-device="{{ pendingDevice.deviceID }}" translate-value-address="{{ pendingDevice.address }}" translate-value-name="{{ pendingDevice.name }}">
+                  <span translate translate-value-device="{{ deviceID }}" translate-value-address="{{ pendingDevice.address }}" translate-value-name="{{ pendingDevice.name }}">
                     Device "{%name%}" ({%device%} at {%address%}) wants to connect. Add new device?
                   </span>
                 </p>
               </div>
               <div class="panel-footer clearfix">
                 <div class="pull-right">
-                  <button type="button" class="btn btn-sm btn-success" ng-click="addDevice(pendingDevice.deviceID, pendingDevice.name)">
+                  <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(pendingDevice)">
+                  <button type="button" class="btn btn-sm btn-danger" ng-click="ignoreDevice(deviceID, pendingDevice)">
                     <span class="fas fa-times"></span>&nbsp;<span translate>Ignore</span>
                   </button>
                 </div>
@@ -222,8 +222,8 @@
         </div>
 
         <!-- Panel: New Folder -->
-        <div ng-repeat="device in config.devices">
-          <div ng-repeat="pendingFolder in device.pendingFolders" class="row reject">
+        <div ng-repeat="(folderID, pendingFolder) in pendingFolders">
+          <div ng-repeat="(deviceID, offeringDevice) in pendingFolder.offeredBy" class="row reject">
             <div class="col-md-12">
               <div class="panel panel-warning">
                 <div class="panel-heading">
@@ -231,32 +231,32 @@
                     <div class="panel-icon">
                       <span class="fas fa-folder"></span>
                     </div>
-                    <span translate ng-if="!folders[pendingFolder.id]">New Folder</span>
-                    <span translate ng-if="folders[pendingFolder.id]">Share Folder</span>
-                    <span class="pull-right">{{ pendingFolder.time | date:"yyyy-MM-dd HH:mm:ss" }}</span>
+                    <span translate ng-if="!folders[folderID]">New Folder</span>
+                    <span translate ng-if="folders[folderID]">Share Folder</span>
+                    <span class="pull-right">{{ offeringDevice.time | date:"yyyy-MM-dd HH:mm:ss" }}</span>
                   </h3>
                 </div>
                 <div class="panel-body">
                   <p>
-                    <span ng-if="pendingFolder.label.length == 0" translate translate-value-device="{{ deviceName(devices[device.deviceID]) }}" translate-value-folder="{{ pendingFolder.id }}">
+                    <span ng-if="offeringDevice.label.length == 0" translate translate-value-device="{{ deviceName(devices[deviceID]) }}" translate-value-folder="{{ folderID }}">
                       {%device%} wants to share folder "{%folder%}".
                     </span>
-                    <span ng-if="pendingFolder.label.length != 0" translate translate-value-device="{{ deviceName(devices[device.deviceID]) }}" translate-value-folder="{{ pendingFolder.id }}" translate-value-folderlabel="{{ pendingFolder.label }}">
+                    <span ng-if="offeringDevice.label.length != 0" translate translate-value-device="{{ deviceName(devices[deviceID]) }}" translate-value-folder="{{ folderID }}" translate-value-folderlabel="{{ offeringDevice.label }}">
                       {%device%} wants to share folder "{%folderlabel%}" ({%folder%}).
                     </span>
-                    <span translate ng-if="folders[pendingFolder.id]">Share this folder?</span>
-                    <span translate ng-if="!folders[pendingFolder.id]">Add new folder?</span>
+                    <span translate ng-if="folders[folderID]">Share this folder?</span>
+                    <span translate ng-if="!folders[folderID]">Add new folder?</span>
                   </p>
                 </div>
                 <div class="panel-footer clearfix">
                   <div class="pull-right">
-                    <button type="button" class="btn btn-sm btn-success" ng-click="addFolderAndShare(pendingFolder.id, pendingFolder.label, device.deviceID)" ng-if="!folders[pendingFolder.id]">
+                    <button type="button" class="btn btn-sm btn-success" ng-click="addFolderAndShare(folderID, offeringDevice.label, 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(pendingFolder.id, device.deviceID)" ng-if="folders[pendingFolder.id]">
+                    <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(device.deviceID, pendingFolder)">
+                    <button type="button" class="btn btn-sm btn-danger" ng-click="ignoreFolder(deviceID, folderID, offeringDevice)">
                       <span class="fas fa-times"></span>&nbsp;<span translate>Ignore</span>
                     </button>
                   </div>

+ 56 - 16
gui/default/syncthing/core/syncthingController.js

@@ -38,6 +38,8 @@ angular.module('syncthing.core')
         $scope.upgradeInfo = null;
         $scope.deviceStats = {};
         $scope.folderStats = {};
+        $scope.pendingDevices = {};
+        $scope.pendingFolders = {};
         $scope.progress = {};
         $scope.version = {};
         $scope.needed = {}
@@ -242,6 +244,34 @@ angular.module('syncthing.core')
             }
         });
 
+        $scope.$on(Events.DEVICE_REJECTED, function (event, arg) {
+            var pendingDevice = {
+                time: arg.time,
+                name: arg.data.name,
+                address: arg.data.address
+            };
+            console.log("rejected device:", arg.data.device, pendingDevice);
+
+            $scope.pendingDevices[arg.data.device] = pendingDevice;
+        });
+
+        $scope.$on(Events.FOLDER_REJECTED, function (event, arg) {
+            var offeringDevice = {
+                time: arg.time,
+                label: arg.data.folderLabel
+            };
+            console.log("rejected folder", arg.data.folder, "from device:", arg.data.device, offeringDevice);
+
+            var pendingFolder = $scope.pendingFolders[arg.data.folder];
+            if (pendingFolder === undefined) {
+                pendingFolder = {
+                    offeredBy: {}
+                };
+            }
+            pendingFolder.offeredBy[arg.data.device] = offeringDevice;
+            $scope.pendingFolders[arg.data.folder] = pendingFolder;
+        });
+
         $scope.$on('ConfigLoaded', function () {
             if ($scope.config.options.urAccepted === 0) {
                 // If usage reporting has been neither accepted nor declined,
@@ -391,6 +421,7 @@ angular.module('syncthing.core')
                 });
             });
 
+            refreshCluster();
             refreshNoAuthWarning();
             setDefaultTheme();
 
@@ -455,6 +486,16 @@ angular.module('syncthing.core')
             }
         }
 
+        function refreshCluster() {
+            $http.get(urlbase + '/cluster/pending/devices').success(function (data) {
+                $scope.pendingDevices = data;
+                console.log("refreshCluster devices", data);
+            }).error($scope.emitHTTPError);
+            $http.get(urlbase + '/cluster/pending/folders').success(function (data) {
+                $scope.pendingFolders = data;
+                console.log("refreshCluster folders", data);
+            }).error($scope.emitHTTPError);
+        }
 
         function refreshDiscoveryCache() {
             $http.get(urlbase + '/system/discovery').success(function (data) {
@@ -1012,7 +1053,6 @@ angular.module('syncthing.core')
 
             // loop through all devices
             var deviceCount = 0;
-            var pendingFolders = 0;
             for (var id in $scope.devices) {
                 var status = $scope.deviceStatus({
                     deviceID: id
@@ -1028,14 +1068,11 @@ angular.module('syncthing.core')
                         deviceCount--;
                         break;
                 }
-                pendingFolders += $scope.devices[id].pendingFolders.length;
                 deviceCount++;
             }
 
             // enumerate notifications
-            if ($scope.openNoAuth || !$scope.configInSync || $scope.errorList().length > 0 || !online || (
-                !isEmptyObject($scope.config) && ($scope.config.pendingDevices.length > 0 || pendingFolders > 0)
-            )) {
+            if ($scope.openNoAuth || !$scope.configInSync || $scope.errorList().length > 0 || !online || Object.keys($scope.pendingDevices).length > 0 || Object.keys($scope.pendingFolders).length > 0) {
                 notifyCount++;
             }
 
@@ -1476,7 +1513,6 @@ angular.module('syncthing.core')
                         _addressesStr: 'dynamic',
                         compression: 'metadata',
                         introducer: false,
-                        pendingFolders: [],
                         ignoredFolders: []
                     };
                     $scope.editingExisting = false;
@@ -1549,11 +1585,12 @@ angular.module('syncthing.core')
             $scope.saveConfig();
         };
 
-        $scope.ignoreDevice = function (pendingDevice) {
-            pendingDevice = angular.copy(pendingDevice);
+        $scope.ignoreDevice = function (deviceID, pendingDevice) {
+            var ignoredDevice = angular.copy(pendingDevice);
+            ignoredDevice.deviceID = deviceID;
             // Bump time
-            pendingDevice.time = (new Date()).toISOString();
-            $scope.config.remoteIgnoredDevices.push(pendingDevice);
+            ignoredDevice.time = (new Date()).toISOString();
+            $scope.config.remoteIgnoredDevices.push(ignoredDevice);
             $scope.saveConfig();
         };
 
@@ -1954,13 +1991,16 @@ angular.module('syncthing.core')
             });
         };
 
-        $scope.ignoreFolder = function (device, pendingFolder) {
-            pendingFolder = angular.copy(pendingFolder);
-            // Bump time
-            pendingFolder.time = (new Date()).toISOString();
+        $scope.ignoreFolder = function (device, folderID, offeringDevice) {
+            var ignoredFolder = {
+                id: folderID,
+                label: offeringDevice.label,
+                // Bump time
+                time: (new Date()).toISOString()
+            }
 
-            if (device in $scope.devices) {
-                $scope.devices[device].ignoredFolders.push(pendingFolder);
+            if (id in $scope.devices) {
+                $scope.devices[id].ignoredFolders.push(ignoredFolder);
                 $scope.saveConfig();
             }
         };

+ 59 - 30
lib/api/api.go

@@ -237,36 +237,38 @@ func (s *service) Serve(ctx context.Context) error {
 	restMux := httprouter.New()
 
 	// The GET handlers
-	restMux.HandlerFunc(http.MethodGet, "/rest/db/completion", s.getDBCompletion)           // [device] [folder]
-	restMux.HandlerFunc(http.MethodGet, "/rest/db/file", s.getDBFile)                       // folder file
-	restMux.HandlerFunc(http.MethodGet, "/rest/db/ignores", s.getDBIgnores)                 // folder
-	restMux.HandlerFunc(http.MethodGet, "/rest/db/need", s.getDBNeed)                       // folder [perpage] [page]
-	restMux.HandlerFunc(http.MethodGet, "/rest/db/remoteneed", s.getDBRemoteNeed)           // device folder [perpage] [page]
-	restMux.HandlerFunc(http.MethodGet, "/rest/db/localchanged", s.getDBLocalChanged)       // folder
-	restMux.HandlerFunc(http.MethodGet, "/rest/db/status", s.getDBStatus)                   // folder
-	restMux.HandlerFunc(http.MethodGet, "/rest/db/browse", s.getDBBrowse)                   // folder [prefix] [dirsonly] [levels]
-	restMux.HandlerFunc(http.MethodGet, "/rest/folder/versions", s.getFolderVersions)       // folder
-	restMux.HandlerFunc(http.MethodGet, "/rest/folder/errors", s.getFolderErrors)           // folder
-	restMux.HandlerFunc(http.MethodGet, "/rest/folder/pullerrors", s.getFolderErrors)       // folder (deprecated)
-	restMux.HandlerFunc(http.MethodGet, "/rest/events", s.getIndexEvents)                   // [since] [limit] [timeout] [events]
-	restMux.HandlerFunc(http.MethodGet, "/rest/events/disk", s.getDiskEvents)               // [since] [limit] [timeout]
-	restMux.HandlerFunc(http.MethodGet, "/rest/stats/device", s.getDeviceStats)             // -
-	restMux.HandlerFunc(http.MethodGet, "/rest/stats/folder", s.getFolderStats)             // -
-	restMux.HandlerFunc(http.MethodGet, "/rest/svc/deviceid", s.getDeviceID)                // id
-	restMux.HandlerFunc(http.MethodGet, "/rest/svc/lang", s.getLang)                        // -
-	restMux.HandlerFunc(http.MethodGet, "/rest/svc/report", s.getReport)                    // -
-	restMux.HandlerFunc(http.MethodGet, "/rest/svc/random/string", s.getRandomString)       // [length]
-	restMux.HandlerFunc(http.MethodGet, "/rest/system/browse", s.getSystemBrowse)           // current
-	restMux.HandlerFunc(http.MethodGet, "/rest/system/connections", s.getSystemConnections) // -
-	restMux.HandlerFunc(http.MethodGet, "/rest/system/discovery", s.getSystemDiscovery)     // -
-	restMux.HandlerFunc(http.MethodGet, "/rest/system/error", s.getSystemError)             // -
-	restMux.HandlerFunc(http.MethodGet, "/rest/system/ping", s.restPing)                    // -
-	restMux.HandlerFunc(http.MethodGet, "/rest/system/status", s.getSystemStatus)           // -
-	restMux.HandlerFunc(http.MethodGet, "/rest/system/upgrade", s.getSystemUpgrade)         // -
-	restMux.HandlerFunc(http.MethodGet, "/rest/system/version", s.getSystemVersion)         // -
-	restMux.HandlerFunc(http.MethodGet, "/rest/system/debug", s.getSystemDebug)             // -
-	restMux.HandlerFunc(http.MethodGet, "/rest/system/log", s.getSystemLog)                 // [since]
-	restMux.HandlerFunc(http.MethodGet, "/rest/system/log.txt", s.getSystemLogTxt)          // [since]
+	restMux.HandlerFunc(http.MethodGet, "/rest/cluster/pending/devices", s.getPendingDevices) // -
+	restMux.HandlerFunc(http.MethodGet, "/rest/cluster/pending/folders", s.getPendingFolders) // [device]
+	restMux.HandlerFunc(http.MethodGet, "/rest/db/completion", s.getDBCompletion)             // [device] [folder]
+	restMux.HandlerFunc(http.MethodGet, "/rest/db/file", s.getDBFile)                         // folder file
+	restMux.HandlerFunc(http.MethodGet, "/rest/db/ignores", s.getDBIgnores)                   // folder
+	restMux.HandlerFunc(http.MethodGet, "/rest/db/need", s.getDBNeed)                         // folder [perpage] [page]
+	restMux.HandlerFunc(http.MethodGet, "/rest/db/remoteneed", s.getDBRemoteNeed)             // device folder [perpage] [page]
+	restMux.HandlerFunc(http.MethodGet, "/rest/db/localchanged", s.getDBLocalChanged)         // folder
+	restMux.HandlerFunc(http.MethodGet, "/rest/db/status", s.getDBStatus)                     // folder
+	restMux.HandlerFunc(http.MethodGet, "/rest/db/browse", s.getDBBrowse)                     // folder [prefix] [dirsonly] [levels]
+	restMux.HandlerFunc(http.MethodGet, "/rest/folder/versions", s.getFolderVersions)         // folder
+	restMux.HandlerFunc(http.MethodGet, "/rest/folder/errors", s.getFolderErrors)             // folder
+	restMux.HandlerFunc(http.MethodGet, "/rest/folder/pullerrors", s.getFolderErrors)         // folder (deprecated)
+	restMux.HandlerFunc(http.MethodGet, "/rest/events", s.getIndexEvents)                     // [since] [limit] [timeout] [events]
+	restMux.HandlerFunc(http.MethodGet, "/rest/events/disk", s.getDiskEvents)                 // [since] [limit] [timeout]
+	restMux.HandlerFunc(http.MethodGet, "/rest/stats/device", s.getDeviceStats)               // -
+	restMux.HandlerFunc(http.MethodGet, "/rest/stats/folder", s.getFolderStats)               // -
+	restMux.HandlerFunc(http.MethodGet, "/rest/svc/deviceid", s.getDeviceID)                  // id
+	restMux.HandlerFunc(http.MethodGet, "/rest/svc/lang", s.getLang)                          // -
+	restMux.HandlerFunc(http.MethodGet, "/rest/svc/report", s.getReport)                      // -
+	restMux.HandlerFunc(http.MethodGet, "/rest/svc/random/string", s.getRandomString)         // [length]
+	restMux.HandlerFunc(http.MethodGet, "/rest/system/browse", s.getSystemBrowse)             // current
+	restMux.HandlerFunc(http.MethodGet, "/rest/system/connections", s.getSystemConnections)   // -
+	restMux.HandlerFunc(http.MethodGet, "/rest/system/discovery", s.getSystemDiscovery)       // -
+	restMux.HandlerFunc(http.MethodGet, "/rest/system/error", s.getSystemError)               // -
+	restMux.HandlerFunc(http.MethodGet, "/rest/system/ping", s.restPing)                      // -
+	restMux.HandlerFunc(http.MethodGet, "/rest/system/status", s.getSystemStatus)             // -
+	restMux.HandlerFunc(http.MethodGet, "/rest/system/upgrade", s.getSystemUpgrade)           // -
+	restMux.HandlerFunc(http.MethodGet, "/rest/system/version", s.getSystemVersion)           // -
+	restMux.HandlerFunc(http.MethodGet, "/rest/system/debug", s.getSystemDebug)               // -
+	restMux.HandlerFunc(http.MethodGet, "/rest/system/log", s.getSystemLog)                   // [since]
+	restMux.HandlerFunc(http.MethodGet, "/rest/system/log.txt", s.getSystemLogTxt)            // [since]
 
 	// The POST handlers
 	restMux.HandlerFunc(http.MethodPost, "/rest/db/prio", s.postDBPrio)                          // folder file [perpage] [page]
@@ -620,6 +622,33 @@ func (s *service) whenDebugging(h http.Handler) http.Handler {
 	})
 }
 
+func (s *service) getPendingDevices(w http.ResponseWriter, r *http.Request) {
+	devices, err := s.model.PendingDevices()
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	sendJSON(w, devices)
+}
+
+func (s *service) getPendingFolders(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
+	}
+
+	folders, err := s.model.PendingFolders(deviceID)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	sendJSON(w, folders)
+}
+
 func (s *service) restPing(w http.ResponseWriter, r *http.Request) {
 	sendJSON(w, map[string]string{"ping": "pong"})
 }

+ 4 - 0
lib/api/mocked_config_test.go

@@ -130,6 +130,10 @@ func (c *mockedConfig) IgnoredDevice(id protocol.DeviceID) bool {
 	return false
 }
 
+func (c *mockedConfig) IgnoredDevices() []config.ObservedDevice {
+	return nil
+}
+
 func (c *mockedConfig) IgnoredFolder(device protocol.DeviceID, folder string) bool {
 	return false
 }

+ 8 - 0
lib/api/mocked_model_test.go

@@ -125,6 +125,14 @@ func (m *mockedModel) State(folder string) (string, time.Time, error) {
 func (m *mockedModel) UsageReportingStats(r *contract.Report, version int, preview bool) {
 }
 
+func (m *mockedModel) PendingDevices() (map[protocol.DeviceID]db.ObservedDevice, error) {
+	return nil, nil
+}
+
+func (m *mockedModel) PendingFolders(device protocol.DeviceID) (map[string]db.PendingFolder, error) {
+	return nil, nil
+}
+
 func (m *mockedModel) FolderErrors(folder string) ([]model.FileError, error) {
 	return nil, nil
 }

+ 1 - 31
lib/config/config.go

@@ -204,9 +204,6 @@ func (cfg Configuration) Copy() Configuration {
 	newCfg.IgnoredDevices = make([]ObservedDevice, len(cfg.IgnoredDevices))
 	copy(newCfg.IgnoredDevices, cfg.IgnoredDevices)
 
-	newCfg.PendingDevices = make([]ObservedDevice, len(cfg.PendingDevices))
-	copy(newCfg.PendingDevices, cfg.PendingDevices)
-
 	return newCfg
 }
 
@@ -235,9 +232,7 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) error {
 	guiPWIsSet := cfg.GUI.User != "" && cfg.GUI.Password != ""
 	cfg.Options.prepare(guiPWIsSet)
 
-	ignoredDevices := cfg.prepareIgnoredDevices(existingDevices)
-
-	cfg.preparePendingDevices(existingDevices, ignoredDevices)
+	cfg.prepareIgnoredDevices(existingDevices)
 
 	cfg.removeDeprecatedProtocols()
 
@@ -354,31 +349,6 @@ func (cfg *Configuration) prepareIgnoredDevices(existingDevices map[protocol.Dev
 	return ignoredDevices
 }
 
-func (cfg *Configuration) preparePendingDevices(existingDevices, ignoredDevices map[protocol.DeviceID]bool) {
-	// The list of pending devices should not contain devices that were added manually, nor should it contain
-	// ignored devices.
-
-	// Sort by time, so that in case of duplicates latest "time" is used.
-	sort.Slice(cfg.PendingDevices, func(i, j int) bool {
-		return cfg.PendingDevices[i].Time.Before(cfg.PendingDevices[j].Time)
-	})
-
-	newPendingDevices := cfg.PendingDevices[:0]
-nextPendingDevice:
-	for _, pendingDevice := range cfg.PendingDevices {
-		if !existingDevices[pendingDevice.ID] && !ignoredDevices[pendingDevice.ID] {
-			// Deduplicate
-			for _, existingPendingDevice := range newPendingDevices {
-				if existingPendingDevice.ID == pendingDevice.ID {
-					continue nextPendingDevice
-				}
-			}
-			newPendingDevices = append(newPendingDevices, pendingDevice)
-		}
-	}
-	cfg.PendingDevices = newPendingDevices
-}
-
 func (cfg *Configuration) removeDeprecatedProtocols() {
 	// Deprecated protocols are removed from the list of listeners and
 	// device addresses. So far just kcp*.

+ 53 - 52
lib/config/config.pb.go

@@ -24,14 +24,14 @@ var _ = math.Inf
 const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package
 
 type Configuration struct {
-	Version        int                   `protobuf:"varint,1,opt,name=version,proto3,casttype=int" json:"version" xml:"version,attr"`
-	Folders        []FolderConfiguration `protobuf:"bytes,2,rep,name=folders,proto3" json:"folders" xml:"folder"`
-	Devices        []DeviceConfiguration `protobuf:"bytes,3,rep,name=devices,proto3" json:"devices" xml:"device"`
-	GUI            GUIConfiguration      `protobuf:"bytes,4,opt,name=gui,proto3" json:"gui" xml:"gui"`
-	LDAP           LDAPConfiguration     `protobuf:"bytes,5,opt,name=ldap,proto3" json:"ldap" xml:"ldap"`
-	Options        OptionsConfiguration  `protobuf:"bytes,6,opt,name=options,proto3" json:"options" xml:"options"`
-	IgnoredDevices []ObservedDevice      `protobuf:"bytes,7,rep,name=ignored_devices,json=ignoredDevices,proto3" json:"remoteIgnoredDevices" xml:"remoteIgnoredDevice"`
-	PendingDevices []ObservedDevice      `protobuf:"bytes,8,rep,name=pending_devices,json=pendingDevices,proto3" json:"pendingDevices" xml:"pendingDevice"`
+	Version                  int                   `protobuf:"varint,1,opt,name=version,proto3,casttype=int" json:"version" xml:"version,attr"`
+	Folders                  []FolderConfiguration `protobuf:"bytes,2,rep,name=folders,proto3" json:"folders" xml:"folder"`
+	Devices                  []DeviceConfiguration `protobuf:"bytes,3,rep,name=devices,proto3" json:"devices" xml:"device"`
+	GUI                      GUIConfiguration      `protobuf:"bytes,4,opt,name=gui,proto3" json:"gui" xml:"gui"`
+	LDAP                     LDAPConfiguration     `protobuf:"bytes,5,opt,name=ldap,proto3" json:"ldap" xml:"ldap"`
+	Options                  OptionsConfiguration  `protobuf:"bytes,6,opt,name=options,proto3" json:"options" xml:"options"`
+	IgnoredDevices           []ObservedDevice      `protobuf:"bytes,7,rep,name=ignored_devices,json=ignoredDevices,proto3" json:"remoteIgnoredDevices" xml:"remoteIgnoredDevice"`
+	DeprecatedPendingDevices []ObservedDevice      `protobuf:"bytes,8,rep,name=pending_devices,json=pendingDevices,proto3" json:"-" xml:"pendingDevice,omitempty"` // Deprecated: Do not use.
 }
 
 func (m *Configuration) Reset()         { *m = Configuration{} }
@@ -74,42 +74,43 @@ func init() {
 func init() { proto.RegisterFile("lib/config/config.proto", fileDescriptor_baadf209193dc627) }
 
 var fileDescriptor_baadf209193dc627 = []byte{
-	// 547 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x93, 0x4f, 0x8b, 0xd3, 0x40,
-	0x18, 0xc6, 0x13, 0xbb, 0xdb, 0xba, 0xd9, 0x7f, 0x90, 0x15, 0x4d, 0x55, 0x32, 0x75, 0xa8, 0x52,
-	0x45, 0xbb, 0xb0, 0x5e, 0xc4, 0x9b, 0xb5, 0xb8, 0x14, 0x05, 0x65, 0x60, 0x45, 0xbd, 0x48, 0xdb,
-	0xcc, 0xa6, 0x03, 0xed, 0x4c, 0x49, 0xd2, 0xb2, 0x7e, 0x0b, 0xf1, 0x13, 0x78, 0xf5, 0x1b, 0xf8,
-	0x11, 0x7a, 0x6b, 0x8f, 0x9e, 0x06, 0xb6, 0xbd, 0xf5, 0x98, 0xa3, 0x27, 0x99, 0x7f, 0xdd, 0x44,
-	0xa2, 0xa7, 0xe6, 0x7d, 0x9f, 0xe7, 0xf9, 0xbd, 0x2f, 0x6f, 0x13, 0xe7, 0xd6, 0x90, 0xf4, 0x8e,
-	0xfb, 0x8c, 0x9e, 0x93, 0x50, 0xff, 0x34, 0xc7, 0x11, 0x4b, 0x98, 0x5b, 0x56, 0xd5, 0xed, 0x7a,
-	0xc6, 0x70, 0xce, 0x86, 0x01, 0x8e, 0x54, 0x31, 0x89, 0xba, 0x09, 0x61, 0x54, 0xb9, 0x73, 0xae,
-	0x00, 0x4f, 0x49, 0x1f, 0x17, 0xb9, 0xee, 0x65, 0x5c, 0xe1, 0x84, 0x14, 0x59, 0x60, 0xc6, 0x32,
-	0x0c, 0xba, 0xe3, 0x22, 0xcf, 0xfd, 0x8c, 0x87, 0x8d, 0x85, 0x10, 0x17, 0xd9, 0xaa, 0x59, 0x5b,
-	0x2f, 0xc6, 0xd1, 0x14, 0x07, 0x5a, 0xda, 0xc1, 0x17, 0x89, 0x7a, 0x84, 0x3f, 0xcb, 0xce, 0xfe,
-	0xcb, 0x6c, 0xda, 0x45, 0x4e, 0x65, 0x8a, 0xa3, 0x98, 0x30, 0xea, 0xd9, 0x35, 0xbb, 0xb1, 0xdd,
-	0x7a, 0xb6, 0xe6, 0xc0, 0xb4, 0x52, 0x0e, 0xdc, 0x8b, 0xd1, 0xf0, 0x39, 0xd4, 0xf5, 0xe3, 0x6e,
-	0x92, 0x44, 0xf0, 0x37, 0x07, 0x25, 0x42, 0x93, 0xf5, 0xbc, 0xbe, 0x97, 0xed, 0x23, 0x93, 0x72,
-	0xdf, 0x3b, 0x15, 0x75, 0xbc, 0xd8, 0xbb, 0x56, 0x2b, 0x35, 0x76, 0x4f, 0xee, 0x34, 0xf5, 0xb5,
-	0x5f, 0xc9, 0x76, 0x6e, 0x83, 0x16, 0x98, 0x71, 0x60, 0x89, 0xa1, 0x3a, 0x93, 0x72, 0xb0, 0x27,
-	0x87, 0xaa, 0x1a, 0x22, 0x23, 0x08, 0xae, 0x3a, 0x77, 0xec, 0x95, 0xf2, 0xdc, 0xb6, 0x6c, 0xff,
-	0x83, 0xab, 0x33, 0x1b, 0xae, 0xaa, 0x21, 0x32, 0x82, 0x8b, 0x9c, 0x52, 0x38, 0x21, 0xde, 0x56,
-	0xcd, 0x6e, 0xec, 0x9e, 0x78, 0x86, 0x79, 0x7a, 0xd6, 0xc9, 0x03, 0x1f, 0x08, 0xe0, 0x92, 0x83,
-	0xd2, 0xe9, 0x59, 0x67, 0xcd, 0x81, 0xc8, 0xa4, 0x1c, 0xec, 0x48, 0x66, 0x38, 0x21, 0xf0, 0xdb,
-	0xa2, 0x2e, 0x24, 0x24, 0x04, 0xf7, 0xa3, 0xb3, 0x25, 0xfe, 0x51, 0x6f, 0x5b, 0x42, 0xab, 0x06,
-	0xfa, 0xa6, 0xfd, 0xe2, 0x5d, 0x9e, 0xfa, 0x48, 0x53, 0xb7, 0x84, 0xb4, 0xe6, 0x40, 0xc6, 0x52,
-	0x0e, 0x1c, 0xc9, 0x15, 0x85, 0x00, 0x4b, 0x15, 0x49, 0xcd, 0xfd, 0xe0, 0x54, 0xf4, 0x8b, 0xe0,
-	0x95, 0x25, 0xfd, 0xae, 0xa1, 0xbf, 0x55, 0xed, 0xfc, 0x80, 0x9a, 0xb9, 0x83, 0x0e, 0xa5, 0x1c,
-	0xec, 0x4b, 0xb6, 0xae, 0x21, 0x32, 0x8a, 0xfb, 0xc3, 0x76, 0x0e, 0x49, 0x48, 0x59, 0x84, 0x83,
-	0xcf, 0xe6, 0xd2, 0x15, 0x79, 0xe9, 0x9b, 0x9b, 0x11, 0xfa, 0xdd, 0x52, 0x17, 0x6f, 0x0d, 0x34,
-	0xfc, 0x46, 0x84, 0x47, 0x2c, 0xc1, 0x1d, 0x15, 0x6e, 0x6f, 0x2e, 0x5e, 0x95, 0x93, 0x0a, 0x44,
-	0xb8, 0x9e, 0xd7, 0x8f, 0x0a, 0xfa, 0xe9, 0xbc, 0x5e, 0xc8, 0x42, 0x07, 0x24, 0x57, 0xbb, 0xd4,
-	0x39, 0x1c, 0x63, 0x1a, 0x10, 0x1a, 0x6e, 0x56, 0xbd, 0xfe, 0xdf, 0x55, 0x9f, 0xe8, 0x55, 0x0f,
-	0x74, 0xec, 0x6a, 0xc9, 0x23, 0xb9, 0x64, 0xae, 0x0d, 0xd1, 0x5f, 0xb6, 0xd6, 0xeb, 0xd9, 0xa5,
-	0x6f, 0x2d, 0x2e, 0x7d, 0x6b, 0xb6, 0xf4, 0xed, 0xc5, 0xd2, 0xb7, 0xbf, 0xae, 0x7c, 0xeb, 0xfb,
-	0xca, 0xb7, 0x17, 0x2b, 0xdf, 0xfa, 0xb5, 0xf2, 0xad, 0x4f, 0x0f, 0x43, 0x92, 0x0c, 0x26, 0xbd,
-	0x66, 0x9f, 0x8d, 0x8e, 0xe3, 0x2f, 0xb4, 0x9f, 0x0c, 0x08, 0x0d, 0x33, 0x4f, 0x57, 0x5f, 0x68,
-	0xaf, 0x2c, 0x3f, 0xc7, 0xa7, 0x7f, 0x02, 0x00, 0x00, 0xff, 0xff, 0xcb, 0xcf, 0x98, 0x86, 0x91,
-	0x04, 0x00, 0x00,
+	// 575 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x93, 0x4d, 0x8b, 0xd3, 0x5e,
+	0x14, 0xc6, 0x93, 0x7f, 0x3b, 0xed, 0x7f, 0x32, 0x6f, 0x10, 0x45, 0x53, 0x5f, 0x72, 0x6b, 0xa8,
+	0x52, 0x65, 0xec, 0xc0, 0xb8, 0x11, 0x77, 0xd6, 0xe2, 0x58, 0x14, 0x1c, 0x02, 0x23, 0xea, 0x46,
+	0xda, 0xe6, 0x4e, 0x7a, 0xa1, 0xcd, 0x0d, 0xc9, 0x4d, 0x99, 0xf9, 0x08, 0xee, 0xc4, 0x4f, 0xe0,
+	0xd6, 0x6f, 0xd2, 0x5d, 0xbb, 0x70, 0xe1, 0xea, 0xc2, 0xb4, 0xbb, 0x2c, 0xb3, 0x74, 0x25, 0xf7,
+	0xad, 0x26, 0x10, 0x5d, 0x35, 0xe7, 0x3c, 0xcf, 0xf9, 0x9d, 0xc3, 0xd3, 0xc4, 0xb8, 0x39, 0x41,
+	0xc3, 0xa3, 0x11, 0x0e, 0xce, 0x91, 0x2f, 0x7f, 0x3a, 0x61, 0x84, 0x09, 0x36, 0x6b, 0xa2, 0xba,
+	0xd5, 0xca, 0x19, 0xce, 0xf1, 0xc4, 0x83, 0x91, 0x28, 0x92, 0x68, 0x40, 0x10, 0x0e, 0x84, 0xbb,
+	0xe0, 0xf2, 0xe0, 0x0c, 0x8d, 0x60, 0x99, 0xeb, 0x5e, 0xce, 0xe5, 0x27, 0xa8, 0xcc, 0xe2, 0xe4,
+	0x2c, 0x13, 0x6f, 0x10, 0x96, 0x79, 0xee, 0xe7, 0x3c, 0x38, 0x64, 0x42, 0x5c, 0x66, 0x6b, 0xe4,
+	0x6d, 0xc3, 0x18, 0x46, 0x33, 0xe8, 0x49, 0x69, 0x1b, 0x5e, 0x10, 0xf1, 0xe8, 0xfc, 0xa8, 0x19,
+	0x7b, 0x2f, 0xf2, 0xd3, 0xa6, 0x6b, 0xd4, 0x67, 0x30, 0x8a, 0x11, 0x0e, 0x2c, 0xbd, 0xa9, 0xb7,
+	0xb7, 0xba, 0x4f, 0x53, 0x0a, 0x54, 0x2b, 0xa3, 0xc0, 0xbc, 0x98, 0x4e, 0x9e, 0x39, 0xb2, 0x3e,
+	0x1c, 0x10, 0x12, 0x39, 0xbf, 0x28, 0xa8, 0xa0, 0x80, 0xa4, 0x8b, 0xd6, 0x6e, 0xbe, 0xef, 0xaa,
+	0x29, 0xf3, 0x9d, 0x51, 0x17, 0xe1, 0xc5, 0xd6, 0x7f, 0xcd, 0x4a, 0x7b, 0xe7, 0xf8, 0x76, 0x47,
+	0xa6, 0xfd, 0x92, 0xb7, 0x0b, 0x17, 0x74, 0xc1, 0x9c, 0x02, 0x8d, 0x2d, 0x95, 0x33, 0x19, 0x05,
+	0xbb, 0x7c, 0xa9, 0xa8, 0x1d, 0x57, 0x09, 0x8c, 0x2b, 0xe2, 0x8e, 0xad, 0x4a, 0x91, 0xdb, 0xe3,
+	0xed, 0xbf, 0x70, 0xe5, 0xcc, 0x86, 0x2b, 0x6a, 0xc7, 0x55, 0x82, 0xe9, 0x1a, 0x15, 0x3f, 0x41,
+	0x56, 0xb5, 0xa9, 0xb7, 0x77, 0x8e, 0x2d, 0xc5, 0x3c, 0x39, 0xeb, 0x17, 0x81, 0x0f, 0x18, 0x70,
+	0x45, 0x41, 0xe5, 0xe4, 0xac, 0x9f, 0x52, 0xc0, 0x66, 0x32, 0x0a, 0xb6, 0x39, 0xd3, 0x4f, 0x90,
+	0xf3, 0x75, 0xd9, 0x62, 0x92, 0xcb, 0x04, 0xf3, 0x83, 0x51, 0x65, 0xff, 0xa8, 0xb5, 0xc5, 0xa1,
+	0x0d, 0x05, 0x7d, 0xd3, 0x7b, 0x7e, 0x5a, 0xa4, 0x3e, 0x92, 0xd4, 0x2a, 0x93, 0x52, 0x0a, 0xf8,
+	0x58, 0x46, 0x81, 0xc1, 0xb9, 0xac, 0x60, 0x60, 0xae, 0xba, 0x5c, 0x33, 0xdf, 0x1b, 0x75, 0xf9,
+	0x22, 0x58, 0x35, 0x4e, 0xbf, 0xa3, 0xe8, 0x6f, 0x45, 0xbb, 0xb8, 0xa0, 0xa9, 0x72, 0x90, 0x43,
+	0x19, 0x05, 0x7b, 0x9c, 0x2d, 0x6b, 0xc7, 0x55, 0x8a, 0xf9, 0x5d, 0x37, 0x0e, 0x90, 0x1f, 0xe0,
+	0x08, 0x7a, 0x9f, 0x54, 0xd2, 0x75, 0x9e, 0xf4, 0x8d, 0xcd, 0x0a, 0xf9, 0x6e, 0x89, 0xc4, 0xbb,
+	0x63, 0x09, 0xbf, 0x1e, 0xc1, 0x29, 0x26, 0xb0, 0x2f, 0x86, 0x7b, 0x9b, 0xc4, 0x1b, 0x7c, 0x53,
+	0x89, 0xe8, 0xa4, 0x8b, 0xd6, 0xb5, 0x92, 0x7e, 0xb6, 0x68, 0x95, 0xb2, 0xdc, 0x7d, 0x54, 0xa8,
+	0xcd, 0xcf, 0xba, 0x71, 0x10, 0xc2, 0xc0, 0x43, 0x81, 0xbf, 0xb9, 0xf5, 0xff, 0x7f, 0xde, 0xfa,
+	0x4a, 0x26, 0x6d, 0xf5, 0x60, 0x18, 0xc1, 0xd1, 0x80, 0x40, 0xef, 0x54, 0x00, 0x24, 0x33, 0xa5,
+	0x40, 0x7f, 0x9c, 0x51, 0x70, 0x97, 0x1f, 0x1d, 0xe6, 0xb5, 0x43, 0x3c, 0x45, 0x04, 0x4e, 0x43,
+	0x72, 0xe9, 0x58, 0xba, 0xbb, 0x5f, 0xd0, 0xe2, 0xee, 0xeb, 0xf9, 0x95, 0xad, 0x2d, 0xaf, 0x6c,
+	0x6d, 0xbe, 0xb2, 0xf5, 0xe5, 0xca, 0xd6, 0xbf, 0xac, 0x6d, 0xed, 0xdb, 0xda, 0xd6, 0x97, 0x6b,
+	0x5b, 0xfb, 0xb9, 0xb6, 0xb5, 0x8f, 0x0f, 0x7d, 0x44, 0xc6, 0xc9, 0xb0, 0x33, 0xc2, 0xd3, 0xa3,
+	0xf8, 0x32, 0x18, 0x91, 0x31, 0x0a, 0xfc, 0xdc, 0xd3, 0x9f, 0xaf, 0x77, 0x58, 0xe3, 0x9f, 0xea,
+	0x93, 0xdf, 0x01, 0x00, 0x00, 0xff, 0xff, 0x97, 0x39, 0xe5, 0x72, 0xad, 0x04, 0x00, 0x00,
 }
 
 func (m *Configuration) Marshal() (dAtA []byte, err error) {
@@ -132,10 +133,10 @@ func (m *Configuration) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 	_ = i
 	var l int
 	_ = l
-	if len(m.PendingDevices) > 0 {
-		for iNdEx := len(m.PendingDevices) - 1; iNdEx >= 0; iNdEx-- {
+	if len(m.DeprecatedPendingDevices) > 0 {
+		for iNdEx := len(m.DeprecatedPendingDevices) - 1; iNdEx >= 0; iNdEx-- {
 			{
-				size, err := m.PendingDevices[iNdEx].MarshalToSizedBuffer(dAtA[:i])
+				size, err := m.DeprecatedPendingDevices[iNdEx].MarshalToSizedBuffer(dAtA[:i])
 				if err != nil {
 					return 0, err
 				}
@@ -270,8 +271,8 @@ func (m *Configuration) ProtoSize() (n int) {
 			n += 1 + l + sovConfig(uint64(l))
 		}
 	}
-	if len(m.PendingDevices) > 0 {
-		for _, e := range m.PendingDevices {
+	if len(m.DeprecatedPendingDevices) > 0 {
+		for _, e := range m.DeprecatedPendingDevices {
 			l = e.ProtoSize()
 			n += 1 + l + sovConfig(uint64(l))
 		}
@@ -536,7 +537,7 @@ func (m *Configuration) Unmarshal(dAtA []byte) error {
 			iNdEx = postIndex
 		case 8:
 			if wireType != 2 {
-				return fmt.Errorf("proto: wrong wireType = %d for field PendingDevices", wireType)
+				return fmt.Errorf("proto: wrong wireType = %d for field DeprecatedPendingDevices", wireType)
 			}
 			var msglen int
 			for shift := uint(0); ; shift += 7 {
@@ -563,8 +564,8 @@ func (m *Configuration) Unmarshal(dAtA []byte) error {
 			if postIndex > l {
 				return io.ErrUnexpectedEOF
 			}
-			m.PendingDevices = append(m.PendingDevices, ObservedDevice{})
-			if err := m.PendingDevices[len(m.PendingDevices)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
+			m.DeprecatedPendingDevices = append(m.DeprecatedPendingDevices, ObservedDevice{})
+			if err := m.DeprecatedPendingDevices[len(m.DeprecatedPendingDevices)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
 				return err
 			}
 			iNdEx = postIndex

+ 0 - 18
lib/config/config_test.go

@@ -142,7 +142,6 @@ func TestDeviceConfig(t *testing.T) {
 				Compression:     protocol.CompressionMetadata,
 				AllowedNetworks: []string{},
 				IgnoredFolders:  []ObservedFolder{},
-				PendingFolders:  []ObservedFolder{},
 			},
 			{
 				DeviceID:        device4,
@@ -151,7 +150,6 @@ func TestDeviceConfig(t *testing.T) {
 				Compression:     protocol.CompressionMetadata,
 				AllowedNetworks: []string{},
 				IgnoredFolders:  []ObservedFolder{},
-				PendingFolders:  []ObservedFolder{},
 			},
 		}
 		expectedDeviceIDs := []protocol.DeviceID{device1, device4}
@@ -248,21 +246,18 @@ func TestDeviceAddressesDynamic(t *testing.T) {
 			Addresses:       []string{"dynamic"},
 			AllowedNetworks: []string{},
 			IgnoredFolders:  []ObservedFolder{},
-			PendingFolders:  []ObservedFolder{},
 		},
 		device2: {
 			DeviceID:        device2,
 			Addresses:       []string{"dynamic"},
 			AllowedNetworks: []string{},
 			IgnoredFolders:  []ObservedFolder{},
-			PendingFolders:  []ObservedFolder{},
 		},
 		device3: {
 			DeviceID:        device3,
 			Addresses:       []string{"dynamic"},
 			AllowedNetworks: []string{},
 			IgnoredFolders:  []ObservedFolder{},
-			PendingFolders:  []ObservedFolder{},
 		},
 		device4: {
 			DeviceID:        device4,
@@ -271,7 +266,6 @@ func TestDeviceAddressesDynamic(t *testing.T) {
 			Compression:     protocol.CompressionMetadata,
 			AllowedNetworks: []string{},
 			IgnoredFolders:  []ObservedFolder{},
-			PendingFolders:  []ObservedFolder{},
 		},
 	}
 
@@ -295,7 +289,6 @@ func TestDeviceCompression(t *testing.T) {
 			Compression:     protocol.CompressionMetadata,
 			AllowedNetworks: []string{},
 			IgnoredFolders:  []ObservedFolder{},
-			PendingFolders:  []ObservedFolder{},
 		},
 		device2: {
 			DeviceID:        device2,
@@ -303,7 +296,6 @@ func TestDeviceCompression(t *testing.T) {
 			Compression:     protocol.CompressionMetadata,
 			AllowedNetworks: []string{},
 			IgnoredFolders:  []ObservedFolder{},
-			PendingFolders:  []ObservedFolder{},
 		},
 		device3: {
 			DeviceID:        device3,
@@ -311,7 +303,6 @@ func TestDeviceCompression(t *testing.T) {
 			Compression:     protocol.CompressionNever,
 			AllowedNetworks: []string{},
 			IgnoredFolders:  []ObservedFolder{},
-			PendingFolders:  []ObservedFolder{},
 		},
 		device4: {
 			DeviceID:        device4,
@@ -320,7 +311,6 @@ func TestDeviceCompression(t *testing.T) {
 			Compression:     protocol.CompressionMetadata,
 			AllowedNetworks: []string{},
 			IgnoredFolders:  []ObservedFolder{},
-			PendingFolders:  []ObservedFolder{},
 		},
 	}
 
@@ -343,21 +333,18 @@ func TestDeviceAddressesStatic(t *testing.T) {
 			Addresses:       []string{"tcp://192.0.2.1", "tcp://192.0.2.2"},
 			AllowedNetworks: []string{},
 			IgnoredFolders:  []ObservedFolder{},
-			PendingFolders:  []ObservedFolder{},
 		},
 		device2: {
 			DeviceID:        device2,
 			Addresses:       []string{"tcp://192.0.2.3:6070", "tcp://[2001:db8::42]:4242"},
 			AllowedNetworks: []string{},
 			IgnoredFolders:  []ObservedFolder{},
-			PendingFolders:  []ObservedFolder{},
 		},
 		device3: {
 			DeviceID:        device3,
 			Addresses:       []string{"tcp://[2001:db8::44]:4444", "tcp://192.0.2.4:6090"},
 			AllowedNetworks: []string{},
 			IgnoredFolders:  []ObservedFolder{},
-			PendingFolders:  []ObservedFolder{},
 		},
 		device4: {
 			DeviceID:        device4,
@@ -366,7 +353,6 @@ func TestDeviceAddressesStatic(t *testing.T) {
 			Compression:     protocol.CompressionMetadata,
 			AllowedNetworks: []string{},
 			IgnoredFolders:  []ObservedFolder{},
-			PendingFolders:  []ObservedFolder{},
 		},
 	}
 
@@ -1097,10 +1083,6 @@ func TestDeviceConfigObservedNotNil(t *testing.T) {
 		if dev.IgnoredFolders == nil {
 			t.Errorf("Ignored folders nil")
 		}
-
-		if dev.PendingFolders == nil {
-			t.Errorf("Pending folders nil")
-		}
 	}
 }
 

+ 0 - 9
lib/config/deviceconfiguration.go

@@ -33,8 +33,6 @@ func (cfg DeviceConfiguration) Copy() DeviceConfiguration {
 	copy(c.AllowedNetworks, cfg.AllowedNetworks)
 	c.IgnoredFolders = make([]ObservedFolder, len(cfg.IgnoredFolders))
 	copy(c.IgnoredFolders, cfg.IgnoredFolders)
-	c.PendingFolders = make([]ObservedFolder, len(cfg.PendingFolders))
-	copy(c.PendingFolders, cfg.PendingFolders)
 	return c
 }
 
@@ -47,19 +45,12 @@ func (cfg *DeviceConfiguration) prepare(sharedFolders []string) {
 	}
 
 	ignoredFolders := deduplicateObservedFoldersToMap(cfg.IgnoredFolders)
-	pendingFolders := deduplicateObservedFoldersToMap(cfg.PendingFolders)
-
-	for id := range ignoredFolders {
-		delete(pendingFolders, id)
-	}
 
 	for _, sharedFolder := range sharedFolders {
 		delete(ignoredFolders, sharedFolder)
-		delete(pendingFolders, sharedFolder)
 	}
 
 	cfg.IgnoredFolders = sortedObservedFolderSlice(ignoredFolders)
-	cfg.PendingFolders = sortedObservedFolderSlice(pendingFolders)
 }
 
 func (cfg *DeviceConfiguration) IgnoredFolder(folder string) bool {

+ 74 - 72
lib/config/deviceconfiguration.pb.go

@@ -40,7 +40,7 @@ type DeviceConfiguration struct {
 	MaxSendKbps              int                                                  `protobuf:"varint,12,opt,name=max_send_kbps,json=maxSendKbps,proto3,casttype=int" json:"maxSendKbps" xml:"maxSendKbps"`
 	MaxRecvKbps              int                                                  `protobuf:"varint,13,opt,name=max_recv_kbps,json=maxRecvKbps,proto3,casttype=int" json:"maxRecvKbps" xml:"maxRecvKbps"`
 	IgnoredFolders           []ObservedFolder                                     `protobuf:"bytes,14,rep,name=ignored_folders,json=ignoredFolders,proto3" json:"ignoredFolders" xml:"ignoredFolder"`
-	PendingFolders           []ObservedFolder                                     `protobuf:"bytes,15,rep,name=pending_folders,json=pendingFolders,proto3" json:"pendingFolders" xml:"pendingFolder"`
+	DeprecatedPendingFolders []ObservedFolder                                     `protobuf:"bytes,15,rep,name=pending_folders,json=pendingFolders,proto3" json:"-" xml:"pendingFolder,omitempty"` // Deprecated: Do not use.
 	MaxRequestKiB            int                                                  `protobuf:"varint,16,opt,name=max_request_kib,json=maxRequestKib,proto3,casttype=int" json:"maxRequestKiB" xml:"maxRequestKiB"`
 	Untrusted                bool                                                 `protobuf:"varint,17,opt,name=untrusted,proto3" json:"untrusted" xml:"untrusted"`
 	RemoteGUIPort            int                                                  `protobuf:"varint,18,opt,name=remote_gui_port,json=remoteGuiPort,proto3,casttype=int" json:"remoteGUIPort" xml:"remoteGUIPort"`
@@ -88,69 +88,71 @@ func init() {
 }
 
 var fileDescriptor_744b782bd13071dd = []byte{
-	// 980 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0x55, 0x31, 0x6f, 0xdb, 0x46,
-	0x18, 0x15, 0xeb, 0xc4, 0xb6, 0x68, 0xcb, 0xb2, 0x68, 0xc4, 0x61, 0x0c, 0x44, 0x27, 0xb0, 0x1a,
-	0x14, 0x34, 0x95, 0x0b, 0xb7, 0x93, 0xd1, 0x16, 0x28, 0x13, 0xb4, 0x35, 0x8c, 0x26, 0xe9, 0x15,
-	0x5d, 0xbc, 0xb0, 0x24, 0xef, 0xac, 0x1c, 0x2c, 0xf2, 0x58, 0xf2, 0xa8, 0x48, 0x40, 0x87, 0x8e,
-	0x1d, 0x3a, 0x14, 0x59, 0xbb, 0x14, 0x1d, 0x0a, 0xb4, 0xbf, 0xc4, 0x9b, 0x35, 0x16, 0x1d, 0x0e,
-	0x88, 0xbd, 0x71, 0xe4, 0x98, 0xa9, 0xb8, 0x23, 0x45, 0x91, 0x74, 0x5c, 0x04, 0xe8, 0x76, 0xf7,
-	0xde, 0xbb, 0xf7, 0xee, 0xfb, 0xf4, 0x9d, 0xa8, 0xf6, 0xc7, 0xc4, 0xd9, 0x77, 0xa9, 0x7f, 0x4a,
-	0x46, 0xfb, 0x08, 0x4f, 0x88, 0x8b, 0xb3, 0x4d, 0x1c, 0xda, 0x8c, 0x50, 0x7f, 0x18, 0x84, 0x94,
-	0x51, 0x6d, 0x35, 0x03, 0xf7, 0x76, 0x85, 0x5a, 0x42, 0x2e, 0x1d, 0xef, 0x3b, 0x38, 0xc8, 0xf8,
-	0xbd, 0x7b, 0x25, 0x17, 0xea, 0x44, 0x38, 0x9c, 0x60, 0x94, 0x53, 0x4d, 0x3c, 0x65, 0xd9, 0xd2,
-	0xf8, 0x73, 0x5b, 0xdd, 0x79, 0x2c, 0x33, 0x1e, 0x95, 0x33, 0xb4, 0xbf, 0x14, 0xb5, 0x99, 0x65,
-	0x5b, 0x04, 0xe9, 0x4a, 0x4f, 0x19, 0x6c, 0x9a, 0x3f, 0x2b, 0xe7, 0x1c, 0x34, 0xfe, 0xe1, 0xe0,
-	0xa3, 0x11, 0x61, 0xcf, 0x63, 0x67, 0xe8, 0x52, 0x6f, 0x3f, 0x9a, 0xf9, 0x2e, 0x7b, 0x4e, 0xfc,
-	0x51, 0x69, 0x55, 0xbe, 0xd1, 0x30, 0x73, 0x3f, 0x7a, 0x7c, 0xc9, 0xc1, 0xfa, 0x62, 0x9d, 0x70,
-	0xb0, 0x8e, 0xf2, 0x75, 0xca, 0x41, 0x6b, 0xea, 0x8d, 0x0f, 0x0d, 0x82, 0x1e, 0xda, 0x8c, 0x85,
-	0x46, 0x72, 0xd1, 0x5f, 0xcb, 0xd7, 0xe9, 0x45, 0xbf, 0xd0, 0xfd, 0x34, 0xef, 0x2b, 0x2f, 0xe7,
-	0xfd, 0xc2, 0x03, 0x2e, 0x18, 0xa4, 0x3d, 0x53, 0x6f, 0xf9, 0xb6, 0x87, 0xf5, 0x77, 0x7a, 0xca,
-	0xa0, 0x69, 0x7e, 0x9c, 0x70, 0x20, 0xf7, 0x29, 0x07, 0xf7, 0xa4, 0xb3, 0xd8, 0x48, 0xbf, 0x87,
-	0xd4, 0x23, 0x0c, 0x7b, 0x01, 0x9b, 0x89, 0x94, 0x9d, 0x37, 0xe0, 0x50, 0x9e, 0xd4, 0xa6, 0x6a,
-	0xd3, 0x46, 0x28, 0xc4, 0x51, 0x84, 0x23, 0x7d, 0xa5, 0xb7, 0x32, 0x68, 0x9a, 0x27, 0x09, 0x07,
-	0x4b, 0x30, 0xe5, 0xe0, 0x81, 0xf4, 0xce, 0x91, 0x92, 0x73, 0x0f, 0xe1, 0x53, 0x3b, 0x1e, 0xb3,
-	0x43, 0x03, 0xcd, 0x7c, 0xdb, 0x23, 0xae, 0xc8, 0xea, 0x5c, 0xd3, 0xbd, 0xbe, 0xe8, 0xaf, 0xe5,
-	0x02, 0xb8, 0xf4, 0xd5, 0x26, 0xea, 0x86, 0x4b, 0xbd, 0x40, 0xec, 0x08, 0xf5, 0xf5, 0x5b, 0x3d,
-	0x65, 0xb0, 0x75, 0x70, 0x67, 0x58, 0xb4, 0xf3, 0xd1, 0x92, 0x34, 0x3f, 0x49, 0x38, 0x28, 0xab,
-	0x53, 0x0e, 0x76, 0xe5, 0xa5, 0x4a, 0x58, 0xd1, 0xd3, 0xed, 0x3a, 0x08, 0xcb, 0x47, 0x35, 0xac,
-	0x36, 0x5d, 0x1c, 0x32, 0x4b, 0x36, 0xf2, 0xb6, 0x6c, 0xe4, 0x97, 0xe2, 0x67, 0x12, 0xe0, 0x93,
-	0xac, 0x99, 0xf7, 0x33, 0xef, 0x1c, 0x78, 0x43, 0x43, 0xef, 0xde, 0xc0, 0xc1, 0xc2, 0x45, 0x3b,
-	0x51, 0x55, 0xe2, 0xb3, 0x90, 0xa2, 0xd8, 0xc5, 0xa1, 0xbe, 0xda, 0x53, 0x06, 0xeb, 0xe6, 0x61,
-	0xc2, 0x41, 0x09, 0x4d, 0x39, 0xb8, 0x93, 0x0d, 0x44, 0x01, 0x15, 0x45, 0xb4, 0x6b, 0x18, 0x2c,
-	0x9d, 0xd3, 0x7e, 0x57, 0xd4, 0xbd, 0xe8, 0x8c, 0x04, 0xd6, 0x02, 0x13, 0x93, 0x6c, 0x85, 0xd8,
-	0xa3, 0x13, 0x7b, 0x1c, 0xe9, 0x6b, 0x32, 0x0c, 0x25, 0x1c, 0xe8, 0x42, 0x75, 0x54, 0x12, 0xc1,
-	0x5c, 0x93, 0x72, 0xf0, 0xae, 0x8c, 0xbe, 0x49, 0x50, 0x5c, 0xe4, 0xfe, 0x7f, 0x2a, 0xe0, 0x8d,
-	0x09, 0xda, 0x1f, 0x8a, 0xda, 0x2a, 0xee, 0x8c, 0x2c, 0x67, 0xa6, 0xaf, 0xcb, 0xc7, 0xf5, 0xe3,
-	0xff, 0x7a, 0x5c, 0x09, 0x07, 0x9b, 0x4b, 0x57, 0x73, 0x96, 0x72, 0x70, 0xb7, 0xda, 0x43, 0x64,
-	0xce, 0x8a, 0xcb, 0x77, 0xae, 0xa1, 0xe2, 0x71, 0xc1, 0x8a, 0x83, 0x76, 0xa0, 0xae, 0x06, 0x76,
-	0x1c, 0x61, 0xa4, 0x37, 0x65, 0xe3, 0xf6, 0x12, 0x0e, 0x72, 0x24, 0xe5, 0x60, 0x53, 0xba, 0x67,
-	0x5b, 0x03, 0xe6, 0xb8, 0xf6, 0x83, 0xba, 0x6d, 0x8f, 0xc7, 0xf4, 0x05, 0x46, 0x96, 0x8f, 0xd9,
-	0x0b, 0x1a, 0x9e, 0x45, 0xba, 0x2a, 0x5f, 0xcf, 0xd7, 0x09, 0x07, 0xed, 0x9c, 0x7b, 0x92, 0x53,
-	0x29, 0x07, 0xdd, 0xec, 0x0d, 0x55, 0xf0, 0xea, 0x4c, 0xe9, 0x37, 0x91, 0xb0, 0x6e, 0xa7, 0x7d,
-	0xa7, 0xee, 0xd8, 0x31, 0xa3, 0x96, 0xed, 0xba, 0x38, 0x60, 0xd6, 0x29, 0x1d, 0x23, 0x1c, 0x46,
-	0xfa, 0x86, 0xbc, 0xfe, 0x07, 0x09, 0x07, 0x1d, 0x41, 0x7f, 0x26, 0xd9, 0xcf, 0x33, 0xb2, 0xe8,
-	0xd3, 0x35, 0xc6, 0x80, 0xd7, 0xd5, 0xda, 0x53, 0xb5, 0xe5, 0xd9, 0x53, 0x2b, 0xc2, 0x3e, 0xb2,
-	0xce, 0x9c, 0x20, 0xd2, 0x37, 0x7b, 0xca, 0xe0, 0xb6, 0xf9, 0x9e, 0x78, 0x87, 0x9e, 0x3d, 0xfd,
-	0x06, 0xfb, 0xe8, 0xd8, 0x09, 0x84, 0x6b, 0x47, 0xba, 0x96, 0x30, 0xe3, 0x35, 0x07, 0x2b, 0xc4,
-	0x67, 0xb0, 0x2c, 0x5c, 0x18, 0x86, 0xd8, 0x9d, 0x64, 0x86, 0xad, 0x8a, 0x21, 0xc4, 0xee, 0xa4,
-	0x6e, 0xb8, 0xc0, 0x2a, 0x86, 0x0b, 0x50, 0xf3, 0xd5, 0x36, 0x19, 0xf9, 0x34, 0xc4, 0xa8, 0xa8,
-	0x7f, 0xab, 0xb7, 0x32, 0xd8, 0x38, 0xd8, 0x1d, 0x66, 0xdf, 0x82, 0xe1, 0xd3, 0xfc, 0x5b, 0x90,
-	0xd5, 0x64, 0xbe, 0x2f, 0xc6, 0x2e, 0xe1, 0x60, 0x2b, 0x3f, 0xb6, 0x6c, 0xcc, 0x4e, 0x36, 0x40,
-	0x65, 0xd8, 0x80, 0x35, 0x99, 0xc8, 0x0b, 0xb0, 0x8f, 0x88, 0x3f, 0x2a, 0xf2, 0xda, 0x6f, 0x97,
-	0x97, 0x1f, 0xab, 0xe7, 0x55, 0x60, 0x03, 0xd6, 0x64, 0xda, 0xaf, 0x8a, 0xda, 0xce, 0x3a, 0xf6,
-	0x7d, 0x8c, 0x23, 0x66, 0x9d, 0x11, 0x47, 0xdf, 0x96, 0x3d, 0x8b, 0x2e, 0x39, 0x68, 0x7d, 0x25,
-	0x5a, 0x21, 0x99, 0x63, 0x62, 0x26, 0x1c, 0xb4, 0xbc, 0x32, 0x50, 0x84, 0x54, 0xd0, 0x45, 0x23,
-	0x93, 0x8b, 0x7e, 0x4d, 0x5e, 0x07, 0x5e, 0xce, 0xfb, 0xd5, 0x04, 0x58, 0xe1, 0x1d, 0xed, 0x53,
-	0xb5, 0x19, 0xfb, 0x2c, 0x8c, 0x23, 0x86, 0x91, 0xde, 0x91, 0x73, 0xd7, 0x13, 0x9f, 0x8d, 0x02,
-	0x4c, 0x39, 0x68, 0xcb, 0x1b, 0x14, 0x88, 0x01, 0x97, 0xac, 0xac, 0x4e, 0xfc, 0x5f, 0x31, 0x6c,
-	0x8d, 0x62, 0x62, 0x05, 0x34, 0x64, 0xba, 0xb6, 0xac, 0x0e, 0x4a, 0xea, 0x8b, 0x6f, 0x8f, 0x9e,
-	0xd1, 0x90, 0x89, 0xea, 0xc2, 0x32, 0x50, 0x54, 0x57, 0x41, 0xcb, 0xd5, 0x55, 0xe5, 0x75, 0x40,
-	0x54, 0x57, 0x49, 0x80, 0x0b, 0x3e, 0x26, 0x62, 0x6b, 0x1e, 0x9f, 0xbf, 0xea, 0x36, 0xe6, 0xaf,
-	0xba, 0x8d, 0xf3, 0xcb, 0xae, 0x32, 0xbf, 0xec, 0x2a, 0xbf, 0x5c, 0x75, 0x1b, 0xbf, 0x5d, 0x75,
-	0x95, 0xf9, 0x55, 0xb7, 0xf1, 0xf7, 0x55, 0xb7, 0x71, 0xf2, 0xe0, 0x2d, 0xfe, 0xbb, 0xb2, 0xb1,
-	0x70, 0x56, 0xe5, 0x7f, 0xd8, 0x87, 0xff, 0x06, 0x00, 0x00, 0xff, 0xff, 0xe9, 0x0e, 0x8a, 0x12,
-	0xed, 0x08, 0x00, 0x00,
+	// 1009 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xa4, 0x55, 0xbd, 0x6f, 0xdb, 0x46,
+	0x1c, 0x15, 0xeb, 0xc4, 0xb6, 0xce, 0x96, 0x65, 0xd3, 0x88, 0xc3, 0x18, 0x88, 0x4e, 0x50, 0x35,
+	0x28, 0x68, 0x22, 0x17, 0x6e, 0x27, 0xa3, 0x2d, 0x50, 0xc6, 0x68, 0x63, 0x18, 0x4d, 0xdc, 0x2b,
+	0xba, 0x78, 0x61, 0x49, 0xde, 0x59, 0x39, 0x58, 0xfc, 0x28, 0x79, 0x54, 0x2c, 0xa0, 0x43, 0xc7,
+	0x16, 0xe8, 0x50, 0x64, 0xed, 0x52, 0x74, 0xe8, 0xd0, 0xff, 0xa3, 0x80, 0x37, 0x6b, 0x2c, 0x3a,
+	0x1c, 0x10, 0x7b, 0xe3, 0xc8, 0x31, 0x53, 0x71, 0x47, 0x8a, 0x3a, 0xca, 0x71, 0x50, 0x20, 0xdb,
+	0xdd, 0x7b, 0xef, 0xde, 0xef, 0x83, 0xbf, 0x3b, 0x82, 0xee, 0x90, 0x3a, 0x3b, 0x6e, 0xe0, 0x9f,
+	0xd0, 0xc1, 0x0e, 0x26, 0x23, 0xea, 0x92, 0x7c, 0x93, 0x44, 0x36, 0xa3, 0x81, 0xdf, 0x0f, 0xa3,
+	0x80, 0x05, 0xfa, 0x62, 0x0e, 0x6e, 0x6f, 0x09, 0xb5, 0x84, 0xdc, 0x60, 0xb8, 0xe3, 0x90, 0x30,
+	0xe7, 0xb7, 0xef, 0x29, 0x2e, 0x81, 0x13, 0x93, 0x68, 0x44, 0x70, 0x41, 0xd5, 0xc9, 0x19, 0xcb,
+	0x97, 0x9d, 0xbf, 0xd7, 0xc1, 0xe6, 0xbe, 0x8c, 0xf1, 0x58, 0x8d, 0xa1, 0xff, 0xa5, 0x81, 0x7a,
+	0x1e, 0xdb, 0xa2, 0xd8, 0xd0, 0xda, 0x5a, 0x6f, 0xd5, 0xfc, 0x45, 0x3b, 0xe7, 0xb0, 0xf6, 0x2f,
+	0x87, 0x1f, 0x0f, 0x28, 0x7b, 0x9e, 0x38, 0x7d, 0x37, 0xf0, 0x76, 0xe2, 0xb1, 0xef, 0xb2, 0xe7,
+	0xd4, 0x1f, 0x28, 0x2b, 0x35, 0xa3, 0x7e, 0xee, 0x7e, 0xb0, 0x7f, 0xc9, 0xe1, 0xf2, 0x74, 0x9d,
+	0x72, 0xb8, 0x8c, 0x8b, 0x75, 0xc6, 0x61, 0xe3, 0xcc, 0x1b, 0xee, 0x75, 0x28, 0x7e, 0x68, 0x33,
+	0x16, 0x75, 0xd2, 0x8b, 0xee, 0x52, 0xb1, 0xce, 0x2e, 0xba, 0xa5, 0xee, 0xa7, 0x49, 0x57, 0x7b,
+	0x39, 0xe9, 0x96, 0x1e, 0x68, 0xca, 0x60, 0xfd, 0x08, 0xdc, 0xf2, 0x6d, 0x8f, 0x18, 0xef, 0xb5,
+	0xb5, 0x5e, 0xdd, 0xfc, 0x24, 0xe5, 0x50, 0xee, 0x33, 0x0e, 0xef, 0x49, 0x67, 0xb1, 0x91, 0x7e,
+	0x0f, 0x03, 0x8f, 0x32, 0xe2, 0x85, 0x6c, 0x2c, 0xa2, 0x6c, 0xbe, 0x01, 0x47, 0xf2, 0xa4, 0x7e,
+	0x06, 0xea, 0x36, 0xc6, 0x11, 0x89, 0x63, 0x12, 0x1b, 0x0b, 0xed, 0x85, 0x5e, 0xdd, 0x3c, 0x4e,
+	0x39, 0x9c, 0x81, 0x19, 0x87, 0x0f, 0xa4, 0x77, 0x81, 0x28, 0xce, 0x6d, 0x4c, 0x4e, 0xec, 0x64,
+	0xc8, 0xf6, 0x3a, 0x78, 0xec, 0xdb, 0x1e, 0x75, 0x45, 0xac, 0x8d, 0x6b, 0xba, 0xd7, 0x17, 0xdd,
+	0xa5, 0x42, 0x80, 0x66, 0xbe, 0xfa, 0x08, 0xac, 0xb8, 0x81, 0x17, 0x8a, 0x1d, 0x0d, 0x7c, 0xe3,
+	0x56, 0x5b, 0xeb, 0xad, 0xed, 0xde, 0xe9, 0x97, 0xed, 0x7c, 0x3c, 0x23, 0xcd, 0x4f, 0x53, 0x0e,
+	0x55, 0x75, 0xc6, 0xe1, 0x96, 0x4c, 0x4a, 0xc1, 0xca, 0x9e, 0xae, 0xcf, 0x83, 0x48, 0x3d, 0xaa,
+	0x13, 0x50, 0x77, 0x49, 0xc4, 0x2c, 0xd9, 0xc8, 0xdb, 0xb2, 0x91, 0x4f, 0xc4, 0x67, 0x12, 0xe0,
+	0xd3, 0xbc, 0x99, 0xf7, 0x73, 0xef, 0x02, 0x78, 0x43, 0x43, 0xef, 0xde, 0xc0, 0xa1, 0xd2, 0x45,
+	0x3f, 0x06, 0x80, 0xfa, 0x2c, 0x0a, 0x70, 0xe2, 0x92, 0xc8, 0x58, 0x6c, 0x6b, 0xbd, 0x65, 0x73,
+	0x2f, 0xe5, 0x50, 0x41, 0x33, 0x0e, 0xef, 0xe4, 0x03, 0x51, 0x42, 0x65, 0x11, 0xcd, 0x39, 0x0c,
+	0x29, 0xe7, 0xf4, 0x3f, 0x34, 0xb0, 0x1d, 0x9f, 0xd2, 0xd0, 0x9a, 0x62, 0x62, 0x92, 0xad, 0x88,
+	0x78, 0xc1, 0xc8, 0x1e, 0xc6, 0xc6, 0x92, 0x0c, 0x86, 0x53, 0x0e, 0x0d, 0xa1, 0x3a, 0x50, 0x44,
+	0xa8, 0xd0, 0x64, 0x1c, 0xbe, 0x2f, 0x43, 0xdf, 0x24, 0x28, 0x13, 0xb9, 0xff, 0x56, 0x05, 0xba,
+	0x31, 0x82, 0xfe, 0xa7, 0x06, 0x1a, 0x65, 0xce, 0xd8, 0x72, 0xc6, 0xc6, 0xb2, 0xbc, 0x5c, 0x3f,
+	0xbe, 0xd3, 0xe5, 0x4a, 0x39, 0x5c, 0x9d, 0xb9, 0x9a, 0xe3, 0x8c, 0xc3, 0xbb, 0xd5, 0x1e, 0x62,
+	0x73, 0x5c, 0x26, 0xbf, 0x71, 0x0d, 0x15, 0x97, 0x0b, 0x55, 0x1c, 0xf4, 0x5d, 0xb0, 0x18, 0xda,
+	0x49, 0x4c, 0xb0, 0x51, 0x97, 0x8d, 0xdb, 0x4e, 0x39, 0x2c, 0x90, 0x8c, 0xc3, 0x55, 0xe9, 0x9e,
+	0x6f, 0x3b, 0xa8, 0xc0, 0xf5, 0x1f, 0xc0, 0xba, 0x3d, 0x1c, 0x06, 0x2f, 0x08, 0xb6, 0x7c, 0xc2,
+	0x5e, 0x04, 0xd1, 0x69, 0x6c, 0x00, 0x79, 0x7b, 0xbe, 0x4e, 0x39, 0x6c, 0x16, 0xdc, 0xd3, 0x82,
+	0xca, 0x38, 0x6c, 0xe5, 0x77, 0xa8, 0x82, 0x57, 0x67, 0xca, 0xb8, 0x89, 0x44, 0xf3, 0x76, 0xfa,
+	0x77, 0x60, 0xd3, 0x4e, 0x58, 0x60, 0xd9, 0xae, 0x4b, 0x42, 0x66, 0x9d, 0x04, 0x43, 0x4c, 0xa2,
+	0xd8, 0x58, 0x91, 0xe9, 0x7f, 0x98, 0x72, 0xb8, 0x21, 0xe8, 0xcf, 0x25, 0xfb, 0x45, 0x4e, 0x96,
+	0x7d, 0xba, 0xc6, 0x74, 0xd0, 0x75, 0xb5, 0xfe, 0x0c, 0x34, 0x3c, 0xfb, 0xcc, 0x8a, 0x89, 0x8f,
+	0xad, 0x53, 0x27, 0x8c, 0x8d, 0xd5, 0xb6, 0xd6, 0xbb, 0x6d, 0x7e, 0x20, 0xee, 0xa1, 0x67, 0x9f,
+	0x7d, 0x43, 0x7c, 0x7c, 0xe8, 0x84, 0xc2, 0x75, 0x43, 0xba, 0x2a, 0x58, 0xe7, 0x35, 0x87, 0x0b,
+	0xd4, 0x67, 0x48, 0x15, 0x4e, 0x0d, 0x23, 0xe2, 0x8e, 0x72, 0xc3, 0x46, 0xc5, 0x10, 0x11, 0x77,
+	0x34, 0x6f, 0x38, 0xc5, 0x2a, 0x86, 0x53, 0x50, 0xf7, 0x41, 0x93, 0x0e, 0xfc, 0x20, 0x22, 0xb8,
+	0xac, 0x7f, 0xad, 0xbd, 0xd0, 0x5b, 0xd9, 0xdd, 0xea, 0xe7, 0xff, 0x82, 0xfe, 0xb3, 0xe2, 0x5f,
+	0x90, 0xd7, 0x64, 0x3e, 0x12, 0x63, 0x97, 0x72, 0xb8, 0x56, 0x1c, 0x9b, 0x35, 0x66, 0x33, 0x1f,
+	0x20, 0x15, 0xee, 0xa0, 0x39, 0x99, 0xfe, 0xb3, 0x06, 0x9a, 0x21, 0xf1, 0x31, 0xf5, 0x07, 0x65,
+	0xc0, 0xe6, 0x5b, 0x03, 0x3e, 0x11, 0x01, 0x2f, 0x39, 0x34, 0xf6, 0x49, 0x18, 0x11, 0xd7, 0x66,
+	0x04, 0x1f, 0xe5, 0x06, 0x85, 0x67, 0xca, 0xa1, 0xf6, 0xa8, 0x7c, 0x6e, 0x42, 0x95, 0x53, 0x46,
+	0xc3, 0xd0, 0xd0, 0x5a, 0x85, 0x8b, 0xf5, 0xdf, 0x34, 0xd0, 0xcc, 0xbb, 0xf9, 0x7d, 0x42, 0x62,
+	0x66, 0x9d, 0x52, 0xc7, 0x58, 0x97, 0xfd, 0x8c, 0x2f, 0x39, 0x6c, 0x7c, 0x25, 0xda, 0x24, 0x99,
+	0x43, 0x6a, 0xa6, 0x1c, 0x36, 0x3c, 0x15, 0x28, 0x0b, 0xae, 0xa0, 0xd3, 0x26, 0xa7, 0x17, 0xdd,
+	0x39, 0xf9, 0x3c, 0xf0, 0x72, 0xd2, 0xad, 0x46, 0x40, 0x15, 0xde, 0xd1, 0x3f, 0x03, 0xf5, 0xc4,
+	0x67, 0x51, 0x12, 0x33, 0x82, 0x8d, 0x0d, 0x39, 0x93, 0x6d, 0xf1, 0x4b, 0x29, 0xc1, 0x8c, 0xc3,
+	0xa6, 0xcc, 0xa0, 0x44, 0x3a, 0x68, 0xc6, 0xca, 0xea, 0xc4, 0x5b, 0xc6, 0x88, 0x35, 0x48, 0xa8,
+	0x15, 0x06, 0x11, 0x33, 0xf4, 0x59, 0x75, 0x48, 0x52, 0x5f, 0x7e, 0x7b, 0x70, 0x14, 0x44, 0x4c,
+	0x54, 0x17, 0xa9, 0x40, 0x59, 0x5d, 0x05, 0x55, 0xab, 0xab, 0xca, 0xe7, 0x01, 0x51, 0x5d, 0x25,
+	0x02, 0x9a, 0xf2, 0x09, 0x15, 0x5b, 0xf3, 0xf0, 0xfc, 0x55, 0xab, 0x36, 0x79, 0xd5, 0xaa, 0x9d,
+	0x5f, 0xb6, 0xb4, 0xc9, 0x65, 0x4b, 0xfb, 0xf5, 0xaa, 0x55, 0xfb, 0xfd, 0xaa, 0xa5, 0x4d, 0xae,
+	0x5a, 0xb5, 0x7f, 0xae, 0x5a, 0xb5, 0xe3, 0x07, 0xff, 0xe3, 0x5d, 0xcb, 0x27, 0xc6, 0x59, 0x94,
+	0xef, 0xdb, 0x47, 0xff, 0x05, 0x00, 0x00, 0xff, 0xff, 0x1d, 0x01, 0x84, 0xd4, 0x09, 0x09, 0x00,
+	0x00,
 }
 
 func (m *DeviceConfiguration) Marshal() (dAtA []byte, err error) {
@@ -199,10 +201,10 @@ func (m *DeviceConfiguration) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 		i--
 		dAtA[i] = 0x80
 	}
-	if len(m.PendingFolders) > 0 {
-		for iNdEx := len(m.PendingFolders) - 1; iNdEx >= 0; iNdEx-- {
+	if len(m.DeprecatedPendingFolders) > 0 {
+		for iNdEx := len(m.DeprecatedPendingFolders) - 1; iNdEx >= 0; iNdEx-- {
 			{
-				size, err := m.PendingFolders[iNdEx].MarshalToSizedBuffer(dAtA[:i])
+				size, err := m.DeprecatedPendingFolders[iNdEx].MarshalToSizedBuffer(dAtA[:i])
 				if err != nil {
 					return 0, err
 				}
@@ -405,8 +407,8 @@ func (m *DeviceConfiguration) ProtoSize() (n int) {
 			n += 1 + l + sovDeviceconfiguration(uint64(l))
 		}
 	}
-	if len(m.PendingFolders) > 0 {
-		for _, e := range m.PendingFolders {
+	if len(m.DeprecatedPendingFolders) > 0 {
+		for _, e := range m.DeprecatedPendingFolders {
 			l = e.ProtoSize()
 			n += 1 + l + sovDeviceconfiguration(uint64(l))
 		}
@@ -825,7 +827,7 @@ func (m *DeviceConfiguration) Unmarshal(dAtA []byte) error {
 			iNdEx = postIndex
 		case 15:
 			if wireType != 2 {
-				return fmt.Errorf("proto: wrong wireType = %d for field PendingFolders", wireType)
+				return fmt.Errorf("proto: wrong wireType = %d for field DeprecatedPendingFolders", wireType)
 			}
 			var msglen int
 			for shift := uint(0); ; shift += 7 {
@@ -852,8 +854,8 @@ func (m *DeviceConfiguration) Unmarshal(dAtA []byte) error {
 			if postIndex > l {
 				return io.ErrUnexpectedEOF
 			}
-			m.PendingFolders = append(m.PendingFolders, ObservedFolder{})
-			if err := m.PendingFolders[len(m.PendingFolders)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
+			m.DeprecatedPendingFolders = append(m.DeprecatedPendingFolders, ObservedFolder{})
+			if err := m.DeprecatedPendingFolders[len(m.DeprecatedPendingFolders)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
 				return err
 			}
 			iNdEx = postIndex

+ 10 - 49
lib/config/wrapper.go

@@ -9,7 +9,6 @@ package config
 import (
 	"os"
 	"sync/atomic"
-	"time"
 
 	"github.com/syncthing/syncthing/lib/events"
 	"github.com/syncthing/syncthing/lib/osutil"
@@ -85,8 +84,7 @@ type Wrapper interface {
 	SetDevice(DeviceConfiguration) (Waiter, error)
 	SetDevices([]DeviceConfiguration) (Waiter, error)
 
-	AddOrUpdatePendingDevice(device protocol.DeviceID, name, address string)
-	AddOrUpdatePendingFolder(id, label string, device protocol.DeviceID)
+	IgnoredDevices() []ObservedDevice
 	IgnoredDevice(id protocol.DeviceID) bool
 	IgnoredFolder(device protocol.DeviceID, folder string) bool
 
@@ -428,6 +426,15 @@ func (w *wrapper) IgnoredDevice(id protocol.DeviceID) bool {
 	return false
 }
 
+// IgnoredDevices returns a slice of ignored devices.
+func (w *wrapper) IgnoredDevices() []ObservedDevice {
+	w.mut.Lock()
+	defer w.mut.Unlock()
+	res := make([]ObservedDevice, len(w.cfg.IgnoredDevices))
+	copy(res, w.cfg.IgnoredDevices)
+	return res
+}
+
 // IgnoredFolder returns whether or not share attempts for the given
 // folder should be silently ignored.
 func (w *wrapper) IgnoredFolder(device protocol.DeviceID, folder string) bool {
@@ -495,49 +502,3 @@ func (w *wrapper) RequiresRestart() bool {
 func (w *wrapper) setRequiresRestart() {
 	atomic.StoreUint32(&w.requiresRestart, 1)
 }
-
-func (w *wrapper) AddOrUpdatePendingDevice(device protocol.DeviceID, name, address string) {
-	w.mut.Lock()
-	defer w.mut.Unlock()
-
-	for i := range w.cfg.PendingDevices {
-		if w.cfg.PendingDevices[i].ID == device {
-			w.cfg.PendingDevices[i].Time = time.Now().Round(time.Second)
-			w.cfg.PendingDevices[i].Name = name
-			w.cfg.PendingDevices[i].Address = address
-			return
-		}
-	}
-
-	w.cfg.PendingDevices = append(w.cfg.PendingDevices, ObservedDevice{
-		Time:    time.Now().Round(time.Second),
-		ID:      device,
-		Name:    name,
-		Address: address,
-	})
-}
-
-func (w *wrapper) AddOrUpdatePendingFolder(id, label string, device protocol.DeviceID) {
-	w.mut.Lock()
-	defer w.mut.Unlock()
-
-	for i := range w.cfg.Devices {
-		if w.cfg.Devices[i].DeviceID == device {
-			for j := range w.cfg.Devices[i].PendingFolders {
-				if w.cfg.Devices[i].PendingFolders[j].ID == id {
-					w.cfg.Devices[i].PendingFolders[j].Label = label
-					w.cfg.Devices[i].PendingFolders[j].Time = time.Now().Round(time.Second)
-					return
-				}
-			}
-			w.cfg.Devices[i].PendingFolders = append(w.cfg.Devices[i].PendingFolders, ObservedFolder{
-				Time:  time.Now().Round(time.Second),
-				ID:    id,
-				Label: label,
-			})
-			return
-		}
-	}
-
-	panic("bug: adding pending folder for non-existing device")
-}

+ 49 - 0
lib/db/keyer.go

@@ -68,6 +68,12 @@ const (
 
 	// KeyTypeVersion <version hash> = Vector
 	KeyTypeVersion byte = 15
+
+	// KeyTypePendingFolder <int32 device ID> <folder ID as string> = ObservedFolder
+	KeyTypePendingFolder byte = 16
+
+	// KeyTypePendingDevice <device ID in wire format> = ObservedDevice
+	KeyTypePendingDevice byte = 17
 )
 
 type keyer interface {
@@ -108,6 +114,14 @@ type keyer interface {
 
 	// Version vectors
 	GenerateVersionKey(key []byte, hash []byte) versionKey
+
+	// Pending (unshared) folders and devices
+	GeneratePendingFolderKey(key, device, folder []byte) (pendingFolderKey, error)
+	FolderFromPendingFolderKey(key []byte) []byte
+	DeviceFromPendingFolderKey(key []byte) ([]byte, bool)
+
+	GeneratePendingDeviceKey(key, device []byte) pendingDeviceKey
+	DeviceFromPendingDeviceKey(key []byte) []byte
 }
 
 // defaultKeyer implements our key scheme. It needs folder and device
@@ -341,6 +355,41 @@ func (k versionKey) Hash() []byte {
 	return k[keyPrefixLen:]
 }
 
+type pendingFolderKey []byte
+
+func (k defaultKeyer) GeneratePendingFolderKey(key, device, folder []byte) (pendingFolderKey, error) {
+	deviceID, err := k.deviceIdx.ID(device)
+	if err != nil {
+		return nil, err
+	}
+	key = resize(key, keyPrefixLen+keyDeviceLen+len(folder))
+	key[0] = KeyTypePendingFolder
+	binary.BigEndian.PutUint32(key[keyPrefixLen:], deviceID)
+	copy(key[keyPrefixLen+keyDeviceLen:], folder)
+	return key, nil
+}
+
+func (k defaultKeyer) FolderFromPendingFolderKey(key []byte) []byte {
+	return key[keyPrefixLen+keyDeviceLen:]
+}
+
+func (k defaultKeyer) DeviceFromPendingFolderKey(key []byte) ([]byte, bool) {
+	return k.deviceIdx.Val(binary.BigEndian.Uint32(key[keyPrefixLen:]))
+}
+
+type pendingDeviceKey []byte
+
+func (k defaultKeyer) GeneratePendingDeviceKey(key, device []byte) pendingDeviceKey {
+	key = resize(key, keyPrefixLen+len(device))
+	key[0] = KeyTypePendingDevice
+	copy(key[keyPrefixLen:], device)
+	return key
+}
+
+func (k defaultKeyer) DeviceFromPendingDeviceKey(key []byte) []byte {
+	return key[keyPrefixLen:]
+}
+
 // resize returns a byte slice of the specified size, reusing bs if possible
 func resize(bs []byte, size int) []byte {
 	if cap(bs) < size {

+ 172 - 0
lib/db/observed.go

@@ -0,0 +1,172 @@
+// Copyright (C) 2020 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+package db
+
+import (
+	"time"
+
+	"github.com/syncthing/syncthing/lib/protocol"
+)
+
+func (db *Lowlevel) AddOrUpdatePendingDevice(device protocol.DeviceID, name, address string) error {
+	key := db.keyer.GeneratePendingDeviceKey(nil, device[:])
+	od := ObservedDevice{
+		Time:    time.Now().Round(time.Second),
+		Name:    name,
+		Address: address,
+	}
+	bs, err := od.Marshal()
+	if err != nil {
+		return err
+	}
+	return db.Put(key, bs)
+}
+
+func (db *Lowlevel) RemovePendingDevice(device protocol.DeviceID) {
+	key := db.keyer.GeneratePendingDeviceKey(nil, device[:])
+	if err := db.Delete(key); err != nil {
+		l.Warnf("Failed to remove pending device entry: %v", err)
+	}
+}
+
+// PendingDevices enumerates all entries.  Invalid ones are dropped from the database
+// after a warning log message, as a side-effect.
+func (db *Lowlevel) PendingDevices() (map[protocol.DeviceID]ObservedDevice, error) {
+	iter, err := db.NewPrefixIterator([]byte{KeyTypePendingDevice})
+	if err != nil {
+		return nil, err
+	}
+	defer iter.Release()
+	res := make(map[protocol.DeviceID]ObservedDevice)
+	for iter.Next() {
+		keyDev := db.keyer.DeviceFromPendingDeviceKey(iter.Key())
+		deviceID, err := protocol.DeviceIDFromBytes(keyDev)
+		var od ObservedDevice
+		if err != nil {
+			goto deleteKey
+		}
+		if err = od.Unmarshal(iter.Value()); err != nil {
+			goto deleteKey
+		}
+		res[deviceID] = od
+		continue
+	deleteKey:
+		// Deleting invalid entries is the only possible "repair" measure and
+		// appropriate for the importance of pending entries.  They will come back
+		// soon if still relevant.
+		l.Infof("Invalid pending device entry, deleting from database: %x", iter.Key())
+		if err := db.Delete(iter.Key()); err != nil {
+			return nil, err
+		}
+	}
+	return res, nil
+}
+
+func (db *Lowlevel) AddOrUpdatePendingFolder(id, label string, device protocol.DeviceID) error {
+	key, err := db.keyer.GeneratePendingFolderKey(nil, device[:], []byte(id))
+	if err != nil {
+		return err
+	}
+	of := ObservedFolder{
+		Time:  time.Now().Round(time.Second),
+		Label: label,
+	}
+	bs, err := of.Marshal()
+	if err != nil {
+		return err
+	}
+	return db.Put(key, bs)
+}
+
+// RemovePendingFolderForDevice removes entries for specific folder / device combinations.
+func (db *Lowlevel) RemovePendingFolderForDevice(id string, device protocol.DeviceID) {
+	key, err := db.keyer.GeneratePendingFolderKey(nil, device[:], []byte(id))
+	if err != nil {
+		return
+	}
+	if err := db.Delete(key); err != nil {
+		l.Warnf("Failed to remove pending folder entry: %v", err)
+	}
+}
+
+// RemovePendingFolder removes all entries matching a specific folder ID.
+func (db *Lowlevel) RemovePendingFolder(id string) {
+	iter, err := db.NewPrefixIterator([]byte{KeyTypePendingFolder})
+	if err != nil {
+		l.Infof("Could not iterate through pending folder entries: %v", err)
+		return
+	}
+	defer iter.Release()
+	for iter.Next() {
+		if id != string(db.keyer.FolderFromPendingFolderKey(iter.Key())) {
+			continue
+		}
+		if err := db.Delete(iter.Key()); err != nil {
+			l.Warnf("Failed to remove pending folder entry: %v", err)
+		}
+	}
+}
+
+// Consolidated information about a pending folder
+type PendingFolder struct {
+	OfferedBy map[protocol.DeviceID]ObservedFolder `json:"offeredBy"`
+}
+
+func (db *Lowlevel) PendingFolders() (map[string]PendingFolder, error) {
+	return db.PendingFoldersForDevice(protocol.EmptyDeviceID)
+}
+
+// PendingFoldersForDevice enumerates only entries matching the given device ID, unless it
+// is EmptyDeviceID.  Invalid ones are dropped from the database after a warning log
+// message, as a side-effect.
+func (db *Lowlevel) PendingFoldersForDevice(device protocol.DeviceID) (map[string]PendingFolder, error) {
+	var err error
+	prefixKey := []byte{KeyTypePendingFolder}
+	if device != protocol.EmptyDeviceID {
+		prefixKey, err = db.keyer.GeneratePendingFolderKey(nil, device[:], nil)
+		if err != nil {
+			return nil, err
+		}
+	}
+	iter, err := db.NewPrefixIterator(prefixKey)
+	if err != nil {
+		return nil, err
+	}
+	defer iter.Release()
+	res := make(map[string]PendingFolder)
+	for iter.Next() {
+		keyDev, ok := db.keyer.DeviceFromPendingFolderKey(iter.Key())
+		deviceID, err := protocol.DeviceIDFromBytes(keyDev)
+		var of ObservedFolder
+		var folderID string
+		if !ok || err != nil {
+			goto deleteKey
+		}
+		if folderID = string(db.keyer.FolderFromPendingFolderKey(iter.Key())); len(folderID) < 1 {
+			goto deleteKey
+		}
+		if err = of.Unmarshal(iter.Value()); err != nil {
+			goto deleteKey
+		}
+		if _, ok := res[folderID]; !ok {
+			res[folderID] = PendingFolder{
+				OfferedBy: map[protocol.DeviceID]ObservedFolder{},
+			}
+		}
+		res[folderID].OfferedBy[deviceID] = of
+		continue
+	deleteKey:
+		// Deleting invalid entries is the only possible "repair" measure and
+		// appropriate for the importance of pending entries.  They will come back
+		// soon if still relevant.
+		l.Infof("Invalid pending folder entry, deleting from database: %x", iter.Key())
+		if err := db.Delete(iter.Key()); err != nil {
+			return nil, err
+		}
+	}
+	return res, nil
+}

+ 558 - 82
lib/db/structs.pb.go

@@ -7,18 +7,22 @@ import (
 	fmt "fmt"
 	_ "github.com/gogo/protobuf/gogoproto"
 	proto "github.com/gogo/protobuf/proto"
+	github_com_gogo_protobuf_types "github.com/gogo/protobuf/types"
+	_ "github.com/golang/protobuf/ptypes/timestamp"
 	github_com_syncthing_syncthing_lib_protocol "github.com/syncthing/syncthing/lib/protocol"
 	protocol "github.com/syncthing/syncthing/lib/protocol"
 	_ "github.com/syncthing/syncthing/proto/ext"
 	io "io"
 	math "math"
 	math_bits "math/bits"
+	time "time"
 )
 
 // Reference imports to suppress errors if they are not otherwise used.
 var _ = proto.Marshal
 var _ = fmt.Errorf
 var _ = math.Inf
+var _ = time.Kitchen
 
 // This is a compile-time assertion to ensure that this generated file
 // is compatible with the proto package it is being compiled against.
@@ -395,6 +399,83 @@ func (m *VersionListDeprecated) XXX_DiscardUnknown() {
 
 var xxx_messageInfo_VersionListDeprecated proto.InternalMessageInfo
 
+type ObservedFolder struct {
+	Time  time.Time `protobuf:"bytes,1,opt,name=time,proto3,stdtime" json:"time" xml:"time"`
+	Label string    `protobuf:"bytes,2,opt,name=label,proto3" json:"label" xml:"label"`
+}
+
+func (m *ObservedFolder) Reset()         { *m = ObservedFolder{} }
+func (m *ObservedFolder) String() string { return proto.CompactTextString(m) }
+func (*ObservedFolder) ProtoMessage()    {}
+func (*ObservedFolder) Descriptor() ([]byte, []int) {
+	return fileDescriptor_5465d80e8cba02e3, []int{9}
+}
+func (m *ObservedFolder) XXX_Unmarshal(b []byte) error {
+	return m.Unmarshal(b)
+}
+func (m *ObservedFolder) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	if deterministic {
+		return xxx_messageInfo_ObservedFolder.Marshal(b, m, deterministic)
+	} else {
+		b = b[:cap(b)]
+		n, err := m.MarshalToSizedBuffer(b)
+		if err != nil {
+			return nil, err
+		}
+		return b[:n], nil
+	}
+}
+func (m *ObservedFolder) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_ObservedFolder.Merge(m, src)
+}
+func (m *ObservedFolder) XXX_Size() int {
+	return m.ProtoSize()
+}
+func (m *ObservedFolder) XXX_DiscardUnknown() {
+	xxx_messageInfo_ObservedFolder.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_ObservedFolder proto.InternalMessageInfo
+
+type ObservedDevice struct {
+	Time    time.Time `protobuf:"bytes,1,opt,name=time,proto3,stdtime" json:"time" xml:"time"`
+	Name    string    `protobuf:"bytes,2,opt,name=name,proto3" json:"name" xml:"name"`
+	Address string    `protobuf:"bytes,3,opt,name=address,proto3" json:"address" xml:"address"`
+}
+
+func (m *ObservedDevice) Reset()         { *m = ObservedDevice{} }
+func (m *ObservedDevice) String() string { return proto.CompactTextString(m) }
+func (*ObservedDevice) ProtoMessage()    {}
+func (*ObservedDevice) Descriptor() ([]byte, []int) {
+	return fileDescriptor_5465d80e8cba02e3, []int{10}
+}
+func (m *ObservedDevice) XXX_Unmarshal(b []byte) error {
+	return m.Unmarshal(b)
+}
+func (m *ObservedDevice) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	if deterministic {
+		return xxx_messageInfo_ObservedDevice.Marshal(b, m, deterministic)
+	} else {
+		b = b[:cap(b)]
+		n, err := m.MarshalToSizedBuffer(b)
+		if err != nil {
+			return nil, err
+		}
+		return b[:n], nil
+	}
+}
+func (m *ObservedDevice) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_ObservedDevice.Merge(m, src)
+}
+func (m *ObservedDevice) XXX_Size() int {
+	return m.ProtoSize()
+}
+func (m *ObservedDevice) XXX_DiscardUnknown() {
+	xxx_messageInfo_ObservedDevice.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_ObservedDevice proto.InternalMessageInfo
+
 func init() {
 	proto.RegisterType((*FileVersion)(nil), "db.FileVersion")
 	proto.RegisterType((*VersionList)(nil), "db.VersionList")
@@ -405,93 +486,103 @@ func init() {
 	proto.RegisterType((*CountsSet)(nil), "db.CountsSet")
 	proto.RegisterType((*FileVersionDeprecated)(nil), "db.FileVersionDeprecated")
 	proto.RegisterType((*VersionListDeprecated)(nil), "db.VersionListDeprecated")
+	proto.RegisterType((*ObservedFolder)(nil), "db.ObservedFolder")
+	proto.RegisterType((*ObservedDevice)(nil), "db.ObservedDevice")
 }
 
 func init() { proto.RegisterFile("lib/db/structs.proto", fileDescriptor_5465d80e8cba02e3) }
 
 var fileDescriptor_5465d80e8cba02e3 = []byte{
-	// 1289 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x56, 0xcf, 0x6f, 0xdc, 0x44,
-	0x14, 0x5e, 0x67, 0x7f, 0x64, 0x77, 0x76, 0x93, 0x36, 0x2e, 0xad, 0x4c, 0x81, 0x9d, 0x65, 0x9a,
-	0x4a, 0x0b, 0x48, 0x1b, 0x29, 0x55, 0x2b, 0x54, 0x09, 0xaa, 0xba, 0x51, 0xdb, 0x54, 0xa5, 0x45,
-	0x93, 0xaa, 0x20, 0x2e, 0xab, 0xb5, 0x77, 0x92, 0x58, 0x75, 0xec, 0xc5, 0xe3, 0x34, 0xdd, 0xde,
-	0xb8, 0x20, 0x71, 0x43, 0x15, 0x07, 0x84, 0x10, 0xea, 0x89, 0x3f, 0x81, 0xbf, 0xa1, 0xc7, 0x1c,
-	0x11, 0x07, 0x4b, 0x4d, 0x2e, 0xb0, 0xc7, 0x3d, 0x21, 0x4e, 0x68, 0xde, 0x8c, 0xc7, 0xb3, 0x8d,
-	0x8a, 0xda, 0x92, 0x9b, 0xdf, 0xf7, 0xbe, 0xf7, 0x6c, 0xbf, 0xf9, 0xde, 0x9b, 0x87, 0xde, 0x0a,
-	0x03, 0x6f, 0x65, 0xe8, 0xad, 0xf0, 0x34, 0xd9, 0xf5, 0x53, 0xde, 0x1b, 0x25, 0x71, 0x1a, 0xdb,
-	0x73, 0x43, 0xef, 0xec, 0xb9, 0x84, 0x8d, 0x62, 0xbe, 0x02, 0x80, 0xb7, 0xbb, 0xb9, 0xb2, 0x15,
-	0x6f, 0xc5, 0x60, 0xc0, 0x93, 0x24, 0x9e, 0x3d, 0x23, 0xc2, 0xe1, 0xd1, 0x8f, 0xc3, 0x15, 0x8f,
-	0x8d, 0x14, 0xde, 0x60, 0x8f, 0x52, 0xf9, 0x48, 0x7e, 0x99, 0x43, 0xcd, 0xeb, 0x41, 0xc8, 0xee,
-	0xb3, 0x84, 0x07, 0x71, 0x64, 0xdf, 0x46, 0xf3, 0x0f, 0xe5, 0xa3, 0x63, 0x75, 0xac, 0x6e, 0x73,
-	0xf5, 0x64, 0x2f, 0x4f, 0xd0, 0xbb, 0xcf, 0xfc, 0x34, 0x4e, 0xdc, 0xce, 0xb3, 0x0c, 0x97, 0x26,
-	0x19, 0xce, 0x89, 0xd3, 0x0c, 0x2f, 0x3c, 0xda, 0x09, 0x2f, 0x13, 0x65, 0x13, 0x9a, 0x7b, 0xec,
-	0x4b, 0x68, 0x7e, 0xc8, 0x42, 0x96, 0xb2, 0xa1, 0x33, 0xd7, 0xb1, 0xba, 0x75, 0xf7, 0x5d, 0x11,
-	0xa7, 0x20, 0x1d, 0xa7, 0x6c, 0x42, 0x73, 0x8f, 0x7d, 0x51, 0xc4, 0x3d, 0x0c, 0x7c, 0xc6, 0x9d,
-	0x72, 0xa7, 0xdc, 0x6d, 0xb9, 0xef, 0xc8, 0x38, 0x80, 0xa6, 0x19, 0x6e, 0xa9, 0x38, 0x61, 0x43,
-	0x18, 0x38, 0x6c, 0x8a, 0x4e, 0x04, 0xd1, 0xc3, 0x41, 0x18, 0x0c, 0xfb, 0x79, 0x78, 0x05, 0xc2,
-	0x3f, 0x98, 0x64, 0x78, 0x51, 0xb9, 0xd6, 0x74, 0x96, 0x53, 0x90, 0x65, 0x06, 0x26, 0xf4, 0x05,
-	0x1a, 0xf9, 0xc6, 0x42, 0x4d, 0x55, 0x9c, 0xdb, 0x01, 0x4f, 0xed, 0x10, 0xd5, 0xd5, 0xdf, 0x71,
-	0xc7, 0xea, 0x94, 0xbb, 0xcd, 0xd5, 0x13, 0xbd, 0xa1, 0xd7, 0x33, 0x6a, 0xe8, 0x5e, 0x11, 0x05,
-	0x3a, 0xc8, 0x70, 0x93, 0x0e, 0xf6, 0x14, 0xc6, 0x27, 0x19, 0xd6, 0x71, 0x47, 0x0a, 0xf6, 0x64,
-	0x7f, 0xd9, 0xe4, 0x52, 0xcd, 0xbc, 0x5c, 0xf9, 0xf1, 0x29, 0x2e, 0x91, 0xbf, 0x11, 0x5a, 0x12,
-	0x2f, 0x58, 0x8f, 0x36, 0xe3, 0x7b, 0xc9, 0x6e, 0xe4, 0x0f, 0x44, 0x91, 0x3e, 0x44, 0x95, 0x68,
-	0xb0, 0xc3, 0xe0, 0x9c, 0x1a, 0xee, 0x99, 0x49, 0x86, 0xc1, 0x9e, 0x66, 0x18, 0x41, 0x76, 0x61,
-	0x10, 0x0a, 0x98, 0xe0, 0xf2, 0xe0, 0x31, 0x73, 0xca, 0x1d, 0xab, 0x5b, 0x96, 0x5c, 0x61, 0x6b,
-	0xae, 0x30, 0x08, 0x05, 0xcc, 0xbe, 0x82, 0xd0, 0x4e, 0x3c, 0x0c, 0x36, 0x03, 0x36, 0xec, 0x73,
-	0xa7, 0x0a, 0x11, 0x9d, 0x49, 0x86, 0x1b, 0x39, 0xba, 0x31, 0xcd, 0xf0, 0x09, 0x08, 0xd3, 0x08,
-	0xa1, 0x85, 0xd7, 0xfe, 0xcd, 0x42, 0x4d, 0x9d, 0xc1, 0x1b, 0x3b, 0xad, 0x8e, 0xd5, 0xad, 0xb8,
-	0x3f, 0x58, 0xa2, 0x2c, 0x7f, 0x64, 0xf8, 0xc2, 0x56, 0x90, 0x6e, 0xef, 0x7a, 0x3d, 0x3f, 0xde,
-	0x59, 0xe1, 0xe3, 0xc8, 0x4f, 0xb7, 0x83, 0x68, 0xcb, 0x78, 0x32, 0x45, 0xdb, 0xdb, 0xd8, 0x8e,
-	0x93, 0x74, 0x7d, 0x6d, 0x92, 0x61, 0xfd, 0x51, 0xee, 0x78, 0x9a, 0xe1, 0x93, 0x33, 0xef, 0x77,
-	0xc7, 0xe4, 0xa7, 0xfd, 0xe5, 0x37, 0x49, 0x4c, 0x8d, 0xb4, 0xa6, 0xf8, 0x1b, 0xff, 0x5f, 0xfc,
-	0x97, 0x51, 0x9d, 0xb3, 0xaf, 0x77, 0x59, 0xe4, 0x33, 0x07, 0x41, 0x15, 0xdb, 0x42, 0x05, 0x39,
-	0x36, 0xcd, 0xf0, 0xa2, 0xac, 0xbd, 0x02, 0x08, 0xd5, 0x3e, 0xfb, 0x2e, 0x5a, 0xe4, 0xe3, 0x9d,
-	0x30, 0x88, 0x1e, 0xf4, 0xd3, 0x41, 0xb2, 0xc5, 0x52, 0x67, 0x09, 0x4e, 0xb9, 0x3b, 0xc9, 0xf0,
-	0x82, 0xf2, 0xdc, 0x03, 0x87, 0xd6, 0xf1, 0x0c, 0x4a, 0xe8, 0x2c, 0xcb, 0xbe, 0x86, 0x9a, 0x5e,
-	0x18, 0xfb, 0x0f, 0x78, 0x7f, 0x7b, 0xc0, 0xb7, 0x1d, 0xbb, 0x63, 0x75, 0x5b, 0x2e, 0x11, 0x65,
-	0x95, 0xf0, 0xcd, 0x01, 0xdf, 0xd6, 0x65, 0x2d, 0x20, 0x42, 0x0d, 0xbf, 0xfd, 0x29, 0x6a, 0xb0,
-	0xc8, 0x4f, 0xc6, 0x23, 0xd1, 0xd0, 0xa7, 0x20, 0x05, 0x08, 0x43, 0x83, 0x5a, 0x18, 0x1a, 0x21,
-	0xb4, 0xf0, 0xda, 0x2e, 0xaa, 0xa4, 0xe3, 0x11, 0x83, 0x59, 0xb0, 0xb8, 0x7a, 0xa6, 0x28, 0xae,
-	0x16, 0xf7, 0x78, 0xc4, 0xa4, 0x3a, 0x05, 0x4f, 0xab, 0x53, 0x18, 0x84, 0x02, 0x66, 0x5f, 0x47,
-	0xcd, 0x11, 0x4b, 0x76, 0x02, 0x2e, 0x5b, 0xb0, 0xd2, 0xb1, 0xba, 0x0b, 0xee, 0xf2, 0x24, 0xc3,
-	0x26, 0x3c, 0xcd, 0xf0, 0x12, 0x44, 0x1a, 0x18, 0xa1, 0x26, 0xc3, 0xbe, 0x65, 0x68, 0x34, 0xe2,
-	0x4e, 0xb3, 0x63, 0x75, 0xab, 0x30, 0x27, 0xb4, 0x20, 0xee, 0xf0, 0x23, 0x3a, 0xbb, 0xc3, 0xc9,
-	0x3f, 0x19, 0x2e, 0x07, 0x51, 0x4a, 0x0d, 0x9a, 0xbd, 0x89, 0x64, 0x95, 0xfa, 0xd0, 0x63, 0x0b,
-	0x90, 0xea, 0xc6, 0x41, 0x86, 0x5b, 0x74, 0xb0, 0xe7, 0x0a, 0xc7, 0x46, 0xf0, 0x98, 0x89, 0x42,
-	0x79, 0xb9, 0xa1, 0x0b, 0xa5, 0x91, 0x3c, 0xf1, 0x93, 0xfd, 0xe5, 0x99, 0x30, 0x5a, 0x04, 0xd9,
-	0x6b, 0xa8, 0x19, 0xc6, 0xfe, 0x20, 0xec, 0x6f, 0x86, 0x83, 0x2d, 0xee, 0xfc, 0x39, 0x0f, 0x3f,
-	0x0f, 0xa7, 0x08, 0xf8, 0x75, 0x01, 0xeb, 0x8f, 0x2e, 0x20, 0x42, 0x0d, 0xbf, 0x7d, 0x13, 0xb5,
-	0x94, 0x44, 0xa5, 0x16, 0xfe, 0x9a, 0x87, 0x93, 0x84, 0x1a, 0x2a, 0x87, 0x52, 0xc3, 0x92, 0xa9,
-	0x6c, 0x29, 0x07, 0x93, 0x61, 0x8e, 0xf7, 0xda, 0xeb, 0x8c, 0x77, 0x8a, 0xe6, 0xd5, 0x94, 0x75,
-	0xe6, 0x21, 0xee, 0xe3, 0x83, 0x0c, 0x23, 0x3a, 0xd8, 0x5b, 0x97, 0xa8, 0xc8, 0xa2, 0x08, 0x3a,
-	0x8b, 0xb2, 0xc5, 0xac, 0x34, 0x98, 0x34, 0xe7, 0x89, 0x8e, 0x89, 0xe2, 0xbe, 0x29, 0x8d, 0x3a,
-	0xa4, 0x86, 0x8e, 0x89, 0xe2, 0xcf, 0x67, 0xc4, 0x21, 0x3b, 0x66, 0x06, 0x25, 0x74, 0x96, 0xa5,
-	0x46, 0xef, 0x17, 0xa8, 0x01, 0x47, 0x01, 0xb3, 0xff, 0x16, 0xaa, 0xc9, 0x6e, 0x50, 0x93, 0xff,
-	0x54, 0xa1, 0x60, 0x20, 0x09, 0x09, 0xbb, 0xef, 0xa9, 0x09, 0xa1, 0xa8, 0xd3, 0x0c, 0x37, 0x8b,
-	0x93, 0x26, 0x54, 0xc1, 0xe4, 0x57, 0x0b, 0x9d, 0x5e, 0x8f, 0x86, 0x41, 0xc2, 0xfc, 0x54, 0xd5,
-	0x93, 0xf1, 0xbb, 0x51, 0x38, 0x3e, 0x9e, 0x56, 0x3d, 0xb6, 0x43, 0x26, 0x3f, 0x57, 0x50, 0xed,
-	0x5a, 0xbc, 0x1b, 0xa5, 0xdc, 0xbe, 0x88, 0xaa, 0x9b, 0x41, 0xc8, 0x38, 0x5c, 0x39, 0x55, 0x17,
-	0x4f, 0x32, 0x2c, 0x01, 0xfd, 0x93, 0x60, 0xe9, 0x1e, 0x91, 0x4e, 0xfb, 0x33, 0xd4, 0x94, 0xff,
-	0x19, 0x27, 0x01, 0xe3, 0xd0, 0xfd, 0x55, 0xf7, 0x23, 0xf1, 0x25, 0x06, 0xac, 0xbf, 0xc4, 0xc0,
-	0x74, 0x22, 0x93, 0x68, 0x5f, 0x45, 0x75, 0x35, 0xdb, 0x38, 0xdc, 0x67, 0x55, 0xf7, 0x3c, 0xcc,
-	0x55, 0x85, 0x15, 0x73, 0x55, 0x01, 0x3a, 0x8b, 0xa6, 0xd8, 0x9f, 0x14, 0xc2, 0xad, 0x40, 0x86,
-	0x73, 0xff, 0x25, 0xdc, 0x3c, 0x5e, 0xeb, 0xb7, 0x87, 0xaa, 0xde, 0x38, 0x65, 0xf9, 0xe5, 0xe8,
-	0x88, 0x3a, 0x00, 0x50, 0x1c, 0xb6, 0xb0, 0x08, 0x95, 0xe8, 0xcc, 0x4d, 0x50, 0x7b, 0xcd, 0x9b,
-	0x60, 0x03, 0x35, 0xe4, 0x2e, 0xd3, 0x0f, 0x86, 0x70, 0x09, 0xb4, 0xdc, 0x4b, 0x07, 0x19, 0xae,
-	0xcb, 0xfd, 0x04, 0x6e, 0xc6, 0xba, 0x24, 0xac, 0x0f, 0x75, 0xa2, 0x1c, 0x10, 0xdd, 0xa2, 0x99,
-	0x54, 0xf3, 0x84, 0xc4, 0xcc, 0x41, 0x62, 0xbf, 0xc9, 0x1c, 0x51, 0x0d, 0xf2, 0xad, 0x85, 0x1a,
-	0x52, 0x1e, 0x1b, 0x2c, 0xb5, 0xaf, 0xa2, 0x9a, 0x0f, 0x86, 0xea, 0x10, 0x24, 0x76, 0x23, 0xe9,
-	0x2e, 0x1a, 0x43, 0x32, 0x74, 0xad, 0xc0, 0x24, 0x54, 0xc1, 0x62, 0xa8, 0xf8, 0x09, 0x1b, 0xe4,
-	0x3b, 0x63, 0x59, 0x0e, 0x15, 0x05, 0xe9, 0xb3, 0x51, 0x36, 0xa1, 0xb9, 0x87, 0x7c, 0x37, 0x87,
-	0x4e, 0x1b, 0x5b, 0xd8, 0x1a, 0x1b, 0x25, 0x4c, 0x2e, 0x4a, 0xc7, 0xbb, 0xd3, 0xae, 0xa2, 0x9a,
-	0xac, 0x23, 0x7c, 0x5e, 0xcb, 0x3d, 0x2b, 0x7e, 0x49, 0x22, 0x47, 0x36, 0x53, 0x85, 0x8b, 0x7f,
-	0xca, 0x07, 0x5e, 0xb9, 0x18, 0x94, 0x2f, 0x1b, 0x71, 0xc5, 0x50, 0xbb, 0x34, 0xab, 0xd3, 0x57,
-	0x1d, 0xb0, 0x64, 0x0f, 0x9d, 0x36, 0x76, 0x56, 0xa3, 0x14, 0x5f, 0x1e, 0xd9, 0x5e, 0xdf, 0x7e,
-	0x61, 0x7b, 0x2d, 0xc8, 0xee, 0xfb, 0xaa, 0x28, 0x2f, 0x5f, 0x5c, 0x5f, 0xdc, 0x54, 0xdd, 0x1b,
-	0xcf, 0x9e, 0xb7, 0x4b, 0xfb, 0xcf, 0xdb, 0xa5, 0x67, 0x07, 0x6d, 0x6b, 0xff, 0xa0, 0x6d, 0x7d,
-	0x7f, 0xd8, 0x2e, 0x3d, 0x3d, 0x6c, 0x5b, 0xfb, 0x87, 0xed, 0xd2, 0xef, 0x87, 0xed, 0xd2, 0x57,
-	0xe7, 0x5f, 0x61, 0x49, 0x1b, 0x7a, 0x5e, 0x0d, 0x4e, 0xe8, 0xc2, 0xbf, 0x01, 0x00, 0x00, 0xff,
-	0xff, 0xfc, 0x01, 0x79, 0xc2, 0x02, 0x0d, 0x00, 0x00,
+	// 1415 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x57, 0xcf, 0x6f, 0xdb, 0xc6,
+	0x12, 0x36, 0x2d, 0xf9, 0x87, 0x56, 0xb2, 0x13, 0x33, 0x2f, 0x01, 0x9f, 0xdf, 0x7b, 0x5a, 0xbd,
+	0x8d, 0x03, 0xa8, 0x2d, 0x20, 0x03, 0x0e, 0x62, 0x14, 0x01, 0xda, 0x20, 0x8c, 0xe1, 0xc4, 0x41,
+	0x9a, 0x14, 0xeb, 0x20, 0x2d, 0x7a, 0x11, 0xf8, 0x63, 0x2d, 0x13, 0xa1, 0x48, 0x95, 0x4b, 0xdb,
+	0x51, 0x6e, 0xbd, 0x14, 0xe8, 0x2d, 0x08, 0x7a, 0x28, 0x8a, 0xa2, 0xc8, 0xa9, 0x7f, 0x42, 0xff,
+	0x82, 0x1e, 0x72, 0xf4, 0xb1, 0xe8, 0x81, 0x45, 0xec, 0x4b, 0xab, 0xa3, 0x4e, 0x45, 0x4f, 0xc5,
+	0xce, 0x2e, 0x97, 0x54, 0x8c, 0x14, 0x49, 0xea, 0x1b, 0xe7, 0x9b, 0x6f, 0x46, 0xe4, 0xec, 0x37,
+	0xb3, 0x23, 0xf4, 0xaf, 0x30, 0x70, 0x57, 0x7d, 0x77, 0x95, 0xa7, 0xc9, 0x9e, 0x97, 0xf2, 0xce,
+	0x20, 0x89, 0xd3, 0xd8, 0x9c, 0xf6, 0xdd, 0xe5, 0x8b, 0x09, 0x1b, 0xc4, 0x7c, 0x15, 0x00, 0x77,
+	0x6f, 0x67, 0xb5, 0x17, 0xf7, 0x62, 0x30, 0xe0, 0x49, 0x12, 0x97, 0x71, 0x2f, 0x8e, 0x7b, 0x21,
+	0x2b, 0x58, 0x69, 0xd0, 0x67, 0x3c, 0x75, 0xfa, 0x03, 0x45, 0xb8, 0x20, 0xf2, 0xc3, 0xa3, 0x17,
+	0x87, 0xab, 0x2e, 0xcb, 0xf1, 0x1a, 0x7b, 0x94, 0xca, 0x47, 0xf2, 0xfd, 0x34, 0xaa, 0x6f, 0x06,
+	0x21, 0x7b, 0xc0, 0x12, 0x1e, 0xc4, 0x91, 0x79, 0x07, 0xcd, 0xed, 0xcb, 0x47, 0xcb, 0x68, 0x19,
+	0xed, 0xfa, 0xda, 0xd9, 0x4e, 0x9e, 0xa0, 0xf3, 0x80, 0x79, 0x69, 0x9c, 0xd8, 0xad, 0xe7, 0x19,
+	0x9e, 0x1a, 0x65, 0x38, 0x27, 0x8e, 0x33, 0xbc, 0xf0, 0xa8, 0x1f, 0x5e, 0x25, 0xca, 0x26, 0x34,
+	0xf7, 0x98, 0xeb, 0x68, 0xce, 0x67, 0x21, 0x4b, 0x99, 0x6f, 0x4d, 0xb7, 0x8c, 0xf6, 0xbc, 0xfd,
+	0x5f, 0x11, 0xa7, 0x20, 0x1d, 0xa7, 0x6c, 0x42, 0x73, 0x8f, 0x79, 0x45, 0xc4, 0xed, 0x07, 0x1e,
+	0xe3, 0x56, 0xa5, 0x55, 0x69, 0x37, 0xec, 0xff, 0xc8, 0x38, 0x80, 0xc6, 0x19, 0x6e, 0xa8, 0x38,
+	0x61, 0x43, 0x18, 0x38, 0x4c, 0x8a, 0xce, 0x04, 0xd1, 0xbe, 0x13, 0x06, 0x7e, 0x37, 0x0f, 0xaf,
+	0x42, 0xf8, 0x3b, 0xa3, 0x0c, 0x2f, 0x2a, 0xd7, 0x86, 0xce, 0x72, 0x0e, 0xb2, 0x4c, 0xc0, 0x84,
+	0xbe, 0x44, 0x23, 0x5f, 0x18, 0xa8, 0xae, 0x8a, 0x73, 0x27, 0xe0, 0xa9, 0x19, 0xa2, 0x79, 0xf5,
+	0x75, 0xdc, 0x32, 0x5a, 0x95, 0x76, 0x7d, 0xed, 0x4c, 0xc7, 0x77, 0x3b, 0xa5, 0x1a, 0xda, 0xd7,
+	0x44, 0x81, 0x8e, 0x32, 0x5c, 0xa7, 0xce, 0x81, 0xc2, 0xf8, 0x28, 0xc3, 0x3a, 0xee, 0x44, 0xc1,
+	0x9e, 0x1e, 0xae, 0x94, 0xb9, 0x54, 0x33, 0xaf, 0x56, 0xbf, 0x79, 0x86, 0xa7, 0xc8, 0x1f, 0x08,
+	0x2d, 0x89, 0x1f, 0xd8, 0x8a, 0x76, 0xe2, 0xfb, 0xc9, 0x5e, 0xe4, 0x39, 0xa2, 0x48, 0xef, 0xa2,
+	0x6a, 0xe4, 0xf4, 0x19, 0x9c, 0x53, 0xcd, 0xbe, 0x30, 0xca, 0x30, 0xd8, 0xe3, 0x0c, 0x23, 0xc8,
+	0x2e, 0x0c, 0x42, 0x01, 0x13, 0x5c, 0x1e, 0x3c, 0x66, 0x56, 0xa5, 0x65, 0xb4, 0x2b, 0x92, 0x2b,
+	0x6c, 0xcd, 0x15, 0x06, 0xa1, 0x80, 0x99, 0xd7, 0x10, 0xea, 0xc7, 0x7e, 0xb0, 0x13, 0x30, 0xbf,
+	0xcb, 0xad, 0x19, 0x88, 0x68, 0x8d, 0x32, 0x5c, 0xcb, 0xd1, 0xed, 0x71, 0x86, 0xcf, 0x40, 0x98,
+	0x46, 0x08, 0x2d, 0xbc, 0xe6, 0x8f, 0x06, 0xaa, 0xeb, 0x0c, 0xee, 0xd0, 0x6a, 0xb4, 0x8c, 0x76,
+	0xd5, 0xfe, 0xda, 0x10, 0x65, 0xf9, 0x25, 0xc3, 0x97, 0x7b, 0x41, 0xba, 0xbb, 0xe7, 0x76, 0xbc,
+	0xb8, 0xbf, 0xca, 0x87, 0x91, 0x97, 0xee, 0x06, 0x51, 0xaf, 0xf4, 0x54, 0x16, 0x6d, 0x67, 0x7b,
+	0x37, 0x4e, 0xd2, 0xad, 0x8d, 0x51, 0x86, 0xf5, 0x4b, 0xd9, 0xc3, 0x71, 0x86, 0xcf, 0x4e, 0xfc,
+	0xbe, 0x3d, 0x24, 0xdf, 0x1e, 0xae, 0xbc, 0x4d, 0x62, 0x5a, 0x4a, 0x5b, 0x16, 0x7f, 0xed, 0x9f,
+	0x8b, 0xff, 0x2a, 0x9a, 0xe7, 0xec, 0xf3, 0x3d, 0x16, 0x79, 0xcc, 0x42, 0x50, 0xc5, 0xa6, 0x50,
+	0x41, 0x8e, 0x8d, 0x33, 0xbc, 0x28, 0x6b, 0xaf, 0x00, 0x42, 0xb5, 0xcf, 0xbc, 0x87, 0x16, 0xf9,
+	0xb0, 0x1f, 0x06, 0xd1, 0xc3, 0x6e, 0xea, 0x24, 0x3d, 0x96, 0x5a, 0x4b, 0x70, 0xca, 0xed, 0x51,
+	0x86, 0x17, 0x94, 0xe7, 0x3e, 0x38, 0xb4, 0x8e, 0x27, 0x50, 0x42, 0x27, 0x59, 0xe6, 0x0d, 0x54,
+	0x77, 0xc3, 0xd8, 0x7b, 0xc8, 0xbb, 0xbb, 0x0e, 0xdf, 0xb5, 0xcc, 0x96, 0xd1, 0x6e, 0xd8, 0x44,
+	0x94, 0x55, 0xc2, 0xb7, 0x1c, 0xbe, 0xab, 0xcb, 0x5a, 0x40, 0x84, 0x96, 0xfc, 0xe6, 0x87, 0xa8,
+	0xc6, 0x22, 0x2f, 0x19, 0x0e, 0x44, 0x43, 0x9f, 0x83, 0x14, 0x20, 0x0c, 0x0d, 0x6a, 0x61, 0x68,
+	0x84, 0xd0, 0xc2, 0x6b, 0xda, 0xa8, 0x9a, 0x0e, 0x07, 0x0c, 0x66, 0xc1, 0xe2, 0xda, 0x85, 0xa2,
+	0xb8, 0x5a, 0xdc, 0xc3, 0x01, 0x93, 0xea, 0x14, 0x3c, 0xad, 0x4e, 0x61, 0x10, 0x0a, 0x98, 0xb9,
+	0x89, 0xea, 0x03, 0x96, 0xf4, 0x03, 0x2e, 0x5b, 0xb0, 0xda, 0x32, 0xda, 0x0b, 0xf6, 0xca, 0x28,
+	0xc3, 0x65, 0x78, 0x9c, 0xe1, 0x25, 0x88, 0x2c, 0x61, 0x84, 0x96, 0x19, 0xe6, 0xed, 0x92, 0x46,
+	0x23, 0x6e, 0xd5, 0x5b, 0x46, 0x7b, 0x06, 0xe6, 0x84, 0x16, 0xc4, 0x5d, 0x7e, 0x42, 0x67, 0x77,
+	0x39, 0xf9, 0x33, 0xc3, 0x95, 0x20, 0x4a, 0x69, 0x89, 0x66, 0xee, 0x20, 0x59, 0xa5, 0x2e, 0xf4,
+	0xd8, 0x02, 0xa4, 0xba, 0x79, 0x94, 0xe1, 0x06, 0x75, 0x0e, 0x6c, 0xe1, 0xd8, 0x0e, 0x1e, 0x33,
+	0x51, 0x28, 0x37, 0x37, 0x74, 0xa1, 0x34, 0x92, 0x27, 0x7e, 0x7a, 0xb8, 0x32, 0x11, 0x46, 0x8b,
+	0x20, 0x73, 0x03, 0xd5, 0xc3, 0xd8, 0x73, 0xc2, 0xee, 0x4e, 0xe8, 0xf4, 0xb8, 0xf5, 0xdb, 0x1c,
+	0x7c, 0x3c, 0x9c, 0x22, 0xe0, 0x9b, 0x02, 0xd6, 0x2f, 0x5d, 0x40, 0x84, 0x96, 0xfc, 0xe6, 0x2d,
+	0xd4, 0x50, 0x12, 0x95, 0x5a, 0xf8, 0x7d, 0x0e, 0x4e, 0x12, 0x6a, 0xa8, 0x1c, 0x4a, 0x0d, 0x4b,
+	0x65, 0x65, 0x4b, 0x39, 0x94, 0x19, 0xe5, 0xf1, 0x3e, 0xfb, 0x26, 0xe3, 0x9d, 0xa2, 0x39, 0x35,
+	0x65, 0xad, 0x39, 0x88, 0x7b, 0xff, 0x28, 0xc3, 0x88, 0x3a, 0x07, 0x5b, 0x12, 0x15, 0x59, 0x14,
+	0x41, 0x67, 0x51, 0xb6, 0x98, 0x95, 0x25, 0x26, 0xcd, 0x79, 0xa2, 0x63, 0xa2, 0xb8, 0x5b, 0x96,
+	0xc6, 0x3c, 0xa4, 0x86, 0x8e, 0x89, 0xe2, 0x8f, 0x27, 0xc4, 0x21, 0x3b, 0x66, 0x02, 0x25, 0x74,
+	0x92, 0xa5, 0x46, 0xef, 0x27, 0xa8, 0x06, 0x47, 0x01, 0xb3, 0xff, 0x36, 0x9a, 0x95, 0xdd, 0xa0,
+	0x26, 0xff, 0xb9, 0x42, 0xc1, 0x40, 0x12, 0x12, 0xb6, 0xff, 0xa7, 0x26, 0x84, 0xa2, 0x8e, 0x33,
+	0x5c, 0x2f, 0x4e, 0x9a, 0x50, 0x05, 0x93, 0x1f, 0x0c, 0x74, 0x7e, 0x2b, 0xf2, 0x83, 0x84, 0x79,
+	0xa9, 0xaa, 0x27, 0xe3, 0xf7, 0xa2, 0x70, 0x78, 0x3a, 0xad, 0x7a, 0x6a, 0x87, 0x4c, 0xbe, 0xab,
+	0xa2, 0xd9, 0x1b, 0xf1, 0x5e, 0x94, 0x72, 0xf3, 0x0a, 0x9a, 0xd9, 0x09, 0x42, 0xc6, 0xe1, 0xca,
+	0x99, 0xb1, 0xf1, 0x28, 0xc3, 0x12, 0xd0, 0x1f, 0x09, 0x96, 0xee, 0x11, 0xe9, 0x34, 0x3f, 0x42,
+	0x75, 0xf9, 0x9d, 0x71, 0x12, 0x30, 0x0e, 0xdd, 0x3f, 0x63, 0xbf, 0x27, 0xde, 0xa4, 0x04, 0xeb,
+	0x37, 0x29, 0x61, 0x3a, 0x51, 0x99, 0x68, 0x5e, 0x47, 0xf3, 0x6a, 0xb6, 0x71, 0xb8, 0xcf, 0x66,
+	0xec, 0x4b, 0x30, 0x57, 0x15, 0x56, 0xcc, 0x55, 0x05, 0xe8, 0x2c, 0x9a, 0x62, 0x7e, 0x50, 0x08,
+	0xb7, 0x0a, 0x19, 0x2e, 0xfe, 0x9d, 0x70, 0xf3, 0x78, 0xad, 0xdf, 0x0e, 0x9a, 0x71, 0x87, 0x29,
+	0xcb, 0x2f, 0x47, 0x4b, 0xd4, 0x01, 0x80, 0xe2, 0xb0, 0x85, 0x45, 0xa8, 0x44, 0x27, 0x6e, 0x82,
+	0xd9, 0x37, 0xbc, 0x09, 0xb6, 0x51, 0x4d, 0xee, 0x32, 0xdd, 0xc0, 0x87, 0x4b, 0xa0, 0x61, 0xaf,
+	0x1f, 0x65, 0x78, 0x5e, 0xee, 0x27, 0x70, 0x33, 0xce, 0x4b, 0xc2, 0x96, 0xaf, 0x13, 0xe5, 0x80,
+	0xe8, 0x16, 0xcd, 0xa4, 0x9a, 0x27, 0x24, 0x56, 0x1e, 0x24, 0xe6, 0xdb, 0xcc, 0x11, 0xd5, 0x20,
+	0x5f, 0x1a, 0xa8, 0x26, 0xe5, 0xb1, 0xcd, 0x52, 0xf3, 0x3a, 0x9a, 0xf5, 0xc0, 0x50, 0x1d, 0x82,
+	0xc4, 0x6e, 0x24, 0xdd, 0x45, 0x63, 0x48, 0x86, 0xae, 0x15, 0x98, 0x84, 0x2a, 0x58, 0x0c, 0x15,
+	0x2f, 0x61, 0x4e, 0xbe, 0x33, 0x56, 0xe4, 0x50, 0x51, 0x90, 0x3e, 0x1b, 0x65, 0x13, 0x9a, 0x7b,
+	0xc8, 0x57, 0xd3, 0xe8, 0x7c, 0x69, 0x0b, 0xdb, 0x60, 0x83, 0x84, 0xc9, 0x45, 0xe9, 0x74, 0x77,
+	0xda, 0x35, 0x34, 0x2b, 0xeb, 0x08, 0xaf, 0xd7, 0xb0, 0x97, 0xc5, 0x27, 0x49, 0xe4, 0xc4, 0x66,
+	0xaa, 0x70, 0xf1, 0x4d, 0xf9, 0xc0, 0xab, 0x14, 0x83, 0xf2, 0x55, 0x23, 0xae, 0x18, 0x6a, 0xeb,
+	0x93, 0x3a, 0x7d, 0xdd, 0x01, 0x4b, 0x0e, 0xd0, 0xf9, 0xd2, 0xce, 0x5a, 0x2a, 0xc5, 0xa7, 0x27,
+	0xb6, 0xd7, 0x7f, 0xbf, 0xb4, 0xbd, 0x16, 0x64, 0xfb, 0xff, 0xaa, 0x28, 0xaf, 0x5e, 0x5c, 0x4f,
+	0x6c, 0xaa, 0x4f, 0x0c, 0xb4, 0x78, 0xcf, 0xe5, 0x2c, 0xd9, 0x67, 0xfe, 0x66, 0x1c, 0xfa, 0x2c,
+	0x31, 0xef, 0xa2, 0xaa, 0xf8, 0x5f, 0xa2, 0x4a, 0xbf, 0xdc, 0x91, 0x7f, 0x5a, 0x3a, 0xf9, 0x9f,
+	0x96, 0xce, 0xfd, 0xfc, 0x4f, 0x8b, 0xdd, 0x54, 0xbf, 0x07, 0xfc, 0xe2, 0xf2, 0x0f, 0xfa, 0x8c,
+	0x3c, 0xf9, 0x15, 0x1b, 0x14, 0x70, 0xd1, 0x7c, 0xa1, 0xe3, 0xb2, 0x10, 0xca, 0x5f, 0x93, 0xcd,
+	0x07, 0x80, 0x16, 0x14, 0x58, 0x84, 0x4a, 0x94, 0xfc, 0x54, 0x7a, 0x25, 0xd9, 0x0a, 0xa7, 0xfe,
+	0x4a, 0xf9, 0x26, 0x3e, 0xfd, 0x1a, 0x9b, 0xf8, 0x3a, 0x9a, 0x73, 0x7c, 0x3f, 0x61, 0x5c, 0x0e,
+	0xaf, 0x9a, 0x3c, 0x52, 0x05, 0xe9, 0x02, 0x2b, 0x9b, 0xd0, 0xdc, 0x63, 0xdf, 0x7c, 0xfe, 0xa2,
+	0x39, 0x75, 0xf8, 0xa2, 0x39, 0xf5, 0xfc, 0xa8, 0x69, 0x1c, 0x1e, 0x35, 0x8d, 0x27, 0xc7, 0xcd,
+	0xa9, 0x67, 0xc7, 0x4d, 0xe3, 0xf0, 0xb8, 0x39, 0xf5, 0xf3, 0x71, 0x73, 0xea, 0xb3, 0x4b, 0xaf,
+	0xb1, 0xfe, 0xfa, 0xae, 0x3b, 0x0b, 0x9f, 0x79, 0xf9, 0xaf, 0x00, 0x00, 0x00, 0xff, 0xff, 0x9e,
+	0xa0, 0xbe, 0xf2, 0x7d, 0x0e, 0x00, 0x00,
 }
 
 func (m *FileVersion) Marshal() (dAtA []byte, err error) {
@@ -1031,6 +1122,89 @@ func (m *VersionListDeprecated) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 	return len(dAtA) - i, nil
 }
 
+func (m *ObservedFolder) Marshal() (dAtA []byte, err error) {
+	size := m.ProtoSize()
+	dAtA = make([]byte, size)
+	n, err := m.MarshalToSizedBuffer(dAtA[:size])
+	if err != nil {
+		return nil, err
+	}
+	return dAtA[:n], nil
+}
+
+func (m *ObservedFolder) MarshalTo(dAtA []byte) (int, error) {
+	size := m.ProtoSize()
+	return m.MarshalToSizedBuffer(dAtA[:size])
+}
+
+func (m *ObservedFolder) MarshalToSizedBuffer(dAtA []byte) (int, error) {
+	i := len(dAtA)
+	_ = i
+	var l int
+	_ = l
+	if len(m.Label) > 0 {
+		i -= len(m.Label)
+		copy(dAtA[i:], m.Label)
+		i = encodeVarintStructs(dAtA, i, uint64(len(m.Label)))
+		i--
+		dAtA[i] = 0x12
+	}
+	n4, err4 := github_com_gogo_protobuf_types.StdTimeMarshalTo(m.Time, dAtA[i-github_com_gogo_protobuf_types.SizeOfStdTime(m.Time):])
+	if err4 != nil {
+		return 0, err4
+	}
+	i -= n4
+	i = encodeVarintStructs(dAtA, i, uint64(n4))
+	i--
+	dAtA[i] = 0xa
+	return len(dAtA) - i, nil
+}
+
+func (m *ObservedDevice) Marshal() (dAtA []byte, err error) {
+	size := m.ProtoSize()
+	dAtA = make([]byte, size)
+	n, err := m.MarshalToSizedBuffer(dAtA[:size])
+	if err != nil {
+		return nil, err
+	}
+	return dAtA[:n], nil
+}
+
+func (m *ObservedDevice) MarshalTo(dAtA []byte) (int, error) {
+	size := m.ProtoSize()
+	return m.MarshalToSizedBuffer(dAtA[:size])
+}
+
+func (m *ObservedDevice) MarshalToSizedBuffer(dAtA []byte) (int, error) {
+	i := len(dAtA)
+	_ = i
+	var l int
+	_ = l
+	if len(m.Address) > 0 {
+		i -= len(m.Address)
+		copy(dAtA[i:], m.Address)
+		i = encodeVarintStructs(dAtA, i, uint64(len(m.Address)))
+		i--
+		dAtA[i] = 0x1a
+	}
+	if len(m.Name) > 0 {
+		i -= len(m.Name)
+		copy(dAtA[i:], m.Name)
+		i = encodeVarintStructs(dAtA, i, uint64(len(m.Name)))
+		i--
+		dAtA[i] = 0x12
+	}
+	n5, err5 := github_com_gogo_protobuf_types.StdTimeMarshalTo(m.Time, dAtA[i-github_com_gogo_protobuf_types.SizeOfStdTime(m.Time):])
+	if err5 != nil {
+		return 0, err5
+	}
+	i -= n5
+	i = encodeVarintStructs(dAtA, i, uint64(n5))
+	i--
+	dAtA[i] = 0xa
+	return len(dAtA) - i, nil
+}
+
 func encodeVarintStructs(dAtA []byte, offset int, v uint64) int {
 	offset -= sovStructs(v)
 	base := offset
@@ -1270,6 +1444,40 @@ func (m *VersionListDeprecated) ProtoSize() (n int) {
 	return n
 }
 
+func (m *ObservedFolder) ProtoSize() (n int) {
+	if m == nil {
+		return 0
+	}
+	var l int
+	_ = l
+	l = github_com_gogo_protobuf_types.SizeOfStdTime(m.Time)
+	n += 1 + l + sovStructs(uint64(l))
+	l = len(m.Label)
+	if l > 0 {
+		n += 1 + l + sovStructs(uint64(l))
+	}
+	return n
+}
+
+func (m *ObservedDevice) ProtoSize() (n int) {
+	if m == nil {
+		return 0
+	}
+	var l int
+	_ = l
+	l = github_com_gogo_protobuf_types.SizeOfStdTime(m.Time)
+	n += 1 + l + sovStructs(uint64(l))
+	l = len(m.Name)
+	if l > 0 {
+		n += 1 + l + sovStructs(uint64(l))
+	}
+	l = len(m.Address)
+	if l > 0 {
+		n += 1 + l + sovStructs(uint64(l))
+	}
+	return n
+}
+
 func sovStructs(x uint64) (n int) {
 	return (math_bits.Len64(x|1) + 6) / 7
 }
@@ -2797,6 +3005,274 @@ func (m *VersionListDeprecated) Unmarshal(dAtA []byte) error {
 	}
 	return nil
 }
+func (m *ObservedFolder) Unmarshal(dAtA []byte) error {
+	l := len(dAtA)
+	iNdEx := 0
+	for iNdEx < l {
+		preIndex := iNdEx
+		var wire uint64
+		for shift := uint(0); ; shift += 7 {
+			if shift >= 64 {
+				return ErrIntOverflowStructs
+			}
+			if iNdEx >= l {
+				return io.ErrUnexpectedEOF
+			}
+			b := dAtA[iNdEx]
+			iNdEx++
+			wire |= uint64(b&0x7F) << shift
+			if b < 0x80 {
+				break
+			}
+		}
+		fieldNum := int32(wire >> 3)
+		wireType := int(wire & 0x7)
+		if wireType == 4 {
+			return fmt.Errorf("proto: ObservedFolder: wiretype end group for non-group")
+		}
+		if fieldNum <= 0 {
+			return fmt.Errorf("proto: ObservedFolder: illegal tag %d (wire type %d)", fieldNum, wire)
+		}
+		switch fieldNum {
+		case 1:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Time", wireType)
+			}
+			var msglen int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowStructs
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				msglen |= int(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			if msglen < 0 {
+				return ErrInvalidLengthStructs
+			}
+			postIndex := iNdEx + msglen
+			if postIndex < 0 {
+				return ErrInvalidLengthStructs
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			if err := github_com_gogo_protobuf_types.StdTimeUnmarshal(&m.Time, dAtA[iNdEx:postIndex]); err != nil {
+				return err
+			}
+			iNdEx = postIndex
+		case 2:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Label", wireType)
+			}
+			var stringLen uint64
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowStructs
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				stringLen |= uint64(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			intStringLen := int(stringLen)
+			if intStringLen < 0 {
+				return ErrInvalidLengthStructs
+			}
+			postIndex := iNdEx + intStringLen
+			if postIndex < 0 {
+				return ErrInvalidLengthStructs
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.Label = string(dAtA[iNdEx:postIndex])
+			iNdEx = postIndex
+		default:
+			iNdEx = preIndex
+			skippy, err := skipStructs(dAtA[iNdEx:])
+			if err != nil {
+				return err
+			}
+			if skippy < 0 {
+				return ErrInvalidLengthStructs
+			}
+			if (iNdEx + skippy) < 0 {
+				return ErrInvalidLengthStructs
+			}
+			if (iNdEx + skippy) > l {
+				return io.ErrUnexpectedEOF
+			}
+			iNdEx += skippy
+		}
+	}
+
+	if iNdEx > l {
+		return io.ErrUnexpectedEOF
+	}
+	return nil
+}
+func (m *ObservedDevice) Unmarshal(dAtA []byte) error {
+	l := len(dAtA)
+	iNdEx := 0
+	for iNdEx < l {
+		preIndex := iNdEx
+		var wire uint64
+		for shift := uint(0); ; shift += 7 {
+			if shift >= 64 {
+				return ErrIntOverflowStructs
+			}
+			if iNdEx >= l {
+				return io.ErrUnexpectedEOF
+			}
+			b := dAtA[iNdEx]
+			iNdEx++
+			wire |= uint64(b&0x7F) << shift
+			if b < 0x80 {
+				break
+			}
+		}
+		fieldNum := int32(wire >> 3)
+		wireType := int(wire & 0x7)
+		if wireType == 4 {
+			return fmt.Errorf("proto: ObservedDevice: wiretype end group for non-group")
+		}
+		if fieldNum <= 0 {
+			return fmt.Errorf("proto: ObservedDevice: illegal tag %d (wire type %d)", fieldNum, wire)
+		}
+		switch fieldNum {
+		case 1:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Time", wireType)
+			}
+			var msglen int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowStructs
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				msglen |= int(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			if msglen < 0 {
+				return ErrInvalidLengthStructs
+			}
+			postIndex := iNdEx + msglen
+			if postIndex < 0 {
+				return ErrInvalidLengthStructs
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			if err := github_com_gogo_protobuf_types.StdTimeUnmarshal(&m.Time, dAtA[iNdEx:postIndex]); err != nil {
+				return err
+			}
+			iNdEx = postIndex
+		case 2:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType)
+			}
+			var stringLen uint64
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowStructs
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				stringLen |= uint64(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			intStringLen := int(stringLen)
+			if intStringLen < 0 {
+				return ErrInvalidLengthStructs
+			}
+			postIndex := iNdEx + intStringLen
+			if postIndex < 0 {
+				return ErrInvalidLengthStructs
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.Name = string(dAtA[iNdEx:postIndex])
+			iNdEx = postIndex
+		case 3:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Address", wireType)
+			}
+			var stringLen uint64
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowStructs
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				stringLen |= uint64(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			intStringLen := int(stringLen)
+			if intStringLen < 0 {
+				return ErrInvalidLengthStructs
+			}
+			postIndex := iNdEx + intStringLen
+			if postIndex < 0 {
+				return ErrInvalidLengthStructs
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.Address = string(dAtA[iNdEx:postIndex])
+			iNdEx = postIndex
+		default:
+			iNdEx = preIndex
+			skippy, err := skipStructs(dAtA[iNdEx:])
+			if err != nil {
+				return err
+			}
+			if skippy < 0 {
+				return ErrInvalidLengthStructs
+			}
+			if (iNdEx + skippy) < 0 {
+				return ErrInvalidLengthStructs
+			}
+			if (iNdEx + skippy) > l {
+				return io.ErrUnexpectedEOF
+			}
+			iNdEx += skippy
+		}
+	}
+
+	if iNdEx > l {
+		return io.ErrUnexpectedEOF
+	}
+	return nil
+}
 func skipStructs(dAtA []byte) (n int, err error) {
 	l := len(dAtA)
 	iNdEx := 0

+ 96 - 6
lib/model/model.go

@@ -105,6 +105,9 @@ type Model interface {
 	FolderStatistics() (map[string]stats.FolderStatistics, error)
 	UsageReportingStats(report *contract.Report, version int, preview bool)
 
+	PendingDevices() (map[protocol.DeviceID]db.ObservedDevice, error)
+	PendingFolders(device protocol.DeviceID) (map[string]db.PendingFolder, error)
+
 	StartDeadlockDetector(timeout time.Duration)
 	GlobalDirectoryTree(folder, prefix string, levels int, dirsonly bool) map[string]interface{}
 }
@@ -255,8 +258,10 @@ func NewModel(cfg config.Wrapper, id protocol.DeviceID, clientName, clientVersio
 func (m *model) serve(ctx context.Context) error {
 	// Add and start folders
 	cacheIgnoredFiles := m.cfg.Options().CacheIgnoredFiles
-	clusterConfigDevices := make(deviceIDSet, len(m.cfg.Devices()))
-	for _, folderCfg := range m.cfg.Folders() {
+	existingDevices := m.cfg.Devices()
+	existingFolders := m.cfg.Folders()
+	clusterConfigDevices := make(deviceIDSet, len(existingDevices))
+	for _, folderCfg := range existingFolders {
 		if folderCfg.Paused {
 			folderCfg.CreateRoot()
 			continue
@@ -264,6 +269,10 @@ func (m *model) serve(ctx context.Context) error {
 		m.newFolder(folderCfg, cacheIgnoredFiles)
 		clusterConfigDevices.add(folderCfg.DeviceIDs())
 	}
+
+	ignoredDevices := observedDeviceSet(m.cfg.IgnoredDevices())
+	m.cleanPending(existingDevices, existingFolders, ignoredDevices, nil)
+
 	m.resendClusterConfig(clusterConfigDevices.AsSlice())
 	m.cfg.Subscribe(m)
 
@@ -1251,9 +1260,10 @@ func (m *model) ccHandleFolders(folders []protocol.Folder, deviceCfg config.Devi
 				l.Infof("Ignoring folder %s from device %s since we are configured to", folder.Description(), deviceID)
 				continue
 			}
-			m.cfg.AddOrUpdatePendingFolder(folder.ID, folder.Label, deviceID)
+			if err := m.db.AddOrUpdatePendingFolder(folder.ID, folder.Label, deviceID); err != nil {
+				l.Warnf("Failed to persist pending folder entry to database: %v", err)
+			}
 			indexSenders.addPending(cfg, ccDeviceInfos[folder.ID])
-			changed = true
 			m.evLogger.Log(events.FolderRejected, map[string]string{
 				"folder":      folder.ID,
 				"folderLabel": folder.Label,
@@ -1989,8 +1999,9 @@ func (m *model) OnHello(remoteID protocol.DeviceID, addr net.Addr, hello protoco
 
 	cfg, ok := m.cfg.Device(remoteID)
 	if !ok {
-		m.cfg.AddOrUpdatePendingDevice(remoteID, hello.DeviceName, addr.String())
-		_ = m.cfg.Save() // best effort
+		if err := m.db.AddOrUpdatePendingDevice(remoteID, hello.DeviceName, addr.String()); err != nil {
+			l.Warnf("Failed to persist pending device entry to database: %v", err)
+		}
 		m.evLogger.Log(events.DeviceRejected, map[string]string{
 			"name":    hello.DeviceName,
 			"device":  remoteID.String(),
@@ -2577,12 +2588,14 @@ func (m *model) CommitConfiguration(from, to config.Configuration) bool {
 		}
 	}
 
+	removedFolders := make(map[string]struct{})
 	for folderID, fromCfg := range fromFolders {
 		toCfg, ok := toFolders[folderID]
 		if !ok {
 			// The folder was removed.
 			m.removeFolder(fromCfg)
 			clusterConfigDevices.add(fromCfg.DeviceIDs())
+			removedFolders[fromCfg.ID] = struct{}{}
 			continue
 		}
 
@@ -2647,6 +2660,7 @@ func (m *model) CommitConfiguration(from, to config.Configuration) bool {
 			m.evLogger.Log(events.DeviceResumed, map[string]string{"device": deviceID.String()})
 		}
 	}
+	// Clean up after removed devices
 	removedDevices := make([]protocol.DeviceID, 0, len(fromDevices))
 	m.fmut.Lock()
 	for deviceID := range fromDevices {
@@ -2671,6 +2685,9 @@ func (m *model) CommitConfiguration(from, to config.Configuration) bool {
 	// Generating cluster-configs acquires fmut -> must happen outside of pmut.
 	m.resendClusterConfig(clusterConfigDevices.AsSlice())
 
+	ignoredDevices := observedDeviceSet(to.IgnoredDevices)
+	m.cleanPending(toDevices, toFolders, ignoredDevices, removedFolders)
+
 	m.globalRequestLimiter.setCapacity(1024 * to.Options.MaxConcurrentIncomingRequestKiB())
 	m.folderIOLimiter.setCapacity(to.Options.MaxFolderConcurrency())
 
@@ -2685,6 +2702,59 @@ func (m *model) CommitConfiguration(from, to config.Configuration) bool {
 	return true
 }
 
+func (m *model) cleanPending(existingDevices map[protocol.DeviceID]config.DeviceConfiguration, existingFolders map[string]config.FolderConfiguration, ignoredDevices deviceIDSet, removedFolders map[string]struct{}) {
+	pendingFolders, err := m.db.PendingFolders()
+	if err != nil {
+		l.Infof("Could not iterate through pending folder entries for cleanup: %v", err)
+		// Continue with pending devices below, loop is skipped.
+	}
+	for folderID, pf := range pendingFolders {
+		if _, ok := removedFolders[folderID]; ok {
+			// Forget pending folder device associations for recently removed
+			// 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)
+			continue
+		}
+		for deviceID := range pf.OfferedBy {
+			if dev, ok := existingDevices[deviceID]; !ok {
+				l.Debugf("Discarding pending folder %v from unknown device %v", folderID, deviceID)
+				m.db.RemovePendingFolderForDevice(folderID, deviceID)
+				continue
+			} else if dev.IgnoredFolder(folderID) {
+				l.Debugf("Discarding now ignored pending folder %v for device %v", folderID, deviceID)
+				m.db.RemovePendingFolderForDevice(folderID, deviceID)
+				continue
+			}
+			if folderCfg, ok := existingFolders[folderID]; ok {
+				if folderCfg.SharedWith(deviceID) {
+					l.Debugf("Discarding now shared pending folder %v for device %v", folderID, deviceID)
+					m.db.RemovePendingFolderForDevice(folderID, deviceID)
+				}
+			}
+		}
+	}
+
+	pendingDevices, err := m.db.PendingDevices()
+	if err != nil {
+		l.Infof("Could not iterate through pending device entries for cleanup: %v", err)
+		return
+	}
+	for deviceID := range pendingDevices {
+		if _, ok := ignoredDevices[deviceID]; ok {
+			l.Debugf("Discarding now ignored pending device %v", deviceID)
+			m.db.RemovePendingDevice(deviceID)
+			continue
+		}
+		if _, ok := existingDevices[deviceID]; ok {
+			l.Debugf("Discarding now added pending device %v", deviceID)
+			m.db.RemovePendingDevice(deviceID)
+			continue
+		}
+	}
+}
+
 // checkFolderRunningLocked returns nil if the folder is up and running and a
 // descriptive error if not.
 // Need to hold (read) lock on m.fmut when calling this.
@@ -2703,6 +2773,18 @@ func (m *model) checkFolderRunningLocked(folder string) error {
 	return errFolderNotRunning
 }
 
+// PendingDevices lists unknown devices that tried to connect.
+func (m *model) PendingDevices() (map[protocol.DeviceID]db.ObservedDevice, error) {
+	return m.db.PendingDevices()
+}
+
+// PendingFolders lists folders that we don't yet share with the offering devices.  It
+// returns the entries grouped by folder and filters for a given device unless the
+// argument is specified as EmptyDeviceID.
+func (m *model) PendingFolders(device protocol.DeviceID) (map[string]db.PendingFolder, error) {
+	return m.db.PendingFoldersForDevice(device)
+}
+
 // 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 {
@@ -2723,6 +2805,14 @@ func mapDevices(devices []protocol.DeviceID) map[protocol.DeviceID]struct{} {
 	return m
 }
 
+func observedDeviceSet(devices []config.ObservedDevice) deviceIDSet {
+	res := make(deviceIDSet, len(devices))
+	for _, dev := range devices {
+		res[dev.ID] = struct{}{}
+	}
+	return res
+}
+
 func readOffsetIntoBuf(fs fs.Filesystem, file string, offset int64, buf []byte) error {
 	fd, err := fs.Open(file)
 	if err != nil {

+ 72 - 0
lib/model/model_test.go

@@ -4323,6 +4323,78 @@ func TestCCFolderNotRunning(t *testing.T) {
 	}
 }
 
+func TestPendingFolder(t *testing.T) {
+	w, _ := tmpDefaultWrapper()
+	m := setupModel(w)
+	defer cleanupModel(m)
+
+	waiter, err := w.SetDevice(config.DeviceConfiguration{DeviceID: device2})
+	if err != nil {
+		t.Fatal(err)
+	}
+	waiter.Wait()
+	pfolder := "default"
+	if err := m.db.AddOrUpdatePendingFolder(pfolder, pfolder, device2); err != nil {
+		t.Fatal(err)
+	}
+	deviceFolders, err := m.PendingFolders(protocol.EmptyDeviceID)
+	if err != nil {
+		t.Fatal(err)
+	} else if pf, ok := deviceFolders[pfolder]; !ok {
+		t.Errorf("folder %v not pending", pfolder)
+	} else if _, ok := pf.OfferedBy[device2]; !ok {
+		t.Errorf("folder %v not pending for device %v", pfolder, device2)
+	} else if len(pf.OfferedBy) > 1 {
+		t.Errorf("folder %v pending for too many devices %v", pfolder, pf.OfferedBy)
+	}
+
+	device3, err := protocol.DeviceIDFromString("AIBAEAQ-CAIBAEC-AQCAIBA-EAQCAIA-BAEAQCA-IBAEAQC-CAIBAEA-QCAIBA7")
+	waiter, err = w.SetDevice(config.DeviceConfiguration{DeviceID: device3})
+	if err != nil {
+		t.Fatal(err)
+	}
+	waiter.Wait()
+	if err := m.db.AddOrUpdatePendingFolder(pfolder, pfolder, device3); err != nil {
+		t.Fatal(err)
+	}
+	deviceFolders, err = m.PendingFolders(device2)
+	if err != nil {
+		t.Fatal(err)
+	} else if pf, ok := deviceFolders[pfolder]; !ok {
+		t.Errorf("folder %v not pending when filtered", pfolder)
+	} else if _, ok := pf.OfferedBy[device2]; !ok {
+		t.Errorf("folder %v not pending for device %v when filtered", pfolder, device2)
+	} else if _, ok := pf.OfferedBy[device3]; ok {
+		t.Errorf("folder %v pending for device %v, but not filtered out", pfolder, device3)
+	}
+
+	waiter, err = w.RemoveDevice(device3)
+	if err != nil {
+		t.Fatal(err)
+	}
+	waiter.Wait()
+	deviceFolders, err = m.PendingFolders(protocol.EmptyDeviceID)
+	if err != nil {
+		t.Fatal(err)
+	} else if pf, ok := deviceFolders[pfolder]; !ok {
+		t.Errorf("folder %v not pending", pfolder)
+	} else if _, ok := pf.OfferedBy[device3]; ok {
+		t.Errorf("folder %v pending for removed device %v", pfolder, device3)
+	}
+
+	waiter, err = w.RemoveFolder(pfolder)
+	if err != nil {
+		t.Fatal(err)
+	}
+	waiter.Wait()
+	deviceFolders, err = m.PendingFolders(protocol.EmptyDeviceID)
+	if err != nil {
+		t.Fatal(err)
+	} else if _, ok := deviceFolders[pfolder]; ok {
+		t.Errorf("folder %v still pending after local removal", pfolder)
+	}
+}
+
 func equalStringsInAnyOrder(a, b []string) bool {
 	if len(a) != len(b) {
 		return false

+ 1 - 1
proto/lib/config/config.proto

@@ -19,5 +19,5 @@ message Configuration {
     LDAPConfiguration            ldap            = 5 [(ext.goname) = "LDAP"];
     OptionsConfiguration         options         = 6;
     repeated ObservedDevice      ignored_devices = 7 [(ext.json) = "remoteIgnoredDevices", (ext.xml) = "remoteIgnoredDevice"];
-    repeated ObservedDevice      pending_devices = 8;
+    repeated ObservedDevice      pending_devices = 8 [deprecated=true];
 }

+ 1 - 1
proto/lib/config/deviceconfiguration.proto

@@ -22,7 +22,7 @@ message DeviceConfiguration {
     int32                   max_send_kbps              = 12;
     int32                   max_recv_kbps              = 13;
     repeated ObservedFolder ignored_folders            = 14;
-    repeated ObservedFolder pending_folders            = 15;
+    repeated ObservedFolder pending_folders            = 15 [deprecated = true];
     int32                   max_request_kib            = 16 [(ext.goname) = "MaxRequestKiB", (ext.xml) = "maxRequestKiB", (ext.json) = "maxRequestKiB"];
     bool                    untrusted                  = 17;
     int32                   remote_gui_port            = 18 [(ext.goname) = "RemoteGUIPort", (ext.xml) = "remoteGUIPort", (ext.json) = "remoteGUIPort"];

+ 12 - 0
proto/lib/db/structs.proto

@@ -3,6 +3,7 @@ syntax = "proto3";
 package db;
 
 import "repos/protobuf/gogoproto/gogo.proto";
+import "google/protobuf/timestamp.proto";
 import "lib/protocol/bep.proto";
 import "ext.proto";
 
@@ -88,3 +89,14 @@ message VersionListDeprecated {
     option (gogoproto.goproto_stringer) = false;
     repeated FileVersionDeprecated versions = 1;
 }
+
+message ObservedFolder {
+    google.protobuf.Timestamp time  = 1 [(gogoproto.stdtime) = true, (gogoproto.nullable) = false];
+    string                    label = 2;
+}
+
+message ObservedDevice {
+    google.protobuf.Timestamp time    = 1 [(gogoproto.stdtime) = true, (gogoproto.nullable) = false];
+    string                    name    = 2;
+    string                    address = 3;
+}