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)
 			c.Conn.Close()
 			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() {
@@ -235,6 +239,10 @@ func (s *connectionSvc) connect() {
 				continue
 			}
 
+			if s.model.IsPaused(deviceID) {
+				continue
+			}
+
 			connected := s.model.ConnectedTo(deviceID)
 
 			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/shutdown", s.postSystemShutdown)      // -
 	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
 	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) {
 	qs := r.URL.Query()
 	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, "stateChanged")
 		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)

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

@@ -113,6 +113,8 @@
    "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 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 wait": "Please wait",
    "Preview": "Preview",
@@ -130,6 +132,7 @@
    "Restart": "Restart",
    "Restart Needed": "Restart Needed",
    "Restarting": "Restarting",
+   "Resume": "Resume",
    "Reused": "Reused",
    "Save": "Save",
    "Scanning": "Scanning",

+ 12 - 5
gui/index.html

@@ -425,6 +425,7 @@
                   <span ng-switch-when="syncing">
                     <span class="hidden-xs" translate>Syncing</span> ({{completion[deviceCfg.deviceID]._total | number:0}}%)
                   </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="unused"><span class="hidden-xs" translate>Unused</span><span class="visible-xs">&#9724;</span></span>
                 </span>
@@ -434,18 +435,18 @@
               <div class="panel-body">
                 <table class="table table-condensed table-striped">
                   <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>
                       <td class="text-right">{{connections[deviceCfg.deviceID].inbps | binary}}B/s ({{connections[deviceCfg.deviceID].inBytesTotal | binary}}B)</td>
                     </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>
                       <td class="text-right">{{connections[deviceCfg.deviceID].outbps | binary}}B/s ({{connections[deviceCfg.deviceID].outBytesTotal | binary}}B)</td>
                     </tr>
                     <tr>
                       <th>
 												<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>
                       </th>
                       <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>
                       <td translate class="text-right">Yes</td>
                     </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>
                       <td class="text-right">{{connections[deviceCfg.deviceID].clientVersion}}</td>
                     </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>
                       <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>
@@ -482,6 +483,12 @@
                   <button type="button" class="btn btn-sm btn-default" ng-click="editDevice(deviceCfg)">
                     <span class="fa fa-pencil"></span>&nbsp;<span translate>Edit</span>
                   </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>
                 <div class="clearfix"></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_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_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
             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

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

@@ -165,7 +165,7 @@ angular.module('syncthing.core')
         });
 
         $scope.$on(Events.DEVICE_DISCONNECTED, function (event, arg) {
-            delete $scope.connections[arg.data.id];
+            $scope.connections[arg.data.id].connected = false;
             refreshDeviceStats();
         });
 
@@ -209,6 +209,14 @@ angular.module('syncthing.core')
             $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.folderRejections[arg.data.folder + "-" + arg.data.device] = arg;
         });
@@ -625,7 +633,15 @@ angular.module('syncthing.core')
                 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) {
                     return 'insync';
                 } else {
@@ -643,7 +659,15 @@ angular.module('syncthing.core')
                 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) {
                     return 'success';
                 } else {
@@ -657,7 +681,7 @@ angular.module('syncthing.core')
 
         $scope.deviceAddr = function (deviceCfg) {
             var conn = $scope.connections[deviceCfg.deviceID];
-            if (conn) {
+            if (conn && conn.connected) {
                 return conn.address;
             }
             return '?';
@@ -702,6 +726,14 @@ angular.module('syncthing.core')
             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 () {
             // Make a working copy
             $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
 	DeviceDisconnected
 	DeviceRejected
+	DevicePaused
+	DeviceResumed
 	LocalIndexUpdated
 	RemoteIndexUpdated
 	ItemStarted
@@ -78,6 +80,10 @@ func (t EventType) String() string {
 		return "FolderCompletion"
 	case FolderErrors:
 		return "FolderErrors"
+	case DevicePaused:
+		return "DevicePaused"
+	case DeviceResumed:
+		return "DeviceResumed"
 	default:
 		return "Unknown"
 	}

+ 46 - 10
lib/model/model.go

@@ -87,9 +87,10 @@ type Model struct {
 	folderStatRefs map[string]*stats.FolderStatisticsReference            // folder -> statsRef
 	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
 	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),
 		conn:               make(map[protocol.DeviceID]Connection),
 		deviceVer:          make(map[protocol.DeviceID]string),
+		devicePaused:       make(map[protocol.DeviceID]bool),
 		reqValidationCache: make(map[string]time.Time),
 
 		fmut:  sync.NewRWMutex(),
@@ -217,6 +219,8 @@ func (m *Model) StartFolderRO(folder string) {
 
 type ConnectionInfo struct {
 	protocol.Statistics
+	Connected     bool
+	Paused        bool
 	Address       string
 	ClientVersion string
 	Type          ConnectionType
@@ -227,9 +231,11 @@ func (info ConnectionInfo) MarshalJSON() ([]byte, error) {
 		"at":            info.At,
 		"inBytesTotal":  info.InBytesTotal,
 		"outBytesTotal": info.OutBytesTotal,
+		"connected":     info.Connected,
+		"paused":        info.Paused,
 		"address":       info.Address,
-		"type":          info.Type.String(),
 		"clientVersion": info.ClientVersion,
+		"type":          info.Type.String(),
 	})
 }
 
@@ -242,16 +248,21 @@ func (m *Model) ConnectionStats() map[string]interface{} {
 	m.pmut.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{
-			Statistics:    conn.Statistics(),
 			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.Connected = ok
+			ci.Statistics = conn.Statistics()
+			if addr := conn.RemoteAddr(); addr != nil {
+				ci.Address = addr.String()
+			}
 		}
 
 		conns[device.String()] = ci
@@ -956,6 +967,31 @@ func (m *Model) AddConnection(conn Connection) {
 	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 {
 	m.fmut.Lock()
 	defer m.fmut.Unlock()

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