filemanager: add restart logic

This commit is contained in:
Johannes Zellner
2023-05-23 11:38:57 +02:00
parent 2011dd9a83
commit c4d267ecb1
5 changed files with 147 additions and 203 deletions

View File

@@ -0,0 +1,104 @@
// keep in sync with box/src/apps.js
const ISTATES = {
PENDING_INSTALL: 'pending_install',
PENDING_CLONE: 'pending_clone',
PENDING_CONFIGURE: 'pending_configure',
PENDING_UNINSTALL: 'pending_uninstall',
PENDING_RESTORE: 'pending_restore',
PENDING_IMPORT: 'pending_import',
PENDING_UPDATE: 'pending_update',
PENDING_BACKUP: 'pending_backup',
PENDING_RECREATE_CONTAINER: 'pending_recreate_container', // env change or addon change
PENDING_LOCATION_CHANGE: 'pending_location_change',
PENDING_DATA_DIR_MIGRATION: 'pending_data_dir_migration',
PENDING_RESIZE: 'pending_resize',
PENDING_DEBUG: 'pending_debug',
PENDING_START: 'pending_start',
PENDING_STOP: 'pending_stop',
PENDING_RESTART: 'pending_restart',
ERROR: 'error',
INSTALLED: 'installed'
};
const HSTATES = {
HEALTHY: 'healthy',
UNHEALTHY: 'unhealthy',
ERROR: 'error',
DEAD: 'dead'
};
const RSTATES ={
RUNNING: 'running',
STOPPED: 'stopped'
};
const ERROR = {
ACCESS_DENIED: 'Access Denied',
ALREADY_EXISTS: 'Already Exists',
BAD_FIELD: 'Bad Field',
COLLECTD_ERROR: 'Collectd Error',
CONFLICT: 'Conflict',
DATABASE_ERROR: 'Database Error',
DNS_ERROR: 'DNS Error',
DOCKER_ERROR: 'Docker Error',
EXTERNAL_ERROR: 'External Error',
FS_ERROR: 'FileSystem Error',
INTERNAL_ERROR: 'Internal Error',
LOGROTATE_ERROR: 'Logrotate Error',
NETWORK_ERROR: 'Network Error',
NOT_FOUND: 'Not found',
REVERSEPROXY_ERROR: 'ReverseProxy Error',
TASK_ERROR: 'Task Error',
UNKNOWN_ERROR: 'Unknown Error' // only used for portin,
};
const ROLES = {
OWNER: 'owner',
ADMIN: 'admin',
MAIL_MANAGER: 'mailmanager',
USER_MANAGER: 'usermanager',
USER: 'user'
};
// sync up with tasks.js
const TASK_TYPES = {
TASK_APP: 'app',
TASK_BACKUP: 'backup',
TASK_UPDATE: 'update',
TASK_CHECK_CERTS: 'checkCerts',
TASK_SETUP_DNS_AND_CERT: 'setupDnsAndCert',
TASK_CLEAN_BACKUPS: 'cleanBackups',
TASK_SYNC_EXTERNAL_LDAP: 'syncExternalLdap',
TASK_CHANGE_MAIL_LOCATION: 'changeMailLocation',
TASK_SYNC_DNS_RECORDS: 'syncDnsRecords',
TASK_UPDATE_DISK_USAGE: 'updateDiskUsage',
};
const APP_TYPES = {
APP: 'app', //default
LINK: 'link',
PROXIED: 'proxied'
};
// named exports
export {
APP_TYPES,
ERROR,
HSTATES,
ISTATES,
RSTATES,
ROLES,
TASK_TYPES
};
// default export
export default {
APP_TYPES,
ERROR,
HSTATES,
ISTATES,
RSTATES,
ROLES,
TASK_TYPES
};

View File

@@ -1,196 +0,0 @@
import { filesize } from 'filesize';
function prettyDate(value) {
var date = new Date(value),
diff = (((new Date()).getTime() - date.getTime()) / 1000),
day_diff = Math.floor(diff / 86400);
if (isNaN(day_diff) || day_diff < 0)
return;
return day_diff === 0 && (
diff < 60 && 'just now' ||
diff < 120 && '1 min ago' ||
diff < 3600 && Math.floor( diff / 60 ) + ' min ago' ||
diff < 7200 && '1 hour ago' ||
diff < 86400 && Math.floor( diff / 3600 ) + ' hours ago') ||
day_diff === 1 && 'Yesterday' ||
day_diff < 7 && day_diff + ' days ago' ||
day_diff < 31 && Math.ceil( day_diff / 7 ) + ' weeks ago' ||
day_diff < 365 && Math.round( day_diff / 30 ) + ' months ago' ||
Math.round( day_diff / 365 ) + ' years ago';
}
function prettyLongDate(value) {
if (!value) return 'unkown';
var date = new Date(value);
return `${date.getDate()}.${date.getMonth() + 1}.${date.getFullYear()} ${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`;
}
function prettyFileSize(value) {
if (typeof value !== 'number') return 'unkown';
return filesize(value);
}
function sanitize(path) {
path = '/' + path;
return path.replace(/\/+/g, '/');
}
function encode(path) {
return path.split('/').map(encodeURIComponent).join('/');
}
function decode(path) {
return path.split('/').map(decodeURIComponent).join('/');
}
// TODO create share links instead of using access token
function getDirectLink(entry) {
if (entry.share) {
let link = window.location.origin + '/api/v1/shares/' + entry.share.id + '?type=raw&path=' + encodeURIComponent(entry.filePath);
return link;
} else {
return window.location.origin + '/api/v1/files?type=raw&path=' + encodeURIComponent(entry.filePath);
}
}
// TODO the url might actually return a 412 in which case we have to keep reloading
function getPreviewUrl(entry) {
if (!entry.previewUrl) return '';
return entry.previewUrl;
}
function getShareLink(shareId) {
return window.location.origin + '/api/v1/shares/' + shareId + '?type=raw';
}
function download(entries, name) {
if (!entries.length) return;
if (entries.length === 1) {
if (entries[0].share) window.location.href = '/api/v1/shares/' + entries[0].share.id + '?type=download&path=' + encodeURIComponent(entries[0].filePath);
else window.location.href = '/api/v1/files?type=download&path=' + encodeURIComponent(entries[0].filePath);
return;
}
const params = new URLSearchParams();
// be a bit smart about the archive name and folder tree
const folderPath = entries[0].filePath.slice(0, -entries[0].fileName.length);
const archiveName = name || folderPath.slice(folderPath.slice(0, -1).lastIndexOf('/')+1).slice(0, -1);
params.append('name', archiveName);
params.append('skipPath', folderPath);
params.append('entries', JSON.stringify(entries.map(function (entry) {
return {
filePath: entry.filePath,
shareId: entry.share ? entry.share.id : undefined
};
})));
window.location.href = '/api/v1/download?' + params.toString();
}
function getFileTypeGroup(entry) {
return entry.mimeType.split('/')[0];
}
// simple extension detection, does not work with double extension like .tar.gz
function getExtension(entry) {
if (entry.isFile) return entry.fileName.slice(entry.fileName.lastIndexOf('.') + 1);
return '';
}
function copyToClipboard(value) {
var elem = document.createElement('input');
elem.value = value;
document.body.append(elem);
elem.select();
document.execCommand('copy');
elem.remove();
}
function clearSelection() {
if(document.selection && document.selection.empty) {
document.selection.empty();
} else if(window.getSelection) {
var sel = window.getSelection();
sel.removeAllRanges();
}
}
function urlSearchQuery() {
return decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
}
// those paths contain the internal type and path reference eg. shares/:shareId/folder/filename or files/folder/filename
function parseResourcePath(resourcePath) {
var result = {
type: '',
path: '',
shareId: '',
apiPath: '',
resourcePath: ''
};
if (resourcePath.indexOf('files/') === 0) {
result.type = 'files';
result.path = resourcePath.slice('files'.length) || '/';
result.apiPath = '/api/v1/files';
result.resourcePath = result.type + result.path;
} else if (resourcePath.indexOf('shares/') === 0) {
result.type = 'shares';
result.shareId = resourcePath.split('/')[1];
result.path = resourcePath.slice((result.type + '/' + result.shareId).length) || '/';
result.apiPath = '/api/v1/shares/' + result.shareId;
// without shareId we show the root (share listing)
result.resourcePath = result.type + '/' + (result.shareId ? (result.shareId + result.path) : '');
} else {
console.error('Unknown resource path', resourcePath);
}
return result;
}
function getEntryIdentifier(entry) {
return (entry.share ? (entry.share.id + '/') : '') + entry.filePath;
}
function entryListSort(list, prop, desc) {
var tmp = list.sort(function (a, b) {
var av = a[prop];
var bv = b[prop];
if (typeof av === 'string') return (av.toUpperCase() < bv.toUpperCase()) ? -1 : 1;
else return (av < bv) ? -1 : 1;
});
if (desc) return tmp;
return tmp.reverse();
}
export {
getDirectLink,
getPreviewUrl,
getShareLink,
getFileTypeGroup,
prettyDate,
prettyLongDate,
prettyFileSize,
sanitize,
encode,
decode,
download,
getExtension,
copyToClipboard,
clearSelection,
urlSearchQuery,
parseResourcePath,
getEntryIdentifier,
entryListSort
};

View File

@@ -42,7 +42,8 @@
<Button type="button" label="Upload" icon="pi pi-upload" @click="onUploadMenu" aria-haspopup="true" aria-controls="upload_menu" style="margin-right: 5px" />
<Menu ref="uploadMenu" id="upload_menu" :model="uploadMenuModel" :popup="true" />
<a class="p-button p-button-secondary" style="margin-left: 20px; margin-right: 5px;" :href="'/logs.html?appId=' + resourceId" target="_blank" v-show="resourceType === 'app'"><span class="p-button-icon p-button-icon-left pi pi-align-left"></span> Logs</a>
<a class="p-button p-button-secondary" :href="'/terminal.html?id=' + resourceId" target="_blank" v-show="resourceType === 'app'"><span class="p-button-icon p-button-icon-left pi pi-desktop"></span> Terminal</a>
<a class="p-button p-button-secondary" style="margin-right: 5px;" :href="'/terminal.html?id=' + resourceId" target="_blank" v-show="resourceType === 'app'"><span class="p-button-icon p-button-icon-left pi pi-desktop"></span> Terminal</a>
<Button type="button" label="Restart" severity="secondary" icon="pi pi-sync" @click="onRestartApp" :loading="busyRestart" v-show="resourceType === 'app'"/>
</template>
</TopBar>
</template>
@@ -100,7 +101,9 @@ import Menu from 'primevue/menu';
import { useConfirm } from 'primevue/useconfirm';
import { DirectoryView, TopBar, PathBreadcrumbs, BottomBar, MainLayout, FileUploader } from 'pankow';
import { sanitize, buildFilePath } from 'pankow/utils';
import { sanitize, buildFilePath, sleep } from 'pankow/utils';
import { ISTATES } from '../constants.js';
import PreviewPanel from '../components/PreviewPanel.vue';
import { createDirectoryModel } from '../models/DirectoryModel.js';
@@ -128,6 +131,7 @@ export default {
fallbackIcon: '/mime-types/none.svg',
cwd: '/',
busyRefresh: false,
busyRestart: false,
fatalError: false,
activeItem: null,
activeDirectoryItem: {},
@@ -360,6 +364,38 @@ export default {
mimeType: 'inode/directory',
icon: `${BASE_URL}mime-types/inode-directory.svg`
};
},
async onRestartApp() {
if (this.resourceType !== 'app') return;
this.busyRestart = true;
let error, result;
try {
result = await superagent.post(`${this.apiOrigin}/api/v1/apps/${this.resourceId}/restart`).query({ access_token: this.accessToken });
} catch (e) {
error = e;
}
if (error || result.statusCode !== 202) {
console.error(`Failed to restart app ${this.resourceId}`, error || result.statusCode);
return;
}
while(true) {
let error, result;
try {
result = await superagent.get(`${this.apiOrigin}/api/v1/apps/${this.resourceId}`).query({ access_token: this.accessToken });
} catch (e) {
error = e;
}
if (result && result.statusCode === 200 && result.body.installationState === ISTATES.INSTALLED) break;
await sleep(2000);
}
this.busyRestart = false;
}
},
async mounted() {