Ver código fonte

all: Add a global change list

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3694
Nathan Morrison 8 anos atrás
pai
commit
0725e3af38

+ 2 - 2
cmd/syncthing/main.go

@@ -554,8 +554,8 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
 	// Event subscription for the API; must start early to catch the early
 	// events. The LocalChangeDetected event might overwhelm the event
 	// receiver in some situations so we will not subscribe to it here.
-	apiSub := events.NewBufferedSubscription(events.Default.Subscribe(events.AllEvents&^events.LocalChangeDetected), 1000)
-	diskSub := events.NewBufferedSubscription(events.Default.Subscribe(events.LocalChangeDetected), 1000)
+	apiSub := events.NewBufferedSubscription(events.Default.Subscribe(events.AllEvents&^events.LocalChangeDetected&^events.RemoteChangeDetected), 1000)
+	diskSub := events.NewBufferedSubscription(events.Default.Subscribe(events.LocalChangeDetected|events.RemoteChangeDetected), 1000)
 
 	if len(os.Getenv("GOMAXPROCS")) == 0 {
 		runtime.GOMAXPROCS(runtime.NumCPU())

+ 4 - 1
cmd/syncthing/verboseservice.go

@@ -94,9 +94,12 @@ func (s *verboseService) formatEvent(ev events.Event) string {
 
 	case events.LocalChangeDetected:
 		data := ev.Data.(map[string]string)
-		// Local change detected in folder "foo": modified file /Users/jb/whatever
 		return fmt.Sprintf("Local change detected in folder %q: %s %s %s", data["folder"], data["action"], data["type"], data["path"])
 
+	case events.RemoteChangeDetected:
+		data := ev.Data.(map[string]string)
+		return fmt.Sprintf("Remote change detected in folder %q: %s %s %s", data["folder"], data["action"], data["type"], data["path"])
+
 	case events.RemoteIndexUpdated:
 		data := ev.Data.(map[string]interface{})
 		return fmt.Sprintf("Device %v sent an index update for %q with %d items", data["device"], data["folder"], data["items"])

+ 9 - 3
gui/default/index.html

@@ -617,9 +617,14 @@
           </div>
         </div>
         <div class="form-group">
-          <button type="button" class="btn btn-sm btn-default pull-right" ng-click="addDevice()">
-            <span class="fa fa-plus"></span>&nbsp;<span translate>Add Remote Device</span>
-          </button>
+          <span class="pull-right">
+            <button type="button" class="btn btn-sm btn-default" ng-click="globalChanges()">
+              <span class="fa fa-fw fa-history"></span>&nbsp;<span translate>Global Changes</span>
+            </button>
+            <button type="button" class="btn btn-sm btn-default" ng-click="addDevice()">
+              <span class="fa fa-plus"></span>&nbsp;<span translate>Add Remote Device</span>
+            </button>
+          </span>
           <div class="clearfix"></div>
         </div>
       </div>
@@ -651,6 +656,7 @@
   <ng-include src="'syncthing/core/shutdownDialogView.html'"></ng-include>
   <ng-include src="'syncthing/device/idqrModalView.html'"></ng-include>
   <ng-include src="'syncthing/device/editDeviceModalView.html'"></ng-include>
+  <ng-include src="'syncthing/device/globalChangesModalView.html'"></ng-include>
   <ng-include src="'syncthing/folder/editFolderModalView.html'"></ng-include>
   <ng-include src="'syncthing/folder/editIgnoresModalView.html'"></ng-include>
   <ng-include src="'syncthing/settings/settingsModalView.html'"></ng-include>

+ 26 - 1
gui/default/syncthing/core/syncthingController.js

@@ -51,6 +51,7 @@ angular.module('syncthing.core')
         $scope.failedPageSize = 10;
         $scope.scanProgress = {};
         $scope.themes = [];
+        $scope.globalChangeEvents = {};
 
         $scope.localStateTotal = {
             bytes: 0,
@@ -186,6 +187,7 @@ angular.module('syncthing.core')
 
         $scope.$on(Events.LOCAL_INDEX_UPDATED, function (event, arg) {
             refreshFolderStats();
+            refreshGlobalChanges();
         });
 
         $scope.$on(Events.DEVICE_DISCONNECTED, function (event, arg) {
@@ -629,6 +631,15 @@ angular.module('syncthing.core')
             }).error($scope.emitHTTPError);
         }, 2500);
 
+        var refreshGlobalChanges = debounce(function () {
+            $http.get(urlbase + "/events/disk?limit=15").success(function (data) {
+                data = data.reverse();
+                $scope.globalChangeEvents = data;
+
+                console.log("refreshGlobalChanges", data);
+            }).error($scope.emitHTTPError);
+        }, 2500);
+
         $scope.refresh = function () {
             refreshSystem();
             refreshDiscoveryCache();
@@ -912,6 +923,16 @@ angular.module('syncthing.core')
             return '';
         };
 
+        $scope.friendlyNameFromShort = function (shortID) {
+            var matches = $scope.devices.filter(function (n) {
+                return n.deviceID.substr(0, 7) === shortID;
+            });
+            if (matches.length !== 1) {
+                return shortID;
+            }
+            return matches[0].name;
+        };
+
         $scope.findDevice = function (deviceID) {
             var matches = $scope.devices.filter(function (n) {
                 return n.deviceID === deviceID;
@@ -1268,7 +1289,11 @@ angular.module('syncthing.core')
                     $scope.folderEditor = form;
                     break;
             }
-        }
+        };
+
+        $scope.globalChanges = function () {
+            $('#globalChanges').modal();
+        };
 
         $scope.editFolder = function (folderCfg) {
             $scope.currentFolder = angular.copy(folderCfg);

+ 27 - 0
gui/default/syncthing/device/globalChangesModalView.html

@@ -0,0 +1,27 @@
+<style> th, td { padding: 6px; } </style>
+<modal id="globalChanges" status="default" icon="{{'history'}}" heading="{{'Global Changes' | translate}}" large="yes" closeable="yes">
+    <div class="modal-body">
+        <table>
+          <tr>
+            <th translate>Device</th>
+            <th translate>Action</th>
+            <th translate>Type</th>
+            <th translate>Path</th>
+            <th translate>Time</th>
+          </tr>
+          <tr ng-repeat="changeEvent in globalChangeEvents">
+            <td ng-if="changeEvent.data.modifiedBy">{{friendlyNameFromShort(changeEvent.data.modifiedBy)}}</td>
+            <td ng-if="!changeEvent.data.modifiedBy"><span translate>Unknown</span></td>
+            <td>{{changeEvent.data.action}}</td>
+            <td>{{changeEvent.data.type}}</td>
+            <td>{{changeEvent.data.path}}</td>
+            <td>{{changeEvent.time | date:'medium'}}</td>
+          </tr>
+        </table>
+    </div>
+    <div class="modal-footer">
+        <button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
+        <span class="fa fa-times"></span>&nbsp;<span translate>Close</span>
+        </button>
+    </div>
+</model>

+ 28 - 0
lib/db/structs.pb.go

@@ -58,6 +58,7 @@ type FileInfoTruncated struct {
 	Permissions   uint32                `protobuf:"varint,4,opt,name=permissions,proto3" json:"permissions,omitempty"`
 	ModifiedS     int64                 `protobuf:"varint,5,opt,name=modified_s,json=modifiedS,proto3" json:"modified_s,omitempty"`
 	ModifiedNs    int32                 `protobuf:"varint,11,opt,name=modified_ns,json=modifiedNs,proto3" json:"modified_ns,omitempty"`
+	ModifiedBy    protocol.ShortID      `protobuf:"varint,12,opt,name=modified_by,json=modifiedBy,proto3,customtype=protocol.ShortID" json:"modified_by"`
 	Deleted       bool                  `protobuf:"varint,6,opt,name=deleted,proto3" json:"deleted,omitempty"`
 	Invalid       bool                  `protobuf:"varint,7,opt,name=invalid,proto3" json:"invalid,omitempty"`
 	NoPermissions bool                  `protobuf:"varint,8,opt,name=no_permissions,json=noPermissions,proto3" json:"no_permissions,omitempty"`
@@ -226,6 +227,11 @@ func (m *FileInfoTruncated) MarshalTo(data []byte) (int, error) {
 		i++
 		i = encodeVarintStructs(data, i, uint64(m.ModifiedNs))
 	}
+	if m.ModifiedBy != 0 {
+		data[i] = 0x60
+		i++
+		i = encodeVarintStructs(data, i, uint64(m.ModifiedBy))
+	}
 	if len(m.SymlinkTarget) > 0 {
 		data[i] = 0x8a
 		i++
@@ -324,6 +330,9 @@ func (m *FileInfoTruncated) ProtoSize() (n int) {
 	if m.ModifiedNs != 0 {
 		n += 1 + sovStructs(uint64(m.ModifiedNs))
 	}
+	if m.ModifiedBy != 0 {
+		n += 1 + sovStructs(uint64(m.ModifiedBy))
+	}
 	l = len(m.SymlinkTarget)
 	if l > 0 {
 		n += 2 + l + sovStructs(uint64(l))
@@ -798,6 +807,25 @@ func (m *FileInfoTruncated) Unmarshal(data []byte) error {
 					break
 				}
 			}
+		case 12:
+			if wireType != 0 {
+				return fmt.Errorf("proto: wrong wireType = %d for field ModifiedBy", wireType)
+			}
+			m.ModifiedBy = 0
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowStructs
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := data[iNdEx]
+				iNdEx++
+				m.ModifiedBy |= (protocol.ShortID(b) & 0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
 		case 17:
 			if wireType != 2 {
 				return fmt.Errorf("proto: wrong wireType = %d for field SymlinkTarget", wireType)

+ 1 - 0
lib/db/structs.proto

@@ -28,6 +28,7 @@ message FileInfoTruncated {
     uint32                permissions    = 4;
     int64                 modified_s     = 5;
     int32                 modified_ns    = 11;
+    uint64                modified_by    = 12 [(gogoproto.customtype) = "protocol.ShortID", (gogoproto.nullable) = false];
     bool                  deleted        = 6;
     bool                  invalid        = 7;
     bool                  no_permissions = 8;

+ 3 - 0
lib/events/events.go

@@ -29,6 +29,7 @@ const (
 	DevicePaused
 	DeviceResumed
 	LocalChangeDetected
+	RemoteChangeDetected
 	LocalIndexUpdated
 	RemoteIndexUpdated
 	ItemStarted
@@ -68,6 +69,8 @@ func (t EventType) String() string {
 		return "DeviceRejected"
 	case LocalChangeDetected:
 		return "LocalChangeDetected"
+	case RemoteChangeDetected:
+		return "RemoteChangeDetected"
 	case LocalIndexUpdated:
 		return "LocalIndexUpdated"
 	case RemoteIndexUpdated:

+ 19 - 9
lib/model/model.go

@@ -1551,12 +1551,18 @@ func (m *Model) updateLocalsFromScanning(folder string, fs []protocol.FileInfo)
 	m.fmut.RLock()
 	folderCfg := m.folderCfgs[folder]
 	m.fmut.RUnlock()
-	// Fire the LocalChangeDetected event to notify listeners about local updates.
-	m.localChangeDetected(folderCfg, fs)
+
+	m.diskChangeDetected(folderCfg, fs, events.LocalChangeDetected)
 }
 
 func (m *Model) updateLocalsFromPulling(folder string, fs []protocol.FileInfo) {
 	m.updateLocals(folder, fs)
+
+	m.fmut.RLock()
+	folderCfg := m.folderCfgs[folder]
+	m.fmut.RUnlock()
+
+	m.diskChangeDetected(folderCfg, fs, events.RemoteChangeDetected)
 }
 
 func (m *Model) updateLocals(folder string, fs []protocol.FileInfo) {
@@ -1582,7 +1588,7 @@ func (m *Model) updateLocals(folder string, fs []protocol.FileInfo) {
 	})
 }
 
-func (m *Model) localChangeDetected(folderCfg config.FolderConfiguration, files []protocol.FileInfo) {
+func (m *Model) diskChangeDetected(folderCfg config.FolderConfiguration, files []protocol.FileInfo, typeOfEvent events.EventType) {
 	path := strings.Replace(folderCfg.Path(), `\\?\`, "", 1)
 
 	for _, file := range files {
@@ -1611,12 +1617,14 @@ func (m *Model) localChangeDetected(folderCfg config.FolderConfiguration, files
 		// for windows paths, strip unwanted chars from the front.
 		path := filepath.Join(path, filepath.FromSlash(file.Name))
 
-		events.Default.Log(events.LocalChangeDetected, map[string]string{
-			"folderID": folderCfg.ID,
-			"label":    folderCfg.Label,
-			"action":   action,
-			"type":     objType,
-			"path":     path,
+		// Two different events can be fired here based on what EventType is passed into function
+		events.Default.Log(typeOfEvent, map[string]string{
+			"folderID":   folderCfg.ID,
+			"label":      folderCfg.Label,
+			"action":     action,
+			"type":       objType,
+			"path":       path,
+			"modifiedBy": file.ModifiedBy.String(),
 		})
 	}
 }
@@ -1859,6 +1867,7 @@ func (m *Model) internalScanFolderSubdirs(folder string, subDirs []string) error
 					Size:          f.Size,
 					ModifiedS:     f.ModifiedS,
 					ModifiedNs:    f.ModifiedNs,
+					ModifiedBy:    m.id.Short(),
 					Permissions:   f.Permissions,
 					NoPermissions: f.NoPermissions,
 					Invalid:       true,
@@ -1884,6 +1893,7 @@ func (m *Model) internalScanFolderSubdirs(folder string, subDirs []string) error
 						Size:       0,
 						ModifiedS:  f.ModifiedS,
 						ModifiedNs: f.ModifiedNs,
+						ModifiedBy: m.id.Short(),
 						Deleted:    true,
 						Version:    f.Version.Update(m.shortID),
 					}

+ 103 - 20
lib/protocol/bep.pb.go

@@ -298,6 +298,7 @@ type FileInfo struct {
 	Permissions   uint32       `protobuf:"varint,4,opt,name=permissions,proto3" json:"permissions,omitempty"`
 	ModifiedS     int64        `protobuf:"varint,5,opt,name=modified_s,json=modifiedS,proto3" json:"modified_s,omitempty"`
 	ModifiedNs    int32        `protobuf:"varint,11,opt,name=modified_ns,json=modifiedNs,proto3" json:"modified_ns,omitempty"`
+	ModifiedBy    ShortID      `protobuf:"varint,12,opt,name=modified_by,json=modifiedBy,proto3,customtype=ShortID" json:"modified_by"`
 	Deleted       bool         `protobuf:"varint,6,opt,name=deleted,proto3" json:"deleted,omitempty"`
 	Invalid       bool         `protobuf:"varint,7,opt,name=invalid,proto3" json:"invalid,omitempty"`
 	NoPermissions bool         `protobuf:"varint,8,opt,name=no_permissions,json=noPermissions,proto3" json:"no_permissions,omitempty"`
@@ -381,7 +382,7 @@ type FileDownloadProgressUpdate struct {
 	UpdateType   FileDownloadProgressUpdateType `protobuf:"varint,1,opt,name=update_type,json=updateType,proto3,enum=protocol.FileDownloadProgressUpdateType" json:"update_type,omitempty"`
 	Name         string                         `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
 	Version      Vector                         `protobuf:"bytes,3,opt,name=version" json:"version"`
-	BlockIndexes []int32                        `protobuf:"varint,4,rep,name=block_indexes,json=blockIndexes" json:"block_indexes,omitempty"`
+	BlockIndexes []int32                        `protobuf:"varint,4,rep,packed,name=block_indexes,json=blockIndexes" json:"block_indexes,omitempty"`
 }
 
 func (m *FileDownloadProgressUpdate) Reset()                    { *m = FileDownloadProgressUpdate{} }
@@ -858,6 +859,11 @@ func (m *FileInfo) MarshalTo(data []byte) (int, error) {
 		i++
 		i = encodeVarintBep(data, i, uint64(m.ModifiedNs))
 	}
+	if m.ModifiedBy != 0 {
+		data[i] = 0x60
+		i++
+		i = encodeVarintBep(data, i, uint64(m.ModifiedBy))
+	}
 	if len(m.Blocks) > 0 {
 		for _, msg := range m.Blocks {
 			data[i] = 0x82
@@ -1146,11 +1152,22 @@ func (m *FileDownloadProgressUpdate) MarshalTo(data []byte) (int, error) {
 	}
 	i += n3
 	if len(m.BlockIndexes) > 0 {
-		for _, num := range m.BlockIndexes {
-			data[i] = 0x20
-			i++
-			i = encodeVarintBep(data, i, uint64(num))
+		data5 := make([]byte, len(m.BlockIndexes)*10)
+		var j4 int
+		for _, num1 := range m.BlockIndexes {
+			num := uint64(num1)
+			for num >= 1<<7 {
+				data5[j4] = uint8(uint64(num)&0x7f | 0x80)
+				num >>= 7
+				j4++
+			}
+			data5[j4] = uint8(num)
+			j4++
 		}
+		data[i] = 0x22
+		i++
+		i = encodeVarintBep(data, i, uint64(j4))
+		i += copy(data[i:], data5[:j4])
 	}
 	return i, nil
 }
@@ -1403,6 +1420,9 @@ func (m *FileInfo) ProtoSize() (n int) {
 	if m.ModifiedNs != 0 {
 		n += 1 + sovBep(uint64(m.ModifiedNs))
 	}
+	if m.ModifiedBy != 0 {
+		n += 1 + sovBep(uint64(m.ModifiedBy))
+	}
 	if len(m.Blocks) > 0 {
 		for _, e := range m.Blocks {
 			l = e.ProtoSize()
@@ -1534,9 +1554,11 @@ func (m *FileDownloadProgressUpdate) ProtoSize() (n int) {
 	l = m.Version.ProtoSize()
 	n += 1 + l + sovBep(uint64(l))
 	if len(m.BlockIndexes) > 0 {
+		l = 0
 		for _, e := range m.BlockIndexes {
-			n += 1 + sovBep(uint64(e))
+			l += sovBep(uint64(e))
 		}
+		n += 1 + sovBep(uint64(l)) + l
 	}
 	return n
 }
@@ -2841,6 +2863,25 @@ func (m *FileInfo) Unmarshal(data []byte) error {
 					break
 				}
 			}
+		case 12:
+			if wireType != 0 {
+				return fmt.Errorf("proto: wrong wireType = %d for field ModifiedBy", wireType)
+			}
+			m.ModifiedBy = 0
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowBep
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := data[iNdEx]
+				iNdEx++
+				m.ModifiedBy |= (ShortID(b) & 0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
 		case 16:
 			if wireType != 2 {
 				return fmt.Errorf("proto: wrong wireType = %d for field Blocks", wireType)
@@ -3782,25 +3823,67 @@ func (m *FileDownloadProgressUpdate) Unmarshal(data []byte) error {
 			}
 			iNdEx = postIndex
 		case 4:
-			if wireType != 0 {
-				return fmt.Errorf("proto: wrong wireType = %d for field BlockIndexes", wireType)
-			}
-			var v int32
-			for shift := uint(0); ; shift += 7 {
-				if shift >= 64 {
-					return ErrIntOverflowBep
+			if wireType == 2 {
+				var packedLen int
+				for shift := uint(0); ; shift += 7 {
+					if shift >= 64 {
+						return ErrIntOverflowBep
+					}
+					if iNdEx >= l {
+						return io.ErrUnexpectedEOF
+					}
+					b := data[iNdEx]
+					iNdEx++
+					packedLen |= (int(b) & 0x7F) << shift
+					if b < 0x80 {
+						break
+					}
 				}
-				if iNdEx >= l {
+				if packedLen < 0 {
+					return ErrInvalidLengthBep
+				}
+				postIndex := iNdEx + packedLen
+				if postIndex > l {
 					return io.ErrUnexpectedEOF
 				}
-				b := data[iNdEx]
-				iNdEx++
-				v |= (int32(b) & 0x7F) << shift
-				if b < 0x80 {
-					break
+				for iNdEx < postIndex {
+					var v int32
+					for shift := uint(0); ; shift += 7 {
+						if shift >= 64 {
+							return ErrIntOverflowBep
+						}
+						if iNdEx >= l {
+							return io.ErrUnexpectedEOF
+						}
+						b := data[iNdEx]
+						iNdEx++
+						v |= (int32(b) & 0x7F) << shift
+						if b < 0x80 {
+							break
+						}
+					}
+					m.BlockIndexes = append(m.BlockIndexes, v)
 				}
+			} else if wireType == 0 {
+				var v int32
+				for shift := uint(0); ; shift += 7 {
+					if shift >= 64 {
+						return ErrIntOverflowBep
+					}
+					if iNdEx >= l {
+						return io.ErrUnexpectedEOF
+					}
+					b := data[iNdEx]
+					iNdEx++
+					v |= (int32(b) & 0x7F) << shift
+					if b < 0x80 {
+						break
+					}
+				}
+				m.BlockIndexes = append(m.BlockIndexes, v)
+			} else {
+				return fmt.Errorf("proto: wrong wireType = %d for field BlockIndexes", wireType)
 			}
-			m.BlockIndexes = append(m.BlockIndexes, v)
 		default:
 			iNdEx = preIndex
 			skippy, err := skipBep(data[iNdEx:])

+ 1 - 0
lib/protocol/bep.proto

@@ -100,6 +100,7 @@ message FileInfo {
     uint32       permissions    = 4;
     int64        modified_s     = 5;
     int32        modified_ns    = 11;
+    uint64       modified_by    = 12 [(gogoproto.customtype) = "ShortID", (gogoproto.nullable) = false];
     bool         deleted        = 6;
     bool         invalid        = 7;
     bool         no_permissions = 8;

+ 3 - 0
lib/protocol/deviceid.go

@@ -88,6 +88,9 @@ func (n *DeviceID) MarshalText() ([]byte, error) {
 }
 
 func (s ShortID) String() string {
+	if s == 0 {
+		return ""
+	}
 	var bs [8]byte
 	binary.BigEndian.PutUint64(bs[:], uint64(s))
 	return base32.StdEncoding.EncodeToString(bs[:])[:7]

+ 2 - 0
lib/scanner/walk.go

@@ -326,6 +326,7 @@ func (w *walker) walkRegular(relPath string, info os.FileInfo, fchan chan protoc
 		NoPermissions: w.IgnorePerms,
 		ModifiedS:     info.ModTime().Unix(),
 		ModifiedNs:    int32(info.ModTime().Nanosecond()),
+		ModifiedBy:    w.ShortID,
 		Size:          info.Size(),
 	}
 	l.Debugln("to hash:", relPath, f)
@@ -361,6 +362,7 @@ func (w *walker) walkDir(relPath string, info os.FileInfo, dchan chan protocol.F
 		NoPermissions: w.IgnorePerms,
 		ModifiedS:     info.ModTime().Unix(),
 		ModifiedNs:    int32(info.ModTime().Nanosecond()),
+		ModifiedBy:    w.ShortID,
 	}
 	l.Debugln("dir:", relPath, f)