syncthing/gui/default/syncthing/core/syncthingController.js

3147 lines
124 KiB
JavaScript
Raw Normal View History

angular.module('syncthing.core')
.config(function ($locationProvider) {
$locationProvider.html5Mode({ enabled: true, requireBase: false }).hashPrefix('!');
2015-03-22 16:35:08 +01:00
})
2018-03-18 01:42:31 +01:00
.controller('SyncthingController', function ($scope, $http, $location, LocaleService, Events, $filter, $q, $compile, $timeout, $rootScope, $translate) {
'use strict';
// private/helper definitions
var prevDate = 0;
var navigatingAway = false;
var online = false;
var restarting = false;
function initController() {
LocaleService.autoConfigLocale();
setInterval($scope.refresh, 10000);
Events.start();
}
// public/scope definitions
$scope.completion = {};
$scope.config = {};
$scope.configInSync = true;
$scope.connections = {};
$scope.errors = [];
$scope.model = {};
$scope.myID = '';
$scope.devices = {};
$scope.discoveryCache = {};
$scope.protocolChanged = false;
$scope.reportData = {};
$scope.reportDataPreview = '';
$scope.reportPreview = false;
$scope.folders = {};
$scope.seenError = '';
$scope.upgradeInfo = null;
$scope.deviceStats = {};
$scope.folderStats = {};
$scope.pendingDevices = {};
$scope.pendingFolders = {};
$scope.progress = {};
$scope.version = {};
2021-05-03 12:14:54 +02:00
$scope.needed = {};
$scope.neededFolder = '';
$scope.failed = {};
$scope.localChanged = {};
2015-08-27 00:49:06 +02:00
$scope.scanProgress = {};
$scope.themes = [];
$scope.globalChangeEvents = {};
$scope.metricRates = false;
$scope.folderPathErrors = {};
$scope.currentSharing = {};
$scope.currentFolder = {};
$scope.currentDevice = {};
$scope.ignores = {
text: '',
error: null,
disabled: false,
originalLines: [],
defaultLines: [],
saved: false,
};
resetRemoteNeed();
try {
$scope.metricRates = (window.localStorage["metricRates"] == "true");
} catch (exception) { }
$scope.versioningDefaults = {
selector: "none",
trashcanClean: 0,
cleanupIntervalS: 3600,
simpleKeep: 5,
staggeredMaxAge: 365,
externalCommand: "",
};
$scope.localStateTotal = {
bytes: 0,
directories: 0,
files: 0
};
$(window).bind('beforeunload', function () {
navigatingAway = true;
});
$scope.$on("$locationChangeSuccess", function () {
LocaleService.useLocale($location.search().lang);
});
$scope.needActions = {
'rm': 'Del',
'rmdir': 'Del (dir)',
'sync': 'Sync',
'touch': 'Update'
};
$scope.needIcons = {
2018-05-24 20:59:32 +02:00
'rm': 'far fa-fw fa-trash-alt',
'rmdir': 'far fa-fw fa-trash-alt',
'sync': 'far fa-fw fa-arrow-alt-circle-down',
2018-05-24 20:59:32 +02:00
'touch': 'fas fa-fw fa-asterisk'
};
$scope.$on(Events.ONLINE, function () {
if (online && !restarting) {
return;
}
console.log('UIOnline');
refreshDeviceStats();
refreshFolderStats();
refreshGlobalChanges();
refreshThemes();
$q.all([
refreshSystem(),
refreshDiscoveryCache(),
refreshConfig(),
refreshCluster(),
refreshConnectionStats(),
]).then(function() {
$http.get(urlbase + '/system/version').success(function (data) {
console.log("version", data);
if ($scope.version.version && $scope.version.version !== data.version) {
// We already have a version response, but it differs from
// the new one. Reload the full GUI in case it's changed.
document.location.reload(true);
}
$scope.version = data;
}).error($scope.emitHTTPError);
$http.get(urlbase + '/svc/report').success(function (data) {
$scope.reportData = data;
if ($scope.system && $scope.config.options.urAccepted > -1 && $scope.config.options.urSeen < $scope.system.urVersionMax && $scope.config.options.urAccepted < $scope.system.urVersionMax) {
// Usage reporting format has changed, prompt the user to re-accept.
$('#ur').modal();
}
}).error($scope.emitHTTPError);
$http.get(urlbase + '/system/upgrade').success(function (data) {
$scope.upgradeInfo = data;
}).error(function () {
$scope.upgradeInfo = null;
});
online = true;
restarting = false;
$('#networkError').modal('hide');
$('#restarting').modal('hide');
$('#shutdown').modal('hide');
}).catch($scope.emitHTTPError);
});
$scope.$on(Events.OFFLINE, function () {
if (navigatingAway || !online) {
return;
}
console.log('UIOffline');
online = false;
if (!restarting) {
$('#networkError').modal();
}
});
$scope.$on('HTTPError', function (event, arg) {
// Emitted when a HTTP call fails. We use the status code to try
// to figure out what's wrong.
if (navigatingAway || !online) {
return;
}
console.log('HTTPError', arg);
online = false;
// We sometimes get arg == null from angularjs - no idea why
if (!restarting && arg) {
if (arg.status === 0) {
// A network error, not an HTTP error
$scope.$emit(Events.OFFLINE);
} else if (arg.status >= 400 && arg.status <= 599) {
// A genuine HTTP error
$('#networkError').modal('hide');
$('#restarting').modal('hide');
$('#shutdown').modal('hide');
$('#httpError').modal();
}
}
});
$scope.$on(Events.STATE_CHANGED, function (event, arg) {
var data = arg.data;
if ($scope.model[data.folder]) {
$scope.model[data.folder].state = data.to;
$scope.model[data.folder].error = data.error;
2015-08-27 00:49:06 +02:00
// If a folder has started scanning, then any scan progress is
// also obsolete.
if (data.to === 'scanning') {
delete $scope.scanProgress[data.folder];
}
// If a folder finished scanning, then refresh folder stats
// to update last scan time.
if (data.from === 'scanning' && data.to === 'idle') {
refreshFolderStats();
}
}
});
$scope.$on(Events.LOCAL_INDEX_UPDATED, function (event, arg) {
refreshFolderStats();
refreshGlobalChanges();
});
$scope.$on(Events.DEVICE_DISCONNECTED, function (event, arg) {
if (!$scope.connections[arg.data.id]) {
return;
}
2015-08-23 21:56:10 +02:00
$scope.connections[arg.data.id].connected = false;
refreshDeviceStats();
});
$scope.$on(Events.DEVICE_CONNECTED, function (event, arg) {
if (!$scope.connections[arg.data.id]) {
$scope.connections[arg.data.id] = {
inbps: 0,
outbps: 0,
inBytesTotal: 0,
outBytesTotal: 0,
type: arg.data.type,
address: arg.data.addr
};
$scope.completion[arg.data.id] = {
_total: 100,
_needBytes: 0,
_needItems: 0
};
}
});
lib/model: Forget pending folders no longer announced in ClusterConfig (fixes #5187) (#7205) * lib/db: Add ExpirePendingFolders(). Use-case is to drop any no-longer-pending folders for a specific device when parsing its ClusterConfig message where previously offered folders are not mentioned any more. The timestamp in ObservedFolder is stored with only second precision, so round to seconds here as well. This allows calling the function within the same second of adding or updating entries. * lib/model: Weed out pending folders when receiving ClusterConfig. Filter the entries by timestamp, which must be newer than or equal to the reception time of the ClusterConfig. For just mentioned ones, this assumption will hold as AddOrUpdatePendingFolder() updates the timestamp. * lib/model, gui: Notify when one or more pending folders expired. Introduce new event type FolderOfferCancelled and use it to trigger a complete refreshCluster() cycle. Listing individual entries would be much more code and probably just as much work to answer the API request. * lib/model: Add comment and rename ExpirePendingFolders(). * lib/events: Rename FolderOfferCancelled to ClusterPendingChanged. * lib/model: Reuse ClusterPendingChanged event for cleanPending() Changing the config does not necessarily mean that the /resut/cluster/pending endpoints need to be refreshed, but only if something was actually removed. Detect this and indicate it through the ClusterPendingChanged event, which is already hooked up to requery respective endpoints within the GUI. No more need for a separate refreshCluster() in reaction to ConfigSaved event or calling refreshConfig(). * lib/model: Gofmt. * lib/db: Warn instead of info log for failed removal. * gui: Fix pending notifications not loading on GUI start. * lib/db: Use short device ID in log message. * lib/db: Return list of expired folder IDs after deleting them. * lib/model: Refactor Pending...Changed events. * lib/model: Adjust format of removed pending folders enumeration. Use an array of objects with device / folder ID properties, matching the other places where it's used. * lib/db: Drop invalid entries in RemovePendingFoldersBeforeTime(). * lib/model: Gofmt. My local gofmt did not complain here, strangely... * gui: Handle PendingDevicesChanged event. Even though it currently only holds one device at a time, wrap the contents in an array under the "added" property name. * lib/model: Fix null values in PendingFoldersChanged removed member. * gui: Handle PendingFoldersChanged event. * lib/model: Simplify construction of expiredPendingList. * lib/model: Reduce code duplication in cleanPending(). Use goto and a label for the common parts of calling the DB removal function and building the event data part. * lib/events, gui: Mark ...Rejected events deprecated. Extend comments explaining the conditions when the replacement event types are emitted. * lib/model: Wrap removed devices in array of objects as well. * lib/db: Use iter.Value() instead of needless db.Get(iter.Key()) * lib/db: Add comment explaining RemovePendingFoldersBeforeTime(). * lib/model: Rename fields folderID and deviceID in event data. * lib/db: Only list actually expired IDs as removed. Skip entries where Delete() failed as well as invalid entries that got removed automatically. * lib/model: Gofmt
2021-01-25 11:58:10 +01:00
$scope.$on(Events.PENDING_DEVICES_CHANGED, function (event, arg) {
if (!(arg.data.added || arg.data.removed)) {
// Not enough information to update in place, just refresh it completely
refreshCluster();
return;
}
if (arg.data.added) {
arg.data.added.forEach(function (rejected) {
var pendingDevice = {
time: arg.time,
name: rejected.name,
address: rejected.address
};
console.log("rejected device:", rejected.deviceID, pendingDevice);
$scope.pendingDevices[rejected.deviceID] = pendingDevice;
});
}
lib/model: Forget pending folders no longer announced in ClusterConfig (fixes #5187) (#7205) * lib/db: Add ExpirePendingFolders(). Use-case is to drop any no-longer-pending folders for a specific device when parsing its ClusterConfig message where previously offered folders are not mentioned any more. The timestamp in ObservedFolder is stored with only second precision, so round to seconds here as well. This allows calling the function within the same second of adding or updating entries. * lib/model: Weed out pending folders when receiving ClusterConfig. Filter the entries by timestamp, which must be newer than or equal to the reception time of the ClusterConfig. For just mentioned ones, this assumption will hold as AddOrUpdatePendingFolder() updates the timestamp. * lib/model, gui: Notify when one or more pending folders expired. Introduce new event type FolderOfferCancelled and use it to trigger a complete refreshCluster() cycle. Listing individual entries would be much more code and probably just as much work to answer the API request. * lib/model: Add comment and rename ExpirePendingFolders(). * lib/events: Rename FolderOfferCancelled to ClusterPendingChanged. * lib/model: Reuse ClusterPendingChanged event for cleanPending() Changing the config does not necessarily mean that the /resut/cluster/pending endpoints need to be refreshed, but only if something was actually removed. Detect this and indicate it through the ClusterPendingChanged event, which is already hooked up to requery respective endpoints within the GUI. No more need for a separate refreshCluster() in reaction to ConfigSaved event or calling refreshConfig(). * lib/model: Gofmt. * lib/db: Warn instead of info log for failed removal. * gui: Fix pending notifications not loading on GUI start. * lib/db: Use short device ID in log message. * lib/db: Return list of expired folder IDs after deleting them. * lib/model: Refactor Pending...Changed events. * lib/model: Adjust format of removed pending folders enumeration. Use an array of objects with device / folder ID properties, matching the other places where it's used. * lib/db: Drop invalid entries in RemovePendingFoldersBeforeTime(). * lib/model: Gofmt. My local gofmt did not complain here, strangely... * gui: Handle PendingDevicesChanged event. Even though it currently only holds one device at a time, wrap the contents in an array under the "added" property name. * lib/model: Fix null values in PendingFoldersChanged removed member. * gui: Handle PendingFoldersChanged event. * lib/model: Simplify construction of expiredPendingList. * lib/model: Reduce code duplication in cleanPending(). Use goto and a label for the common parts of calling the DB removal function and building the event data part. * lib/events, gui: Mark ...Rejected events deprecated. Extend comments explaining the conditions when the replacement event types are emitted. * lib/model: Wrap removed devices in array of objects as well. * lib/db: Use iter.Value() instead of needless db.Get(iter.Key()) * lib/db: Add comment explaining RemovePendingFoldersBeforeTime(). * lib/model: Rename fields folderID and deviceID in event data. * lib/db: Only list actually expired IDs as removed. Skip entries where Delete() failed as well as invalid entries that got removed automatically. * lib/model: Gofmt
2021-01-25 11:58:10 +01:00
if (arg.data.removed) {
arg.data.removed.forEach(function (dev) {
console.log("no longer pending device:", dev.deviceID);
delete $scope.pendingDevices[dev.deviceID];
});
}
});
lib/model: Forget pending folders no longer announced in ClusterConfig (fixes #5187) (#7205) * lib/db: Add ExpirePendingFolders(). Use-case is to drop any no-longer-pending folders for a specific device when parsing its ClusterConfig message where previously offered folders are not mentioned any more. The timestamp in ObservedFolder is stored with only second precision, so round to seconds here as well. This allows calling the function within the same second of adding or updating entries. * lib/model: Weed out pending folders when receiving ClusterConfig. Filter the entries by timestamp, which must be newer than or equal to the reception time of the ClusterConfig. For just mentioned ones, this assumption will hold as AddOrUpdatePendingFolder() updates the timestamp. * lib/model, gui: Notify when one or more pending folders expired. Introduce new event type FolderOfferCancelled and use it to trigger a complete refreshCluster() cycle. Listing individual entries would be much more code and probably just as much work to answer the API request. * lib/model: Add comment and rename ExpirePendingFolders(). * lib/events: Rename FolderOfferCancelled to ClusterPendingChanged. * lib/model: Reuse ClusterPendingChanged event for cleanPending() Changing the config does not necessarily mean that the /resut/cluster/pending endpoints need to be refreshed, but only if something was actually removed. Detect this and indicate it through the ClusterPendingChanged event, which is already hooked up to requery respective endpoints within the GUI. No more need for a separate refreshCluster() in reaction to ConfigSaved event or calling refreshConfig(). * lib/model: Gofmt. * lib/db: Warn instead of info log for failed removal. * gui: Fix pending notifications not loading on GUI start. * lib/db: Use short device ID in log message. * lib/db: Return list of expired folder IDs after deleting them. * lib/model: Refactor Pending...Changed events. * lib/model: Adjust format of removed pending folders enumeration. Use an array of objects with device / folder ID properties, matching the other places where it's used. * lib/db: Drop invalid entries in RemovePendingFoldersBeforeTime(). * lib/model: Gofmt. My local gofmt did not complain here, strangely... * gui: Handle PendingDevicesChanged event. Even though it currently only holds one device at a time, wrap the contents in an array under the "added" property name. * lib/model: Fix null values in PendingFoldersChanged removed member. * gui: Handle PendingFoldersChanged event. * lib/model: Simplify construction of expiredPendingList. * lib/model: Reduce code duplication in cleanPending(). Use goto and a label for the common parts of calling the DB removal function and building the event data part. * lib/events, gui: Mark ...Rejected events deprecated. Extend comments explaining the conditions when the replacement event types are emitted. * lib/model: Wrap removed devices in array of objects as well. * lib/db: Use iter.Value() instead of needless db.Get(iter.Key()) * lib/db: Add comment explaining RemovePendingFoldersBeforeTime(). * lib/model: Rename fields folderID and deviceID in event data. * lib/db: Only list actually expired IDs as removed. Skip entries where Delete() failed as well as invalid entries that got removed automatically. * lib/model: Gofmt
2021-01-25 11:58:10 +01:00
$scope.$on(Events.PENDING_FOLDERS_CHANGED, function (event, arg) {
if (!(arg.data.added || arg.data.removed)) {
// Not enough information to update in place, just refresh it completely
refreshCluster();
return;
}
lib/model: Forget pending folders no longer announced in ClusterConfig (fixes #5187) (#7205) * lib/db: Add ExpirePendingFolders(). Use-case is to drop any no-longer-pending folders for a specific device when parsing its ClusterConfig message where previously offered folders are not mentioned any more. The timestamp in ObservedFolder is stored with only second precision, so round to seconds here as well. This allows calling the function within the same second of adding or updating entries. * lib/model: Weed out pending folders when receiving ClusterConfig. Filter the entries by timestamp, which must be newer than or equal to the reception time of the ClusterConfig. For just mentioned ones, this assumption will hold as AddOrUpdatePendingFolder() updates the timestamp. * lib/model, gui: Notify when one or more pending folders expired. Introduce new event type FolderOfferCancelled and use it to trigger a complete refreshCluster() cycle. Listing individual entries would be much more code and probably just as much work to answer the API request. * lib/model: Add comment and rename ExpirePendingFolders(). * lib/events: Rename FolderOfferCancelled to ClusterPendingChanged. * lib/model: Reuse ClusterPendingChanged event for cleanPending() Changing the config does not necessarily mean that the /resut/cluster/pending endpoints need to be refreshed, but only if something was actually removed. Detect this and indicate it through the ClusterPendingChanged event, which is already hooked up to requery respective endpoints within the GUI. No more need for a separate refreshCluster() in reaction to ConfigSaved event or calling refreshConfig(). * lib/model: Gofmt. * lib/db: Warn instead of info log for failed removal. * gui: Fix pending notifications not loading on GUI start. * lib/db: Use short device ID in log message. * lib/db: Return list of expired folder IDs after deleting them. * lib/model: Refactor Pending...Changed events. * lib/model: Adjust format of removed pending folders enumeration. Use an array of objects with device / folder ID properties, matching the other places where it's used. * lib/db: Drop invalid entries in RemovePendingFoldersBeforeTime(). * lib/model: Gofmt. My local gofmt did not complain here, strangely... * gui: Handle PendingDevicesChanged event. Even though it currently only holds one device at a time, wrap the contents in an array under the "added" property name. * lib/model: Fix null values in PendingFoldersChanged removed member. * gui: Handle PendingFoldersChanged event. * lib/model: Simplify construction of expiredPendingList. * lib/model: Reduce code duplication in cleanPending(). Use goto and a label for the common parts of calling the DB removal function and building the event data part. * lib/events, gui: Mark ...Rejected events deprecated. Extend comments explaining the conditions when the replacement event types are emitted. * lib/model: Wrap removed devices in array of objects as well. * lib/db: Use iter.Value() instead of needless db.Get(iter.Key()) * lib/db: Add comment explaining RemovePendingFoldersBeforeTime(). * lib/model: Rename fields folderID and deviceID in event data. * lib/db: Only list actually expired IDs as removed. Skip entries where Delete() failed as well as invalid entries that got removed automatically. * lib/model: Gofmt
2021-01-25 11:58:10 +01:00
if (arg.data.added) {
arg.data.added.forEach(function (rejected) {
var offeringDevice = {
time: arg.time,
label: rejected.folderLabel,
receiveEncrypted: rejected.receiveEncrypted,
lib/model: Forget pending folders no longer announced in ClusterConfig (fixes #5187) (#7205) * lib/db: Add ExpirePendingFolders(). Use-case is to drop any no-longer-pending folders for a specific device when parsing its ClusterConfig message where previously offered folders are not mentioned any more. The timestamp in ObservedFolder is stored with only second precision, so round to seconds here as well. This allows calling the function within the same second of adding or updating entries. * lib/model: Weed out pending folders when receiving ClusterConfig. Filter the entries by timestamp, which must be newer than or equal to the reception time of the ClusterConfig. For just mentioned ones, this assumption will hold as AddOrUpdatePendingFolder() updates the timestamp. * lib/model, gui: Notify when one or more pending folders expired. Introduce new event type FolderOfferCancelled and use it to trigger a complete refreshCluster() cycle. Listing individual entries would be much more code and probably just as much work to answer the API request. * lib/model: Add comment and rename ExpirePendingFolders(). * lib/events: Rename FolderOfferCancelled to ClusterPendingChanged. * lib/model: Reuse ClusterPendingChanged event for cleanPending() Changing the config does not necessarily mean that the /resut/cluster/pending endpoints need to be refreshed, but only if something was actually removed. Detect this and indicate it through the ClusterPendingChanged event, which is already hooked up to requery respective endpoints within the GUI. No more need for a separate refreshCluster() in reaction to ConfigSaved event or calling refreshConfig(). * lib/model: Gofmt. * lib/db: Warn instead of info log for failed removal. * gui: Fix pending notifications not loading on GUI start. * lib/db: Use short device ID in log message. * lib/db: Return list of expired folder IDs after deleting them. * lib/model: Refactor Pending...Changed events. * lib/model: Adjust format of removed pending folders enumeration. Use an array of objects with device / folder ID properties, matching the other places where it's used. * lib/db: Drop invalid entries in RemovePendingFoldersBeforeTime(). * lib/model: Gofmt. My local gofmt did not complain here, strangely... * gui: Handle PendingDevicesChanged event. Even though it currently only holds one device at a time, wrap the contents in an array under the "added" property name. * lib/model: Fix null values in PendingFoldersChanged removed member. * gui: Handle PendingFoldersChanged event. * lib/model: Simplify construction of expiredPendingList. * lib/model: Reduce code duplication in cleanPending(). Use goto and a label for the common parts of calling the DB removal function and building the event data part. * lib/events, gui: Mark ...Rejected events deprecated. Extend comments explaining the conditions when the replacement event types are emitted. * lib/model: Wrap removed devices in array of objects as well. * lib/db: Use iter.Value() instead of needless db.Get(iter.Key()) * lib/db: Add comment explaining RemovePendingFoldersBeforeTime(). * lib/model: Rename fields folderID and deviceID in event data. * lib/db: Only list actually expired IDs as removed. Skip entries where Delete() failed as well as invalid entries that got removed automatically. * lib/model: Gofmt
2021-01-25 11:58:10 +01:00
};
console.log("rejected folder", rejected.folderID, "from device:", rejected.deviceID, offeringDevice);
var pendingFolder = $scope.pendingFolders[rejected.folderID];
if (pendingFolder === undefined) {
pendingFolder = {
offeredBy: {}
};
}
pendingFolder.offeredBy[rejected.deviceID] = offeringDevice;
$scope.pendingFolders[rejected.folderID] = pendingFolder;
});
}
if (arg.data.removed) {
arg.data.removed.forEach(function (folderDev) {
console.log("no longer pending folder", folderDev.folderID, "from device:", folderDev.deviceID);
if (folderDev.deviceID === undefined) {
delete $scope.pendingFolders[folderDev.folderID];
} else if ($scope.pendingFolders[folderDev.folderID]) {
delete $scope.pendingFolders[folderDev.folderID].offeredBy[folderDev.deviceID];
}
});
}
});
$scope.$on('ConfigLoaded', function () {
if ($scope.config.options.urAccepted === 0) {
// If usage reporting has been neither accepted nor declined,
// we want to ask the user to make a choice. But we don't want
// to bug them during initial setup, so we set a cookie with
// the time of the first visit. When that cookie is present
// and the time is more than four hours ago, we ask the
// question.
var firstVisit = document.cookie.replace(/(?:(?:^|.*;\s*)firstVisit\s*\=\s*([^;]*).*$)|^.*$/, "$1");
if (!firstVisit) {
document.cookie = "firstVisit=" + Date.now() + ";max-age=" + 30 * 24 * 3600;
} else {
if (+firstVisit < Date.now() - 4 * 3600 * 1000) {
$('#ur').modal();
}
}
}
});
$scope.$on(Events.CONFIG_SAVED, function (event, arg) {
updateLocalConfig(arg.data);
$http.get(urlbase + '/config/insync').success(function (data) {
$scope.configInSync = data.configInSync;
}).error($scope.emitHTTPError);
});
$scope.$on(Events.DOWNLOAD_PROGRESS, function (event, arg) {
var stats = arg.data;
var progress = {};
for (var folder in stats) {
progress[folder] = {};
for (var file in stats[folder]) {
var s = stats[folder][file];
var reused = 100 * s.reused / s.total;
var copiedFromOrigin = 100 * s.copiedFromOrigin / s.total;
var copiedFromElsewhere = 100 * s.copiedFromElsewhere / s.total;
var pulled = 100 * s.pulled / s.total;
var pulling = 100 * s.pulling / s.total;
2015-04-28 17:34:55 +02:00
// We try to round up pulling to at least a percent so that it would be at least a bit visible.
2014-12-04 00:44:39 +01:00
if (pulling < 1 && pulled + copiedFromElsewhere + copiedFromOrigin + reused <= 99) {
pulling = 1;
}
progress[folder][file] = {
reused: reused,
copiedFromOrigin: copiedFromOrigin,
copiedFromElsewhere: copiedFromElsewhere,
pulled: pulled,
pulling: pulling,
bytesTotal: s.bytesTotal,
bytesDone: s.bytesDone,
};
}
}
for (var folder in $scope.progress) {
if (!(folder in progress)) {
if ($scope.neededFolder === folder) {
$scope.refreshNeed($scope.needed.page, $scope.needed.perpage);
}
} else if ($scope.neededFolder === folder) {
for (file in $scope.progress[folder]) {
if (!(file in progress[folder])) {
$scope.refreshNeed($scope.needed.page, $scope.needed.perpage);
break;
}
}
}
}
$scope.progress = progress;
console.log("DownloadProgress", $scope.progress);
});
$scope.$on(Events.FOLDER_SUMMARY, function (event, arg) {
var data = arg.data;
$scope.model[data.folder] = data.summary;
recalcLocalStateTotal();
});
$scope.$on(Events.FOLDER_COMPLETION, function (event, arg) {
var data = arg.data;
if (!$scope.completion[data.device]) {
$scope.completion[data.device] = {};
}
$scope.completion[data.device][data.folder] = data;
recalcCompletion(data.device);
});
$scope.$on(Events.FOLDER_ERRORS, function (event, arg) {
if (!$scope.model[arg.data.folder]) {
console.log("Dropping folder errors event for unknown folder", arg.data.folder)
return;
}
$scope.model[arg.data.folder].errors = arg.data.errors.length;
});
2015-08-27 00:49:06 +02:00
$scope.$on(Events.FOLDER_SCAN_PROGRESS, function (event, arg) {
var data = arg.data;
$scope.scanProgress[data.folder] = {
current: data.current,
2015-11-17 21:08:36 +01:00
total: data.total,
rate: data.rate
2015-08-27 00:49:06 +02:00
};
console.log("FolderScanProgress", data);
});
// May be called through .error with the presented arguments, or through
// .catch with the http response object containing the same arguments.
$scope.emitHTTPError = function (data, status, headers, config) {
var out = data;
if (data && !data.data) {
out = { data: data, status: status, headers: headers, config: config };
}
$scope.$emit('HTTPError', out);
};
var debouncedFuncs = {};
function refreshFolder(folder) {
if ($scope.folders[folder].paused) {
return;
}
var key = "refreshFolder" + folder;
if (!debouncedFuncs[key]) {
debouncedFuncs[key] = debounce(function () {
$http.get(urlbase + '/db/status?folder=' + encodeURIComponent(folder)).success(function (data) {
$scope.model[folder] = data;
recalcLocalStateTotal();
console.log("refreshFolder", folder, data);
}).error($scope.emitHTTPError);
}, 1000);
}
debouncedFuncs[key]();
}
function updateLocalConfig(config) {
var hasConfig = !isEmptyObject($scope.config);
$scope.config = config;
$scope.config.options._listenAddressesStr = $scope.config.options.listenAddresses.join(', ');
$scope.config.options._globalAnnounceServersStr = $scope.config.options.globalAnnounceServers.join(', ');
$scope.config.options._urAcceptedStr = "" + $scope.config.options.urAccepted;
$scope.devices = deviceMap($scope.config.devices);
for (var id in $scope.devices) {
$scope.completion[id] = {
_total: 100,
_needBytes: 0,
_needItems: 0
};
};
$scope.folders = folderMap($scope.config.folders);
Object.keys($scope.folders).forEach(function (folder) {
refreshFolder(folder);
$scope.folders[folder].devices.forEach(function (deviceCfg) {
refreshCompletion(deviceCfg.deviceID, folder);
});
});
refreshNoAuthWarning();
setDefaultTheme();
if (!hasConfig) {
$scope.$emit('ConfigLoaded');
}
}
function refreshSystem() {
return $http.get(urlbase + '/system/status').success(function (data) {
$scope.myID = data.myID;
$scope.system = data;
if ($scope.reportDataPreviewVersion === '') {
$scope.reportDataPreviewVersion = $scope.system.urVersionMax;
}
var listenersFailed = [];
var listenersRunning = [];
for (var address in data.connectionServiceStatus) {
if (data.connectionServiceStatus[address].error) {
listenersFailed.push(address + ": " + data.connectionServiceStatus[address].error);
} else {
listenersRunning.push(address);
}
}
$scope.listenersFailed = listenersFailed;
$scope.listenersRunning = listenersRunning;
$scope.listenersTotal = $scope.sizeOf(data.connectionServiceStatus);
var discoveryFailed = [];
var discoveryRunning = [];
for (var disco in data.discoveryStatus) {
if (data.discoveryStatus[disco] && data.discoveryStatus[disco].error) {
discoveryFailed.push(disco + ": " + data.discoveryStatus[disco].error);
} else {
discoveryRunning.push(disco);
}
}
$scope.discoveryFailed = discoveryFailed;
$scope.discoveryRunning = discoveryRunning;
$scope.discoveryTotal = $scope.sizeOf(data.discoveryStatus);
refreshNoAuthWarning();
console.log("refreshSystem", data);
}).error($scope.emitHTTPError);
}
function refreshNoAuthWarning() {
if (!$scope.system || !$scope.config || !$scope.config.gui) {
// We need all to be able to determine the state.
return
}
// If we're not listening on localhost, and there is no
// authentication configured, and the magic setting to silence the
// warning isn't set, then yell at the user.
var addr = $scope.system.guiAddressUsed;
var guiCfg = $scope.config.gui;
$scope.openNoAuth = addr.substr(0, 4) !== "127."
&& addr.substr(0, 6) !== "[::1]:"
&& addr.substr(0, 1) !== "/"
&& (!guiCfg.user || !guiCfg.password)
&& guiCfg.authMode !== 'ldap'
&& !guiCfg.insecureAdminAccess;
if (guiCfg.user && guiCfg.password) {
$scope.dismissNotification('authenticationUserAndPassword');
}
}
function refreshCluster() {
return $q.all([
$http.get(urlbase + '/cluster/pending/devices').success(function (data) {
$scope.pendingDevices = data;
console.log("refreshCluster devices", data);
}).error($scope.emitHTTPError),
$http.get(urlbase + '/cluster/pending/folders').success(function (data) {
$scope.pendingFolders = data;
console.log("refreshCluster folders", data);
}).error($scope.emitHTTPError),
]);
}
function refreshDiscoveryCache() {
return $http.get(urlbase + '/system/discovery').success(function (data) {
for (var device in data) {
for (var i = 0; i < data[device].addresses.length; i++) {
// Relay addresses are URLs with
// .../?foo=barlongstuff that we strip away here. We
// remove the final slash as well for symmetry with
// tcp://192.0.2.42:1234 type addresses.
data[device].addresses[i] = data[device].addresses[i].replace(/\/\?.*/, '');
}
}
$scope.discoveryCache = data;
console.log("refreshDiscoveryCache", data);
}).error($scope.emitHTTPError);
}
function recalcLocalStateTotal() {
$scope.localStateTotal = {
bytes: 0,
directories: 0,
files: 0
};
for (var f in $scope.model) {
$scope.localStateTotal.bytes += $scope.model[f].localBytes;
$scope.localStateTotal.files += $scope.model[f].localFiles;
$scope.localStateTotal.directories += $scope.model[f].localDirectories;
}
}
function recalcCompletion(device) {
var total = 0, needed = 0, deletes = 0, items = 0;
for (var folder in $scope.completion[device]) {
if (folder === "_total" || folder === '_needBytes' || folder === '_needItems') {
continue;
}
total += $scope.completion[device][folder].globalBytes;
needed += $scope.completion[device][folder].needBytes;
items += $scope.completion[device][folder].needItems;
deletes += $scope.completion[device][folder].needDeletes;
}
if (total == 0) {
$scope.completion[device]._total = 100;
$scope.completion[device]._needBytes = 0;
$scope.completion[device]._needItems = 0;
} else {
$scope.completion[device]._total = Math.floor(100 * (1 - needed / total));
2019-04-23 19:37:51 +02:00
$scope.completion[device]._needBytes = needed;
$scope.completion[device]._needItems = items + deletes;
}
if (needed == 0 && deletes + items > 0 ) {
// We don't need any data, but we have deletes or
// dirs/links/empty files that we need to do. Drop down the
// completion percentage to indicate that we have stuff to do.
$scope.completion[device]._total = 95;
}
console.log("recalcCompletion", device, $scope.completion[device]);
}
function refreshCompletion(device, folder) {
if (device === $scope.myID) {
return;
}
$http.get(urlbase + '/db/completion?device=' + device + '&folder=' + encodeURIComponent(folder)).success(function (data) {
if (!$scope.completion[device]) {
$scope.completion[device] = {};
}
$scope.completion[device][folder] = data;
recalcCompletion(device);
}).error(function(data, status, headers, config) {
if (status === 404) {
console.log("refreshCompletion:", data);
} else {
$scope.emitHTTPError(data, status, headers, config);
}
});
}
function refreshConnectionStats() {
return $http.get(urlbase + '/system/connections').success(function (data) {
var now = Date.now(),
td = (now - prevDate) / 1000,
id;
prevDate = now;
2015-04-09 12:14:15 +02:00
try {
data.total.inbps = Math.max(0, (data.total.inBytesTotal - $scope.connectionsTotal.inBytesTotal) / td);
data.total.outbps = Math.max(0, (data.total.outBytesTotal - $scope.connectionsTotal.outBytesTotal) / td);
} catch (e) {
data.total.inbps = 0;
data.total.outbps = 0;
}
$scope.connectionsTotal = data.total;
2015-04-07 14:57:26 +02:00
data = data.connections;
for (id in data) {
if (!data.hasOwnProperty(id)) {
continue;
}
try {
data[id].inbps = Math.max(0, (data[id].inBytesTotal - $scope.connections[id].inBytesTotal) / td);
data[id].outbps = Math.max(0, (data[id].outBytesTotal - $scope.connections[id].outBytesTotal) / td);
} catch (e) {
data[id].inbps = 0;
data[id].outbps = 0;
}
}
$scope.connections = data;
console.log("refreshConnections", data);
}).error($scope.emitHTTPError);
}
function refreshErrors() {
$http.get(urlbase + '/system/error').success(function (data) {
$scope.errors = data.errors;
console.log("refreshErrors", data);
}).error($scope.emitHTTPError);
}
function refreshConfig() {
return $q.all([
$http.get(urlbase + '/config').success(function (data) {
updateLocalConfig(data);
console.log("refreshConfig", data);
}),
$http.get(urlbase + '/config/insync').success(function (data) {
$scope.configInSync = data.configInSync;
}),
]);
}
$scope.refreshNeed = function (page, perpage) {
if (!$scope.neededFolder) {
return;
}
var url = urlbase + "/db/need?folder=" + encodeURIComponent($scope.neededFolder);
url += "&page=" + page;
url += "&perpage=" + perpage;
$http.get(url).success(function (data) {
console.log("refreshNeed", $scope.neededFolder, data);
parseNeeded(data);
}).error($scope.emitHTTPError);
2021-05-03 12:14:54 +02:00
};
function needAction(file) {
var fDelete = 4096;
var fDirectory = 16384;
if ((file.flags & (fDelete + fDirectory)) === fDelete + fDirectory) {
return 'rmdir';
} else if ((file.flags & fDelete) === fDelete) {
return 'rm';
} else if ((file.flags & fDirectory) === fDirectory) {
return 'touch';
} else {
return 'sync';
}
}
function parseNeeded(data) {
$scope.needed = data;
var merged = [];
data.progress.forEach(function (item) {
item.type = "progress";
item.action = needAction(item);
merged.push(item);
});
data.queued.forEach(function (item) {
item.type = "queued";
item.action = needAction(item);
merged.push(item);
});
data.rest.forEach(function (item) {
item.type = "rest";
item.action = needAction(item);
merged.push(item);
});
$scope.needed.items = merged;
}
function pathJoin(base, name) {
base = expandTilde(base);
if (base[base.length - 1] !== $scope.system.pathSeparator) {
return base + $scope.system.pathSeparator + name;
}
return base + name;
}
function expandTilde(path) {
if (path && path.trim().charAt(0) === '~') {
return $scope.system.tilde + path.trim().substring(1);
}
return path;
}
function shouldSetDefaultFolderPath() {
return $scope.config.defaults.folder.path && $scope.folderEditor.folderPath.$pristine && $scope.editingFolderNew();
}
function resetRemoteNeed() {
$scope.remoteNeed = {};
$scope.remoteNeedFolders = [];
$scope.remoteNeedDevice = undefined;
}
function setDefaultTheme() {
if (!document.getElementById("fallback-theme-css")) {
// check if no support for prefers-color-scheme
var colorSchemeNotSupported = typeof window.matchMedia === "undefined" || window.matchMedia('(prefers-color-scheme: dark)').media === 'not all';
if ($scope.config.gui.theme === "default" && colorSchemeNotSupported) {
document.documentElement.style.display = 'none';
document.head.insertAdjacentHTML(
'beforeend',
'<link id="fallback-theme-css" rel="stylesheet" href="theme-assets/light/assets/css/theme.css" onload="document.documentElement.style.display = \'\'">'
);
}
}
}
function saveIgnores(ignores) {
return $http.post(urlbase + '/db/ignores?folder=' + encodeURIComponent($scope.currentFolder.id), {
ignore: ignores
});
};
function initShareEditing(editing) {
$scope.currentSharing = {};
$scope.currentSharing.editing = editing;
$scope.currentSharing.shared = [];
$scope.currentSharing.unrelated = [];
$scope.currentSharing.selected = {};
$scope.currentSharing.encryptionPasswords = {};
if (editing === 'folder') {
initShareEditingFolder();
}
};
function initShareEditingFolder() {
$scope.currentFolder.devices.forEach(function (n) {
if (n.deviceID !== $scope.myID) {
$scope.currentSharing.shared.push($scope.devices[n.deviceID]);
}
if (n.encryptionPassword !== '') {
$scope.currentSharing.encryptionPasswords[n.deviceID] = n.encryptionPassword;
}
$scope.currentSharing.selected[n.deviceID] = true;
});
$scope.currentSharing.shared.sort(deviceCompare);
$scope.currentSharing.unrelated = $scope.deviceList().filter(function (n) {
return n.deviceID !== $scope.myID && !$scope.currentSharing.selected[n.deviceID];
});
}
$scope.pendingIsRemoteEncrypted = function(folderID, deviceID) {
var pending = $scope.pendingFolders[folderID];
if (!pending || !pending.offeredBy || !pending.offeredBy[deviceID]) {
return false;
}
return pending.offeredBy[deviceID].remoteEncrypted;
};
$scope.refreshFailed = function (page, perpage) {
if (!$scope.failed || !$scope.failed.folder) {
return;
}
var url = urlbase + '/folder/errors?folder=' + encodeURIComponent($scope.failed.folder);
url += "&page=" + page + "&perpage=" + perpage;
$http.get(url).success(function (data) {
$scope.failed = data;
}).error($scope.emitHTTPError);
};
$scope.refreshRemoteNeed = function (folder, page, perpage) {
if (!$scope.remoteNeedDevice) {
return;
}
var url = urlbase + '/db/remoteneed?device=' + $scope.remoteNeedDevice.deviceID;
url += '&folder=' + encodeURIComponent(folder);
url += "&page=" + page + "&perpage=" + perpage;
$http.get(url).success(function (data) {
$scope.remoteNeed[folder] = data;
}).error(function (err) {
$scope.remoteNeed[folder] = undefined;
$scope.emitHTTPError(err);
});
};
$scope.refreshLocalChanged = function (page, perpage) {
if (!$scope.localChangedFolder) {
return;
}
var url = urlbase + '/db/localchanged?folder=';
url += encodeURIComponent($scope.localChangedFolder);
url += "&page=" + page + "&perpage=" + perpage;
$http.get(url).success(function (data) {
$scope.localChanged = data;
}).error($scope.emitHTTPError);
};
var refreshDeviceStats = debounce(function () {
$http.get(urlbase + "/stats/device").success(function (data) {
$scope.deviceStats = data;
for (var device in $scope.deviceStats) {
$scope.deviceStats[device].lastSeen = new Date($scope.deviceStats[device].lastSeen);
$scope.deviceStats[device].lastSeenDays = (new Date() - $scope.deviceStats[device].lastSeen) / 1000 / 86400;
}
console.log("refreshDeviceStats", data);
}).error($scope.emitHTTPError);
}, 2500);
var refreshFolderStats = debounce(function () {
$http.get(urlbase + "/stats/folder").success(function (data) {
$scope.folderStats = data;
for (var folder in $scope.folderStats) {
if ($scope.folderStats[folder].lastFile) {
$scope.folderStats[folder].lastFile.at = new Date($scope.folderStats[folder].lastFile.at);
}
$scope.folderStats[folder].lastScan = new Date($scope.folderStats[folder].lastScan);
$scope.folderStats[folder].lastScanDays = (new Date() - $scope.folderStats[folder].lastScan) / 1000 / 86400;
}
console.log("refreshfolderStats", data);
}).error($scope.emitHTTPError);
}, 2500);
var refreshThemes = debounce(function () {
$http.get("themes.json").success(function (data) { // no urlbase here as this is served by the asset handler
$scope.themes = data.themes;
}).error($scope.emitHTTPError);
}, 2500);
var refreshGlobalChanges = debounce(function () {
$http.get(urlbase + "/events/disk?limit=25").success(function (data) {
if (!data) {
// For reasons unknown this is called with data being the empty
// string on shutdown, causing an error on .reverse().
return;
}
data = data.reverse();
$scope.globalChangeEvents = data;
console.log("refreshGlobalChanges", data);
}).error($scope.emitHTTPError);
}, 2500);
$scope.refresh = function () {
refreshSystem();
refreshDiscoveryCache();
refreshConnectionStats();
refreshErrors();
};
$scope.folderStatus = function (folderCfg) {
if (folderCfg.paused) {
return 'paused';
}
var folderInfo = $scope.model[folderCfg.id];
// after restart syncthing process state may be empty
if (typeof folderInfo === 'undefined' || !folderInfo.state) {
return 'unknown';
}
var state = '' + folderInfo.state;
if (state === 'error') {
return 'stopped'; // legacy, the state is called "stopped" in the GUI
}
if (state !== 'idle') {
return state;
}
if (folderInfo.needTotalItems > 0) {
return 'outofsync';
}
if ($scope.hasFailedFiles(folderCfg.id)) {
return 'faileditems';
}
if ($scope.hasReceiveOnlyChanged(folderCfg)) {
if (folderCfg.type === "receiveonly") {
return 'localadditions';
}
return 'localunencrypted';
}
if (folderCfg.devices.length <= 1) {
return 'unshared';
}
return state;
};
$scope.folderClass = function (folderCfg) {
var status = $scope.folderStatus(folderCfg);
if (status === 'idle' || status === 'localadditions') {
return 'success';
}
if (status == 'paused') {
return 'default';
}
if (status === 'syncing' || status === 'sync-preparing' || status === 'scanning' || status === 'cleaning') {
return 'primary';
}
if (status === 'unknown') {
return 'info';
}
if (status === 'stopped' || status === 'outofsync' || status === 'error' || status === 'faileditems' || status === 'localunencrypted') {
return 'danger';
}
if (status === 'unshared' || status === 'scan-waiting' || status === 'sync-waiting' || status === 'clean-waiting') {
return 'warning';
}
return 'info';
};
$scope.syncPercentage = function (folder) {
if (typeof $scope.model[folder] === 'undefined') {
return 100;
}
if ($scope.model[folder].needTotalItems === 0) {
return 100;
}
if (($scope.model[folder].needBytes == 0 && $scope.model[folder].needDeletes > 0) || $scope.model[folder].globalBytes == 0) {
// We don't need any data, but we have deletes that we need
// to do. Drop down the completion percentage to indicate
// that we have stuff to do.
// Do the same thing in case we only have zero byte files to sync.
return 95;
}
var pct = 100 * $scope.model[folder].inSyncBytes / $scope.model[folder].globalBytes;
return Math.floor(pct);
};
2015-08-27 00:49:06 +02:00
$scope.scanPercentage = function (folder) {
if (!$scope.scanProgress[folder]) {
return undefined;
}
var pct = 100 * $scope.scanProgress[folder].current / $scope.scanProgress[folder].total;
return Math.floor(pct);
};
2015-08-27 00:49:06 +02:00
2015-11-17 21:08:36 +01:00
$scope.scanRate = function (folder) {
if (!$scope.scanProgress[folder]) {
return 0;
}
return $scope.scanProgress[folder].rate;
};
2015-11-17 21:08:36 +01:00
$scope.scanRemaining = function (folder) {
// Formats the remaining scan time as a string. Includes days and
// hours only when relevant, resulting in time stamps like:
// 00m 40s
// 32m 40s
// 2h 32m
// 4d 2h
// In case remaining scan time appears to be >31d, omit the
// details, i.e.:
// > 1 month
if (!$scope.scanProgress[folder]) {
return "";
}
// Calculate remaining bytes and seconds based on our current
// rate.
var remainingBytes = $scope.scanProgress[folder].total - $scope.scanProgress[folder].current;
var seconds = remainingBytes / $scope.scanProgress[folder].rate;
// Round up to closest ten seconds to avoid flapping too much to
// and fro.
seconds = Math.ceil(seconds / 10) * 10;
// Separate out the number of days.
var days = 0;
var res = [];
if (seconds >= 86400) {
days = Math.floor(seconds / 86400);
if (days > 31) {
return '> 1 month';
}
2021-05-03 12:14:54 +02:00
res.push('' + days + 'd');
seconds = seconds % 86400;
}
// Separate out the number of hours.
var hours = 0;
if (seconds > 3600) {
hours = Math.floor(seconds / 3600);
2021-05-03 12:14:54 +02:00
res.push('' + hours + 'h');
seconds = seconds % 3600;
}
var d = new Date(1970, 0, 1).setSeconds(seconds);
if (days === 0) {
// Format minutes only if we're within a day of completion.
var f = $filter('date')(d, "m'm'");
res.push(f);
}
if (days === 0 && hours === 0) {
// Format seconds only when we're within an hour of completion.
var f = $filter('date')(d, "ss's'");
res.push(f);
}
return res.join(' ');
};
$scope.deviceStatus = function (deviceCfg) {
var status = '';
if ($scope.deviceFolders(deviceCfg).length === 0) {
status = 'unused-';
}
2015-08-23 21:56:10 +02:00
if (typeof $scope.connections[deviceCfg.deviceID] === 'undefined') {
return 'unknown';
}
if (deviceCfg.paused) {
return status + 'paused';
2015-08-23 21:56:10 +02:00
}
if ($scope.connections[deviceCfg.deviceID].connected) {
if ($scope.completion[deviceCfg.deviceID] && $scope.completion[deviceCfg.deviceID]._total === 100) {
return status + 'insync';
} else {
return 'syncing';
}
}
// Disconnected
return status + 'disconnected';
};
$scope.deviceClass = function (deviceCfg) {
2015-08-23 21:56:10 +02:00
if (typeof $scope.connections[deviceCfg.deviceID] === 'undefined') {
return 'info';
}
if (deviceCfg.paused) {
2015-08-23 21:56:10 +02:00
return 'default';
}
if ($scope.connections[deviceCfg.deviceID].connected) {
if ($scope.completion[deviceCfg.deviceID] && $scope.completion[deviceCfg.deviceID]._total === 100) {
return 'success';
} else {
return 'primary';
}
}
// Disconnected
return 'info';
};
$scope.syncthingStatus = function () {
var syncCount = 0;
var notifyCount = 0;
var pauseCount = 0;
// loop through all folders
var folderListCache = $scope.folderList();
for (var i = 0; i < folderListCache.length; i++) {
var status = $scope.folderStatus(folderListCache[i]);
switch (status) {
case 'sync-preparing':
case 'syncing':
syncCount++;
break;
case 'stopped':
case 'unknown':
case 'outofsync':
case 'error':
notifyCount++;
break;
}
}
// loop through all devices
var deviceCount = 0;
for (var id in $scope.devices) {
var status = $scope.deviceStatus({
deviceID: id
});
switch (status) {
case 'unknown':
notifyCount++;
break;
case 'paused':
pauseCount++;
break;
case 'unused':
deviceCount--;
break;
}
deviceCount++;
}
// enumerate notifications
if ($scope.openNoAuth || !$scope.configInSync || $scope.errorList().length > 0 || !online || Object.keys($scope.pendingDevices).length > 0 || Object.keys($scope.pendingFolders).length > 0) {
notifyCount++;
}
// at least one folder is syncing
if (syncCount > 0) {
return 'sync';
}
// a device is unknown or a folder is stopped/unknown/outofsync/error or some other notification is open or gui offline
if (notifyCount > 0) {
return 'notify';
}
// all used devices are paused except (this) one
if (pauseCount === deviceCount - 1) {
return 'pause';
}
return 'default';
};
$scope.deviceAddr = function (deviceCfg) {
var conn = $scope.connections[deviceCfg.deviceID];
2015-08-23 21:56:10 +02:00
if (conn && conn.connected) {
return conn.address;
}
return '?';
};
$scope.hasRemoteGUIAddress = function (deviceCfg) {
if (!deviceCfg.remoteGUIPort)
return false;
var conn = $scope.connections[deviceCfg.deviceID];
return conn && conn.connected && conn.address && conn.type.indexOf('Relay') == -1;
};
$scope.remoteGUIAddress = function (deviceCfg) {
// Assume hasRemoteGUIAddress is true or we would not be here
var conn = $scope.connections[deviceCfg.deviceID];
return 'http://' + replaceAddressPort(conn.address, deviceCfg.remoteGUIPort);
};
function replaceAddressPort(address, newPort) {
for (var index = address.length - 1; index >= 0; index--) {
if (address[index] === ":") {
return address.substr(0, index) + ":" + newPort.toString();
}
}
return address;
}
$scope.friendlyNameFromShort = function (shortID) {
var matches = Object.keys($scope.devices).filter(function (id) {
return id.substr(0, 7) === shortID;
});
if (matches.length !== 1) {
return shortID;
}
return $scope.friendlyNameFromID(matches[0]);
};
$scope.friendlyNameFromID = function (deviceID) {
var match = $scope.devices[deviceID];
if (match) {
return $scope.deviceName(match);
}
return deviceID.substr(0, 6);
};
$scope.deviceName = function (deviceCfg) {
if (typeof deviceCfg === 'undefined') {
return "";
}
if (deviceCfg.name) {
return deviceCfg.name;
}
return $scope.deviceShortID(deviceCfg.deviceID);
};
$scope.deviceShortID = function (deviceID) {
if (typeof deviceID === 'undefined') {
return "";
}
return deviceID.substr(0, 6);
};
$scope.thisDeviceName = function () {
var device = $scope.thisDevice();
if (typeof device === 'undefined') {
return "(unknown device)";
}
if (device.name) {
return device.name;
}
return device.deviceID.substr(0, 6);
};
$scope.showDeviceIdentification = function (deviceCfg) {
$scope.currentDevice = deviceCfg;
$('#idqr').modal();
};
$scope.setDevicePause = function (device, pause) {
$scope.devices[device].paused = pause;
$scope.config.devices = $scope.deviceList();
$scope.saveConfig();
2015-08-23 21:56:10 +02:00
};
$scope.setFolderPause = function (folder, pause) {
var cfg = $scope.folders[folder];
if (cfg) {
cfg.paused = pause;
$scope.config.folders = folderList($scope.folders);
return $scope.saveConfig();
}
return $q.when();
2015-08-23 21:56:10 +02:00
};
$scope.showListenerStatus = function () {
var params = {
type: 'listeners',
};
if ($scope.listenersFailed.length > 0) {
params.status = 'danger';
params.heading = $translate.instant("Listener Failures");
} else {
params.status = 'default';
params.heading = $translate.instant("Listener Status");
}
$scope.connectivityStatusParams = params;
$('#connectivity-status').modal();
};
$scope.showDiscoveryStatus = function () {
var params = {
type: 'discovery',
};
if ($scope.discoveryFailed.length > 0) {
params.status = 'danger';
params.heading = $translate.instant("Discovery Failures");
} else {
params.status = 'default';
params.heading = $translate.instant("Discovery Status");
}
$scope.connectivityStatusParams = params;
$('#connectivity-status').modal();
};
$scope.logging = {
facilities: {},
refreshFacilities: function () {
$http.get(urlbase + '/system/debug').success(function (data) {
var facilities = {};
data.enabled = data.enabled || [];
$.each(data.facilities, function (key, value) {
facilities[key] = {
description: value,
enabled: data.enabled.indexOf(key) > -1
}
})
$scope.logging.facilities = facilities;
}).error($scope.emitHTTPError);
},
show: function () {
$scope.logging.paused = false;
$scope.logging.refreshFacilities();
$scope.logging.timer = $timeout($scope.logging.fetch);
var textArea = $('#logViewerText');
textArea.on("scroll", $scope.logging.onScroll);
$('#logViewer').modal().one('shown.bs.modal', function () {
// Scroll to bottom.
textArea.scrollTop(textArea[0].scrollHeight);
}).one('hidden.bs.modal', function () {
$timeout.cancel($scope.logging.timer);
textArea.off("scroll", $scope.logging.onScroll);
$scope.logging.timer = null;
$scope.logging.entries = [];
});
},
onFacilityChange: function (facility) {
var enabled = $scope.logging.facilities[facility].enabled;
// Disable checkboxes while we're in flight.
$.each($scope.logging.facilities, function (key) {
$scope.logging.facilities[key].enabled = null;
})
$http.post(urlbase + '/system/debug?' + (enabled ? 'enable=' : 'disable=') + facility)
.success($scope.logging.refreshFacilities)
.error($scope.emitHTTPError);
},
onScroll: function () {
var textArea = $('#logViewerText');
var scrollTop = textArea.prop('scrollTop');
var scrollHeight = textArea.prop('scrollHeight');
$scope.logging.paused = scrollHeight > (scrollTop + textArea.outerHeight());
// Browser events do not cause redraw, trigger manually.
$scope.$apply();
},
timer: null,
entries: [],
paused: false,
content: function () {
var content = "";
$.each($scope.logging.entries, function (idx, entry) {
content += entry.when.split('.')[0].replace('T', ' ') + ' ' + entry.message + "\n";
});
return content;
},
fetch: function () {
var textArea = $('#logViewerText');
if ($scope.logging.paused) {
if (!$scope.logging.timer) return;
$scope.logging.timer = $timeout($scope.logging.fetch, 500);
return;
}
var last = null;
if ($scope.logging.entries.length > 0) {
last = $scope.logging.entries[$scope.logging.entries.length - 1].when;
}
$http.get(urlbase + '/system/log' + (last ? '?since=' + encodeURIComponent(last) : '')).success(function (data) {
if (!$scope.logging.timer) return;
$scope.logging.timer = $timeout($scope.logging.fetch, 2000);
if (!$scope.logging.paused) {
if (data.messages) {
$scope.logging.entries.push.apply($scope.logging.entries, data.messages);
// Wait for the text area to be redrawn, adding new lines, and then scroll to bottom.
$timeout(function () {
textArea.scrollTop(textArea[0].scrollHeight);
});
}
}
});
}
2019-04-23 19:37:51 +02:00
};
gui, api: Show internal config and state paths (fixes #8323) (#8324) * lib/locations: Fix enum values camelCase. * lib/locations: Remove unused FailuresFile. * cmd/syncthing: Turn around role of locations storage. Previously the locations package was used to provide default paths, possibly with an overridden home directory. Extra paths supplied on the command line were handled and passed around in the options object. To make the changed paths available to any other interested package, override the location setting from the option if supplied, instead of vice versa when not supplied. Adapt code using this to read from the locations package instead of passing through the options object. * lib/locations: Refactor showPaths to locations package. Generate a reusable string in locations.PrettyPrintPaths(). Enumerating all possible locations in different packages is error prone, so add a new public function to generate the listing as a string in the locations package. Adapt cmd/syncthing --paths to use that instead of its own console output. * lib/locations: Include CSRF token in pretty printed paths. * lib/api: New endpoint /rest/system/paths. The paths should be available for troubleshooting from a running instance. Using the --paths CLI option is not easy in some environments, so expose the locations mapping to a JSON endpoint. Add utility function ListExpandedPaths() that also filters out any entries which still contain variable placeholders. * gui: List runtime paths in separate log viewer tab. * Wrap paths. * lib/syncthing: Utilize locations.Get() instead of passing an arg. * Include base directories, move label to table caption. * gui: Switch to hard-coded paths instead of iterating over all. * gui: Break aboutModalView into tabs. Use tabs to separate authors from included third-party software. * gui: Move paths from log viewer to about modal. * lib/locations: Adjust pretty print output order to match GUI. * gui, authors: Remove additional bot names and fix indent. The indentation changed because of the tabbed about dialog, fix the authors script to respect that. Skip Syncthing*Automation in authors list as well. * Update AUTHORS list to remove bot names. * Revert AUTHORS email order change. * Do not emphasize DB and log file locations. * Review line wrapping. * review part 1: strings.Builder, naming * Rename and extend locations.Set() with error handling. Remodel the Override() function along the existing SetBaseDir() and rename it to simply Set(). Make sure to use absolute paths when given log file or GUI assets override options. Add proper error reporting if that goes wrong. * Remove obsolete comment about empty logfile option. * Don't filter out unexpanded baseDir placeholders, only ${timestamp}. * Restore behavior regarding special "-" logfile argument. If the option is given, but with empty value, assume the no log file (same as "-"). Don't try to convert the special value to an absolute path though and document this fact in a comment for the Set() function. * Use template to check for location key validity. * Don't filter out timestamp placeholders. * lib/api: Remove paths from /rest/system/status. * lib/ur: Properly initialize map in failure data (fixes #8479) Co-authored-by: Jakob Borg <jakob@kastelo.net>
2022-08-10 08:25:13 +02:00
$scope.about = {
paths: {},
refreshPaths: function () {
$http.get(urlbase + '/system/paths').success(function (data) {
$scope.about.paths = data;
}).error($scope.emitHTTPError);
},
show: function () {
$scope.about.refreshPaths();
$('#about').modal("show");
},
};
$scope.discardChangedSettings = function () {
$("#discard-changes-confirmation").modal("hide");
$("#settings").off("hide.bs.modal").modal("hide");
};
$scope.showSettings = function () {
// Make a working copy
$scope.tmpOptions = angular.copy($scope.config.options);
$scope.tmpOptions.deviceName = $scope.thisDevice().name;
$scope.tmpOptions.upgrades = "none";
if ($scope.tmpOptions.autoUpgradeIntervalH > 0) {
$scope.tmpOptions.upgrades = "stable";
}
if ($scope.tmpOptions.upgradeToPreReleases) {
$scope.tmpOptions.upgrades = "candidate";
}
$scope.tmpGUI = angular.copy($scope.config.gui);
$scope.tmpRemoteIgnoredDevices = angular.copy($scope.config.remoteIgnoredDevices);
$scope.tmpDevices = angular.copy($scope.config.devices);
$('#settings').modal("show");
$("#settings a[href='#settings-general']").tab("show");
$("#settings").on('hide.bs.modal', function (event) {
if ($scope.settingsModified()) {
event.preventDefault();
$("#discard-changes-confirmation").modal("show");
} else {
$("#settings").off("hide.bs.modal");
}
});
};
$scope.saveConfig = function () {
var cfg = JSON.stringify($scope.config);
var opts = {
headers: {
'Content-Type': 'application/json'
}
};
return $http.put(urlbase + '/config', cfg, opts).finally(refreshConfig).catch($scope.emitHTTPError);
};
$scope.urVersions = function () {
var result = [];
if ($scope.system) {
for (var i = $scope.system.urVersionMax; i >= 2; i--) {
result.push("" + i);
}
}
return result;
};
$scope.settingsModified = function () {
// Options has artificial properties injected into the temp config.
// Need to recompute them before we can check equality
var options = angular.copy($scope.config.options);
options.deviceName = $scope.thisDevice().name;
options.upgrades = "none";
if (options.autoUpgradeIntervalH > 0) {
options.upgrades = "stable";
}
if (options.upgradeToPreReleases) {
options.upgrades = "candidate";
}
var optionsEqual = angular.equals(options, $scope.tmpOptions);
var guiEquals = angular.equals($scope.config.gui, $scope.tmpGUI);
var ignoredDevicesEquals = angular.equals($scope.config.remoteIgnoredDevices, $scope.tmpRemoteIgnoredDevices);
var ignoredFoldersEquals = angular.equals($scope.config.devices, $scope.tmpDevices);
console.log("settings equals - options: " + optionsEqual + " gui: " + guiEquals + " ignDev: " + ignoredDevicesEquals + " ignFol: " + ignoredFoldersEquals);
return !optionsEqual || !guiEquals || !ignoredDevicesEquals || !ignoredFoldersEquals;
2019-04-23 19:37:51 +02:00
};
$scope.saveSettings = function () {
// Make sure something changed
if ($scope.settingsModified()) {
var themeChanged = $scope.config.gui.theme !== $scope.tmpGUI.theme;
// Angular has issues with selects with numeric values, so we handle strings here.
$scope.tmpOptions.urAccepted = parseInt($scope.tmpOptions._urAcceptedStr);
// Check if auto-upgrade has been enabled or disabled. This
// also has an effect on usage reporting, so do the check
// for that later.
if ($scope.tmpOptions.upgrades == "candidate") {
$scope.tmpOptions.autoUpgradeIntervalH = $scope.tmpOptions.autoUpgradeIntervalH || 12;
$scope.tmpOptions.upgradeToPreReleases = true;
$scope.tmpOptions.urAccepted = $scope.system.urVersionMax;
$scope.tmpOptions.urSeen = $scope.system.urVersionMax;
} else if ($scope.tmpOptions.upgrades == "stable") {
$scope.tmpOptions.autoUpgradeIntervalH = $scope.tmpOptions.autoUpgradeIntervalH || 12;
$scope.tmpOptions.upgradeToPreReleases = false;
} else {
$scope.tmpOptions.autoUpgradeIntervalH = 0;
$scope.tmpOptions.upgradeToPreReleases = false;
}
// Check if protocol will need to be changed on restart
if ($scope.config.gui.useTLS !== $scope.tmpGUI.useTLS) {
$scope.protocolChanged = true;
}
// Parse strings to arrays before copying over
['listenAddresses', 'globalAnnounceServers'].forEach(function (key) {
$scope.tmpOptions[key] = $scope.tmpOptions["_" + key + "Str"].split(/[ ,]+/).map(function (x) {
return x.trim();
});
});
// Apply new settings locally
$scope.thisDeviceIn($scope.tmpDevices).name = $scope.tmpOptions.deviceName;
$scope.config.options = angular.copy($scope.tmpOptions);
$scope.config.gui = angular.copy($scope.tmpGUI);
$scope.config.remoteIgnoredDevices = angular.copy($scope.tmpRemoteIgnoredDevices);
$scope.config.devices = angular.copy($scope.tmpDevices);
// $scope.devices is updated by updateLocalConfig based on
// the config changed event, but settingsModified will look
// at it before that and conclude that the settings are
// modified (even though we just saved) unless we update
// here as well...
$scope.devices = deviceMap($scope.config.devices);
$scope.saveConfig().then(function () {
if (themeChanged) {
document.location.reload(true);
}
});
}
$("#settings").off("hide.bs.modal").modal("hide");
};
$scope.saveAdvanced = function () {
$scope.config = $scope.advancedConfig;
$scope.saveConfig();
$('#advanced').modal("hide");
};
$scope.restart = function () {
restarting = true;
$('#restarting').modal();
$http.post(urlbase + '/system/restart');
$scope.configInSync = true;
// Switch webpage protocol if needed
if ($scope.protocolChanged) {
var protocol = 'http';
if ($scope.config.gui.useTLS) {
protocol = 'https';
}
setTimeout(function () {
window.location.protocol = protocol;
}, 2500);
$scope.protocolChanged = false;
}
};
$scope.upgrade = function () {
restarting = true;
$('#upgrade').modal('hide');
2015-04-22 14:41:08 +02:00
$('#majorUpgrade').modal('hide');
$('#upgrading').modal();
$http.post(urlbase + '/system/upgrade').success(function () {
$('#restarting').modal();
$('#upgrading').modal('hide');
}).error(function () {
$('#upgrading').modal('hide');
});
};
$scope.shutdown = function () {
restarting = true;
$http.post(urlbase + '/system/shutdown').success(function () {
$('#shutdown').modal();
}).error($scope.emitHTTPError);
$scope.configInSync = true;
};
function editDeviceModal() {
$scope.currentDevice._addressesStr = $scope.currentDevice.addresses.join(', ');
$scope.deviceEditor.$setPristine();
$('#editDevice').modal();
}
$scope.editDeviceModalTitle = function() {
if ($scope.editingDeviceDefaults()) {
return $translate.instant("Edit Device Defaults");
}
var title = '';
if ($scope.editingDeviceExisting()) {
title += $translate.instant("Edit Device");
} else {
title += $translate.instant("Add Device");
}
var name = $scope.deviceName($scope.currentDevice);
if (name !== '') {
title += ' (' + name + ')';
}
return title;
};
$scope.editDeviceModalIcon = function() {
if ($scope.has(["existing", "defaults"], $scope.currentDevice._editing)) {
return 'fas fa-pencil-alt';
}
return 'fas fa-desktop';
};
$scope.editingDeviceDefaults = function() {
return $scope.currentDevice._editing == 'defaults';
}
$scope.editingDeviceExisting = function() {
return $scope.currentDevice._editing == 'existing';
}
$scope.editingDeviceNew = function() {
// The "new-pending" value is intentionally disregarded here.
return $scope.currentDevice._editing == 'new';
}
$scope.editDeviceExisting = function (deviceCfg) {
$scope.currentDevice = $.extend({}, deviceCfg);
$scope.currentDevice._editing = "existing";
$scope.willBeReintroducedBy = undefined;
if (deviceCfg.introducedBy) {
var introducerDevice = $scope.devices[deviceCfg.introducedBy];
if (introducerDevice && introducerDevice.introducer) {
$scope.willBeReintroducedBy = $scope.deviceName(introducerDevice);
}
}
initShareEditing('device');
$scope.deviceFolders($scope.currentDevice).forEach(function (folderID) {
$scope.currentSharing.shared.push($scope.folders[folderID]);
$scope.currentSharing.selected[folderID] = true;
var folderdevices = $scope.folders[folderID].devices;
for (var i = 0; i < folderdevices.length; i++) {
if (folderdevices[i].deviceID === deviceCfg.deviceID) {
$scope.currentSharing.encryptionPasswords[folderID] = folderdevices[i].encryptionPassword;
break;
}
}
});
$scope.currentSharing.unrelated = $scope.folderList().filter(function (n) {
return !$scope.currentSharing.selected[n.id];
});
editDeviceModal();
};
$scope.editDeviceDefaults = function () {
$http.get(urlbase + '/config/defaults/device').then(function (p) {
$scope.currentDevice = p.data;
$scope.currentDevice._editing = "defaults";
editDeviceModal();
}, $scope.emitHTTPError);
};
$scope.selectAllSharedFolders = function (state) {
var folders = $scope.currentSharing.shared;
for (var i = 0; i < folders.length; i++) {
$scope.currentSharing.selected[folders[i].id] = !!state;
}
};
$scope.selectAllUnrelatedFolders = function (state) {
var folders = $scope.currentSharing.unrelated;
for (var i = 0; i < folders.length; i++) {
$scope.currentSharing.selected[folders[i].id] = !!state;
}
};
$scope.addDevice = function (deviceID, name) {
$scope.discoveryUnknown = [];
for (var id in $scope.discoveryCache) {
if ($scope.discoveryUnknown.length === 100) {
break;
}
if (id in $scope.devices) {
continue
}
$scope.discoveryUnknown.push(id);
}
return $http.get(urlbase + '/config/defaults/device').then(function (p) {
$scope.currentDevice = p.data;
$scope.currentDevice.name = name;
$scope.currentDevice.deviceID = deviceID;
if (deviceID) {
$scope.currentDevice._editing = "new-pending";
} else {
$scope.currentDevice._editing = "new";
}
initShareEditing('device');
$scope.currentSharing.unrelated = $scope.folderList();
editDeviceModal();
}, $scope.emitHTTPError);
};
$scope.deleteDevice = function () {
$('#editDevice').modal('hide');
if ($scope.currentDevice._editing != "existing") {
return;
}
var id = $scope.currentDevice.deviceID
delete $scope.devices[id];
$scope.config.devices = $scope.deviceList();
for (var id in $scope.folders) {
$scope.folders[id].devices = $scope.folders[id].devices.filter(function (n) {
return n.deviceID !== $scope.currentDevice.deviceID;
});
}
$scope.saveConfig();
};
$scope.saveDevice = function () {
$('#editDevice').modal('hide');
$scope.currentDevice.addresses = $scope.currentDevice._addressesStr.split(',').map(function (x) {
return x.trim();
});
delete $scope.currentDevice._addressesStr;
if ($scope.currentDevice._editing == "defaults") {
$scope.config.defaults.device = $scope.currentDevice;
} else {
setDeviceConfig();
}
delete $scope.currentSharing;
$scope.currentDevice = {};
$scope.saveConfig();
};
function setDeviceConfig() {
var currentID = $scope.currentDevice.deviceID;
$scope.devices[currentID] = $scope.currentDevice;
$scope.config.devices = deviceList($scope.devices);
for (var id in $scope.currentSharing.selected) {
if ($scope.currentSharing.selected[id]) {
var found = false;
for (i = 0; i < $scope.folders[id].devices.length; i++) {
if ($scope.folders[id].devices[i].deviceID === currentID) {
found = true;
// Update encryption pw
$scope.folders[id].devices[i].encryptionPassword = $scope.currentSharing.encryptionPasswords[id];
break;
}
}
if (!found) {
// Add device to folder
$scope.folders[id].devices.push({
deviceID: currentID,
encryptionPassword: $scope.currentSharing.encryptionPasswords[id],
});
}
} else {
// Remove device from folder
$scope.folders[id].devices = $scope.folders[id].devices.filter(function (n) {
return n.deviceID !== currentID;
});
}
}
$scope.config.folders = folderList($scope.folders);
};
$scope.ignoreDevice = function (deviceID, pendingDevice) {
var ignoredDevice = angular.copy(pendingDevice);
ignoredDevice.deviceID = deviceID;
// Bump time
ignoredDevice.time = (new Date()).toISOString();
$scope.config.remoteIgnoredDevices.push(ignoredDevice);
$scope.saveConfig();
};
$scope.dismissPendingDevice = function (deviceID) {
$http.delete(urlbase + '/cluster/pending/devices?device=' + encodeURIComponent(deviceID));
};
$scope.unignoreDeviceFromTemporaryConfig = function (ignoredDevice) {
$scope.tmpRemoteIgnoredDevices = $scope.tmpRemoteIgnoredDevices.filter(function (existingIgnoredDevice) {
return ignoredDevice.deviceID !== existingIgnoredDevice.deviceID;
});
};
$scope.ignoredFoldersCountTmpConfig = function () {
var count = 0;
($scope.tmpDevices || []).forEach(function (deviceCfg) {
count += deviceCfg.ignoredFolders.length;
});
return count;
};
$scope.unignoreFolderFromTemporaryConfig = function (device, ignoredFolderID) {
for (var i = 0; i < $scope.tmpDevices.length; i++) {
if ($scope.tmpDevices[i].deviceID == device) {
$scope.tmpDevices[i].ignoredFolders = $scope.tmpDevices[i].ignoredFolders.filter(function (existingIgnoredFolder) {
return existingIgnoredFolder.id !== ignoredFolderID;
});
return;
}
}
};
$scope.otherDevices = function () {
return $scope.deviceList().filter(function (n) {
return n.deviceID !== $scope.myID;
});
};
$scope.thisDevice = function () {
return $scope.devices[$scope.myID];
2019-04-23 19:37:51 +02:00
};
$scope.thisDeviceIn = function (l) {
for (var i = 0; i < l.length; i++) {
var n = l[i];
if (n.deviceID === $scope.myID) {
return n;
}
}
};
$scope.allDevices = function () {
var devices = $scope.otherDevices();
devices.push($scope.thisDevice());
return devices;
};
$scope.setAllDevicesPause = function (pause) {
for (var id in $scope.devices) {
$scope.devices[id].paused = pause;
};
$scope.config.devices = deviceList($scope.devices);
$scope.saveConfig();
2021-05-03 12:14:54 +02:00
};
$scope.isAtleastOneDevicePausedStateSetTo = function (pause) {
for (var id in $scope.devices) {
if ($scope.devices[id].paused == pause) {
return true;
}
}
2021-05-03 12:14:54 +02:00
return false;
};
$scope.errorList = function () {
if (!$scope.errors) {
return [];
}
return $scope.errors.filter(function (e) {
2015-10-12 07:18:40 +02:00
return e.when > $scope.seenError;
});
};
$scope.clearErrors = function () {
2015-10-12 07:18:40 +02:00
$scope.seenError = $scope.errors[$scope.errors.length - 1].when;
$http.post(urlbase + '/system/error/clear');
};
$scope.fsWatcherErrorMap = function () {
var errs = {}
$.each($scope.folders, function (id, cfg) {
if (cfg.fsWatcherEnabled && $scope.model[cfg.id] && $scope.model[id].watchError && !cfg.paused && $scope.folderStatus(cfg) !== 'stopped') {
errs[id] = $scope.model[id].watchError;
}
});
return errs;
2019-04-23 19:37:51 +02:00
};
$scope.friendlyDevices = function (str) {
for (var id in $scope.devices) {
str = str.replace(id, $scope.deviceName($scope.devices[id]));
}
return str;
};
$scope.folderList = function () {
return folderList($scope.folders);
};
$scope.deviceList = function () {
return deviceList($scope.devices);
};
$scope.directoryList = [];
$scope.$watch('currentFolder.path', function (newvalue) {
if (!newvalue) {
return;
}
$scope.currentFolder.path = expandTilde(newvalue);
$http.get(urlbase + '/system/browse', {
params: { current: newvalue }
}).success(function (data) {
$scope.directoryList = data;
}).error($scope.emitHTTPError);
});
$scope.$watch('currentFolder.label', function (newvalue) {
if (!newvalue || !shouldSetDefaultFolderPath()) {
return;
}
$scope.currentFolder.path = pathJoin($scope.config.defaults.folder.path, newvalue);
});
$scope.$watch('currentFolder.id', function (newvalue) {
if (!newvalue || !shouldSetDefaultFolderPath() || $scope.currentFolder.label) {
return;
}
$scope.currentFolder.path = pathJoin($scope.config.defaults.folder.path, newvalue);
});
$scope.setFSWatcherIntervalDefault = function () {
var defaultRescanIntervals = [60, 3600, 3600*24];
if (defaultRescanIntervals.indexOf($scope.currentFolder.rescanIntervalS) === -1) {
return;
}
var idx;
if ($scope.currentFolder.fsWatcherEnabled) {
idx = 1;
} else if ($scope.currentFolder.type === 'receiveencrypted') {
idx = 2;
} else {
idx = 0;
}
$scope.currentFolder.rescanIntervalS = defaultRescanIntervals[idx];
};
$scope.setDefaultsForFolderType = function () {
if ($scope.currentFolder.type === 'receiveencrypted') {
$scope.currentFolder.fsWatcherEnabled = false;
$scope.currentFolder.ignorePerms = true;
delete $scope.currentFolder.versioning;
} else {
$scope.currentFolder.fsWatcherEnabled = true;
}
$scope.setFSWatcherIntervalDefault();
};
$scope.loadFormIntoScope = function (form) {
console.log('loadFormIntoScope', form.$name);
switch (form.$name) {
case 'deviceEditor':
$scope.deviceEditor = form;
break;
case 'folderEditor':
$scope.folderEditor = form;
break;
}
};
$scope.globalChanges = function () {
$('#globalChanges').modal();
};
function editFolderModal(initialTab) {
initVersioningEditing();
$scope.currentFolder._recvEnc = $scope.currentFolder.type === 'receiveencrypted';
$scope.folderPathErrors = {};
$scope.folderEditor.$setPristine();
if (!initialTab) {
initialTab = "#folder-general";
}
$('.nav-tabs a[href="' + initialTab + '"]').tab('show');
$('#editFolder').modal().one('shown.bs.tab', function (e) {
if (e.target.attributes.href.value === "#folder-ignores") {
$('#folder-ignores textarea').focus();
}
}).one('hidden.bs.modal', function () {
var p = $q.when();
// If the modal was closed default patterns should still apply
if ($scope.currentFolder._editing == "new-ignores" && !$scope.ignores.saved && $scope.ignores.defaultLines) {
p = saveFolderAddIgnores($scope.currentFolder.id, true);
}
p.then(function () {
window.location.hash = "";
$scope.currentFolder = {};
$scope.ignores = {};
});
});
};
$scope.editFolderModalTitle = function() {
if ($scope.editingFolderDefaults()) {
return $translate.instant("Edit Folder Defaults");
}
var title = '';
switch ($scope.currentFolder._editing) {
case "existing":
title = $translate.instant("Edit Folder");
break;
case "new":
case "new-pending":
title = $translate.instant("Add Folder");
break;
case "new-ignores":
title = $translate.instant("Set Ignores on Added Folder");
break;
}
if ($scope.currentFolder.id !== '') {
title += ' (' + $scope.folderLabel($scope.currentFolder.id) + ')';
}
return title;
};
$scope.editFolderModalIcon = function() {
if ($scope.has(["existing", "defaults"], $scope.currentFolder._editing)) {
return 'fas fa-pencil-alt';
}
return 'fas fa-folder';
};
$scope.editingFolderDefaults = function() {
return $scope.currentFolder._editing == 'defaults';
}
$scope.editingFolderExisting = function() {
return $scope.currentFolder._editing == 'existing';
}
$scope.editingFolderNew = function() {
return $scope.has(['new', 'new-pending'], $scope.currentFolder._editing);
}
function editFolder(initialTab) {
if ($scope.currentFolder.path.length > 1 && $scope.currentFolder.path.slice(-1) === $scope.system.pathSeparator) {
$scope.currentFolder.path = $scope.currentFolder.path.slice(0, -1);
} else if (!$scope.currentFolder.path) {
// undefined path leads to invalid input field
$scope.currentFolder.path = '';
}
initShareEditing('folder');
editFolderModal(initialTab);
}
$scope.internalVersioningEnabled = function(guiVersioning) {
if (!$scope.currentFolder._guiVersioning) {
return false;
}
return ['none', 'external'].indexOf($scope.currentFolder._guiVersioning.selector) === -1;
};
function initVersioningEditing() {
$scope.currentFolder._guiVersioning = angular.copy($scope.versioningDefaults);
var currentVersioning = $scope.currentFolder.versioning;
if (!currentVersioning || !currentVersioning.type || currentVersioning.type === 'none') {
return;
}
$scope.currentFolder._guiVersioning.cleanupIntervalS = +currentVersioning.cleanupIntervalS;
$scope.currentFolder._guiVersioning.selector = currentVersioning.type;
// Apply parameters currently in use
switch (currentVersioning.type) {
case "trashcan":
$scope.currentFolder._guiVersioning.trashcanClean = +currentVersioning.params.cleanoutDays;
break;
case "simple":
$scope.currentFolder._guiVersioning.simpleKeep = +currentVersioning.params.keep;
$scope.currentFolder._guiVersioning.trashcanClean = +currentVersioning.params.cleanoutDays;
break;
case "staggered":
$scope.currentFolder._guiVersioning.staggeredMaxAge = Math.floor(+currentVersioning.params.maxAge / 86400);
break;
case "external":
$scope.currentFolder._guiVersioning.externalCommand = currentVersioning.params.command;
break;
}
};
$scope.editFolderExisting = function(folderCfg, initialTab) {
$scope.currentFolder = angular.copy(folderCfg);
$scope.currentFolder._editing = "existing";
editFolderLoadIgnores();
editFolder(initialTab);
};
function editFolderLoadingIgnores() {
$scope.ignores.text = 'Loading...';
$scope.ignores.error = null;
$scope.ignores.disabled = true;
}
function editFolderGetIgnores() {
return $http.get(urlbase + '/db/ignores?folder=' + encodeURIComponent($scope.currentFolder.id))
.then(function(r) {
return r.data;
}, function (response) {
$scope.ignores.text = $translate.instant("Failed to load ignore patterns.");
return $q.reject(response);
});
};
function editFolderLoadIgnores() {
editFolderLoadingIgnores();
return editFolderGetIgnores().then(function(data) {
if (!data) {
return;
}
editFolderInitIgnores(data.ignore, data.error);
}, $scope.emitHTTPError);
}
$scope.editFolderDefaults = function() {
$q.all([
$http.get(urlbase + '/config/defaults/folder').then(function (response) {
$scope.currentFolder = response.data;
$scope.currentFolder._editing = "defaults";
}),
getDefaultIgnores().then(editFolderInitIgnores),
]).then(editFolder, $scope.emitHTTPError);
};
function getDefaultIgnores() {
return $http.get(urlbase + '/config/defaults/ignores').then(function (r) {
return r.data.lines;
});
}
function editFolderInitIgnores(lines, error) {
$scope.ignores.originalLines = lines || [];
setIgnoresText(lines);
$scope.ignores.error = error;
$scope.ignores.disabled = false;
}
function setIgnoresText(lines) {
$scope.ignores.text = lines ? lines.join('\n') : "";
}
$scope.selectAllSharedDevices = function (state) {
var devices = $scope.currentSharing.shared;
for (var i = 0; i < devices.length; i++) {
$scope.currentSharing.selected[devices[i].deviceID] = !!state;
}
};
$scope.selectAllUnrelatedDevices = function (state) {
var devices = $scope.currentSharing.unrelated;
for (var i = 0; i < devices.length; i++) {
$scope.currentSharing.selected[devices[i].deviceID] = !!state;
}
};
$scope.addFolder = function () {
$http.get(urlbase + '/svc/random/string?length=10').success(function (data) {
var folderID = (data.random.substr(0, 5) + '-' + data.random.substr(5, 5)).toLowerCase();
addFolderInit(folderID).then(function() {
// Triggers the watch that sets the path
$scope.currentFolder._editing = "new";
$scope.currentFolder.label = $scope.currentFolder.label;
editFolderModal();
});
});
};
$scope.addFolderAndShare = function (folderID, pendingFolder, device) {
addFolderInit(folderID).then(function() {
$scope.currentSharing.selected[device] = true;
$scope.currentFolder.label = pendingFolder.offeredBy[device].label;
for (var k in pendingFolder.offeredBy) {
if (pendingFolder.offeredBy[k].receiveEncrypted) {
$scope.currentFolder.type = "receiveencrypted";
$scope.setDefaultsForFolderType();
break;
}
}
$scope.currentFolder._editing = "new-pending";
editFolderModal();
});
};
function addFolderInit(folderID) {
return $http.get(urlbase + '/config/defaults/folder').then(function (response) {
$scope.currentFolder = response.data;
$scope.currentFolder.id = folderID;
initShareEditing('folder');
$scope.currentSharing.unrelated = $scope.currentSharing.unrelated.concat($scope.currentSharing.shared);
$scope.currentSharing.shared = [];
// Ignores don't need to be initialized here, as that happens in
// a second step if the user indicates in the creation modal
// that they want to set ignores
}, $scope.emitHTTPError);
}
$scope.shareFolderWithDevice = function (folder, device) {
var folderCfg = $scope.folders[folder];
if (folderCfg.type == "receiveencrypted" || !$scope.pendingIsRemoteEncrypted(folder, device)) {
$scope.folders[folder].devices.push({
deviceID: device
});
$scope.config.folders = folderList($scope.folders);
$scope.saveConfig();
} else {
// Open edit folder dialog to enter encryption password
$scope.editFolderExisting(folderCfg, "#folder-sharing");
$scope.currentSharing.selected[device] = true;
}
};
$scope.saveFolder = function () {
if ($scope.currentFolder._editing == "new-ignores") {
// On modal being hidden without clicking save, the defaults will be saved.
$scope.ignores.saved = true;
saveFolderAddIgnores($scope.currentFolder.id);
hideFolderModal();
return;
}
var folderCfg = angular.copy($scope.currentFolder);
$scope.currentSharing.selected[$scope.myID] = true;
var newDevices = [];
folderCfg.devices.forEach(function (dev) {
if ($scope.currentSharing.selected[dev.deviceID] === true) {
dev.encryptionPassword = $scope.currentSharing.encryptionPasswords[dev.deviceID];
newDevices.push(dev);
delete $scope.currentSharing.selected[dev.deviceID];
};
});
for (var deviceID in $scope.currentSharing.selected) {
if ($scope.currentSharing.selected[deviceID] === true) {
newDevices.push({
deviceID: deviceID,
encryptionPassword: $scope.currentSharing.encryptionPasswords[deviceID],
});
}
}
folderCfg.devices = newDevices;
delete $scope.currentSharing;
if (!folderCfg.versioning) {
folderCfg.versioning = {params: {}};
}
folderCfg.versioning.type = folderCfg._guiVersioning.selector;
if ($scope.internalVersioningEnabled()) {
folderCfg.versioning.cleanupIntervalS = folderCfg._guiVersioning.cleanupIntervalS;
}
switch (folderCfg._guiVersioning.selector) {
case "trashcan":
folderCfg.versioning.params.cleanoutDays = '' + folderCfg._guiVersioning.trashcanClean;
break;
case "simple":
folderCfg.versioning.params.keep = '' + folderCfg._guiVersioning.simpleKeep,
folderCfg.versioning.params.cleanoutDays = '' + folderCfg._guiVersioning.trashcanClean;
break;
case "staggered":
folderCfg.versioning.params.maxAge = '' + (folderCfg._guiVersioning.staggeredMaxAge * 86400);
break;
case "external":
folderCfg.versioning.params.command = '' + folderCfg._guiVersioning.externalCommand;
break;
default:
folderCfg.versioning = {type: ''};
}
delete folderCfg._guiVersioning;
if ($scope.currentFolder._editing == "defaults") {
hideFolderModal();
$scope.config.defaults.ignores.lines = ignoresArray();
$scope.config.defaults.folder = folderCfg;
$scope.saveConfig();
return;
}
// This is a new folder where ignores should apply before it first starts.
if ($scope.currentFolder._addIgnores) {
folderCfg.paused = true;
}
$scope.folders[folderCfg.id] = folderCfg;
$scope.config.folders = folderList($scope.folders);
if ($scope.currentFolder._editing == "existing") {
hideFolderModal();
saveFolderIgnoresExisting();
$scope.saveConfig();
return;
}
// No ignores to be set on the new folder, save directly.
if (!$scope.currentFolder._addIgnores) {
hideFolderModal();
$scope.saveConfig();
return;
}
// Add folder (paused), load existing ignores and if there are none,
// load default ignores, then let the user edit them.
$scope.saveConfig().then(function() {
editFolderLoadingIgnores();
$scope.currentFolder._editing = "new-ignores";
$('.nav-tabs a[href="#folder-ignores"]').tab('show');
return editFolderGetIgnores();
}).then(function(data) {
// Error getting ignores -> leave error message.
if (!data) {
return;
}
if ((data.ignore && data.ignore.length > 0) || data.error) {
editFolderInitIgnores(data.ignore, data.error);
} else {
getDefaultIgnores().then(function(lines) {
setIgnoresText(lines);
$scope.ignores.defaultLines = lines;
$scope.ignores.disabled = false;
});
}
}, $scope.emitHTTPError);
};
function saveFolderIgnoresExisting() {
if ($scope.ignores.disabled) {
return;
}
var ignores = ignoresArray();
function arrayDiffers(a, b) {
return !a !== !b || a.length !== b.length || a.some(function(v, i) { return v !== b[i]; });
}
if (arrayDiffers(ignores, $scope.ignores.originalLines)) {
return saveIgnores(ignores);
};
}
function saveFolderAddIgnores(folderID, useDefault) {
var ignores = useDefault ? $scope.ignores.defaultLines : ignoresArray();
return saveIgnores(ignores).then(function () {
return $scope.setFolderPause(folderID, $scope.currentFolder.paused);
});
};
function ignoresArray() {
var ignores = $scope.ignores.text.split('\n');
// Split always returns a minimum 1-length array even for no patterns
if (ignores.length === 1 && ignores[0] === "") {
ignores = [];
}
return ignores;
}
$scope.ignoreFolder = function (device, folderID, offeringDevice) {
var ignoredFolder = {
id: folderID,
label: offeringDevice.label,
// Bump time
time: (new Date()).toISOString()
}
if (device in $scope.devices) {
$scope.devices[device].ignoredFolders.push(ignoredFolder);
$scope.saveConfig();
}
};
$scope.dismissPendingFolder = function (folderID, deviceID) {
$http.delete(urlbase + '/cluster/pending/folders?folder=' + encodeURIComponent(folderID)
+ '&device=' + encodeURIComponent(deviceID));
};
$scope.deviceNameMarkRemoteState = function (deviceID, folderID) {
var name = $scope.deviceName($scope.devices[deviceID]);
// Add footnote if sharing was not accepted on the remote device
if (deviceID in $scope.completion && folderID in $scope.completion[deviceID]) {
if ($scope.completion[deviceID][folderID].remoteState == 'notSharing') {
name += '<sup>1</sup>';
} else if ($scope.completion[deviceID][folderID].remoteState == 'paused') {
name += '<sup>2</sup>';
}
}
return name;
};
$scope.sharesFolder = function (folderCfg) {
var names = [];
folderCfg.devices.forEach(function (device) {
if (device.deviceID !== $scope.myID) {
names.push($scope.deviceNameMarkRemoteState(device.deviceID, folderCfg.id));
}
});
names.sort();
return names.join(", ");
};
$scope.folderHasUnacceptedDevices = function (folderCfg) {
for (var deviceID in $scope.completion) {
if (deviceID in $scope.devices
&& folderCfg.id in $scope.completion[deviceID]
&& $scope.completion[deviceID][folderCfg.id].remoteState == 'notSharing') {
return true;
}
}
return false;
};
$scope.folderHasPausedDevices = function (folderCfg) {
for (var deviceID in $scope.completion) {
if (deviceID in $scope.devices
&& folderCfg.id in $scope.completion[deviceID]
&& $scope.completion[deviceID][folderCfg.id].remoteState == 'paused') {
return true;
}
}
return false;
};
$scope.deviceFolders = function (deviceCfg) {
var folders = [];
$scope.folderList().forEach(function (folder) {
for (var i = 0; i < folder.devices.length; i++) {
if (folder.devices[i].deviceID === deviceCfg.deviceID) {
folders.push(folder.id);
break;
}
}
});
return folders;
};
$scope.folderLabel = function (folderID) {
if (!$scope.folders[folderID]) {
return folderID;
}
var label = $scope.folders[folderID].label;
return label && label.length > 0 ? label : folderID;
2019-04-23 19:37:51 +02:00
};
$scope.folderLabelMarkRemoteState = function (folderID, deviceID) {
var label = $scope.folderLabel(folderID);
// Add footnote if sharing was not accepted on the remote device
if (deviceID in $scope.completion && folderID in $scope.completion[deviceID]) {
if ($scope.completion[deviceID][folderID].remoteState == 'notSharing') {
label += '<sup>1</sup>';
} else if ($scope.completion[deviceID][folderID].remoteState == 'paused') {
label += '<sup>2</sup>';
}
}
return label;
};
$scope.sharedFolders = function (deviceCfg) {
var labels = [];
$scope.deviceFolders(deviceCfg).forEach(function (folderID) {
labels.push($scope.folderLabelMarkRemoteState(folderID, deviceCfg.deviceID));
});
return labels.join(', ');
};
$scope.deviceHasUnacceptedFolders = function (deviceCfg) {
if (!(deviceCfg.deviceID in $scope.completion)) {
return false;
}
for (var folderID in $scope.completion[deviceCfg.deviceID]) {
if (folderID in $scope.folders
&& $scope.completion[deviceCfg.deviceID][folderID].remoteState == 'notSharing') {
return true;
}
}
return false;
};
$scope.deviceHasPausedFolders = function (deviceCfg) {
if (!(deviceCfg.deviceID in $scope.completion)) {
return false;
}
for (var folderID in $scope.completion[deviceCfg.deviceID]) {
if (folderID in $scope.folders
&& $scope.completion[deviceCfg.deviceID][folderID].remoteState == 'paused') {
return true;
}
}
return false;
};
$scope.deleteFolder = function (id) {
hideFolderModal();
if ($scope.currentFolder._editing != "existing") {
return;
}
delete $scope.folders[id];
delete $scope.model[id];
$scope.config.folders = folderList($scope.folders);
recalcLocalStateTotal();
$scope.saveConfig();
};
function hideFolderModal() {
$('#editFolder').modal('hide');
}
function resetRestoreVersions() {
$scope.restoreVersions = {
folder: null,
selections: {},
versions: null,
tree: null,
errors: null,
filters: {},
massAction: function (name, action) {
$.each($scope.restoreVersions.versions, function (key) {
if (key.indexOf(name + '/') == 0 && (!$scope.restoreVersions.filters.text || key.indexOf($scope.restoreVersions.filters.text) > -1)) {
if (action == 'unset') {
delete $scope.restoreVersions.selections[key];
return;
}
var availableVersions = [];
$.each($scope.restoreVersions.filterVersions($scope.restoreVersions.versions[key]), function (idx, version) {
availableVersions.push(version.versionTime);
})
if (availableVersions.length) {
availableVersions.sort(function (a, b) { return a - b; });
if (action == 'latest') {
$scope.restoreVersions.selections[key] = availableVersions.pop();
} else if (action == 'oldest') {
$scope.restoreVersions.selections[key] = availableVersions.shift();
}
}
}
});
},
filterVersions: function (versions) {
var filteredVersions = [];
$.each(versions, function (idx, version) {
if (moment(version.versionTime).isBetween($scope.restoreVersions.filters['start'], $scope.restoreVersions.filters['end'], null, '[]')) {
filteredVersions.push(version);
}
});
return filteredVersions;
},
selectionCount: function () {
var count = 0;
$.each($scope.restoreVersions.selections, function (key, value) {
if (value) {
count++;
}
});
return count;
},
restore: function () {
$scope.restoreVersions.tree.clear();
$scope.restoreVersions.tree = null;
$scope.restoreVersions.versions = null;
var selections = {};
$.each($scope.restoreVersions.selections, function (key, value) {
if (value) {
selections[key] = value;
}
});
$scope.restoreVersions.selections = {};
$http.post(urlbase + '/folder/versions?folder=' + encodeURIComponent($scope.restoreVersions.folder), selections).success(function (data) {
if (Object.keys(data).length == 0) {
$('#restoreVersions').modal('hide');
} else {
$scope.restoreVersions.errors = data;
}
});
},
show: function (folder) {
$scope.restoreVersions.folder = folder;
var closed = false;
var modalShown = $q.defer();
$('#restoreVersions').modal().one('hidden.bs.modal', function () {
closed = true;
resetRestoreVersions();
}).one('shown.bs.modal', function () {
modalShown.resolve();
});
var dataReceived = $http.get(urlbase + '/folder/versions?folder=' + encodeURIComponent($scope.restoreVersions.folder))
.success(function (data) {
$.each(data, function (key, values) {
$.each(values, function (idx, value) {
value.modTime = new Date(value.modTime);
value.versionTime = new Date(value.versionTime);
});
values.sort(function (a, b) {
return b.versionTime - a.versionTime;
});
});
if (closed) return;
$scope.restoreVersions.versions = data;
});
$q.all([dataReceived, modalShown.promise]).then(function () {
$timeout(function () {
if (closed) {
resetRestoreVersions();
return;
}
$scope.restoreVersions.tree = $("#restoreTree").fancytree({
extensions: ["table", "filter", "glyph"],
quicksearch: true,
filter: {
hideExpanders: true,
mode: "hide"
},
glyph: {
preset: "awesome5",
},
table: {
indentation: 24,
},
strings: {
loading: $translate.instant("Loading..."),
loadError: $translate.instant("Failed to load file versions."),
noData: $translate.instant("There are no file versions to restore.")
},
// Set to '1' to silence errors after pressing arrow keys on file nodes.
// Happens on the official option configuration from the developer's site
// too, so probably a bug?
debugLevel: 1,
source: buildTree($scope.restoreVersions.versions),
renderColumns: function (event, data) {
// Case insensitive sort with folders on top.
var cmp = function(a, b) {
var x = (a.isFolder() ? "0" : "1") + a.title.toLowerCase(),
y = (b.isFolder() ? "0" : "1") + b.title.toLowerCase();
return x === y ? 0 : x > y ? 1 : -1;
};
data.tree.getRootNode().sortChildren(cmp, true);
var node = data.node,
$tdList = $(node.tr).find(">td"),
template;
if (node.folder) {
template = '<div ng-include="\'syncthing/folder/restoreVersionsMassActions.html\'"/>';
} else {
template = '<div ng-include="\'syncthing/folder/restoreVersionsVersionSelector.html\'"/>';
}
var scope = $rootScope.$new(true);
scope.key = node.key;
scope.restoreVersions = $scope.restoreVersions;
$tdList.eq(1).html(
$compile(template)(scope)
);
// Force angular to redraw.
$timeout(function () {
$scope.$apply();
});
}
}).fancytree("getTree");
var minDate = moment(),
maxDate = moment(0, 'X'),
date;
// Find version window.
$.each($scope.restoreVersions.versions, function (key) {
$.each($scope.restoreVersions.versions[key], function (idx, version) {
date = moment(version.versionTime);
if (date.isBefore(minDate)) {
minDate = date;
}
if (date.isAfter(maxDate)) {
maxDate = date;
}
});
});
$scope.restoreVersions.filters['start'] = minDate;
$scope.restoreVersions.filters['end'] = maxDate;
var ranges = {};
ranges[$translate.instant("All Time")] = [minDate, maxDate];
ranges[$translate.instant("Today")] = [moment().startOf('day'), moment()];
ranges[$translate.instant("Yesterday")] = [moment().subtract(1, 'days').startOf('day'), moment().startOf('day')];
ranges[$translate.instant("Last 7 Days")] = [moment().subtract(6, 'days').startOf('day'), moment()];
ranges[$translate.instant("Last 30 Days")] = [moment().subtract(29, 'days').startOf('day'), moment()];
ranges[$translate.instant("This Month")] = [moment().startOf('month'), moment()];
ranges[$translate.instant("Last Month")] = [moment().subtract(1, 'month').startOf('month'), moment().startOf('month')];
// Filter out invalid ranges.
$.each(ranges, function (key, range) {
if (!range[0].isBetween(minDate, maxDate, null, '[]') && !range[1].isBetween(minDate, maxDate, null, '[]')) {
delete ranges[key];
}
});
$("#restoreVersionDateRange").daterangepicker({
timePicker: true,
timePicker24Hour: true,
timePickerSeconds: true,
opens: "left",
drops: "up",
startDate: minDate,
endDate: maxDate,
minDate: minDate,
maxDate: maxDate,
ranges: ranges,
locale: {
applyLabel: $translate.instant("Apply"),
cancelLabel: $translate.instant("Cancel"),
customRangeLabel: $translate.instant("Custom Range"),
format: 'YYYY/MM/DD HH:mm:ss',
}
}).on('apply.daterangepicker', function (ev, picker) {
$scope.restoreVersions.filters['start'] = picker.startDate;
$scope.restoreVersions.filters['end'] = picker.endDate;
// Events for this UI element are not managed by angular.
// Force angular to wake up.
$timeout(function () {
$scope.$apply();
});
});
});
});
}
};
}
resetRestoreVersions();
$scope.$watchCollection('restoreVersions.filters', function () {
if (!$scope.restoreVersions.tree) return;
$scope.restoreVersions.tree.filterNodes(function (node) {
if (node.folder) return false;
if ($scope.restoreVersions.filters.text && node.key.indexOf($scope.restoreVersions.filters.text) < 0) {
return false;
}
if ($scope.restoreVersions.filterVersions(node.data.versions).length == 0) {
return false;
}
return true;
});
});
$scope.setAPIKey = function (cfg) {
$http.get(urlbase + '/svc/random/string?length=32').success(function (data) {
cfg.apiKey = data.random;
});
};
$scope.acceptUR = function () {
$scope.config.options.urAccepted = $scope.system.urVersionMax;
$scope.config.options.urSeen = $scope.system.urVersionMax;
$scope.saveConfig();
$('#ur').modal('hide');
};
$scope.declineUR = function () {
if ($scope.config.options.urAccepted === 0) {
$scope.config.options.urAccepted = -1;
}
$scope.config.options.urSeen = $scope.system.urVersionMax;
$scope.saveConfig();
$('#ur').modal('hide');
};
$scope.showNeed = function (folder) {
$scope.neededFolder = folder;
$scope.refreshNeed(1, 10);
$('#needed').modal().one('hidden.bs.modal', function () {
$scope.needed = undefined;
$scope.neededFolder = '';
});
};
$scope.showRemoteNeed = function (device) {
resetRemoteNeed();
$scope.remoteNeedDevice = device;
$scope.deviceFolders(device).forEach(function (folder) {
var comp = $scope.completion[device.deviceID][folder];
if (comp !== undefined && comp.needItems + comp.needDeletes === 0) {
return;
}
$scope.remoteNeedFolders.push(folder);
$scope.refreshRemoteNeed(folder, 1, 10);
});
$('#remoteNeed').modal().one('hidden.bs.modal', function () {
resetRemoteNeed();
});
};
$scope.showFailed = function (folder) {
$scope.failed.folder = folder;
$scope.failed = $scope.refreshFailed(1, 10);
$('#failed').modal().one('hidden.bs.modal', function () {
$scope.failed = {};
});
};
$scope.hasFailedFiles = function (folder) {
if (!$scope.model[folder]) {
return false;
}
return $scope.model[folder].errors !== 0;
};
$scope.showLocalChanged = function (folder, folderType) {
$scope.localChangedFolder = folder;
$scope.localChangedType = folderType;
$scope.localChanged = $scope.refreshLocalChanged(1, 10);
$('#localChanged').modal().one('hidden.bs.modal', function () {
$scope.localChanged = {};
$scope.localChangedFolder = undefined;
$scope.localChangedType = undefined;
});
};
$scope.hasReceiveOnlyChanged = function (folderCfg) {
if (!folderCfg || folderCfg.type !== ["receiveonly", "receiveencrypted"].indexOf(folderCfg.type) === -1) {
return false;
}
var counts = $scope.model[folderCfg.id];
return counts && counts.receiveOnlyTotalItems > 0;
};
$scope.revertOverride = function () {
$http.post(
urlbase + "/db/" + $scope.revertOverrideParams.operation +"?folder="
+encodeURIComponent($scope.revertOverrideParams.folderID));
};
$scope.revertOverrideConfirmationModal = function (type, folderID) {
var params = {
type: type,
folderID: folderID,
};
switch (type) {
case "override":
params.heading = $translate.instant("Override Changes");
params.icon = "fas fa-arrow-circle-up"
params.operation = "override";
break;
case "revert":
params.heading = $translate.instant("Revert Local Changes");
params.icon = "fas fa-arrow-circle-down"
params.operation = "revert";
break;
case "deleteEnc":
params.heading = $translate.instant("Delete Unexpected Items");
params.icon = "fas fa-minus-circle"
params.operation = "revert";
break;
}
$scope.revertOverrideParams = params;
$('#revert-override-confirmation').modal('show');
};
$scope.advanced = function () {
$scope.advancedConfig = angular.copy($scope.config);
$scope.advancedConfig.devices.sort(deviceCompare);
$scope.advancedConfig.folders.sort(folderCompare);
$scope.advancedConfig.defaults.ignores._lines = function (newValue) {
if (arguments.length) {
$scope.advancedConfig.defaults.ignores.lines = newValue.split('\n');
}
return $scope.advancedConfig.defaults.ignores.lines.join('\n');
};
$('#advanced').modal('show');
};
$scope.showReportPreview = function () {
$scope.reportPreview = true;
};
$scope.refreshReportDataPreview = function (ver, diff) {
$scope.reportDataPreview = '';
if (!ver) {
return;
}
var version = parseInt(ver);
if (diff && version > 2) {
$q.all([
$http.get(urlbase + '/svc/report?version=' + version),
$http.get(urlbase + '/svc/report?version=' + (version - 1)),
]).then(function (responses) {
var newReport = responses[0].data;
var oldReport = responses[1].data;
angular.forEach(oldReport, function (_, key) {
delete newReport[key];
});
$scope.reportDataPreview = newReport;
});
} else {
$http.get(urlbase + '/svc/report?version=' + version).success(function (data) {
$scope.reportDataPreview = data;
}).error($scope.emitHTTPError);
}
};
$scope.rescanAllFolders = function () {
$http.post(urlbase + "/db/scan");
};
$scope.rescanFolder = function (folder) {
$http.post(urlbase + "/db/scan?folder=" + encodeURIComponent(folder));
};
$scope.setAllFoldersPause = function (pause) {
var folderListCache = $scope.folderList();
for (var i = 0; i < folderListCache.length; i++) {
folderListCache[i].paused = pause;
}
$scope.config.folders = folderList(folderListCache);
$scope.saveConfig();
};
$scope.isAtleastOneFolderPausedStateSetTo = function (pause) {
var folderListCache = $scope.folderList();
for (var i = 0; i < folderListCache.length; i++) {
if (folderListCache[i].paused == pause) {
return true;
}
}
return false;
};
$scope.activateAllFsWatchers = function () {
var folders = $scope.folderList();
$.each(folders, function (i) {
if (folders[i].fsWatcherEnabled) {
return;
}
folders[i].fsWatcherEnabled = true;
if (folders[i].rescanIntervalS === 0) {
return;
}
// Delay full scans, but scan at least once per day
folders[i].rescanIntervalS *= 60;
if (folders[i].rescanIntervalS > 86400) {
folders[i].rescanIntervalS = 86400;
}
});
$scope.config.folders = folders;
$scope.saveConfig();
};
$scope.bumpFile = function (folder, file) {
var url = urlbase + "/db/prio?folder=" + encodeURIComponent(folder) + "&file=" + encodeURIComponent(file);
// In order to get the right view of data in the response.
url += "&page=" + $scope.needed.page;
url += "&perpage=" + $scope.needed.perpage;
$http.post(url).success(function (data) {
if ($scope.neededFolder === folder) {
console.log("bumpFile", folder, data);
parseNeeded(data);
}
}).error($scope.emitHTTPError);
};
$scope.versionString = function () {
if (!$scope.version.version) {
return '';
}
var os = {
'darwin': 'macOS',
'dragonfly': 'DragonFly BSD',
'freebsd': 'FreeBSD',
'openbsd': 'OpenBSD',
'netbsd': 'NetBSD',
'linux': 'Linux',
'windows': 'Windows',
'solaris': 'Solaris'
}[$scope.version.os] || $scope.version.os;
var arch = {
2020-12-10 11:23:47 +01:00
'386': '32-bit Intel/AMD',
'amd64': '64-bit Intel/AMD',
'arm': '32-bit ARM',
'arm64': '64-bit ARM',
'ppc64': '64-bit PowerPC',
'ppc64le': '64-bit PowerPC (LE)',
'mips': '32-bit MIPS',
'mipsle': '32-bit MIPS (LE)',
'mips64': '64-bit MIPS',
'mips64le': '64-bit MIPS (LE)',
'riscv64': '64-bit RISC-V',
's390x': '64-bit z/Architecture',
}[$scope.version.arch] || $scope.version.arch;
return $scope.version.version + ', ' + os + ' (' + arch + ')';
};
$scope.versionBase = function () {
if (!$scope.version.version) {
return '';
}
var version = $scope.version.version;
var pos = version.indexOf('-');
if (pos > 0) {
version = version.slice(0, pos);
}
return version;
};
$scope.docsURL = function (path) {
var url = 'https://docs.syncthing.net';
if (!path) {
// Undefined or null should become a valid string.
path = '';
}
var hash = path.indexOf('#');
if (hash != -1) {
url += '/' + path.slice(0, hash);
url += '?version=' + $scope.versionBase();
url += path.slice(hash);
} else {
url += '/' + path;
url += '?version=' + $scope.versionBase();
}
return url;
};
$scope.inputTypeFor = function (key, value) {
if (key.substr(0, 1) === '_') {
return 'skip';
}
if (value === null) {
return 'null';
}
if (typeof value === 'number') {
return 'number';
}
if (typeof value === 'boolean') {
return 'checkbox';
}
if (value instanceof Array) {
return 'list';
}
if (typeof value === 'object') {
return 'skip';
}
return 'text';
};
2016-03-27 08:40:50 +02:00
$scope.themeName = function (theme) {
var translation = $translate.instant("theme-name-" + theme);
if (translation.indexOf("theme-name-") == 0) {
// Fall back to simple Title Casing on missing translation
translation = theme.toLowerCase().replace(/(?:^|\s)\S/g, function (a) {
return a.toUpperCase();
});
}
return translation;
2016-03-27 08:40:50 +02:00
};
$scope.modalLoaded = function () {
// once all modal elements have been processed
if ($('modal').length === 0) {
// pseudo main. called on all definitions assigned
initController();
}
};
$scope.toggleUnits = function () {
$scope.metricRates = !$scope.metricRates;
try {
window.localStorage["metricRates"] = $scope.metricRates;
} catch (exception) { }
};
$scope.sizeOf = function (dict) {
if (dict === undefined) {
return 0;
}
return Object.keys(dict).length;
};
$scope.has = function (array, element) {
return array.indexOf(element) >= 0;
};
$scope.dismissNotification = function (id) {
var idx = $scope.config.options.unackedNotificationIDs.indexOf(id);
if (idx > -1) {
$scope.config.options.unackedNotificationIDs.splice(idx, 1);
$scope.saveConfig();
}
};
$scope.abbreviatedError = function (addr) {
var status = $scope.system.lastDialStatus[addr];
if (!status || !status.error) {
return null;
}
var time = $filter('date')(status.when, "HH:mm:ss")
var err = status.error.replace(/.+: /, '');
return err + " (" + time + ")";
2021-05-03 12:14:54 +02:00
};
$scope.setCrashReportingEnabled = function (enabled) {
$scope.config.options.crashReportingEnabled = enabled;
$scope.saveConfig();
};
$scope.isUnixAddress = function (address) {
return address != null &&
(address.indexOf('/') == 0 ||
address.indexOf('unix://') == 0 ||
address.indexOf('unixs://') == 0);
2021-05-03 12:14:54 +02:00
};
})
.directive('shareTemplate', function () {
return {
templateUrl: 'syncthing/core/editShareTemplate.html',
scope: {
selected: '=',
encryptionPasswords: '=',
id: '@',
label: '@',
folderType: '@',
untrusted: '=',
},
link: function(scope, elem, attrs) {
var plain = false;
scope.togglePasswordVisibility = function() {
scope.plain = !scope.plain;
};
},
}
});