diff --git a/cmd/syncthing/main.go b/cmd/syncthing/main.go
index 12eef8d1..57d1cb50 100644
--- a/cmd/syncthing/main.go
+++ b/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())
diff --git a/cmd/syncthing/verboseservice.go b/cmd/syncthing/verboseservice.go
index 4803c625..9d3aff5d 100644
--- a/cmd/syncthing/verboseservice.go
+++ b/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"])
diff --git a/gui/default/index.html b/gui/default/index.html
index 218de2e8..eeb28e75 100644
--- a/gui/default/index.html
+++ b/gui/default/index.html
@@ -617,9 +617,14 @@
@@ -651,6 +656,7 @@
+
diff --git a/gui/default/syncthing/core/syncthingController.js b/gui/default/syncthing/core/syncthingController.js
index 1b0037ae..e0d51f08 100755
--- a/gui/default/syncthing/core/syncthingController.js
+++ b/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);
diff --git a/gui/default/syncthing/device/globalChangesModalView.html b/gui/default/syncthing/device/globalChangesModalView.html
new file mode 100644
index 00000000..00c3f738
--- /dev/null
+++ b/gui/default/syncthing/device/globalChangesModalView.html
@@ -0,0 +1,27 @@
+
+
+
+
+
+ | Device |
+ Action |
+ Type |
+ Path |
+ Time |
+
+
+ | {{friendlyNameFromShort(changeEvent.data.modifiedBy)}} |
+ Unknown |
+ {{changeEvent.data.action}} |
+ {{changeEvent.data.type}} |
+ {{changeEvent.data.path}} |
+ {{changeEvent.time | date:'medium'}} |
+
+
+
+
+
diff --git a/lib/db/structs.pb.go b/lib/db/structs.pb.go
index c63f3b02..47373739 100644
--- a/lib/db/structs.pb.go
+++ b/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)
diff --git a/lib/db/structs.proto b/lib/db/structs.proto
index 3d061af0..75d50328 100644
--- a/lib/db/structs.proto
+++ b/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;
diff --git a/lib/events/events.go b/lib/events/events.go
index 9f1d043e..25c6fcba 100644
--- a/lib/events/events.go
+++ b/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:
diff --git a/lib/model/model.go b/lib/model/model.go
index 77e5d0c8..be08c629 100644
--- a/lib/model/model.go
+++ b/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),
}
diff --git a/lib/protocol/bep.pb.go b/lib/protocol/bep.pb.go
index 494fa169..7d6ba839 100644
--- a/lib/protocol/bep.pb.go
+++ b/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:])
diff --git a/lib/protocol/bep.proto b/lib/protocol/bep.proto
index 9c97e2a0..f658f00d 100644
--- a/lib/protocol/bep.proto
+++ b/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;
diff --git a/lib/protocol/deviceid.go b/lib/protocol/deviceid.go
index cab70e39..e2b5d15d 100644
--- a/lib/protocol/deviceid.go
+++ b/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]
diff --git a/lib/scanner/walk.go b/lib/scanner/walk.go
index c296c362..7017508f 100644
--- a/lib/scanner/walk.go
+++ b/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)