mountpoint provider supports prefix (except not via UI). It's more natural for the user to enter the actual mountpoint than the filesystem path directly.
2261 lines
90 KiB
JavaScript
2261 lines
90 KiB
JavaScript
'use strict';
|
|
|
|
/* global angular */
|
|
/* global $ */
|
|
/* global async */
|
|
/* global RSTATES */
|
|
/* global ISTATES */
|
|
/* global ERROR */
|
|
/* global Chart */
|
|
/* global Clipboard */
|
|
/* global SECRET_PLACEHOLDER */
|
|
/* global APP_TYPES, STORAGE_PROVIDERS, BACKUP_FORMATS */
|
|
/* global REGIONS_S3, REGIONS_WASABI, REGIONS_DIGITALOCEAN, REGIONS_EXOSCALE, REGIONS_SCALEWAY, REGIONS_LINODE, REGIONS_OVH, REGIONS_IONOS, REGIONS_UPCLOUD, REGIONS_VULTR */
|
|
/* global onAppClick */
|
|
|
|
angular.module('Application').controller('AppController', ['$scope', '$location', '$translate', '$timeout', '$interval', '$route', '$routeParams', 'Client', function ($scope, $location, $translate, $timeout, $interval, $route, $routeParams, Client) {
|
|
$scope.s3Regions = REGIONS_S3;
|
|
$scope.wasabiRegions = REGIONS_WASABI;
|
|
$scope.doSpacesRegions = REGIONS_DIGITALOCEAN;
|
|
$scope.exoscaleSosRegions = REGIONS_EXOSCALE;
|
|
$scope.scalewayRegions = REGIONS_SCALEWAY;
|
|
$scope.linodeRegions = REGIONS_LINODE;
|
|
$scope.ovhRegions = REGIONS_OVH;
|
|
$scope.ionosRegions = REGIONS_IONOS;
|
|
$scope.upcloudRegions = REGIONS_UPCLOUD;
|
|
$scope.vultrRegions = REGIONS_VULTR;
|
|
$scope.contaboRegions = REGIONS_VULTR;
|
|
|
|
$scope.storageProviders = STORAGE_PROVIDERS;
|
|
|
|
$scope.formats = BACKUP_FORMATS;
|
|
|
|
// Avoid full reload on path change
|
|
// https://stackoverflow.com/a/22614334
|
|
// reloadOnUrl: false in $routeProvider did not work!
|
|
var lastRoute = $route.current;
|
|
$scope.$on('$locationChangeSuccess', function (/* event */) {
|
|
if (lastRoute.$$route.originalPath === $route.current.$$route.originalPath) {
|
|
$route.current = lastRoute;
|
|
}
|
|
});
|
|
|
|
var appId = $routeParams.appId;
|
|
if (!appId) return $location.path('/apps');
|
|
|
|
$scope.view = '';
|
|
$scope.app = null;
|
|
$scope.config = Client.getConfig();
|
|
$scope.user = Client.getUserInfo();
|
|
|
|
// note: these variables will remain empty for operators
|
|
$scope.domains = [];
|
|
$scope.volumes = [];
|
|
$scope.groups = [];
|
|
$scope.users = [];
|
|
$scope.backupConfig = null;
|
|
$scope.diskUsage = -1;
|
|
$scope.diskUsageDate = 0;
|
|
|
|
$scope.APP_TYPES = APP_TYPES;
|
|
$scope.HOST_PORT_MIN = 1;
|
|
$scope.HOST_PORT_MAX = 65535;
|
|
$scope.ROBOTS_DISABLE_INDEXING_TEMPLATE = '# Disable search engine indexing\n\nUser-agent: *\nDisallow: /';
|
|
|
|
$scope.setView = function (view, skipViewShow) {
|
|
if ($scope.view === view) return;
|
|
|
|
$route.updateParams({ view: view });
|
|
if (!skipViewShow) $scope[view].show();
|
|
$scope.view = view;
|
|
};
|
|
|
|
$scope.stopAppTask = function (taskId) {
|
|
Client.stopTask(taskId, function (error) {
|
|
// we can ignore a call trying to cancel an already done task
|
|
if (error && error.statusCode !== 409) Client.error(error);
|
|
});
|
|
};
|
|
|
|
$scope.appPostInstallConfirm = {
|
|
app: {},
|
|
message: '',
|
|
confirmed: false,
|
|
|
|
show: function (app) {
|
|
$scope.appPostInstallConfirm.app = app;
|
|
$scope.appPostInstallConfirm.message = app.manifest.postInstallMessage;
|
|
$scope.appPostInstallConfirm.confirmed = false;
|
|
|
|
$('#appPostInstallConfirmModal').modal('show');
|
|
|
|
return false; // prevent propagation and default
|
|
},
|
|
|
|
submit: function () {
|
|
if (!$scope.appPostInstallConfirm.confirmed) return;
|
|
|
|
$scope.appPostInstallConfirm.app.pendingPostInstallConfirmation = false;
|
|
delete localStorage['confirmPostInstall_' + $scope.appPostInstallConfirm.app.id];
|
|
|
|
$('#appPostInstallConfirmModal').modal('hide');
|
|
}
|
|
};
|
|
|
|
$scope.postInstallMessage = {
|
|
confirmed: false,
|
|
openApp: false,
|
|
|
|
show: function (openApp) {
|
|
$scope.postInstallMessage.confirmed = false;
|
|
$scope.postInstallMessage.openApp = !!openApp;
|
|
|
|
if (!$scope.app.manifest.postInstallMessage) return;
|
|
$('#postInstallModal').modal('show');
|
|
},
|
|
|
|
submit: function () {
|
|
if (!$scope.postInstallMessage.confirmed) return;
|
|
|
|
$scope.app.pendingPostInstallConfirmation = false;
|
|
delete localStorage['confirmPostInstall_' + $scope.app.id];
|
|
|
|
$('#postInstallModal').modal('hide');
|
|
}
|
|
};
|
|
|
|
$scope.getAppBackupDownloadLink = function (backup) {
|
|
return Client.getAppBackupDownloadLink($scope.app.id, backup.id);
|
|
};
|
|
|
|
$scope.onAppClick = function (app, $event) { onAppClick(app, $event, true /* always operator */, $scope); };
|
|
|
|
$scope.sftpInfo = {
|
|
show: function () {
|
|
$('#sftpInfoModal').modal('show');
|
|
}
|
|
};
|
|
|
|
$scope.display = {
|
|
busy: false,
|
|
error: {},
|
|
success: false,
|
|
|
|
tags: '',
|
|
label: '',
|
|
icon: { data: null },
|
|
|
|
iconUrl: function () {
|
|
if (!$scope.app) return '';
|
|
|
|
if ($scope.display.icon.data === '__original__') { // user clicked reset
|
|
return $scope.app.iconUrl + '&original=true';
|
|
} else if ($scope.display.icon.data) { // user uploaded icon
|
|
return $scope.display.icon.data;
|
|
} else { // current icon
|
|
return $scope.app.iconUrl;
|
|
}
|
|
},
|
|
|
|
resetCustomIcon: function () {
|
|
$scope.display.icon.data = '__original__';
|
|
},
|
|
|
|
showCustomIconSelector: function () {
|
|
$('#iconFileInput').click();
|
|
},
|
|
|
|
show: function () {
|
|
var app = $scope.app;
|
|
|
|
$scope.display.error = {};
|
|
|
|
// translate for tag-input
|
|
$scope.display.tags = app.tags ? app.tags.join(' ') : '';
|
|
|
|
$scope.display.label = $scope.app.label || '';
|
|
$scope.display.icon = { data: null };
|
|
},
|
|
|
|
submit: function () {
|
|
$scope.display.busy = true;
|
|
$scope.display.error = {};
|
|
|
|
function done(error) {
|
|
if (error) Client.error(error);
|
|
|
|
$scope.displayForm.$setPristine();
|
|
$scope.display.success = true;
|
|
|
|
refreshApp($scope.app.id, function (error) {
|
|
if (error) Client.error(error);
|
|
|
|
$scope.display.show(); // "refresh" view with latest data
|
|
|
|
$timeout(function () { $scope.display.busy = false; }, 1000);
|
|
});
|
|
}
|
|
|
|
var NOOP = function (next) { return next(); };
|
|
var configureLabel = $scope.display.label === $scope.app.label ? NOOP : Client.configureApp.bind(null, $scope.app.id, 'label', { label: $scope.display.label });
|
|
|
|
configureLabel(function (error) {
|
|
if (error) return done(error);
|
|
|
|
var tags = $scope.display.tags.split(' ').map(function (t) { return t.trim(); }).filter(function (t) { return !!t; });
|
|
|
|
var configureTags = angular.equals(tags, $scope.app.tags) ? NOOP : Client.configureApp.bind(null, $scope.app.id, 'tags', { tags: tags });
|
|
|
|
configureTags(function (error) {
|
|
if (error) return done(error);
|
|
|
|
// skip if icon is unchanged
|
|
if ($scope.display.icon.data === null) return done();
|
|
|
|
var icon;
|
|
if ($scope.display.icon.data === '__original__') { // user reset the icon
|
|
icon = '';
|
|
} else if ($scope.display.icon.data) { // user loaded custom icon
|
|
icon = $scope.display.icon.data.replace(/^data:image\/[a-z]+;base64,/, '');
|
|
}
|
|
|
|
Client.configureApp($scope.app.id, 'icon', { icon: icon }, function (error) {
|
|
if (error) return done(error);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
$scope.location = {
|
|
busy: false,
|
|
error: {},
|
|
domainCollisions: [],
|
|
|
|
domain: null, // object and not the string
|
|
subdomain: '',
|
|
secondaryDomains: {},
|
|
redirectDomains: [],
|
|
aliasDomains: [],
|
|
portBindings: {},
|
|
portBindingsEnabled: {},
|
|
portBindingsInfo: {},
|
|
|
|
addRedirectDomain: function (event) {
|
|
event.preventDefault();
|
|
$scope.location.redirectDomains.push({
|
|
domain: $scope.domains.filter(function (d) { return d.domain === $scope.app.domain; })[0], // pre-select app's domain by default
|
|
subdomain: ''
|
|
});
|
|
|
|
setTimeout(function () {
|
|
document.getElementById('redirectDomainsInput-' + ($scope.location.redirectDomains.length-1)).focus();
|
|
}, 200);
|
|
},
|
|
|
|
delRedirectDomain: function (event, index) {
|
|
event.preventDefault();
|
|
$scope.location.redirectDomains.splice(index, 1);
|
|
},
|
|
|
|
addAliasDomain: function (event) {
|
|
event.preventDefault();
|
|
$scope.location.aliasDomains.push({
|
|
domain: $scope.domains.filter(function (d) { return d.domain === $scope.app.domain; })[0], // pre-select app's domain by default
|
|
subdomain: ''
|
|
});
|
|
|
|
setTimeout(function () {
|
|
document.getElementById('aliasDomainsInput-' + ($scope.location.aliasDomains.length-1)).focus();
|
|
}, 200);
|
|
},
|
|
|
|
delAliasDomain: function (event, index) {
|
|
event.preventDefault();
|
|
$scope.location.aliasDomains.splice(index, 1);
|
|
},
|
|
|
|
show: function () {
|
|
var app = $scope.app;
|
|
|
|
$scope.location.error = {};
|
|
$scope.location.domainCollisions = [];
|
|
$scope.location.subdomain = app.subdomain;
|
|
$scope.location.domain = $scope.domains.filter(function (d) { return d.domain === app.domain; })[0];
|
|
|
|
// for compat, secondary domain can be empty after an upgrade. so it may not exist in app.secondaryDomains
|
|
$scope.location.secondaryDomains = {};
|
|
var httpPorts = app.manifest.httpPorts || {};
|
|
for (var env2 in httpPorts) {
|
|
$scope.location.secondaryDomains[env2] = {
|
|
subdomain: httpPorts[env2].defaultValue || '',
|
|
domain: $scope.location.domain
|
|
};
|
|
}
|
|
|
|
// now fill secondaryDomains with real values, if it exists
|
|
app.secondaryDomains.forEach(function (sd) {
|
|
$scope.location.secondaryDomains[sd.environmentVariable] = {
|
|
subdomain: sd.subdomain,
|
|
domain: $scope.domains.filter(function (d) { return d.domain === sd.domain; })[0]
|
|
};
|
|
});
|
|
|
|
$scope.location.portBindingsInfo = angular.extend({}, app.manifest.tcpPorts, app.manifest.udpPorts); // Portbinding map only for information
|
|
$scope.location.redirectDomains = app.redirectDomains.map(function (a) { return { subdomain: a.subdomain, domain: $scope.domains.filter(function (d) { return d.domain === a.domain; })[0] };});
|
|
$scope.location.aliasDomains = app.aliasDomains.map(function (a) { return { subdomain: a.subdomain, domain: $scope.domains.filter(function (d) { return d.domain === a.domain; })[0] };});
|
|
|
|
// fill the portBinding structures. There might be holes in the app.portBindings, which signalizes a disabled port
|
|
for (var env in $scope.location.portBindingsInfo) {
|
|
if (app.portBindings && app.portBindings[env]) {
|
|
$scope.location.portBindings[env] = app.portBindings[env];
|
|
$scope.location.portBindingsEnabled[env] = true;
|
|
} else {
|
|
$scope.location.portBindings[env] = $scope.location.portBindingsInfo[env].defaultValue || 0;
|
|
$scope.location.portBindingsEnabled[env] = false;
|
|
}
|
|
}
|
|
},
|
|
|
|
submit: function (overwriteDns) {
|
|
$('#domainCollisionsModal').modal('hide');
|
|
|
|
$scope.location.busy = true;
|
|
$scope.location.error = {};
|
|
$scope.location.domainCollisions = [];
|
|
|
|
var secondaryDomains = {};
|
|
for (var env2 in $scope.location.secondaryDomains) {
|
|
secondaryDomains[env2] = {
|
|
subdomain: $scope.location.secondaryDomains[env2].subdomain,
|
|
domain: $scope.location.secondaryDomains[env2].domain.domain
|
|
};
|
|
}
|
|
|
|
// only use enabled ports from portBindings
|
|
var portBindings = {};
|
|
for (var env in $scope.location.portBindings) {
|
|
if ($scope.location.portBindingsEnabled[env]) {
|
|
portBindings[env] = $scope.location.portBindings[env];
|
|
}
|
|
}
|
|
|
|
var data = {
|
|
overwriteDns: !!overwriteDns,
|
|
subdomain: $scope.location.subdomain,
|
|
domain: $scope.location.domain.domain,
|
|
portBindings: portBindings,
|
|
secondaryDomains: secondaryDomains,
|
|
redirectDomains: $scope.location.redirectDomains.map(function (a) { return { subdomain: a.subdomain, domain: a.domain.domain };}),
|
|
aliasDomains: $scope.location.aliasDomains.map(function (a) { return { subdomain: a.subdomain, domain: a.domain.domain };})
|
|
};
|
|
|
|
// pre-flight only for changed domains
|
|
var domains = [];
|
|
if ($scope.app.domain !== data.domain || $scope.app.subdomain !== data.subdomain) domains.push({ subdomain: data.subdomain, domain: data.domain, type: 'primary' });
|
|
Object.keys(data.secondaryDomains).forEach(function (env) {
|
|
var subdomain = data.secondaryDomains[env].subdomain, domain = data.secondaryDomains[env].domain;
|
|
if ($scope.app.secondaryDomains.some(function (d) { return d.domain === domain && d.subdomain === subdomain; })) return;
|
|
domains.push({ subdomain: subdomain, domain: domain, type: 'secondary' });
|
|
});
|
|
data.redirectDomains.forEach(function (a) {
|
|
if ($scope.app.redirectDomains.some(function (d) { return d.domain === a.domain && d.subdomain === a.subdomain; })) return;
|
|
domains.push({ subdomain: a.subdomain, domain: a.domain, type: 'redirect' });
|
|
});
|
|
data.aliasDomains.forEach(function (a) {
|
|
if ($scope.app.aliasDomains.some(function (d) { return d.domain === a.domain && d.subdomain === a.subdomain; })) return;
|
|
domains.push({ subdomain: a.subdomain, domain: a.domain, type: 'alias' });
|
|
});
|
|
|
|
var canConfigure = true;
|
|
|
|
async.eachSeries(domains, function (domain, callback) {
|
|
if (overwriteDns) return callback();
|
|
|
|
Client.checkDNSRecords(domain.domain, domain.subdomain, function (error, result) {
|
|
if (error) return callback(error);
|
|
if (result.error) {
|
|
if (domain.type === 'primary') {
|
|
$scope.location.error.location = domain.domain + ' ' + result.error.message;
|
|
} else if (domain.type === 'alias') {
|
|
$scope.location.error.aliasDomains = domain.domain + ' ' + result.error.message;
|
|
} else {
|
|
$scope.location.error.redirectDomains = domain.domain + ' ' + result.error.message;
|
|
}
|
|
$scope.location.busy = false;
|
|
canConfigure = false;
|
|
} else if (result.needsOverwrite) {
|
|
$scope.location.domainCollisions.push(domain);
|
|
canConfigure = false;
|
|
}
|
|
|
|
callback();
|
|
});
|
|
}, function (error) {
|
|
if (error) {
|
|
$scope.location.busy = false;
|
|
return Client.error(error);
|
|
}
|
|
|
|
if (!canConfigure) {
|
|
$scope.location.busy = false;
|
|
return $('#domainCollisionsModal').modal('show');
|
|
}
|
|
|
|
Client.configureApp($scope.app.id, 'location', data, function (error) {
|
|
if (error && (error.statusCode === 409 || error.statusCode === 400)) {
|
|
var errorMessage = error.message.toLowerCase();
|
|
if (errorMessage.indexOf('location') !== -1) {
|
|
if (errorMessage.indexOf('primary') !== -1) {
|
|
$scope.location.error.location = error.message;
|
|
$scope.locationForm.$setPristine();
|
|
} else if (errorMessage.indexOf('secondary') !== -1) {
|
|
$scope.location.error.secondaryDomain = error.message;
|
|
} else if (errorMessage.indexOf('redirect') !== -1) {
|
|
$scope.location.error.redirectDomains = error.message;
|
|
} else if (errorMessage.indexOf('alias') !== -1) {
|
|
$scope.location.error.aliasDomains = error.message;
|
|
}
|
|
} else if (errorMessage.indexOf('port') !== -1) {
|
|
$scope.location.error.port = error.message;
|
|
} else {
|
|
$scope.location.error.location = error.message; // fallback
|
|
}
|
|
|
|
$scope.location.busy = false;
|
|
return;
|
|
}
|
|
if (error) return Client.error(error);
|
|
|
|
refreshApp($scope.app.id, function (error) {
|
|
if (error) return Client.error(error);
|
|
|
|
$scope.locationForm.$setPristine();
|
|
$timeout(function () { $scope.location.busy = false; }, 1000);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
$scope.access = {
|
|
busy: false,
|
|
error: {},
|
|
success: false,
|
|
|
|
ftp: false,
|
|
ssoAuth: false,
|
|
accessRestrictionOption: 'any',
|
|
accessRestrictionOptionCur: 'any',
|
|
accessRestriction: { users: [], groups: [] },
|
|
|
|
operators: { users: [], groups: [] },
|
|
|
|
isAccessRestrictionValid: function () {
|
|
var tmp = $scope.access.accessRestriction;
|
|
return !!(tmp.users.length || tmp.groups.length);
|
|
},
|
|
|
|
show: function () {
|
|
var app = $scope.app;
|
|
|
|
$scope.access.error = {};
|
|
$scope.access.ftp = app.manifest.addons.localstorage && app.manifest.addons.localstorage.ftp;
|
|
$scope.access.ssoAuth = (app.manifest.addons['ldap'] || app.manifest.addons['oidc'] || app.manifest.addons['proxyAuth']) && app.sso;
|
|
$scope.access.accessRestrictionOption = app.accessRestriction ? 'groups' : 'any';
|
|
$scope.access.accessRestrictionOptionCur = app.accessRestriction ? 'groups' : 'any';
|
|
$scope.access.accessRestriction = { users: [], groups: [] };
|
|
|
|
$scope.access.operators = { users: [], groups: [] };
|
|
|
|
var userSet, groupSet;
|
|
|
|
if (app.accessRestriction) {
|
|
userSet = {};
|
|
app.accessRestriction.users.forEach(function (uid) { userSet[uid] = true; });
|
|
$scope.users.forEach(function (u) { if (userSet[u.id] === true) $scope.access.accessRestriction.users.push(u); });
|
|
|
|
groupSet = {};
|
|
if (app.accessRestriction.groups) app.accessRestriction.groups.forEach(function (gid) { groupSet[gid] = true; });
|
|
$scope.groups.forEach(function (g) { if (groupSet[g.id] === true) $scope.access.accessRestriction.groups.push(g); });
|
|
}
|
|
|
|
if (app.operators) {
|
|
userSet = {};
|
|
app.operators.users.forEach(function (uid) { userSet[uid] = true; });
|
|
$scope.users.forEach(function (u) { if (userSet[u.id] === true) $scope.access.operators.users.push(u); });
|
|
|
|
groupSet = {};
|
|
if (app.operators.groups) app.operators.groups.forEach(function (gid) { groupSet[gid] = true; });
|
|
$scope.groups.forEach(function (g) { if (groupSet[g.id] === true) $scope.access.operators.groups.push(g); });
|
|
}
|
|
},
|
|
|
|
submit: function () {
|
|
$scope.access.busy = true;
|
|
$scope.access.error = {};
|
|
|
|
var accessRestriction = null;
|
|
if ($scope.access.accessRestrictionOption === 'groups') {
|
|
accessRestriction = { users: [], groups: [] };
|
|
accessRestriction.users = $scope.access.accessRestriction.users.map(function (u) { return u.id; });
|
|
accessRestriction.groups = $scope.access.accessRestriction.groups.map(function (g) { return g.id; });
|
|
}
|
|
|
|
var operators = null;
|
|
if ($scope.access.operators.users.length || $scope.access.operators.groups.length) {
|
|
operators = { users: [], groups: [] };
|
|
operators.users = $scope.access.operators.users.map(function (u) { return u.id; });
|
|
operators.groups = $scope.access.operators.groups.map(function (g) { return g.id; });
|
|
}
|
|
|
|
async.series([
|
|
function (callback) {
|
|
if ($scope.access.accessRestrictionOption === $scope.access.accessRestrictionOptionCur && !$scope.accessForm.accessUsersSelect.$dirty && !$scope.accessForm.accessGroupsSelect.$dirty) return callback();
|
|
|
|
Client.configureApp($scope.app.id, 'access_restriction', { accessRestriction: accessRestriction }, callback);
|
|
},
|
|
function (callback) {
|
|
if (!$scope.accessForm.operatorsUsersSelect.$dirty && !$scope.accessForm.operatorsGroupsSelect.$dirty) return callback();
|
|
|
|
Client.configureApp($scope.app.id, 'operators', { operators: operators }, callback);
|
|
}
|
|
], function (error) {
|
|
if (error) return Client.error(error);
|
|
|
|
$scope.accessForm.$setPristine();
|
|
|
|
$scope.access.accessRestrictionOptionCur = $scope.access.accessRestrictionOption;
|
|
$timeout(function () {
|
|
$scope.access.success = true;
|
|
$scope.access.busy = false;
|
|
}, 3000);
|
|
});
|
|
}
|
|
};
|
|
|
|
$scope.resources = {
|
|
error: {},
|
|
|
|
busy: false,
|
|
currentMemoryLimit: 0,
|
|
memoryLimit: 0,
|
|
memoryTicks: [],
|
|
|
|
currentCpuShares: 0,
|
|
cpuShares: 0,
|
|
|
|
show: function () {
|
|
var app = $scope.app;
|
|
$scope.resources.busy = true;
|
|
|
|
$scope.resources.error = {};
|
|
$scope.resources.currentMemoryLimit = app.memoryLimit || app.manifest.memoryLimit || (256 * 1024 * 1024);
|
|
|
|
Client.getAppLimits(app.id, function (error, limits) {
|
|
if (error) return console.error(error);
|
|
|
|
// create ticks starting from manifest memory limit. the memory limit here is currently split into ram+swap (and thus *2 below)
|
|
// TODO: the *2 will overallocate since 4GB is max swap that cloudron itself allocates
|
|
$scope.resources.memoryTicks = [];
|
|
var npow2 = Math.pow(2, Math.ceil(Math.log(limits.memory.memory)/Math.log(2)));
|
|
for (var i = 256; i <= (npow2*2/1024/1024); i *= 2) {
|
|
if (i >= (app.manifest.memoryLimit/1024/1024 || 0)) $scope.resources.memoryTicks.push(i * 1024 * 1024);
|
|
}
|
|
if (app.manifest.memoryLimit && $scope.resources.memoryTicks[0] !== app.manifest.memoryLimit) {
|
|
$scope.resources.memoryTicks.unshift(app.manifest.memoryLimit);
|
|
}
|
|
});
|
|
|
|
// for firefox widget update
|
|
$timeout(function() {
|
|
$scope.resources.currentCpuShares = $scope.resources.cpuShares = app.cpuShares;
|
|
$scope.resources.memoryLimit = $scope.resources.currentMemoryLimit;
|
|
$scope.resources.busy = false;
|
|
}, 500);
|
|
},
|
|
|
|
submitMemoryLimit: function () {
|
|
$scope.resources.busy = true;
|
|
$scope.resources.error = {};
|
|
|
|
const tmp = parseInt($scope.resources.memoryLimit);
|
|
const memoryLimit = tmp === $scope.resources.memoryTicks[0] ? 0 : tmp;
|
|
|
|
Client.configureApp($scope.app.id, 'memory_limit', { memoryLimit }, function (error) {
|
|
if (error && error.statusCode === 400) {
|
|
$scope.resources.busy = false;
|
|
$scope.resources.error.memoryLimit = true;
|
|
return;
|
|
}
|
|
if (error) return Client.error(error);
|
|
|
|
$scope.resources.currentMemoryLimit = $scope.resources.memoryLimit;
|
|
|
|
refreshApp($scope.app.id, function (error) {
|
|
if (error) return Client.error(error);
|
|
|
|
$timeout(function () { $scope.resources.busy = false; }, 1000);
|
|
});
|
|
});
|
|
},
|
|
|
|
submitCpuShares: function () {
|
|
$scope.resources.busy = true;
|
|
$scope.resources.error = {};
|
|
|
|
Client.configureApp($scope.app.id, 'cpu_shares', { cpuShares: parseInt($scope.resources.cpuShares) }, function (error) {
|
|
if (error) return Client.error(error);
|
|
|
|
$scope.resources.currentCpuShares = $scope.resources.cpuShares;
|
|
|
|
refreshApp($scope.app.id, function (error) {
|
|
if (error) return Client.error(error);
|
|
|
|
$timeout(function () { $scope.resources.busy = false; }, 1000);
|
|
});
|
|
});
|
|
},
|
|
};
|
|
|
|
$scope.services = {
|
|
error: {},
|
|
|
|
busy: false,
|
|
enableTurn: '1', // curse of radio buttons
|
|
enableRedis: '1',
|
|
|
|
show: function () {
|
|
var app = $scope.app;
|
|
|
|
$scope.services.error = {};
|
|
$scope.services.enableTurn = app.enableTurn ? '1' : '0';
|
|
$scope.services.enableRedis = app.enableRedis ? '1' : '0';
|
|
},
|
|
|
|
submitTurn: function () {
|
|
$scope.services.busy = true;
|
|
$scope.services.error = {};
|
|
|
|
Client.configureApp($scope.app.id, 'turn', { enable: $scope.services.enableTurn === '1' }, function (error) {
|
|
if (error && error.statusCode === 400) {
|
|
$scope.services.busy = false;
|
|
$scope.services.error.turn = true;
|
|
return;
|
|
}
|
|
if (error) return Client.error(error);
|
|
|
|
$timeout(function () { $scope.services.busy = false; }, 1000);
|
|
});
|
|
},
|
|
|
|
submitRedis: function () {
|
|
$scope.services.busy = true;
|
|
$scope.services.error = {};
|
|
|
|
Client.configureApp($scope.app.id, 'redis', { enable: $scope.services.enableRedis === '1' }, function (error) {
|
|
if (error && error.statusCode === 400) {
|
|
$scope.services.busy = false;
|
|
$scope.services.error.redis = true;
|
|
return;
|
|
}
|
|
if (error) return Client.error(error);
|
|
|
|
$timeout(function () { $scope.services.busy = false; }, 1000);
|
|
});
|
|
},
|
|
};
|
|
|
|
$scope.storage = {
|
|
error: {},
|
|
|
|
busy: false,
|
|
|
|
busyDataDir: false,
|
|
storageVolumeId: null,
|
|
storageVolumePrefix: '',
|
|
|
|
location: null,
|
|
locationOptions: [],
|
|
|
|
busyBinds: false,
|
|
mounts: [], // { volume, readOnly }
|
|
|
|
show: function () {
|
|
var app = $scope.app;
|
|
|
|
$scope.storage.error = {};
|
|
$scope.storage.storageVolumeId = app.storageVolumeId;
|
|
$scope.storage.storageVolumePrefix = app.storageVolumePrefix || '';
|
|
$scope.storage.mounts = [];
|
|
|
|
$scope.storage.locationOptions = [
|
|
{ id: 'default', type: 'default', displayName: 'Default - /home/yellowtent/appsdata/' + app.id },
|
|
];
|
|
|
|
$scope.volumes.forEach(function (volume) {
|
|
$scope.storage.locationOptions.push({ id: volume.id, type: 'volume', value: volume.id, displayName: 'Volume - ' + volume.name, mountType: volume.mountType });
|
|
});
|
|
|
|
$scope.storage.location = $scope.storage.locationOptions.find(function (l) { return l.id === (app.storageVolumeId || 'default'); });
|
|
|
|
app.mounts.forEach(function (mount) { // { volumeId, readOnly }
|
|
var volume = $scope.volumes.find(function (v) { return v.id === mount.volumeId; });
|
|
$scope.storage.mounts.push({ volume: volume, readOnly: mount.readOnly ? 'true' : 'false' });
|
|
});
|
|
},
|
|
|
|
submitDataDir: function () {
|
|
$scope.storage.busyDataDir = true;
|
|
$scope.storage.error = {};
|
|
|
|
var data = { storageVolumeId: null, storageVolumePrefix: null };
|
|
if ($scope.storage.location.id !== 'default') {
|
|
data.storageVolumeId = $scope.storage.location.id;
|
|
data.storageVolumePrefix = $scope.storage.storageVolumePrefix;
|
|
}
|
|
|
|
Client.configureApp($scope.app.id, 'storage', data, function (error) {
|
|
if (error && error.statusCode === 400) {
|
|
$scope.storage.error.storageVolumePrefix = error.message;
|
|
$scope.storage.busyDataDir = false;
|
|
return;
|
|
}
|
|
if (error) return Client.error(error);
|
|
|
|
$scope.storageDataDirForm.$setPristine();
|
|
|
|
refreshApp($scope.app.id, function (error) {
|
|
if (error) return Client.error(error);
|
|
|
|
$timeout(function () { $scope.storage.busyDataDir = false; }, 1000);
|
|
});
|
|
});
|
|
},
|
|
|
|
addMount: function (event) {
|
|
event.preventDefault();
|
|
$scope.storage.mounts.push({
|
|
volume: $scope.volumes[0],
|
|
readOnly: true
|
|
});
|
|
},
|
|
|
|
delMount: function (event, index) {
|
|
event.preventDefault();
|
|
$scope.storage.mounts.splice(index, 1);
|
|
},
|
|
|
|
submitMounts: function () {
|
|
$scope.storage.busyMounts = true;
|
|
$scope.storage.error = {};
|
|
|
|
var data = [];
|
|
$scope.storage.mounts.forEach(function (mount) {
|
|
data.push({ volumeId: mount.volume.id, readOnly: mount.readOnly === 'true' });
|
|
});
|
|
|
|
Client.configureApp($scope.app.id, 'mounts', { mounts: data }, function (error) {
|
|
if (error && error.statusCode === 400) {
|
|
$scope.storage.error.mounts = error.message;
|
|
$scope.storage.busyMounts = false;
|
|
return;
|
|
}
|
|
if (error) return Client.error(error);
|
|
|
|
refreshApp($scope.app.id, function (error) {
|
|
if (error) return Client.error(error);
|
|
|
|
$timeout(function () { $scope.storage.busyMounts = false; }, 1000);
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
$scope.graphs = {
|
|
error: {},
|
|
busy: true,
|
|
|
|
period: 6,
|
|
memoryChart: null,
|
|
diskChart: null,
|
|
|
|
blockReadTotal: 0,
|
|
blockWriteTotal: 0,
|
|
networkReadTotal: 0,
|
|
networkWriteTotal: 0,
|
|
|
|
setPeriod: function (hours) {
|
|
$scope.graphs.period = hours;
|
|
$scope.graphs.show();
|
|
},
|
|
|
|
show: function () {
|
|
$scope.graphs.busy = true;
|
|
// in minutes
|
|
var timePeriod = $scope.graphs.period * 60;
|
|
|
|
// keep in sync with graphs.js
|
|
var timeBucketSizeMinutes = timePeriod > (24 * 60) ? (6*60) : 5;
|
|
var steps = Math.floor(timePeriod/timeBucketSizeMinutes);
|
|
|
|
var labels = new Array(steps).fill(0);
|
|
labels = labels.map(function (v, index) {
|
|
var dateTime = new Date(Date.now() - ((timePeriod - (index * timeBucketSizeMinutes)) * 60 * 1000));
|
|
|
|
if ($scope.graphs.period > 24) {
|
|
return dateTime.toLocaleDateString();
|
|
} else {
|
|
return dateTime.toLocaleTimeString();
|
|
}
|
|
});
|
|
|
|
var borderColors = [ '#2196F3', '#FF6384' ];
|
|
var backgroundColors = [ '#82C4F844', '#FF63844F' ];
|
|
|
|
function fillGraph(canvasId, contents, chartPropertyName, divisor, max, format, formatDivisor, stepSize) {
|
|
if (!contents || !contents[0]) return; // no data available yet
|
|
|
|
var datasets = [];
|
|
|
|
contents.forEach(function (content, index) {
|
|
|
|
// fill holes with previous value
|
|
var cur = 0;
|
|
content.data.forEach(function (d) {
|
|
if (d[0] === null) d[0] = cur;
|
|
else cur = d[0];
|
|
});
|
|
|
|
var datapoints = Array(steps).map(function () { return '0'; });
|
|
|
|
// walk backwards and fill up the datapoints
|
|
content.data.reverse().forEach(function (d, index) {
|
|
datapoints[datapoints.length-1-index] = (d[0] / divisor).toFixed(2);
|
|
// return parseInt((d[0] / divisor).toFixed(2));
|
|
});
|
|
|
|
datasets.push({
|
|
label: content.label,
|
|
backgroundColor: backgroundColors[index],
|
|
borderColor: borderColors[index],
|
|
borderWidth: 1,
|
|
radius: 0,
|
|
data: datapoints,
|
|
cubicInterpolationMode: 'monotone',
|
|
tension: 0.4
|
|
});
|
|
});
|
|
|
|
var graphData = {
|
|
labels: labels,
|
|
datasets: datasets
|
|
};
|
|
|
|
var options = {
|
|
responsive: true,
|
|
maintainAspectRatio: true,
|
|
aspectRatio: 2.5,
|
|
animation: false,
|
|
plugins: {
|
|
legend: {
|
|
display: false
|
|
}
|
|
},
|
|
interaction: {
|
|
intersect: false,
|
|
mode: 'index',
|
|
},
|
|
scales: {
|
|
x: {
|
|
ticks: { autoSkipPadding: 50, maxRotation: 0 }
|
|
},
|
|
y: {
|
|
ticks: { maxTicksLimit: 6 },
|
|
min: 0,
|
|
beginAtZero: true
|
|
}
|
|
}
|
|
};
|
|
|
|
if (format) options.scales.y.ticks.callback = function (value) {
|
|
if (!formatDivisor) return value + ' ' + format;
|
|
return (value/formatDivisor).toLocaleString('en-US', { maximumFractionDigits: 6 }) + ' ' + format;
|
|
};
|
|
if (max) options.scales.y.max = max;
|
|
if (stepSize) options.scales.y.ticks.stepSize = stepSize;
|
|
|
|
var ctx = $(canvasId).get(0).getContext('2d');
|
|
|
|
if ($scope.graphs[chartPropertyName]) $scope.graphs[chartPropertyName].destroy();
|
|
$scope.graphs[chartPropertyName] = new Chart(ctx, { type: 'line', data: graphData, options: options });
|
|
}
|
|
|
|
Client.getAppGraphs(appId, timePeriod, function (error, result) {
|
|
if (error) return console.error(error);
|
|
|
|
var currentMemoryLimit = $scope.app.memoryLimit || $scope.app.manifest.memoryLimit || 0;
|
|
var maxGraphMemory = currentMemoryLimit < (512 * 1024 * 1024) ? (512 * 1024 * 1024) : currentMemoryLimit;
|
|
var cpuCount = result.cpuCount;
|
|
var ioDivisor = 1000 * 1000;
|
|
|
|
$scope.graphs.blockReadTotal = (result.blockReadTotal / ioDivisor / 1000).toFixed(2) + ' MB';
|
|
$scope.graphs.blockWriteTotal = (result.blockWriteTotal / ioDivisor / 1000).toFixed(2) + ' MB';
|
|
$scope.graphs.networkReadTotal = (result.networkReadTotal / ioDivisor / 1000).toFixed(2) + ' MB';
|
|
$scope.graphs.networkWriteTotal = (result.networkWriteTotal / ioDivisor / 1000).toFixed(2) + ' MB';
|
|
|
|
fillGraph('#graphsMemoryChart', [{ data: result.memory, label: 'Memory' }], 'memoryChart', 1024 * 1024, maxGraphMemory / 1024 / 1024, 'GiB', 1024, (maxGraphMemory / 1024 / 1024) <= 1024 ? 256 : 512);
|
|
fillGraph('#graphsCpuChart', [{ data: result.cpu, label: 'CPU' }], 'cpuChart', 1, cpuCount * 100, '%');
|
|
fillGraph('#graphsDiskChart', [{ data: result.blockRead, label: 'read' }, { data: result.blockWrite, label: 'write' }], 'diskChart', ioDivisor, null, 'kB/s');
|
|
fillGraph('#graphsNetworkChart', [{ data: result.networkRead, label: 'inbound' }, { data: result.networkWrite, label: 'outbound' }], 'networkChart', ioDivisor, null, 'kB/s');
|
|
|
|
$scope.graphs.busy = false;
|
|
});
|
|
}
|
|
};
|
|
|
|
function findInbox(inboxes, app) {
|
|
return inboxes.find(function (i) { return i.name === app.inboxName && i.domain === (app.inboxDomain || app.domain); });
|
|
}
|
|
|
|
$scope.email = {
|
|
enableMailbox: true,
|
|
mailboxName: '',
|
|
mailboxDomain: null,
|
|
mailboxDisplayName: '',
|
|
currentMailboxName: '',
|
|
currentMailboxDomainName: '',
|
|
mailboxError: {},
|
|
mailboxBusy: false,
|
|
|
|
inboxError: {},
|
|
inboxBusy: false,
|
|
enableInbox: true,
|
|
inboxes: [],
|
|
currentInbox: null,
|
|
inbox: null,
|
|
|
|
show: function () {
|
|
var app = $scope.app;
|
|
|
|
$scope.emailForm.$setPristine();
|
|
$scope.email.mailboxError = {};
|
|
$scope.email.enableMailbox = app.enableMailbox ? '1' : '0';
|
|
$scope.email.mailboxName = app.mailboxName || '';
|
|
$scope.email.mailboxDisplayName = app.mailboxDisplayName || '';
|
|
$scope.email.mailboxDomain = $scope.domains.filter(function (d) { return d.domain === (app.mailboxDomain || app.domain); })[0];
|
|
$scope.email.currentMailboxName = app.mailboxName || '';
|
|
$scope.email.currentMailboxDomainName = $scope.email.mailboxDomain ? $scope.email.mailboxDomain.domain : '';
|
|
|
|
$scope.email.inboxError = {};
|
|
$scope.email.enableInbox = app.enableInbox ? true : false;
|
|
|
|
Client.getAllMailboxes(function (error, mailboxes) {
|
|
if (error) console.error('Failed to list mailboxes.', error);
|
|
|
|
$scope.email.inboxes = mailboxes.map(function (m) { return { display: m.name + '@' + m.domain, name: m.name, domain: m.domain }; });
|
|
$scope.email.currentInbox = findInbox($scope.email.inboxes, app);
|
|
$scope.email.inbox = findInbox($scope.email.inboxes, app);
|
|
});
|
|
},
|
|
|
|
submitMailbox: function () {
|
|
$scope.email.error = {};
|
|
$scope.email.mailboxBusy = true;
|
|
|
|
var data = {
|
|
enable: $scope.email.enableMailbox === '1'
|
|
};
|
|
|
|
if (data.enable) {
|
|
data.mailboxName = $scope.email.mailboxName || null;
|
|
data.mailboxDomain = $scope.email.mailboxDomain.domain;
|
|
data.mailboxDisplayName = $scope.email.mailboxDisplayName;
|
|
}
|
|
|
|
Client.configureApp($scope.app.id, 'mailbox', data, function (error) {
|
|
if (error && error.statusCode === 400) {
|
|
$scope.email.mailboxBusy = false;
|
|
$scope.email.error.mailboxName = error.message;
|
|
$scope.emailForm.$setPristine();
|
|
return;
|
|
}
|
|
if (error) return Client.error(error);
|
|
|
|
$scope.emailForm.$setPristine();
|
|
|
|
refreshApp($scope.app.id, function (error) {
|
|
if (error) return Client.error(error);
|
|
|
|
// when the mailboxName is 'reset', this will fill it up with the default again
|
|
$scope.email.enableMailbox = $scope.app.enableMailbox ? '1' : '0';
|
|
$scope.email.mailboxName = $scope.app.mailboxName || '';
|
|
$scope.email.mailboxDomain = $scope.domains.filter(function (d) { return d.domain === ($scope.app.mailboxDomain || $scope.app.domain); })[0];
|
|
$scope.email.currentMailboxName = $scope.app.mailboxName || '';
|
|
$scope.email.currentMailboxDomainName = $scope.email.mailboxDomain ? $scope.email.mailboxDomain.domain : '';
|
|
|
|
$timeout(function () { $scope.email.mailboxBusy = false; }, 1000);
|
|
});
|
|
});
|
|
},
|
|
|
|
submitInbox: function () {
|
|
$scope.email.error = {};
|
|
$scope.email.inboxBusy = true;
|
|
|
|
var data = {
|
|
enable: $scope.email.enableInbox
|
|
};
|
|
|
|
if (data.enable) {
|
|
data.inboxName = $scope.email.inbox.name;
|
|
data.inboxDomain = $scope.email.inbox.domain;
|
|
}
|
|
|
|
Client.configureApp($scope.app.id, 'inbox', data, function (error) {
|
|
if (error && error.statusCode === 400) {
|
|
$scope.email.inboxBusy = false;
|
|
$scope.email.error.inboxName = error.message;
|
|
return;
|
|
}
|
|
if (error) return Client.error(error);
|
|
|
|
refreshApp($scope.app.id, function (error) {
|
|
if (error) return Client.error(error);
|
|
|
|
// when the mailboxName is 'reset', this will fill it up with the default again
|
|
$scope.email.enableInbox = $scope.app.enableInbox ? true : false;
|
|
$scope.email.currentInbox = findInbox($scope.email.inboxes, $scope.app);
|
|
$scope.email.inbox = findInbox($scope.email.inboxes, $scope.app);
|
|
|
|
$timeout(function () { $scope.email.inboxBusy = false; }, 1000);
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
$scope.eventlog = {
|
|
busy: false,
|
|
eventLogs: [],
|
|
activeEventLog: null,
|
|
currentPage: 1,
|
|
perPage: 15,
|
|
|
|
show: function () {
|
|
$scope.eventlog.refresh();
|
|
},
|
|
|
|
refresh: function () {
|
|
$scope.eventlog.busy = true;
|
|
|
|
Client.getAppEventLog($scope.app.id, $scope.eventlog.currentPage, $scope.eventlog.perPage, function (error, result) {
|
|
if (error) return console.error('Failed to get events:', error);
|
|
|
|
$scope.eventlog.eventLogs = [];
|
|
result.forEach(function (e) {
|
|
$scope.eventlog.eventLogs.push({ raw: e, details: Client.eventLogDetails(e, $scope.app.id), source: Client.eventLogSource(e) });
|
|
});
|
|
|
|
$scope.eventlog.busy = false;
|
|
});
|
|
},
|
|
|
|
showDetails: function (eventLog) {
|
|
if ($scope.eventlog.activeEventLog === eventLog) $scope.eventlog.activeEventLog = null;
|
|
else $scope.eventlog.activeEventLog = eventLog;
|
|
},
|
|
|
|
showNextPage: function () {
|
|
$scope.eventlog.currentPage++;
|
|
$scope.eventlog.refresh();
|
|
},
|
|
|
|
showPrevPage: function () {
|
|
if ($scope.eventlog.currentPage > 1) $scope.eventlog.currentPage--;
|
|
else $scope.eventlog.currentPage = 1;
|
|
|
|
$scope.eventlog.refresh();
|
|
}
|
|
};
|
|
|
|
$scope.cron = {
|
|
busy: false,
|
|
error: {},
|
|
|
|
commonPatterns: [
|
|
{ value: '* * * * *', label: $translate.instant('app.cron.commonPattern.everyMinute') },
|
|
{ value: '0 * * * *', label: $translate.instant('app.cron.commonPattern.everyHour') },
|
|
{ value: '*/30 * * * *', label: $translate.instant('app.cron.commonPattern.twicePerHour') },
|
|
{ value: '0 0 * * *', label: $translate.instant('app.cron.commonPattern.everyDay') },
|
|
{ value: '0 */12 * * *', label: $translate.instant('app.cron.commonPattern.twicePerDay') },
|
|
{ value: '0 0 * * 0', label: $translate.instant('app.cron.commonPattern.everySunday') },
|
|
|
|
{ value: '@daily', label: $translate.instant('app.cron.commonPattern.daily') },
|
|
{ value: '@hourly', label: $translate.instant('app.cron.commonPattern.hourly') },
|
|
{ value: '@service', label: $translate.instant('app.cron.commonPattern.service') }
|
|
],
|
|
|
|
crontab: '',
|
|
|
|
crontabDefault: ''
|
|
+ '# +------------------------ minute (0 - 59)\n'
|
|
+ '# | +------------------- hour (0 - 23)\n'
|
|
+ '# | | +-------------- day of month (1 - 31)\n'
|
|
+ '# | | | +--------- month (1 - 12)\n'
|
|
+ '# | | | | +---- day of week (0 - 6) (Sunday=0 or 7)\n'
|
|
+ '# | | | | |\n'
|
|
+ '# * * * * * command to be executed\n\n',
|
|
|
|
show: function () {
|
|
$scope.cronForm.$setPristine();
|
|
$scope.cron.error = {};
|
|
$scope.cron.crontab = $scope.app.crontab;
|
|
if ($scope.cron.crontab === null) $scope.cron.crontab = $scope.cron.crontabDefault; // only when null, not when ''
|
|
},
|
|
|
|
submit: function () {
|
|
$scope.cron.error = {};
|
|
$scope.cron.busy = true;
|
|
|
|
Client.configureApp($scope.app.id, 'crontab', { crontab: $scope.cron.crontab }, function (error) {
|
|
if (error && error.statusCode === 400) {
|
|
$scope.cron.busy = false;
|
|
$scope.cron.error.crontab = error.message;
|
|
$scope.cronForm.$setPristine();
|
|
return;
|
|
}
|
|
if (error) return Client.error(error);
|
|
|
|
$scope.cronForm.$setPristine();
|
|
|
|
$timeout(function () { $scope.cron.busy = false; }, 1000);
|
|
});
|
|
},
|
|
|
|
addCommonPattern: function (pattern) {
|
|
$scope.cron.crontab += pattern + ' /path/to/command\n';
|
|
}
|
|
};
|
|
|
|
$scope.security = {
|
|
busy: false,
|
|
error: {},
|
|
success: false,
|
|
|
|
robotsTxt: '',
|
|
csp: '',
|
|
hstsPreload: false,
|
|
|
|
show: function () {
|
|
$scope.security.error = {};
|
|
$scope.security.robotsTxt = $scope.app.reverseProxyConfig.robotsTxt || '';
|
|
$scope.security.csp = $scope.app.reverseProxyConfig.csp || '';
|
|
$scope.security.hstsPreload = $scope.app.reverseProxyConfig.hstsPreload || false;
|
|
},
|
|
|
|
submit: function () {
|
|
$scope.security.busy = true;
|
|
$scope.security.error = {};
|
|
|
|
var reverseProxyConfig = {
|
|
robotsTxt: $scope.security.robotsTxt || null, // empty string resets
|
|
csp: $scope.security.csp || null, // empty string resets
|
|
hstsPreload: $scope.security.hstsPreload
|
|
};
|
|
|
|
Client.configureApp($scope.app.id, 'reverse_proxy', reverseProxyConfig, function (error) {
|
|
if (error) return Client.error(error);
|
|
|
|
$timeout(function () {
|
|
$scope.security.success = true;
|
|
$scope.security.busy = false;
|
|
}, 1000);
|
|
});
|
|
}
|
|
};
|
|
|
|
$scope.proxy = {
|
|
busy: false,
|
|
error: null,
|
|
success: false,
|
|
|
|
upstreamUri: '',
|
|
|
|
show: function () {
|
|
$scope.proxyForm.$setPristine();
|
|
$scope.proxy.error = null;
|
|
$scope.proxy.upstreamUri = $scope.app.upstreamUri || '';
|
|
},
|
|
|
|
submit: function () {
|
|
$scope.proxy.busy = true;
|
|
$scope.proxy.error = null;
|
|
|
|
var upstreamUri = $scope.proxy.upstreamUri.replace(/\/$/, '');
|
|
Client.configureApp($scope.app.id, 'upstream_uri', { upstreamUri: upstreamUri }, function (error) {
|
|
$scope.proxy.busy = false;
|
|
|
|
if (error && error.statusCode === 400) {
|
|
$scope.proxy.error = error.message;
|
|
$scope.proxyForm.$setPristine();
|
|
return;
|
|
}
|
|
if (error) return Client.error(error);
|
|
|
|
$scope.proxyForm.$setPristine();
|
|
|
|
$timeout(function () {
|
|
$scope.proxy.success = true;
|
|
}, 1000);
|
|
});
|
|
}
|
|
};
|
|
|
|
$scope.updates = {
|
|
busy: false,
|
|
busyCheck: false,
|
|
busyUpdate: false,
|
|
busyAutomaticUpdates: false,
|
|
skipBackup: false,
|
|
|
|
show: function () {
|
|
$scope.updates.skipBackup = false;
|
|
},
|
|
|
|
toggleAutomaticUpdates: function () {
|
|
$scope.updates.busyAutomaticUpdates = true;
|
|
|
|
Client.configureApp($scope.app.id, 'automatic_update', { enable: !$scope.app.enableAutomaticUpdate }, function (error) {
|
|
if (error) return Client.error(error);
|
|
|
|
refreshApp($scope.app.id, function (error) {
|
|
if (error) console.error(error);
|
|
|
|
$scope.updates.busyAutomaticUpdates = false;
|
|
});
|
|
});
|
|
},
|
|
|
|
check: function () {
|
|
$scope.updates.busyCheck = true;
|
|
|
|
Client.checkForAppUpdates($scope.app.id, function (error) {
|
|
if (error) Client.error(error);
|
|
|
|
$scope.updates.busyCheck = false;
|
|
});
|
|
},
|
|
|
|
askUpdate: function () {
|
|
$scope.updates.busyUpdate = false;
|
|
$('#updateModal').modal('show');
|
|
},
|
|
|
|
confirmUpdate: function () {
|
|
$scope.updates.busyUpdate = true;
|
|
|
|
Client.updateApp($scope.app.id, $scope.config.update[$scope.app.id].manifest, { skipBackup: $scope.updates.skipBackup }, function (error) {
|
|
$scope.updates.busyUpdate = false;
|
|
if (error) return Client.error(error);
|
|
|
|
$('#updateModal').modal('hide');
|
|
refreshApp($scope.app.id);
|
|
});
|
|
}
|
|
};
|
|
|
|
$scope.backupDetails = {
|
|
backup: null,
|
|
|
|
show: function (backup) {
|
|
$scope.backupDetails.backup = backup;
|
|
$('#backupDetailsModal').modal('show');
|
|
}
|
|
};
|
|
|
|
$scope.backups = {
|
|
busy: false,
|
|
busyCreate: false,
|
|
busyAutomaticBackups: false,
|
|
error: {},
|
|
|
|
enableBackup: false,
|
|
backups: [],
|
|
|
|
createBackup: function () {
|
|
$scope.backups.busyCreate = true;
|
|
|
|
Client.backupApp($scope.app.id, function (error) {
|
|
if (error) Client.error(error);
|
|
|
|
refreshApp($scope.app.id, function () {
|
|
$scope.backups.busyCreate = false;
|
|
|
|
waitForAppTask(function (error) {
|
|
if (error) return Client.error(error);
|
|
|
|
$scope.backups.show(); // refresh backup listing
|
|
});
|
|
});
|
|
});
|
|
},
|
|
|
|
show: function () {
|
|
var app = $scope.app;
|
|
|
|
$scope.backups.error = {};
|
|
$scope.backups.enableBackup = app.enableBackup;
|
|
|
|
Client.getAppBackups(app.id, function (error, backups) {
|
|
if (error) return Client.error(error);
|
|
|
|
$scope.backups.backups = backups;
|
|
|
|
Client.getAppEventLog(app.id, 1, 1, function (error, result) {
|
|
if (error) return console.error('Failed to get events:', error);
|
|
|
|
if (result.length !== 0 && result[0].action == 'app.backup.finish') {
|
|
$scope.backups.error.message = result[0].data.errorMessage;
|
|
}
|
|
});
|
|
});
|
|
},
|
|
|
|
refresh: function () {
|
|
Client.getAppBackups($scope.app.id, function (error, backups) {
|
|
if (error) return Client.error(error);
|
|
|
|
$scope.backups.backups = backups;
|
|
});
|
|
},
|
|
|
|
toggleAutomaticBackups: function () {
|
|
$scope.backups.busyAutomaticBackups = true;
|
|
$scope.backups.error = {};
|
|
|
|
Client.configureApp($scope.app.id, 'automatic_backup', { enable: !$scope.backups.enableBackup }, function (error) {
|
|
if (error) return Client.error(error);
|
|
|
|
$timeout(function () {
|
|
$scope.backups.enableBackup = !$scope.backups.enableBackup;
|
|
$scope.backups.busyAutomaticBackups = false;
|
|
}, 1000);
|
|
});
|
|
}
|
|
};
|
|
|
|
$scope.s3like = function (provider) {
|
|
return provider === 's3' || provider === 'minio' || provider === 's3-v4-compat'
|
|
|| provider === 'exoscale-sos' || provider === 'digitalocean-spaces'
|
|
|| provider === 'scaleway-objectstorage' || provider === 'wasabi' || provider === 'backblaze-b2' || provider === 'cloudflare-r2'
|
|
|| provider === 'linode-objectstorage' || provider === 'ovh-objectstorage' || provider === 'ionos-objectstorage'
|
|
|| provider === 'vultr-objectstorage' || provider === 'upcloud-objectstorage' || provider === 'idrive-e2'
|
|
|| provider === 'contabo-objectstorage';
|
|
};
|
|
|
|
$scope.mountlike = function (provider) {
|
|
return provider === 'sshfs' || provider === 'cifs' || provider === 'nfs' || provider === 'mountpoint' || provider === 'ext4' || provider === 'xfs';
|
|
};
|
|
|
|
$scope.importBackup = {
|
|
busy: false,
|
|
error: {},
|
|
|
|
// variables here have to match the import config logic!
|
|
provider: '',
|
|
bucket: '',
|
|
prefix: '',
|
|
mountPoint: '',
|
|
accessKeyId: '',
|
|
secretAccessKey: '',
|
|
gcsKey: { keyFileName: '', content: '' },
|
|
region: '',
|
|
endpoint: '',
|
|
acceptSelfSignedCerts: false,
|
|
format: 'tgz',
|
|
remotePath: '',
|
|
password: '',
|
|
encryptedFilenames: true,
|
|
mountOptions: {}, // host, port, username, password, remoteDir, diskPath, user, privateKey
|
|
encrypted: false, // helps with ng-required when backupConfig is read from file
|
|
|
|
clearForm: function () {
|
|
// $scope.importBackup.provider = ''; // do not clear since we call this function on provider change
|
|
$scope.importBackup.bucket = '';
|
|
$scope.importBackup.mountPoint = '';
|
|
$scope.importBackup.accessKeyId = '';
|
|
$scope.importBackup.secretAccessKey = '';
|
|
$scope.importBackup.gcsKey.keyFileName = '';
|
|
$scope.importBackup.gcsKey.content = '';
|
|
$scope.importBackup.endpoint = '';
|
|
$scope.importBackup.region = '';
|
|
$scope.importBackup.format = 'tgz';
|
|
$scope.importBackup.acceptSelfSignedCerts = false;
|
|
$scope.importBackup.password = '';
|
|
$scope.importBackup.encryptedFilenames = true;
|
|
$scope.importBackup.remotePath = '';
|
|
$scope.importBackup.mountOptions = {};
|
|
},
|
|
|
|
submit: function () {
|
|
$scope.importBackup.error = {};
|
|
$scope.importBackup.busy = true;
|
|
|
|
var backupConfig = {
|
|
provider: $scope.importBackup.provider,
|
|
};
|
|
if ($scope.importBackup.password) {
|
|
backupConfig.password = $scope.importBackup.password;
|
|
backupConfig.encryptedFilenames = $scope.importBackup.encryptedFilenames;
|
|
}
|
|
|
|
var remotePath = $scope.importBackup.remotePath;
|
|
|
|
// only set provider specific fields, this will clear them in the db
|
|
if ($scope.s3like(backupConfig.provider)) {
|
|
backupConfig.bucket = $scope.importBackup.bucket;
|
|
backupConfig.prefix = '';
|
|
backupConfig.accessKeyId = $scope.importBackup.accessKeyId;
|
|
backupConfig.secretAccessKey = $scope.importBackup.secretAccessKey;
|
|
|
|
if ($scope.importBackup.endpoint) backupConfig.endpoint = $scope.importBackup.endpoint;
|
|
|
|
if (backupConfig.provider === 's3') {
|
|
if ($scope.importBackup.region) backupConfig.region = $scope.importBackup.region;
|
|
delete backupConfig.endpoint;
|
|
} else if (backupConfig.provider === 'minio' || backupConfig.provider === 's3-v4-compat') {
|
|
backupConfig.region = backupConfig.region || 'us-east-1';
|
|
backupConfig.acceptSelfSignedCerts = $scope.importBackup.acceptSelfSignedCerts;
|
|
backupConfig.s3ForcePathStyle = true; // might want to expose this in the UI
|
|
} else if (backupConfig.provider === 'exoscale-sos') {
|
|
backupConfig.region = 'us-east-1';
|
|
backupConfig.signatureVersion = 'v4';
|
|
} else if (backupConfig.provider === 'wasabi') {
|
|
backupConfig.region = $scope.wasabiRegions.find(function (x) { return x.value === $scope.importBackup.endpoint; }).region;
|
|
backupConfig.signatureVersion = 'v4';
|
|
} else if (backupConfig.provider === 'scaleway-objectstorage') {
|
|
backupConfig.region = $scope.scalewayRegions.find(function (x) { return x.value === $scope.importBackup.endpoint; }).region;
|
|
backupConfig.signatureVersion = 'v4';
|
|
} else if (backupConfig.provider === 'linode-objectstorage') {
|
|
backupConfig.region = $scope.linodeRegions.find(function (x) { return x.value === $scope.importBackup.endpoint; }).region;
|
|
backupConfig.signatureVersion = 'v4';
|
|
} else if (backupConfig.provider === 'ovh-objectstorage') {
|
|
backupConfig.region = $scope.ovhRegions.find(function (x) { return x.value === $scope.importBackup.endpoint; }).region;
|
|
backupConfig.signatureVersion = 'v4';
|
|
} else if (backupConfig.provider === 'ionos-objectstorage') {
|
|
backupConfig.region = $scope.ionosRegions.find(function (x) { return x.value === $scope.importBackup.endpoint; }).region;
|
|
backupConfig.signatureVersion = 'v4';
|
|
} else if (backupConfig.provider === 'vultr-objectstorage') {
|
|
backupConfig.region = $scope.vultrRegions.find(function (x) { return x.value === $scope.importBackup.endpoint; }).region;
|
|
backupConfig.signatureVersion = 'v4';
|
|
} else if (backupConfig.provider === 'contabo-objectstorage') {
|
|
backupConfig.region = $scope.contaboRegions.find(function (x) { return x.value === $scope.importBackup.endpoint; }).region;
|
|
backupConfig.signatureVersion = 'v4';
|
|
backupConfig.s3ForcePathStyle = true; // https://docs.contabo.com/docs/products/Object-Storage/technical-description (no virtual buckets)
|
|
} else if (backupConfig.provider === 'upcloud-objectstorage') {
|
|
var m = /^.*\.(.*)\.upcloudobjects.com$/.exec(backupConfig.endpoint);
|
|
backupConfig.region = m ? m[1] : 'us-east-1'; // let it fail in validation phase if m is not valid
|
|
backupConfig.signatureVersion = 'v4';
|
|
} else if (backupConfig.provider === 'digitalocean-spaces') {
|
|
backupConfig.region = 'us-east-1';
|
|
}
|
|
} else if (backupConfig.provider === 'gcs') {
|
|
backupConfig.bucket = $scope.importBackup.bucket;
|
|
backupConfig.prefix = '';
|
|
try {
|
|
var serviceAccountKey = JSON.parse($scope.importBackup.gcsKey.content);
|
|
backupConfig.projectId = serviceAccountKey.project_id;
|
|
backupConfig.credentials = {
|
|
client_email: serviceAccountKey.client_email,
|
|
private_key: serviceAccountKey.private_key
|
|
};
|
|
|
|
if (!backupConfig.projectId || !backupConfig.credentials || !backupConfig.credentials.client_email || !backupConfig.credentials.private_key) {
|
|
throw 'fields_missing';
|
|
}
|
|
} catch (e) {
|
|
$scope.importBackup.error.generic = 'Cannot parse Google Service Account Key: ' + e.message;
|
|
$scope.importBackup.error.gcsKeyInput = true;
|
|
$scope.importBackup.busy = false;
|
|
return;
|
|
}
|
|
} else if (backupConfig.provider === 'sshfs' || backupConfig.provider === 'cifs' || backupConfig.provider === 'nfs' || backupConfig.provider === 'ext4' || backupConfig.provider === 'xfs') {
|
|
backupConfig.mountOptions = $scope.importBackup.mountOptions;
|
|
backupConfig.prefix = '';
|
|
} else if (backupConfig.provider === 'mountpoint') {
|
|
backupConfig.prefix = '';
|
|
backupConfig.mountPoint = $scope.importBackup.mountPoint;
|
|
} else if (backupConfig.provider === 'filesystem') {
|
|
var parts = remotePath.split('/');
|
|
remotePath = parts.pop() || parts.pop(); // removes any trailing slash. this is basename()
|
|
backupConfig.backupFolder = parts.join('/'); // this is dirname()
|
|
}
|
|
|
|
if ($scope.importBackup.format === 'tgz') {
|
|
if (remotePath.substring(remotePath.length - '.tar.gz'.length, remotePath.length) === '.tar.gz') { // endsWith
|
|
remotePath = remotePath.replace(/.tar.gz$/, '');
|
|
} else if (remotePath.substring(remotePath.length - '.tar.gz.enc'.length, remotePath.length) === '.tar.gz.enc') { // endsWith
|
|
remotePath = remotePath.replace(/.tar.gz.enc$/, '');
|
|
}
|
|
}
|
|
|
|
Client.importBackup($scope.app.id, remotePath, $scope.importBackup.format, backupConfig, function (error) {
|
|
if (error) {
|
|
$scope.importBackup.busy = false;
|
|
|
|
if (error.statusCode === 424) {
|
|
$scope.importBackup.error.generic = error.message;
|
|
|
|
if (error.message.indexOf('AWS Access Key Id') !== -1) {
|
|
$scope.importBackup.error.accessKeyId = true;
|
|
$scope.importBackupForm.accessKeyId.$setPristine();
|
|
$('#inputImportBackupAccessKeyId').focus();
|
|
} else if (error.message.indexOf('not match the signature') !== -1 || error.message.indexOf('Signature') !== -1) {
|
|
$scope.importBackup.error.secretAccessKey = true;
|
|
$scope.importBackupForm.secretAccessKey.$setPristine();
|
|
$('#inputImportBackupSecretAccessKey').focus();
|
|
} else if (error.message.toLowerCase() === 'access denied') {
|
|
$scope.importBackup.error.accessKeyId = true;
|
|
$scope.importBackupForm.accessKeyId.$setPristine();
|
|
$('#inputImportBackupBucket').focus();
|
|
} else if (error.message.indexOf('ECONNREFUSED') !== -1) {
|
|
$scope.importBackup.error.generic = 'Unknown region';
|
|
$scope.importBackup.error.region = true;
|
|
$scope.importBackupForm.region.$setPristine();
|
|
$('#inputImportBackupDORegion').focus();
|
|
} else if (error.message.toLowerCase() === 'wrong region') {
|
|
$scope.importBackup.error.generic = 'Wrong S3 Region';
|
|
$scope.importBackup.error.region = true;
|
|
$scope.importBackupForm.region.$setPristine();
|
|
$('#inputImportBackupS3Region').focus();
|
|
} else {
|
|
$scope.importBackup.error.bucket = true;
|
|
$('#inputImportBackupBucket').focus();
|
|
$scope.importBackupForm.bucket.$setPristine();
|
|
}
|
|
} else if (error.statusCode === 400) {
|
|
$scope.importBackup.error.generic = error.message;
|
|
|
|
if ($scope.importBackup.provider === 'filesystem') {
|
|
$scope.importBackup.error.backupFolder = true;
|
|
}
|
|
} else {
|
|
Client.error(error);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
$('#importBackupModal').modal('hide');
|
|
|
|
// clear potential post-install flag
|
|
$scope.app.pendingPostInstallConfirmation = false;
|
|
delete localStorage['confirmPostInstall_' + $scope.app.id];
|
|
|
|
refreshApp($scope.app.id, function (error) {
|
|
if (error) return Client.error(error);
|
|
|
|
$timeout(function () { $scope.importBackup.busy = false; }, 1000);
|
|
});
|
|
});
|
|
},
|
|
|
|
show: function () {
|
|
$scope.importBackup.clearForm();
|
|
$('#importBackupModal').modal('show');
|
|
},
|
|
};
|
|
|
|
$scope.editBackup = {
|
|
busy: false,
|
|
error: null,
|
|
backup: null,
|
|
|
|
label: '',
|
|
persist: false,
|
|
|
|
show: function (backup) {
|
|
$scope.editBackup.backup = backup;
|
|
$scope.editBackup.label = backup.label;
|
|
$scope.editBackup.persist = backup.preserveSecs === -1;
|
|
$scope.editBackup.error = null;
|
|
$scope.editBackup.busy = false;
|
|
|
|
$('#editBackupModal').modal('show');
|
|
},
|
|
|
|
submit: function () {
|
|
$scope.editBackup.error = null;
|
|
$scope.editBackup.busy = true;
|
|
|
|
Client.editAppBackup($scope.app.id, $scope.editBackup.backup.id, $scope.editBackup.label, $scope.editBackup.persist ? -1 : 0, function (error) {
|
|
$scope.editBackup.busy = false;
|
|
if (error) return $scope.editBackup.error = error.message;
|
|
|
|
$scope.backups.refresh();
|
|
|
|
$('#editBackupModal').modal('hide');
|
|
});
|
|
}
|
|
};
|
|
|
|
$scope.backupDetails = {
|
|
backup: null,
|
|
|
|
show: function (backup) {
|
|
$scope.backupDetails.backup = backup;
|
|
$('#backupDetailsModal').modal('show');
|
|
}
|
|
};
|
|
|
|
$scope.uninstall = {
|
|
busy: false,
|
|
error: {},
|
|
busyRunState: false,
|
|
startButton: false,
|
|
|
|
toggleRunState: function (confirmStop) {
|
|
if (confirmStop && $scope.app.runState !== RSTATES.STOPPED) {
|
|
$('#stopModal').modal('show');
|
|
return;
|
|
}
|
|
|
|
$('#stopModal').modal('hide');
|
|
|
|
var func = $scope.app.runState === RSTATES.STOPPED ? Client.startApp : Client.stopApp;
|
|
$scope.uninstall.busyRunState = true;
|
|
|
|
func($scope.app.id, function (error) {
|
|
if (error) return Client.error(error);
|
|
|
|
refreshApp($scope.app.id, function (error) {
|
|
if (error) return Client.error(error);
|
|
|
|
$timeout(function () { $scope.uninstall.busyRunState = false; }, 1000);
|
|
});
|
|
});
|
|
},
|
|
|
|
show: function () {
|
|
$scope.uninstall.error = {};
|
|
},
|
|
|
|
ask: function () {
|
|
$('#uninstallModal').modal('show');
|
|
},
|
|
|
|
submit: function () {
|
|
$scope.uninstall.busy = true;
|
|
|
|
var NOOP = function (next) { return next(); };
|
|
var stopAppTask = $scope.app.taskId ? Client.stopTask.bind(null, $scope.app.taskId) : NOOP;
|
|
|
|
stopAppTask(function () { // ignore error
|
|
Client.uninstallApp($scope.app.id, function (error) {
|
|
if (error && error.statusCode === 402) { // unpurchase failed
|
|
Client.error('Relogin to Cloudron App Store');
|
|
} else if (error) {
|
|
Client.error(error);
|
|
} else {
|
|
$('#uninstallModal').modal('hide');
|
|
$location.path('/apps');
|
|
}
|
|
|
|
$scope.uninstall.busy = false;
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
$scope.restore = {
|
|
busy: false,
|
|
error: {},
|
|
|
|
backup: null,
|
|
|
|
show: function (backup) {
|
|
$scope.restore.error = {};
|
|
$scope.restore.backup = backup;
|
|
|
|
$('#restoreModal').modal('show');
|
|
},
|
|
|
|
submit: function () {
|
|
$scope.restore.busy = true;
|
|
|
|
Client.restoreApp($scope.app.id, $scope.restore.backup.id, function (error) {
|
|
$scope.restore.busy = false;
|
|
|
|
if (error) {
|
|
Client.error(error);
|
|
return;
|
|
}
|
|
|
|
$('#restoreModal').modal('hide');
|
|
|
|
refreshApp($scope.app.id);
|
|
});
|
|
}
|
|
};
|
|
|
|
$scope.clone = {
|
|
busy: false,
|
|
error: {},
|
|
|
|
backup: null,
|
|
subdomain: '',
|
|
domain: null,
|
|
secondaryDomains: {},
|
|
needsOverwrite: false,
|
|
overwriteDns: false,
|
|
portBindings: {},
|
|
portBindingsInfo: {},
|
|
portBindingsEnabled: {},
|
|
|
|
show: function (backup) {
|
|
var app = $scope.app;
|
|
|
|
$scope.clone.error = {};
|
|
$scope.clone.backup = backup;
|
|
$scope.clone.domain = $scope.domains.find(function (d) { return app.domain === d.domain; }); // pre-select the app's domain
|
|
|
|
$scope.clone.needsOverwrite = false;
|
|
$scope.clone.overwriteDns = false;
|
|
|
|
$scope.clone.secondaryDomains = {};
|
|
|
|
var httpPorts = backup.manifest.httpPorts || {};
|
|
for (var env2 in httpPorts) {
|
|
$scope.clone.secondaryDomains[env2] = {
|
|
subdomain: httpPorts[env2].defaultValue || '',
|
|
domain: $scope.clone.domain
|
|
};
|
|
}
|
|
|
|
$scope.clone.portBindingsInfo = angular.extend({}, backup.manifest.tcpPorts, backup.manifest.udpPorts); // Portbinding map only for information
|
|
// set default ports
|
|
for (var env in $scope.clone.portBindingsInfo) {
|
|
$scope.clone.portBindings[env] = $scope.clone.portBindingsInfo[env].defaultValue || 0;
|
|
$scope.clone.portBindingsEnabled[env] = true;
|
|
}
|
|
|
|
$('#appCloneModal').modal('show');
|
|
},
|
|
|
|
submit: function () {
|
|
$scope.clone.busy = true;
|
|
|
|
var secondaryDomains = {};
|
|
for (var env2 in $scope.clone.secondaryDomains) {
|
|
secondaryDomains[env2] = {
|
|
subdomain: $scope.clone.secondaryDomains[env2].subdomain,
|
|
domain: $scope.clone.secondaryDomains[env2].domain.domain
|
|
};
|
|
}
|
|
|
|
// only use enabled ports from portBindings
|
|
var finalPortBindings = {};
|
|
for (var env in $scope.clone.portBindings) {
|
|
if ($scope.clone.portBindingsEnabled[env]) {
|
|
finalPortBindings[env] = $scope.clone.portBindings[env];
|
|
}
|
|
}
|
|
|
|
var data = {
|
|
subdomain: $scope.clone.subdomain,
|
|
domain: $scope.clone.domain.domain,
|
|
secondaryDomains: secondaryDomains,
|
|
portBindings: finalPortBindings,
|
|
backupId: $scope.clone.backup.id,
|
|
overwriteDns: $scope.clone.overwriteDns
|
|
};
|
|
|
|
var allDomains = [{ domain: data.domain, subdomain: data.subdomain }].concat(Object.keys(secondaryDomains).map(function (k) {
|
|
return {
|
|
domain: secondaryDomains[k].domain,
|
|
subdomain: secondaryDomains[k].subdomain
|
|
};
|
|
}));
|
|
async.eachSeries(allDomains, function (domain, callback) {
|
|
if ($scope.clone.overwriteDns) return callback();
|
|
|
|
Client.checkDNSRecords(domain.domain, domain.subdomain, function (error, result) {
|
|
if (error) return callback(error);
|
|
|
|
var fqdn = domain.subdomain + '.' + domain.domain;
|
|
|
|
if (result.error) {
|
|
if (result.error.reason === ERROR.ACCESS_DENIED) return callback({ type: 'provider', fqdn: fqdn, message: 'DNS credentials for ' + domain.domain + ' are invalid. Update it in Domains & Certs view' });
|
|
return callback({ type: 'provider', fqdn: fqdn, message: result.error.message });
|
|
}
|
|
if (result.needsOverwrite) {
|
|
$scope.clone.needsOverwrite = true;
|
|
$scope.clone.overwriteDns = true;
|
|
return callback({ type: 'externally_exists', fqdn: fqdn, message: 'DNS Record already exists. Confirm that the domain is not in use for services external to Cloudron' });
|
|
}
|
|
|
|
callback();
|
|
});
|
|
}, function (error) {
|
|
if (error) {
|
|
if (error.type) {
|
|
$scope.clone.error.location = error;
|
|
$scope.clone.busy = false;
|
|
} else {
|
|
Client.error(error);
|
|
}
|
|
|
|
$scope.clone.error.location = error;
|
|
$scope.clone.busy = false;
|
|
return;
|
|
}
|
|
|
|
Client.cloneApp($scope.app.id, data, function (error/*, clonedApp */) {
|
|
$scope.clone.busy = false;
|
|
|
|
if (error) {
|
|
var errorMessage = error.message.toLowerCase();
|
|
if (errorMessage.indexOf('port') !== -1) {
|
|
$scope.clone.error.port = error.message;
|
|
} else if (error.message.indexOf('location') !== -1 || error.message.indexOf('subdomain') !== -1) {
|
|
// TODO extract fqdn from error message, currently we just set it always to the main location
|
|
$scope.clone.error.location = { type: 'internally_exists', fqdn: data.subdomain + '.' + data.domain, message: error.message };
|
|
$('#cloneLocationInput').focus();
|
|
} else {
|
|
Client.error(error);
|
|
}
|
|
return;
|
|
}
|
|
|
|
$('#appCloneModal').modal('hide');
|
|
|
|
$location.path('/apps');
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
$scope.repair = {
|
|
retryBusy: false,
|
|
error: {},
|
|
|
|
subdomain: null,
|
|
domain: null,
|
|
redirectDomains: [],
|
|
aliasDomains: [],
|
|
backups: [],
|
|
|
|
backupId: '',
|
|
|
|
show: function () {},
|
|
|
|
// this prepares the repair dialog with whatever is required for repair action
|
|
confirm: function () {
|
|
$scope.repair.error = {};
|
|
$scope.repair.retryBusy = false;
|
|
$scope.repair.subdomain = null;
|
|
$scope.repair.domain = null;
|
|
$scope.repair.redirectDomains = [];
|
|
$scope.repair.aliasDomains = [];
|
|
$scope.repair.backupId = '';
|
|
|
|
var app = $scope.app;
|
|
|
|
var errorState = ($scope.app.error && $scope.app.error.installationState) || ISTATES.PENDING_CONFIGURE;
|
|
|
|
if (errorState === ISTATES.PENDING_LOCATION_CHANGE) {
|
|
$scope.repair.subdomain = app.subdomain;
|
|
$scope.repair.domain = $scope.domains.filter(function (d) { return d.domain === app.domain; })[0];
|
|
|
|
$scope.repair.aliasDomains = $scope.app.aliasDomains;
|
|
$scope.repair.aliasDomains = $scope.app.aliasDomains.map(function (aliasDomain) {
|
|
return {
|
|
subdomain: aliasDomain.subdomain,
|
|
enabled: true,
|
|
domain: $scope.domains.filter(function (d) { return d.domain === aliasDomain.domain; })[0]
|
|
};
|
|
});
|
|
|
|
$scope.repair.redirectDomains = $scope.app.redirectDomains;
|
|
$scope.repair.redirectDomains = $scope.app.redirectDomains.map(function (altDomain) {
|
|
return {
|
|
subdomain: altDomain.subdomain,
|
|
enabled: true,
|
|
domain: $scope.domains.filter(function (d) { return d.domain === altDomain.domain; })[0]
|
|
};
|
|
});
|
|
}
|
|
|
|
if (errorState === ISTATES.PENDING_RESTORE || errorState === ISTATES.PENDING_IMPORT) {
|
|
Client.getAppBackups($scope.app.id, function (error, backups) {
|
|
if (error) return Client.error(error);
|
|
|
|
$scope.repair.backups = backups;
|
|
$scope.repair.backupId = '';
|
|
|
|
$('#repairModal').modal('show');
|
|
});
|
|
return;
|
|
}
|
|
|
|
$('#repairModal').modal('show');
|
|
},
|
|
|
|
submit: function () {
|
|
$scope.repair.error = {};
|
|
$scope.repair.retryBusy = true;
|
|
|
|
var errorState = ($scope.app.error && $scope.app.error.installationState) || ISTATES.PENDING_CONFIGURE;
|
|
var data = {};
|
|
var repairFunc;
|
|
|
|
switch (errorState) {
|
|
case ISTATES.PENDING_INSTALL:
|
|
case ISTATES.PENDING_CLONE: // if manifest or bad image, use CLI to provide new manifest
|
|
repairFunc = Client.repairApp.bind(null, $scope.app.id, {}); // this will trigger a re-install
|
|
break;
|
|
|
|
case ISTATES.PENDING_LOCATION_CHANGE:
|
|
data.subdomain = $scope.repair.subdomain;
|
|
data.domain = $scope.repair.domain.domain;
|
|
data.aliasDomains = $scope.repair.aliasDomains.filter(function (a) { return a.enabled; })
|
|
.map(function (d) { return { subdomain: d.subdomain, domain: d.domain.domain }; });
|
|
data.redirectDomains = $scope.repair.redirectDomains.filter(function (a) { return a.enabled; })
|
|
.map(function (d) { return { subdomain: d.subdomain, domain: d.domain.domain }; });
|
|
data.overwriteDns = true; // always overwriteDns. user can anyway check and uncheck above
|
|
repairFunc = Client.configureApp.bind(null, $scope.app.id, 'location', data);
|
|
break;
|
|
|
|
case ISTATES.PENDING_DATA_DIR_MIGRATION:
|
|
repairFunc = Client.configureApp.bind(null, $scope.app.id, 'storage', { storageVolumeId: null, storageVolumePrefix: null });
|
|
break;
|
|
|
|
// this also happens for import faliures. this UI can only show backup listing. use CLI for arbit id/config
|
|
case ISTATES.PENDING_RESTORE:
|
|
case ISTATES.PENDING_IMPORT:
|
|
if ($scope.repair.backups.length === 0) { // this can happen when you give some invalid backup via CLI and restore via UI
|
|
repairFunc = Client.repairApp.bind(null, $scope.app.id, {}); // this will trigger a re-install
|
|
} else {
|
|
repairFunc = Client.restoreApp.bind(null, $scope.app.id, $scope.repair.backupId);
|
|
}
|
|
break;
|
|
|
|
case ISTATES.PENDING_UNINSTALL:
|
|
repairFunc = Client.uninstallApp.bind(null, $scope.app.id);
|
|
break;
|
|
|
|
case ISTATES.PENDING_START:
|
|
case ISTATES.PENDING_STOP:
|
|
case ISTATES.PENDING_RESTART:
|
|
case ISTATES.PENDING_RESIZE:
|
|
case ISTATES.PENDING_DEBUG:
|
|
case ISTATES.PENDING_RECREATE_CONTAINER:
|
|
case ISTATES.PENDING_CONFIGURE:
|
|
case ISTATES.PENDING_BACKUP: // can happen if the backup task was killed/rebooted
|
|
case ISTATES.PENDING_UPDATE: // when update failed, just bring it back to current state and user can click update again
|
|
default:
|
|
repairFunc = Client.repairApp.bind(null, $scope.app.id, {});
|
|
break;
|
|
}
|
|
|
|
repairFunc(function (error) {
|
|
$scope.repair.retryBusy = false;
|
|
if (error) return Client.error(error);
|
|
|
|
$scope.repair.retryBusy = false;
|
|
$('#repairModal').modal('hide');
|
|
});
|
|
},
|
|
|
|
restartBusy: false,
|
|
restartApp: function () {
|
|
$scope.repair.restartBusy = true;
|
|
|
|
Client.restartApp($scope.app.id, function (error) {
|
|
if (error) return console.error(error);
|
|
|
|
refreshApp($scope.app.id, function (error) {
|
|
if (error) return Client.error(error);
|
|
|
|
$timeout(function () { $scope.repair.restartBusy = false; }, 1000);
|
|
});
|
|
});
|
|
},
|
|
|
|
pauseBusy: false,
|
|
|
|
pauseAppBegin: function () {
|
|
$scope.repair.pauseBusy = true;
|
|
|
|
Client.debugApp($scope.app.id, true, function (error) {
|
|
if (error) return console.error(error);
|
|
|
|
refreshApp($scope.app.id, function (error) {
|
|
if (error) return Client.error(error);
|
|
|
|
$timeout(function () { $scope.repair.pauseBusy = false; }, 1000);
|
|
});
|
|
});
|
|
},
|
|
|
|
pauseAppDone: function () {
|
|
$scope.repair.pauseBusy = true;
|
|
|
|
Client.debugApp($scope.app.id, false, function (error) {
|
|
if (error) return console.error(error);
|
|
|
|
refreshApp($scope.app.id, function (error) {
|
|
if (error) return Client.error(error);
|
|
|
|
$timeout(function () { $scope.repair.pauseBusy = false; }, 1000);
|
|
});
|
|
});
|
|
}
|
|
|
|
};
|
|
|
|
function fetchUsers(callback) {
|
|
Client.getAllUsers(function (error, users) {
|
|
if (error) return callback(error);
|
|
|
|
$scope.users = users;
|
|
|
|
callback();
|
|
});
|
|
}
|
|
|
|
function fetchGroups(callback) {
|
|
Client.getGroups(function (error, groups) {
|
|
if (error) return callback(error);
|
|
|
|
$scope.groups = groups;
|
|
|
|
callback();
|
|
});
|
|
}
|
|
|
|
function fetchDiskUsage(callback) {
|
|
$scope.diskUsage = -1;
|
|
$scope.diskUsageDate = 0;
|
|
|
|
Client.diskUsage(function (error, result) {
|
|
if (error) return callback(error);
|
|
if (!result.usage) return callback(); // no usage date yet
|
|
|
|
$scope.diskUsageDate = result.usage.ts;
|
|
|
|
for (var diskName in result.usage.disks) {
|
|
var disk = result.usage.disks[diskName];
|
|
var content = disk.contents.find(function (c) { return c.id === appId; });
|
|
|
|
if (content) {
|
|
$scope.diskUsage = content.usage;
|
|
break;
|
|
}
|
|
}
|
|
|
|
callback();
|
|
});
|
|
}
|
|
|
|
function getDomains(callback) {
|
|
Client.getDomains(function (error, result) {
|
|
if (error) return callback(error);
|
|
|
|
$scope.domains = result;
|
|
|
|
callback();
|
|
});
|
|
}
|
|
|
|
function getVolumes(callback) {
|
|
Client.getVolumes(function (error, result) {
|
|
if (error) return callback(error);
|
|
|
|
$scope.volumes = result;
|
|
|
|
callback();
|
|
});
|
|
}
|
|
|
|
function getBackupConfig(callback) {
|
|
Client.getBackupConfig(function (error, backupConfig) {
|
|
if (error) return callback(error);
|
|
|
|
$scope.backupConfig = backupConfig;
|
|
|
|
callback();
|
|
});
|
|
}
|
|
|
|
function refreshApp(appId, callback) {
|
|
callback = callback || function () {};
|
|
|
|
Client.getAppWithTask(appId, function (error, app) {
|
|
if (error && error.statusCode === 404) return $location.path('/apps');
|
|
if (error) return callback(error);
|
|
|
|
$scope.app = app;
|
|
|
|
// show 'Start App' if app is starting or is stopped
|
|
if (app.installationState === ISTATES.PENDING_START || app.installationState === ISTATES.PENDING_STOP) {
|
|
$scope.uninstall.startButton = app.installationState === ISTATES.PENDING_START;
|
|
} else {
|
|
$scope.uninstall.startButton = app.runState === RSTATES.STOPPED;
|
|
}
|
|
|
|
callback();
|
|
});
|
|
}
|
|
|
|
function waitForAppTask(callback) {
|
|
callback = callback || function () {};
|
|
|
|
if (!$scope.app.taskId) return callback();
|
|
|
|
// app will be refreshed on interval
|
|
$timeout(waitForAppTask.bind(null, callback), 2000); // not yet done
|
|
}
|
|
|
|
// https://stackoverflow.com/questions/3665115/how-to-create-a-file-in-memory-for-user-to-download-but-not-through-server#18197341
|
|
function download(filename, text) {
|
|
var element = document.createElement('a');
|
|
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
|
|
element.setAttribute('download', filename);
|
|
|
|
element.style.display = 'none';
|
|
document.body.appendChild(element);
|
|
|
|
element.click();
|
|
|
|
document.body.removeChild(element);
|
|
}
|
|
|
|
$scope.downloadConfig = function (backup) {
|
|
// secrets and tokens already come with placeholder characters we remove them
|
|
var tmp = {
|
|
remotePath: backup.remotePath,
|
|
encrypted: !!$scope.backupConfig.password // we add this just to help the import UI
|
|
};
|
|
|
|
Object.keys($scope.backupConfig).forEach(function (k) {
|
|
var v = $scope.backupConfig[k];
|
|
if (v && typeof v === 'object') { // to hide mountOptions.password and the likes
|
|
tmp[k] = {};
|
|
Object.keys(v).forEach(function (j) {
|
|
if (v[j] !== SECRET_PLACEHOLDER) tmp[k][j] = v[j];
|
|
});
|
|
} else {
|
|
if ($scope.backupConfig[k] !== SECRET_PLACEHOLDER) tmp[k] = v;
|
|
}
|
|
});
|
|
|
|
var filename = 'app-backup-config-' + (new Date()).toISOString().replace(/:|T/g,'-').replace(/\..*/,'') + ' (' + $scope.app.fqdn + ')' + '.json';
|
|
download(filename, JSON.stringify(tmp, null, 4));
|
|
};
|
|
|
|
document.getElementById('backupConfigFileInput').onchange = function (event) {
|
|
var reader = new FileReader();
|
|
reader.onload = function (result) {
|
|
if (!result.target || !result.target.result) return console.error('Unable to read backup config');
|
|
|
|
var backupConfig;
|
|
try {
|
|
backupConfig = JSON.parse(result.target.result);
|
|
let prefix = backupConfig.prefix;
|
|
backupConfig.prefix = ''; // so it can clear the form as well when we apply keys below
|
|
if (backupConfig.provider === 'filesystem') { // patch the remotePath to have the full path
|
|
backupConfig.remotePath = (prefix ? prefix + '/' : '') + backupConfig.backupFolder + '/' + backupConfig.remotePath;
|
|
delete backupConfig.backupFolder;
|
|
} else {
|
|
backupConfig.remotePath = (prefix ? prefix + '/' : '') + backupConfig.remotePath;
|
|
}
|
|
} catch (e) {
|
|
console.error('Unable to parse backup config');
|
|
return;
|
|
}
|
|
|
|
$scope.$apply(function () {
|
|
// we assume property names match here, this does not yet work for gcs keys
|
|
Object.keys(backupConfig).forEach(function (k) {
|
|
if (k in $scope.importBackup) {
|
|
$scope.importBackup[k] = backupConfig[k];
|
|
}
|
|
});
|
|
});
|
|
};
|
|
reader.readAsText(event.target.files[0]);
|
|
};
|
|
|
|
Client.onReady(function () {
|
|
refreshApp(appId, function (error) {
|
|
if (error) return Client.error(error);
|
|
|
|
if ($scope.app.accessLevel !== 'admin' && $scope.app.accessLevel !== 'operator') return $location.path('/');
|
|
|
|
// skipViewShow because we don't have all the values like domains/users to init the view yet
|
|
if ($routeParams.view) { // explicit route in url bar
|
|
$scope.setView($routeParams.view, true /* skipViewShow */);
|
|
} else { // default
|
|
$scope.setView($scope.app.error ? 'repair' : 'display', true /* skipViewShow */);
|
|
}
|
|
|
|
function done() {
|
|
$scope[$scope.view].show(); // initialize now that we have all the values
|
|
|
|
var refreshTimer = $interval(function () { refreshApp($scope.app.id); }, 5000); // call with inline function to avoid iteration argument passed see $interval docs
|
|
$scope.$on('$destroy', function () {
|
|
$interval.cancel(refreshTimer);
|
|
});
|
|
}
|
|
|
|
if ($scope.app.accessLevel !== 'admin') return done();
|
|
|
|
async.series([
|
|
fetchUsers,
|
|
fetchGroups,
|
|
fetchDiskUsage,
|
|
getDomains,
|
|
getVolumes,
|
|
getBackupConfig
|
|
], function (error) {
|
|
if (error) return Client.error(error);
|
|
|
|
// check for updates, if the app has a pending update. this handles two cases:
|
|
// 1. user got a valid subscription. this will make the updates get the manifest field
|
|
// 2. user has not refreshed the ui in a while or updated via cli tool. this will ensure we are not holding to a dangling update
|
|
if ($scope.config.update[$scope.app.id]) Client.checkForUpdates();
|
|
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
$('#iconFileInput').get(0).onchange = function (event) {
|
|
var fr = new FileReader();
|
|
fr.onload = function () {
|
|
$scope.$apply(function () {
|
|
// var file = event.target.files[0];
|
|
$scope.display.icon.data = fr.result;
|
|
});
|
|
};
|
|
fr.readAsDataURL(event.target.files[0]);
|
|
};
|
|
|
|
// setup all the dialog focus handling
|
|
['appUninstallModal', 'appUpdateModal', 'appRestoreModal', 'appCloneModal', 'editBackupModal'].forEach(function (id) {
|
|
$('#' + id).on('shown.bs.modal', function () {
|
|
$(this).find('[autofocus]:first').focus();
|
|
});
|
|
});
|
|
|
|
var clipboard = new Clipboard('.clipboard');
|
|
clipboard.on('success', function () {
|
|
$scope.$apply(function () { $scope.copyBackupIdDone = true; });
|
|
$timeout(function () { $scope.copyBackupIdDone = false; }, 5000);
|
|
});
|
|
|
|
$('.modal-backdrop').remove();
|
|
}]);
|