Browse Source

Pause and resume devices (ref #215)

Jakob Borg 10 years ago
parent
commit
944d9c84a0

+ 8 - 0
cmd/syncthing/connections.go

@@ -158,6 +158,10 @@ next:
 			l.Infof("Connected to already connected device (%s)", remoteID)
 			l.Infof("Connected to already connected device (%s)", remoteID)
 			c.Conn.Close()
 			c.Conn.Close()
 			continue
 			continue
+		} else if s.model.IsPaused(remoteID) {
+			l.Infof("Connection from paused device (%s)", remoteID)
+			c.Conn.Close()
+			continue
 		}
 		}
 
 
 		for deviceID, deviceCfg := range s.cfg.Devices() {
 		for deviceID, deviceCfg := range s.cfg.Devices() {
@@ -235,6 +239,10 @@ func (s *connectionSvc) connect() {
 				continue
 				continue
 			}
 			}
 
 
+			if s.model.IsPaused(deviceID) {
+				continue
+			}
+
 			connected := s.model.ConnectedTo(deviceID)
 			connected := s.model.ConnectedTo(deviceID)
 
 
 			s.mut.RLock()
 			s.mut.RLock()

+ 28 - 0
cmd/syncthing/gui.go

@@ -169,6 +169,8 @@ func (s *apiSvc) Serve() {
 	postRestMux.HandleFunc("/rest/system/restart", s.postSystemRestart)        // -
 	postRestMux.HandleFunc("/rest/system/restart", s.postSystemRestart)        // -
 	postRestMux.HandleFunc("/rest/system/shutdown", s.postSystemShutdown)      // -
 	postRestMux.HandleFunc("/rest/system/shutdown", s.postSystemShutdown)      // -
 	postRestMux.HandleFunc("/rest/system/upgrade", s.postSystemUpgrade)        // -
 	postRestMux.HandleFunc("/rest/system/upgrade", s.postSystemUpgrade)        // -
+	postRestMux.HandleFunc("/rest/system/pause", s.postSystemPause)            // device
+	postRestMux.HandleFunc("/rest/system/resume", s.postSystemResume)          // device
 
 
 	// Debug endpoints, not for general use
 	// Debug endpoints, not for general use
 	getRestMux.HandleFunc("/rest/debug/peerCompletion", s.getPeerCompletion)
 	getRestMux.HandleFunc("/rest/debug/peerCompletion", s.getPeerCompletion)
@@ -833,6 +835,32 @@ func (s *apiSvc) postSystemUpgrade(w http.ResponseWriter, r *http.Request) {
 	}
 	}
 }
 }
 
 
+func (s *apiSvc) postSystemPause(w http.ResponseWriter, r *http.Request) {
+	var qs = r.URL.Query()
+	var deviceStr = qs.Get("device")
+
+	device, err := protocol.DeviceIDFromString(deviceStr)
+	if err != nil {
+		http.Error(w, err.Error(), 500)
+		return
+	}
+
+	s.model.PauseDevice(device)
+}
+
+func (s *apiSvc) postSystemResume(w http.ResponseWriter, r *http.Request) {
+	var qs = r.URL.Query()
+	var deviceStr = qs.Get("device")
+
+	device, err := protocol.DeviceIDFromString(deviceStr)
+	if err != nil {
+		http.Error(w, err.Error(), 500)
+		return
+	}
+
+	s.model.ResumeDevice(device)
+}
+
 func (s *apiSvc) postDBScan(w http.ResponseWriter, r *http.Request) {
 func (s *apiSvc) postDBScan(w http.ResponseWriter, r *http.Request) {
 	qs := r.URL.Query()
 	qs := r.URL.Query()
 	folder := qs.Get("folder")
 	folder := qs.Get("folder")

+ 9 - 0
cmd/syncthing/verbose.go

@@ -123,6 +123,15 @@ func (s *verboseSvc) formatEvent(ev events.Event) string {
 		delete(sum, "ignorePatterns")
 		delete(sum, "ignorePatterns")
 		delete(sum, "stateChanged")
 		delete(sum, "stateChanged")
 		return fmt.Sprintf("Summary for folder %q is %v", data["folder"], data["summary"])
 		return fmt.Sprintf("Summary for folder %q is %v", data["folder"], data["summary"])
+
+	case events.DevicePaused:
+		data := ev.Data.(map[string]string)
+		device := data["device"]
+		return fmt.Sprintf("Device %v was paused", device)
+	case events.DeviceResumed:
+		data := ev.Data.(map[string]string)
+		device := data["device"]
+		return fmt.Sprintf("Device %v was resumed", device)
 	}
 	}
 
 
 	return fmt.Sprintf("%s %#v", ev.Type, ev)
 	return fmt.Sprintf("%s %#v", ev.Type, ev)

+ 3 - 0
gui/assets/lang/lang-en.json

@@ -113,6 +113,8 @@
    "Override Changes": "Override Changes",
    "Override Changes": "Override Changes",
    "Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for",
    "Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for",
    "Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Path where versions should be stored (leave empty for the default .stversions folder in the folder).",
    "Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Path where versions should be stored (leave empty for the default .stversions folder in the folder).",
+   "Pause": "Pause",
+   "Paused": "Paused",
    "Please consult the release notes before performing a major upgrade.": "Please consult the release notes before performing a major upgrade.",
    "Please consult the release notes before performing a major upgrade.": "Please consult the release notes before performing a major upgrade.",
    "Please wait": "Please wait",
    "Please wait": "Please wait",
    "Preview": "Preview",
    "Preview": "Preview",
@@ -130,6 +132,7 @@
    "Restart": "Restart",
    "Restart": "Restart",
    "Restart Needed": "Restart Needed",
    "Restart Needed": "Restart Needed",
    "Restarting": "Restarting",
    "Restarting": "Restarting",
+   "Resume": "Resume",
    "Reused": "Reused",
    "Reused": "Reused",
    "Save": "Save",
    "Save": "Save",
    "Scanning": "Scanning",
    "Scanning": "Scanning",

+ 12 - 5
gui/index.html

@@ -425,6 +425,7 @@
                   <span ng-switch-when="syncing">
                   <span ng-switch-when="syncing">
                     <span class="hidden-xs" translate>Syncing</span> ({{completion[deviceCfg.deviceID]._total | number:0}}%)
                     <span class="hidden-xs" translate>Syncing</span> ({{completion[deviceCfg.deviceID]._total | number:0}}%)
                   </span>
                   </span>
+                  <span ng-switch-when="paused"><span class="hidden-xs" translate>Paused</span><span class="visible-xs">&#9724;</span></span>
                   <span ng-switch-when="disconnected"><span class="hidden-xs" translate>Disconnected</span><span class="visible-xs">&#9724;</span></span>
                   <span ng-switch-when="disconnected"><span class="hidden-xs" translate>Disconnected</span><span class="visible-xs">&#9724;</span></span>
                   <span ng-switch-when="unused"><span class="hidden-xs" translate>Unused</span><span class="visible-xs">&#9724;</span></span>
                   <span ng-switch-when="unused"><span class="hidden-xs" translate>Unused</span><span class="visible-xs">&#9724;</span></span>
                 </span>
                 </span>
@@ -434,18 +435,18 @@
               <div class="panel-body">
               <div class="panel-body">
                 <table class="table table-condensed table-striped">
                 <table class="table table-condensed table-striped">
                   <tbody>
                   <tbody>
-                    <tr ng-if="connections[deviceCfg.deviceID]">
+                    <tr ng-if="connections[deviceCfg.deviceID].connected">
                       <th><span class="fa fa-fw fa-cloud-download"></span>&nbsp;<span translate>Download Rate</span></th>
                       <th><span class="fa fa-fw fa-cloud-download"></span>&nbsp;<span translate>Download Rate</span></th>
                       <td class="text-right">{{connections[deviceCfg.deviceID].inbps | binary}}B/s ({{connections[deviceCfg.deviceID].inBytesTotal | binary}}B)</td>
                       <td class="text-right">{{connections[deviceCfg.deviceID].inbps | binary}}B/s ({{connections[deviceCfg.deviceID].inBytesTotal | binary}}B)</td>
                     </tr>
                     </tr>
-                    <tr ng-if="connections[deviceCfg.deviceID]">
+                    <tr ng-if="connections[deviceCfg.deviceID].connected">
                       <th><span class="fa fa-fw fa-cloud-upload"></span>&nbsp;<span translate>Upload Rate</span></th>
                       <th><span class="fa fa-fw fa-cloud-upload"></span>&nbsp;<span translate>Upload Rate</span></th>
                       <td class="text-right">{{connections[deviceCfg.deviceID].outbps | binary}}B/s ({{connections[deviceCfg.deviceID].outBytesTotal | binary}}B)</td>
                       <td class="text-right">{{connections[deviceCfg.deviceID].outbps | binary}}B/s ({{connections[deviceCfg.deviceID].outBytesTotal | binary}}B)</td>
                     </tr>
                     </tr>
                     <tr>
                     <tr>
                       <th>
                       <th>
 												<span class="fa fa-fw fa-link"></span>
 												<span class="fa fa-fw fa-link"></span>
-                        <span translate ng-if="connections[deviceCfg.deviceID].type.indexOf('basic') == 0" >Address</span>
+                        <span translate ng-if="connections[deviceCfg.deviceID].type.indexOf('direct') == 0" >Address</span>
                         <span translate ng-if="connections[deviceCfg.deviceID].type.indexOf('relay') == 0" >Relayed via</span>
                         <span translate ng-if="connections[deviceCfg.deviceID].type.indexOf('relay') == 0" >Relayed via</span>
                       </th>
                       </th>
                       <td class="text-right">{{deviceAddr(deviceCfg)}}</td>
                       <td class="text-right">{{deviceAddr(deviceCfg)}}</td>
@@ -461,11 +462,11 @@
                       <th><span class="fa fa-fw fa-thumbs-o-up"></span>&nbsp;<span translate>Introducer</span></th>
                       <th><span class="fa fa-fw fa-thumbs-o-up"></span>&nbsp;<span translate>Introducer</span></th>
                       <td translate class="text-right">Yes</td>
                       <td translate class="text-right">Yes</td>
                     </tr>
                     </tr>
-                    <tr ng-if="connections[deviceCfg.deviceID]">
+                    <tr ng-if="connections[deviceCfg.deviceID].version">
                       <th><span class="fa fa-fw fa-tag"></span>&nbsp;<span translate>Version</span></th>
                       <th><span class="fa fa-fw fa-tag"></span>&nbsp;<span translate>Version</span></th>
                       <td class="text-right">{{connections[deviceCfg.deviceID].clientVersion}}</td>
                       <td class="text-right">{{connections[deviceCfg.deviceID].clientVersion}}</td>
                     </tr>
                     </tr>
-                    <tr ng-if="!connections[deviceCfg.deviceID]">
+                    <tr ng-if="!connections[deviceCfg.deviceID].connected">
                       <th><span class="fa fa-fw fa-eye"></span>&nbsp;<span translate>Last seen</span></th>
                       <th><span class="fa fa-fw fa-eye"></span>&nbsp;<span translate>Last seen</span></th>
                       <td translate ng-if="!deviceStats[deviceCfg.deviceID].lastSeenDays || deviceStats[deviceCfg.deviceID].lastSeenDays >= 365" class="text-right">Never</td>
                       <td translate ng-if="!deviceStats[deviceCfg.deviceID].lastSeenDays || deviceStats[deviceCfg.deviceID].lastSeenDays >= 365" class="text-right">Never</td>
                       <td ng-if="deviceStats[deviceCfg.deviceID].lastSeenDays < 365" class="text-right">{{deviceStats[deviceCfg.deviceID].lastSeen | date:"yyyy-MM-dd HH:mm:ss"}}</td>
                       <td ng-if="deviceStats[deviceCfg.deviceID].lastSeenDays < 365" class="text-right">{{deviceStats[deviceCfg.deviceID].lastSeen | date:"yyyy-MM-dd HH:mm:ss"}}</td>
@@ -482,6 +483,12 @@
                   <button type="button" class="btn btn-sm btn-default" ng-click="editDevice(deviceCfg)">
                   <button type="button" class="btn btn-sm btn-default" ng-click="editDevice(deviceCfg)">
                     <span class="fa fa-pencil"></span>&nbsp;<span translate>Edit</span>
                     <span class="fa fa-pencil"></span>&nbsp;<span translate>Edit</span>
                   </button>
                   </button>
+                  <button ng-if="!connections[deviceCfg.deviceID].paused" type="button" class="btn btn-sm btn-default" ng-click="pauseDevice(deviceCfg.deviceID)">
+                    <span class="fa fa-pause"></span>&nbsp;<span translate>Pause</span>
+                  </button>
+                  <button ng-if="connections[deviceCfg.deviceID].paused" type="button" class="btn btn-sm btn-default" ng-click="resumeDevice(deviceCfg.deviceID)">
+                    <span class="fa fa-play"></span>&nbsp;<span translate>Resume</span>
+                  </button>
                 </span>
                 </span>
                 <div class="clearfix"></div>
                 <div class="clearfix"></div>
               </div>
               </div>

+ 2 - 0
gui/syncthing/core/eventService.js

@@ -63,6 +63,8 @@ angular.module('syncthing.core')
             DEVICE_DISCONNECTED:  'DeviceDisconnected',   // Generated each time a connection to a device has been terminated
             DEVICE_DISCONNECTED:  'DeviceDisconnected',   // Generated each time a connection to a device has been terminated
             DEVICE_DISCOVERED:    'DeviceDiscovered',   // Emitted when a new device is discovered using local discovery
             DEVICE_DISCOVERED:    'DeviceDiscovered',   // Emitted when a new device is discovered using local discovery
             DEVICE_REJECTED:      'DeviceRejected',   // Emitted when there is a connection from a device we are not configured to talk to
             DEVICE_REJECTED:      'DeviceRejected',   // Emitted when there is a connection from a device we are not configured to talk to
+            DEVICE_PAUSED:        'DevicePaused',   // Emitted when a device has been paused
+            DEVICE_RESUMED:       'DeviceResumed',   // Emitted when a device has been resumed
             DOWNLOAD_PROGRESS:    'DownloadProgress',   // Emitted during file downloads for each folder for each file
             DOWNLOAD_PROGRESS:    'DownloadProgress',   // Emitted during file downloads for each folder for each file
             FOLDER_COMPLETION:    'FolderCompletion',   //Emitted when the local or remote contents for a folder changes
             FOLDER_COMPLETION:    'FolderCompletion',   //Emitted when the local or remote contents for a folder changes
             FOLDER_REJECTED:      'FolderRejected',   // Emitted when a device sends index information for a folder we do not have, or have but do not share with the device in question
             FOLDER_REJECTED:      'FolderRejected',   // Emitted when a device sends index information for a folder we do not have, or have but do not share with the device in question

+ 36 - 4
gui/syncthing/core/syncthingController.js

@@ -165,7 +165,7 @@ angular.module('syncthing.core')
         });
         });
 
 
         $scope.$on(Events.DEVICE_DISCONNECTED, function (event, arg) {
         $scope.$on(Events.DEVICE_DISCONNECTED, function (event, arg) {
-            delete $scope.connections[arg.data.id];
+            $scope.connections[arg.data.id].connected = false;
             refreshDeviceStats();
             refreshDeviceStats();
         });
         });
 
 
@@ -209,6 +209,14 @@ angular.module('syncthing.core')
             $scope.deviceRejections[arg.data.device] = arg;
             $scope.deviceRejections[arg.data.device] = arg;
         });
         });
 
 
+        $scope.$on(Events.DEVICE_PAUSED, function (event, arg) {
+            $scope.connections[arg.data.device].paused = true;
+        });
+
+        $scope.$on(Events.DEVICE_RESUMED, function (event, arg) {
+            $scope.connections[arg.data.device].paused = false;
+        });
+
         $scope.$on(Events.FOLDER_REJECTED, function (event, arg) {
         $scope.$on(Events.FOLDER_REJECTED, function (event, arg) {
             $scope.folderRejections[arg.data.folder + "-" + arg.data.device] = arg;
             $scope.folderRejections[arg.data.folder + "-" + arg.data.device] = arg;
         });
         });
@@ -625,7 +633,15 @@ angular.module('syncthing.core')
                 return 'unused';
                 return 'unused';
             }
             }
 
 
-            if ($scope.connections[deviceCfg.deviceID]) {
+            if (typeof $scope.connections[deviceCfg.deviceID] === 'undefined') {
+                return 'unknown';
+            }
+
+            if ($scope.connections[deviceCfg.deviceID].paused) {
+                return 'paused';
+            }
+
+            if ($scope.connections[deviceCfg.deviceID].connected) {
                 if ($scope.completion[deviceCfg.deviceID] && $scope.completion[deviceCfg.deviceID]._total === 100) {
                 if ($scope.completion[deviceCfg.deviceID] && $scope.completion[deviceCfg.deviceID]._total === 100) {
                     return 'insync';
                     return 'insync';
                 } else {
                 } else {
@@ -643,7 +659,15 @@ angular.module('syncthing.core')
                 return 'warning';
                 return 'warning';
             }
             }
 
 
-            if ($scope.connections[deviceCfg.deviceID]) {
+            if (typeof $scope.connections[deviceCfg.deviceID] === 'undefined') {
+                return 'info';
+            }
+
+            if ($scope.connections[deviceCfg.deviceID].paused) {
+                return 'default';
+            }
+
+            if ($scope.connections[deviceCfg.deviceID].connected) {
                 if ($scope.completion[deviceCfg.deviceID] && $scope.completion[deviceCfg.deviceID]._total === 100) {
                 if ($scope.completion[deviceCfg.deviceID] && $scope.completion[deviceCfg.deviceID]._total === 100) {
                     return 'success';
                     return 'success';
                 } else {
                 } else {
@@ -657,7 +681,7 @@ angular.module('syncthing.core')
 
 
         $scope.deviceAddr = function (deviceCfg) {
         $scope.deviceAddr = function (deviceCfg) {
             var conn = $scope.connections[deviceCfg.deviceID];
             var conn = $scope.connections[deviceCfg.deviceID];
-            if (conn) {
+            if (conn && conn.connected) {
                 return conn.address;
                 return conn.address;
             }
             }
             return '?';
             return '?';
@@ -702,6 +726,14 @@ angular.module('syncthing.core')
             return device.deviceID.substr(0, 6);
             return device.deviceID.substr(0, 6);
         };
         };
 
 
+        $scope.pauseDevice = function (device) {
+            $http.post(urlbase + "/system/pause?device=" + device);
+        };
+
+        $scope.resumeDevice = function (device) {
+            $http.post(urlbase + "/system/resume?device=" + device);
+        };
+
         $scope.editSettings = function () {
         $scope.editSettings = function () {
             // Make a working copy
             // Make a working copy
             $scope.tmpOptions = angular.copy($scope.config.options);
             $scope.tmpOptions = angular.copy($scope.config.options);

File diff suppressed because it is too large
+ 1 - 1
lib/auto/gui.files.go


+ 6 - 0
lib/events/events.go

@@ -25,6 +25,8 @@ const (
 	DeviceConnected
 	DeviceConnected
 	DeviceDisconnected
 	DeviceDisconnected
 	DeviceRejected
 	DeviceRejected
+	DevicePaused
+	DeviceResumed
 	LocalIndexUpdated
 	LocalIndexUpdated
 	RemoteIndexUpdated
 	RemoteIndexUpdated
 	ItemStarted
 	ItemStarted
@@ -78,6 +80,10 @@ func (t EventType) String() string {
 		return "FolderCompletion"
 		return "FolderCompletion"
 	case FolderErrors:
 	case FolderErrors:
 		return "FolderErrors"
 		return "FolderErrors"
+	case DevicePaused:
+		return "DevicePaused"
+	case DeviceResumed:
+		return "DeviceResumed"
 	default:
 	default:
 		return "Unknown"
 		return "Unknown"
 	}
 	}

+ 46 - 10
lib/model/model.go

@@ -87,9 +87,10 @@ type Model struct {
 	folderStatRefs map[string]*stats.FolderStatisticsReference            // folder -> statsRef
 	folderStatRefs map[string]*stats.FolderStatisticsReference            // folder -> statsRef
 	fmut           sync.RWMutex                                           // protects the above
 	fmut           sync.RWMutex                                           // protects the above
 
 
-	conn      map[protocol.DeviceID]Connection
-	deviceVer map[protocol.DeviceID]string
-	pmut      sync.RWMutex // protects conn and deviceVer
+	conn         map[protocol.DeviceID]Connection
+	deviceVer    map[protocol.DeviceID]string
+	devicePaused map[protocol.DeviceID]bool
+	pmut         sync.RWMutex // protects the above
 
 
 	reqValidationCache map[string]time.Time // folder / file name => time when confirmed to exist
 	reqValidationCache map[string]time.Time // folder / file name => time when confirmed to exist
 	rvmut              sync.RWMutex         // protects reqValidationCache
 	rvmut              sync.RWMutex         // protects reqValidationCache
@@ -131,6 +132,7 @@ func NewModel(cfg *config.Wrapper, id protocol.DeviceID, deviceName, clientName,
 		folderStatRefs:     make(map[string]*stats.FolderStatisticsReference),
 		folderStatRefs:     make(map[string]*stats.FolderStatisticsReference),
 		conn:               make(map[protocol.DeviceID]Connection),
 		conn:               make(map[protocol.DeviceID]Connection),
 		deviceVer:          make(map[protocol.DeviceID]string),
 		deviceVer:          make(map[protocol.DeviceID]string),
+		devicePaused:       make(map[protocol.DeviceID]bool),
 		reqValidationCache: make(map[string]time.Time),
 		reqValidationCache: make(map[string]time.Time),
 
 
 		fmut:  sync.NewRWMutex(),
 		fmut:  sync.NewRWMutex(),
@@ -217,6 +219,8 @@ func (m *Model) StartFolderRO(folder string) {
 
 
 type ConnectionInfo struct {
 type ConnectionInfo struct {
 	protocol.Statistics
 	protocol.Statistics
+	Connected     bool
+	Paused        bool
 	Address       string
 	Address       string
 	ClientVersion string
 	ClientVersion string
 	Type          ConnectionType
 	Type          ConnectionType
@@ -227,9 +231,11 @@ func (info ConnectionInfo) MarshalJSON() ([]byte, error) {
 		"at":            info.At,
 		"at":            info.At,
 		"inBytesTotal":  info.InBytesTotal,
 		"inBytesTotal":  info.InBytesTotal,
 		"outBytesTotal": info.OutBytesTotal,
 		"outBytesTotal": info.OutBytesTotal,
+		"connected":     info.Connected,
+		"paused":        info.Paused,
 		"address":       info.Address,
 		"address":       info.Address,
-		"type":          info.Type.String(),
 		"clientVersion": info.ClientVersion,
 		"clientVersion": info.ClientVersion,
+		"type":          info.Type.String(),
 	})
 	})
 }
 }
 
 
@@ -242,16 +248,21 @@ func (m *Model) ConnectionStats() map[string]interface{} {
 	m.pmut.RLock()
 	m.pmut.RLock()
 	m.fmut.RLock()
 	m.fmut.RLock()
 
 
-	var res = make(map[string]interface{})
-	conns := make(map[string]ConnectionInfo, len(m.conn))
-	for device, conn := range m.conn {
+	res := make(map[string]interface{})
+	devs := m.cfg.Devices()
+	conns := make(map[string]ConnectionInfo, len(devs))
+	for device := range devs {
 		ci := ConnectionInfo{
 		ci := ConnectionInfo{
-			Statistics:    conn.Statistics(),
 			ClientVersion: m.deviceVer[device],
 			ClientVersion: m.deviceVer[device],
+			Paused:        m.devicePaused[device],
 		}
 		}
-		if addr := m.conn[device].RemoteAddr(); addr != nil {
-			ci.Address = addr.String()
+		if conn, ok := m.conn[device]; ok {
 			ci.Type = conn.Type
 			ci.Type = conn.Type
+			ci.Connected = ok
+			ci.Statistics = conn.Statistics()
+			if addr := conn.RemoteAddr(); addr != nil {
+				ci.Address = addr.String()
+			}
 		}
 		}
 
 
 		conns[device.String()] = ci
 		conns[device.String()] = ci
@@ -956,6 +967,31 @@ func (m *Model) AddConnection(conn Connection) {
 	m.deviceWasSeen(deviceID)
 	m.deviceWasSeen(deviceID)
 }
 }
 
 
+func (m *Model) PauseDevice(device protocol.DeviceID) {
+	m.pmut.Lock()
+	m.devicePaused[device] = true
+	_, ok := m.conn[device]
+	m.pmut.Unlock()
+	if ok {
+		m.Close(device, errors.New("device paused"))
+	}
+	events.Default.Log(events.DevicePaused, map[string]string{"device": device.String()})
+}
+
+func (m *Model) ResumeDevice(device protocol.DeviceID) {
+	m.pmut.Lock()
+	m.devicePaused[device] = false
+	m.pmut.Unlock()
+	events.Default.Log(events.DeviceResumed, map[string]string{"device": device.String()})
+}
+
+func (m *Model) IsPaused(device protocol.DeviceID) bool {
+	m.pmut.Lock()
+	paused := m.devicePaused[device]
+	m.pmut.Unlock()
+	return paused
+}
+
 func (m *Model) deviceStatRef(deviceID protocol.DeviceID) *stats.DeviceStatisticsReference {
 func (m *Model) deviceStatRef(deviceID protocol.DeviceID) *stats.DeviceStatisticsReference {
 	m.fmut.Lock()
 	m.fmut.Lock()
 	defer m.fmut.Unlock()
 	defer m.fmut.Unlock()

Some files were not shown because too many files changed in this diff