filemanager: add restart logic
This commit is contained in:
104
filemanager/src/constants.js
Normal file
104
filemanager/src/constants.js
Normal 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
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user