Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Collect and suggest devices for missing cluster links #5758

Draft
wants to merge 15 commits into
base: master
from
Draft
Changes from all commits
Commits
File filter...
Filter file types
Jump to…
Jump to file or symbol
Failed to load files and symbols.

Always

Just for now

@@ -66,6 +66,14 @@ function folderCompare(a, b) {
return labelA > labelB;
}

function deviceMap(l) {
var m = {};
l.forEach(function (r) {
m[r.deviceID] = r;
});
return m;
}

function folderMap(l) {
var m = {};
l.forEach(function (r) {
@@ -60,7 +60,11 @@ angular.module('syncthing.core')
} catch (exception) { }

$scope.folderDefaults = {
sharedDevices: {},
selectedDevices: {},
suggestedDevices: {},
suggestedUnknownDevices: {},
unrelatedDevices: {},
type: "sendreceive",
rescanIntervalS: 3600,
fsWatcherDelayS: 10,
@@ -1423,6 +1427,22 @@ angular.module('syncthing.core')
});
};

$scope.addDeviceAndShare = function (deviceID, deviceName, folderID) {
$scope.currentDevice = {
name: deviceName,
deviceID: deviceID,
_addressesStr: 'dynamic', //FIXME use info from candidateDevices?
compression: 'metadata',
introducer: false,
selectedFolders: { folderID: true }, //FIXME not working!
pendingFolders: [],
ignoredFolders: []
};
$scope.editingExisting = false;//FIXME conflict when opened from within editFolderModalView!
$scope.deviceEditor.$setPristine();
$('#editDevice').modal();
};

$scope.deleteDevice = function () {
$('#editDevice').modal('hide');
if (!$scope.editingExisting) {
@@ -1658,10 +1678,42 @@ angular.module('syncthing.core')
if ($scope.currentFolder.path.length > 1 && $scope.currentFolder.path.slice(-1) === $scope.system.pathSeparator) {
$scope.currentFolder.path = $scope.currentFolder.path.slice(0, -1);
}
// Cache complete device objects indexed by ID for lookups
var devMap = deviceMap($scope.devices)
$scope.currentFolder.sharedDevices = [];
$scope.currentFolder.selectedDevices = {};
$scope.currentFolder.devices.forEach(function (n) {
if (n.deviceID !== $scope.myID) {
$scope.currentFolder.sharedDevices.push(devMap[n.deviceID]);
}
$scope.currentFolder.selectedDevices[n.deviceID] = true;
});
$scope.currentFolder.suggestedDevices = [];
$scope.currentFolder.suggestedUnknownDevices = [];
$scope.currentFolder.candidateDevices.forEach(function (n) {
if (n.deviceID in devMap) {
$scope.currentFolder.suggestedDevices.push(devMap[n.deviceID]);
} else {
var alias = '';
n.introducedBy.forEach(function (i) {
if (alias === '') {
alias = i.name;
} else if (alias !== i.name) {
alias = undefined;
}
});
$scope.currentFolder.suggestedUnknownDevices.push({
'deviceID': n.deviceID,
'alias': alias
});
//FIXME provide detailed introduced-by info via tooltip?
}
});
$scope.currentFolder.unrelatedDevices = $scope.devices.filter(function (n) {
return n.deviceID !== $scope.myID
&& ! $scope.currentFolder.selectedDevices[n.deviceID]
&& ! $scope.currentFolder.suggestedDevices.includes(n);
});
if ($scope.currentFolder.versioning && $scope.currentFolder.versioning.type === "trashcan") {
$scope.currentFolder.trashcanFileVersioning = true;
$scope.currentFolder.fileVersioningSelector = "trashcan";
@@ -1712,25 +1764,39 @@ angular.module('syncthing.core')
$scope.editFolderModal();
};

$scope.selectAllDevices = function () {
var devices = $scope.otherDevices();
$scope.selectAllSharedDevices = function (state = true) {
var devices = $scope.currentFolder.sharedDevices;
for (var i = 0; i < devices.length; i++) {
$scope.currentFolder.selectedDevices[devices[i].deviceID] = true;
$scope.currentFolder.selectedDevices[devices[i].deviceID] = !!state;
}
};

$scope.deSelectAllDevices = function () {
var devices = $scope.otherDevices();
$scope.deSelectAllSharedDevices = function () { $scope.selectAllSharedDevices(false); };

$scope.selectAllSuggestedDevices = function (state = true) {
var devices = $scope.currentFolder.suggestedDevices;
for (var i = 0; i < devices.length; i++) {
$scope.currentFolder.selectedDevices[devices[i].deviceID] = false;
$scope.currentFolder.selectedDevices[devices[i].deviceID] = !!state;
}
};

$scope.deSelectAllSuggestedDevices = function () { $scope.selectAllSuggestedDevices(false); };

$scope.selectAllUnrelatedDevices = function (state = true) {
var devices = $scope.currentFolder.unrelatedDevices;
for (var i = 0; i < devices.length; i++) {
$scope.currentFolder.selectedDevices[devices[i].deviceID] = !!state;
}
};

$scope.deSelectAllUnrelatedDevices = function () { $scope.selectAllUnrelatedDevices(false); };

$scope.addFolder = function () {
$http.get(urlbase + '/svc/random/string?length=10').success(function (data) {
$scope.editingExisting = false;
$scope.currentFolder = angular.copy($scope.folderDefaults);
$scope.currentFolder.id = (data.random.substr(0, 5) + '-' + data.random.substr(5, 5)).toLowerCase();
$scope.currentFolder.unrelatedDevices = $scope.otherDevices();
$('#folder-ignores textarea').val("");
$('#folder-ignores textarea').removeAttr('disabled');
$scope.editFolderModal();
@@ -1746,6 +1812,7 @@ angular.module('syncthing.core')
importFromOtherDevice: true
};
$scope.currentFolder.selectedDevices[device] = true;
$scope.currentFolder.unrelatedDevices = $scope.otherDevices();
$('#folder-ignores textarea').val("");
$('#folder-ignores textarea').removeAttr('disabled');
$scope.editFolderModal();
@@ -1771,7 +1838,11 @@ angular.module('syncthing.core')
});
}
}
delete folderCfg.sharedDevices;
delete folderCfg.selectedDevices;
delete folderCfg.suggestedDevices;
delete folderCfg.suggestedUnknownDevices;
delete folderCfg.unrelatedDevices;

if (folderCfg.fileVersioningSelector === "trashcan") {
folderCfg.versioning = {
@@ -44,15 +44,61 @@
</div>
</div>
<div id="folder-sharing" class="tab-pane">
<div class="form-group">
<label translate for="devices">Share With Devices</label>
<div class="form-group" ng-if="currentFolder.sharedDevices.length">
<label translate>Currently Shared With Devices</label>
<p class="help-block">
<span translate>Deselect devices to stop sharing this folder with.</span>&emsp;
<small><a href="#" ng-click="selectAllSharedDevices()" translate>Select All</a>&emsp;
<a href="#" ng-click="deSelectAllSharedDevices()" translate>Deselect All</a></small>
</p>
<div class="row">
<div class="col-md-4" ng-repeat="device in currentFolder.sharedDevices">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="currentFolder.selectedDevices[device.deviceID]" /> {{deviceName(device)}}
</label>
</div>
</div>
</div>
</div>
<div class="form-group" ng-if="currentFolder.suggestedDevices.length || currentFolder.suggestedUnknownDevices.length">
<label translate>Indirectly Shared With Devices</label>
<p class="help-block">
<span translate>Select suggested devices where this folder is already available to offer them direct sharing.</span>&emsp;
<small><a href="#" ng-click="selectAllSuggestedDevices()" translate>Select All</a>&emsp;
<a href="#" ng-click="deSelectAllSuggestedDevices()" translate>Deselect All</a></small>
</p>
<div class="row">
<div class="col-md-4" ng-repeat="device in currentFolder.suggestedDevices">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="currentFolder.selectedDevices[device.deviceID]" /> {{deviceName(device)}}
</label>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12" ng-repeat="device in currentFolder.suggestedUnknownDevices">
<div>
<label>
<a ng-click="addDeviceAndShare(device.deviceID, device.alias, currentFolder.id)">
<span class="fas fa-plus"></span>&nbsp;{{device.deviceID}}
<span class="text-nowrap" ng-if="device.alias">&nbsp;"{{device.alias}}"</span>
</a>
</label>
</div>
</div>
</div>
</div>
<div class="form-group" ng-if="currentFolder.unrelatedDevices.length">
<label translate>Unshared Devices</label>
<p class="help-block">
<span translate>Select the devices to share this folder with.</span>&emsp;
<small><a href="#" ng-click="selectAllDevices()" translate>Select All</a>&emsp;
<a href="#" ng-click="deSelectAllDevices()" translate>Deselect All</a></small>
<span translate>Select additional devices to initially share this folder with.</span>&emsp;
<small><a href="#" ng-click="selectAllUnrelatedDevices()" translate>Select All</a>&emsp;
<a href="#" ng-click="deSelectAllUnrelatedDevices()" translate>Deselect All</a></small>
</p>
<div class="row">
<div class="col-md-4" ng-repeat="device in otherDevices()">
<div class="col-md-4" ng-repeat="device in currentFolder.unrelatedDevices">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="currentFolder.selectedDevices[device.deviceID]" /> {{deviceName(device)}}
@@ -74,6 +74,9 @@ func (c *mockedConfig) AddOrUpdatePendingDevice(device protocol.DeviceID, name,

func (c *mockedConfig) AddOrUpdatePendingFolder(id, label string, device protocol.DeviceID) {}

func (c *mockedConfig) AddOrUpdateFolderCandidateDevice(folder string, device, introducer protocol.DeviceID, name, certName string, addresses []string) {
}

func (c *mockedConfig) MyName() string {
return ""
}
@@ -32,6 +32,7 @@ type FolderConfiguration struct {
Path string `xml:"path,attr" json:"path"`
Type FolderType `xml:"type,attr" json:"type"`
Devices []FolderDeviceConfiguration `xml:"device" json:"devices"`
CandidateDevices []ObservedCandidateDevice `xml:"candidateDevice" json:"candidateDevices"`
RescanIntervalS int `xml:"rescanIntervalS,attr" json:"rescanIntervalS" default:"3600"`
FSWatcherEnabled bool `xml:"fsWatcherEnabled,attr" json:"fsWatcherEnabled" default:"true"`
FSWatcherDelayS int `xml:"fsWatcherDelayS,attr" json:"fsWatcherDelayS" default:"10"`
@@ -69,11 +70,12 @@ type FolderDeviceConfiguration struct {

func NewFolderConfiguration(myID protocol.DeviceID, id, label string, fsType fs.FilesystemType, path string) FolderConfiguration {
f := FolderConfiguration{
ID: id,
Label: label,
Devices: []FolderDeviceConfiguration{{DeviceID: myID}},
FilesystemType: fsType,
Path: path,
ID: id,
Label: label,
Devices: []FolderDeviceConfiguration{{DeviceID: myID}},
CandidateDevices: []ObservedCandidateDevice{},
FilesystemType: fsType,
Path: path,
}

util.SetDefaults(&f)
@@ -86,6 +88,8 @@ func (f FolderConfiguration) Copy() FolderConfiguration {
c := f
c.Devices = make([]FolderDeviceConfiguration, len(f.Devices))
copy(c.Devices, f.Devices)
c.CandidateDevices = make([]ObservedCandidateDevice, len(f.CandidateDevices))
copy(c.CandidateDevices, f.CandidateDevices)
c.Versioning = f.Versioning.Copy()
return c
}
@@ -284,3 +288,33 @@ func (f *FolderConfiguration) CheckAvailableSpace(req int64) error {
}
return fmt.Errorf("insufficient space in %v %v", fs.Type(), fs.URI())
}

func (f *FolderConfiguration) AddOrUpdateCandidateDevice(device, introducer protocol.DeviceID, name, certName string, addresses []string) {

//FIXME locking already handled in config wrapper?!?

for i := range f.CandidateDevices {
cDev := &f.CandidateDevices[i]
if cDev.ID == device {
if cDev.CertName != certName {
This conversation was marked as resolved by acolomb

This comment has been minimized.

Copy link
@golangcibot

golangcibot May 30, 2019

SA9003: empty branch (from staticcheck)

//FIXME which one should be used?
}
cDev.CollectAddresses(addresses)
if !introducer.Equals(protocol.EmptyDeviceID) {
// Add / update source information for current candidate
cDev.SetIntroducer(introducer, name)
}
return
}
}

cDev := ObservedCandidateDevice{
ID: device,
CertName: certName,
Addresses: addresses,
}
if !introducer.Equals(protocol.EmptyDeviceID) {
cDev.SetIntroducer(introducer, name)
}
f.CandidateDevices = append(f.CandidateDevices, cDev)
}
@@ -22,5 +22,55 @@ type ObservedDevice struct {
Time time.Time `xml:"time,attr" json:"time"`
ID protocol.DeviceID `xml:"id,attr" json:"deviceID"`
Name string `xml:"name,attr" json:"name"`
Address string `xml:"address,attr" json:"address"`
Address string `xml:"address,attr,omitempty" json:"address"`
}

type ObservedCandidateDevice struct {
ID protocol.DeviceID `xml:"id,attr" json:"deviceID"`
CertName string `xml:"certName,attr,omitempty" json:"certName"`
Addresses []string `xml:"address,omitempty" json:"addresses"`
IntroducedBy []ObservedDevice `xml:"introducedBy,omitempty" json:"introducedBy"`
}

// Generate a map of remote devices who introduced this candidate, keyed by device ID
func (d *ObservedCandidateDevice) Introducers() map[protocol.DeviceID]ObservedDevice {
introducerMap := make(map[protocol.DeviceID]ObservedDevice, len(d.IntroducedBy))
for _, dev := range d.IntroducedBy {
introducerMap[dev.ID] = dev
}
return introducerMap
}

// Add or update information from an introducer to the candidate device description
func (d *ObservedCandidateDevice) SetIntroducer(introducer protocol.DeviceID, name string) {
// Sort devices who introduced this candidate into a map for deduplication
introducerMap := d.Introducers()
introducerMap[introducer] = ObservedDevice{
Time: time.Now().Round(time.Second),
ID: introducer,
Name: name,
}
d.IntroducedBy = make([]ObservedDevice, 0, len(introducerMap))
for _, n := range introducerMap {
d.IntroducedBy = append(d.IntroducedBy, n)
}
}

// Collect addresses to try for contacting a candidate device later
func (d *ObservedCandidateDevice) CollectAddresses(addresses []string) {
if len(addresses) == 0 {
return
}
// Sort addresses into a map for deduplication
addressMap := make(map[string]struct{}, len(d.Addresses))
for _, s := range d.Addresses {
addressMap[s] = struct{}{}
}
for _, s := range addresses {
addressMap[s] = struct{}{}
}
d.Addresses = make([]string, 0, len(addressMap))
for a := range addressMap {
d.Addresses = append(d.Addresses, a)
}
}
@@ -86,6 +86,8 @@ type Wrapper interface {
AddOrUpdatePendingFolder(id, label string, device protocol.DeviceID)
IgnoredDevice(id protocol.DeviceID) bool
IgnoredFolder(device protocol.DeviceID, folder string) bool
AddOrUpdateFolderCandidateDevice(folder string,
device, introducer protocol.DeviceID, name, certName string, addresses []string)

ListenAddresses() []string
GlobalDiscoveryServers() []string
@@ -549,3 +551,21 @@ func (w *wrapper) AddOrUpdatePendingFolder(id, label string, device protocol.Dev

panic("bug: adding pending folder for non-existing device")
}

func (w *wrapper) AddOrUpdateFolderCandidateDevice(folder string,
device, introducer protocol.DeviceID, name, certName string, addresses []string) {
defer w.Save()

This comment has been minimized.

Copy link
@golangcibot

golangcibot May 30, 2019

Error return value of w.Save is not checked (from errcheck)

This comment has been minimized.

Copy link
@acolomb

acolomb May 30, 2019

Author Contributor

Copied verbatim from AddOrUpdatePendingDevice() and others. Sorry I'm new to Go and have no idea how that should be checked in conjunction with defer.

This will not live in the config anyway, long-term.


w.mut.Lock()
defer w.mut.Unlock()

for i := range w.cfg.Folders {
if w.cfg.Folders[i].ID == folder {
w.cfg.Folders[i].AddOrUpdateCandidateDevice(
device, introducer, name, certName, addresses)
return
}
}

panic("bug: adding candidate device for non-existing folder")
}
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.