Port filemanager to composition style api and sync filemanger/terminal/logs toolbar layout
This commit is contained in:
@@ -1,478 +1,480 @@
|
||||
<script>
|
||||
<script setup>
|
||||
|
||||
import { useI18n } from 'vue-i18n';
|
||||
const i18n = useI18n();
|
||||
const t = i18n.t;
|
||||
|
||||
import { ref, onMounted, computed, useTemplateRef } from 'vue';
|
||||
import { useRouter, useRoute, onBeforeRouteUpdate } from 'vue-router';
|
||||
import { marked } from 'marked';
|
||||
import { fetcher, Dialog, DirectoryView, TopBar, Breadcrumb, BottomBar, Button, InputDialog, MainLayout, ButtonGroup, Notification, FileUploader, Spinner } from 'pankow';
|
||||
import { sanitize, sleep } from 'pankow/utils';
|
||||
import { API_ORIGIN, ISTATES } from '../constants.js';
|
||||
import { API_ORIGIN, BASE_URL, ISTATES } from '../constants.js';
|
||||
import PreviewPanel from '../components/PreviewPanel.vue';
|
||||
import { createDirectoryModel } from '../models/DirectoryModel.js';
|
||||
|
||||
const BASE_URL = import.meta.env.BASE_URL || '/';
|
||||
|
||||
const beforeUnloadListener = (event) => {
|
||||
event.preventDefault();
|
||||
return window.confirm('File operation still in progress. Really close?');
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'FolderView',
|
||||
components: {
|
||||
BottomBar,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Dialog,
|
||||
DirectoryView,
|
||||
FileUploader,
|
||||
InputDialog,
|
||||
MainLayout,
|
||||
Notification,
|
||||
Breadcrumb,
|
||||
PreviewPanel,
|
||||
Spinner,
|
||||
TopBar
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
busy: true,
|
||||
fallbackIcon: `${BASE_URL}mime-types/none.svg`,
|
||||
cwd: '/',
|
||||
busyRefresh: false,
|
||||
busyRestart: false,
|
||||
fatalError: false,
|
||||
footerContent: '',
|
||||
activeItem: null,
|
||||
activeDirectoryItem: {},
|
||||
items: [],
|
||||
selectedItems: [],
|
||||
accessToken: localStorage.token,
|
||||
title: 'Cloudron',
|
||||
appLink: '',
|
||||
resourceType: '',
|
||||
resourceId: '',
|
||||
visible: true,
|
||||
uploadRequest: null,
|
||||
breadcrumbHomeItem: {
|
||||
label: '/app/data/',
|
||||
action: () => {
|
||||
this.cwd = '/';
|
||||
}
|
||||
},
|
||||
ownersModel: [],
|
||||
// contextMenuModel will have activeItem attached if any command() is called
|
||||
createMenuModel: [{
|
||||
label: this.$t('filemanager.toolbar.newFile'),
|
||||
icon: 'fa-solid fa-file-circle-plus',
|
||||
action: this.onNewFile
|
||||
}, {
|
||||
label: this.$t('filemanager.toolbar.newFolder'),
|
||||
icon: 'fa-solid fa-folder-plus',
|
||||
action: this.onNewFolder
|
||||
}],
|
||||
uploadMenuModel: [{
|
||||
label: this.$t('filemanager.toolbar.uploadFile'),
|
||||
icon: 'fa-solid fa-file-arrow-up',
|
||||
action: this.onUploadFile
|
||||
}, {
|
||||
label: this.$t('filemanager.toolbar.newFolder'),
|
||||
icon: 'fa-regular fa-folder-open',
|
||||
action: this.onUploadFolder
|
||||
}]
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
breadcrumbItems() {
|
||||
const parts = this.cwd.split('/').filter((p) => !!p.trim())
|
||||
const crumbs = [];
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
parts.forEach((p, i) => {
|
||||
crumbs.push({
|
||||
label: p,
|
||||
action: () => {
|
||||
this.cwd = '/' + parts.slice(0, i+1).join('/');
|
||||
}
|
||||
});
|
||||
});
|
||||
let directoryModel;
|
||||
const accessToken = localStorage.token;
|
||||
|
||||
return crumbs;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
cwd(newCwd, oldCwd) {
|
||||
if (this.resourceType && this.resourceId) this.$router.push(`/home/${this.resourceType}/${this.resourceId}${this.cwd}`);
|
||||
this.loadCwd();
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
this.busy = true;
|
||||
const type = this.$route.params.type || 'app';
|
||||
const resourceId = this.$route.params.resourceId;
|
||||
const cwd = this.$route.params.cwd;
|
||||
const fatalErrorDialog = useTemplateRef('fatalErrorDialog');
|
||||
const inputDialog = useTemplateRef('inputDialog');
|
||||
const fileUploader = useTemplateRef('fileUploader');
|
||||
const pasteInProgressDialog = useTemplateRef('pasteInProgressDialog');
|
||||
const deleteInProgressDialog = useTemplateRef('deleteInProgressDialog');
|
||||
const extractInProgressDialog = useTemplateRef('extractInProgressDialog');
|
||||
|
||||
if (type === 'app') {
|
||||
let error, result;
|
||||
try {
|
||||
result = await fetcher.get(`${API_ORIGIN}/api/v1/apps/${resourceId}`, { access_token: this.accessToken });
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
const busy = ref(true);
|
||||
const fallbackIcon = ref(`${BASE_URL}mime-types/none.svg`);
|
||||
const cwd = ref('/');
|
||||
const busyRefresh = ref(false);
|
||||
const busyRestart = ref(false);
|
||||
const fatalError = ref(false);
|
||||
const footerContent = ref('');
|
||||
const activeItem = ref(null);
|
||||
const activeDirectoryItem = ref({});
|
||||
const items = ref([]);
|
||||
const selectedItems = ref([]);
|
||||
const title = ref('Cloudron');
|
||||
const appLink = ref('');
|
||||
const resourceType = ref('');
|
||||
const resourceId = ref('');
|
||||
const uploadRequest = ref(null);
|
||||
const ownersModel = ref([]);
|
||||
|
||||
if (error || result.status !== 200) {
|
||||
console.error(`Invalid resource ${type} ${resourceId}`, error || result.status);
|
||||
return this.onFatalError(`Invalid resource ${type} ${resourceId}`);
|
||||
}
|
||||
// contextMenuModel will have activeItem attached if any command() is called
|
||||
const createMenuModel = [{
|
||||
icon: 'fa-solid fa-file-circle-plus',
|
||||
label: t('filemanager.toolbar.newFile'),
|
||||
action: onNewFile,
|
||||
}, {
|
||||
icon: 'fa-solid fa-folder-plus',
|
||||
label: t('filemanager.toolbar.newFolder'),
|
||||
action: onNewFolder,
|
||||
}];
|
||||
|
||||
this.appLink = `https://${result.body.fqdn}`;
|
||||
this.title = `${result.body.label || result.body.fqdn} (${result.body.manifest.title})`;
|
||||
} else if (type === 'volume') {
|
||||
let error, result;
|
||||
try {
|
||||
result = await fetcher.get(`${API_ORIGIN}/api/v1/volumes/${resourceId}`, { access_token: this.accessToken });
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
const uploadMenuModel = [{
|
||||
icon: 'fa-solid fa-file-arrow-up',
|
||||
label: t('filemanager.toolbar.uploadFile'),
|
||||
action: onUploadFile,
|
||||
}, {
|
||||
icon: 'fa-regular fa-folder-open',
|
||||
label: t('filemanager.toolbar.newFolder'),
|
||||
action: onUploadFolder,
|
||||
}];
|
||||
|
||||
if (error || result.status !== 200) {
|
||||
console.error(`Invalid resource ${type} ${resourceId}`, error || result.status);
|
||||
return this.onFatalError(`Invalid resource ${type} ${resourceId}`);
|
||||
}
|
||||
const breadcrumbHomeItem = {
|
||||
label: '/app/data/',
|
||||
action: () => onActivateBreadcrumb('/'),
|
||||
};
|
||||
|
||||
this.title = result.body.name;
|
||||
} else {
|
||||
return this.onFatalError(`Unsupported type ${type}`);
|
||||
}
|
||||
const breadcrumbItems = computed(() => {
|
||||
if (!cwd.value) return [];
|
||||
|
||||
try {
|
||||
const result = await fetcher.get(`${API_ORIGIN}/api/v1/dashboard/config`, { access_token: this.accessToken });
|
||||
this.footerContent = marked.parse(result.body.footer);
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch Cloudron config.', e);
|
||||
}
|
||||
const parts = cwd.value.split('/').filter((p) => !!p.trim());
|
||||
const crumbs = [];
|
||||
|
||||
window.document.title = `File Manager - ${this.title}`;
|
||||
|
||||
this.cwd = sanitize('/' + (cwd ? cwd.join('/') : '/'));
|
||||
this.resourceType = type;
|
||||
this.resourceId = resourceId;
|
||||
|
||||
this.directoryModel = createDirectoryModel(API_ORIGIN, this.accessToken, type === 'volume' ? `volumes/${resourceId}` : `apps/${resourceId}`);
|
||||
this.ownersModel = this.directoryModel.ownersModel;
|
||||
|
||||
this.loadCwd();
|
||||
|
||||
this.$watch(() => this.$route.params, (toParams, previousParams) => {
|
||||
if (toParams.type !== 'app' && toParams.type !== 'volume') return this.onFatalError(`Unknown type ${toParams.type}`);
|
||||
|
||||
if ((toParams.type !== this.resourceType) || (toParams.resourceId !== this.resourceId)) {
|
||||
this.resourceType = toParams.type;
|
||||
this.resourceId = toParams.resourceId;
|
||||
|
||||
this.directoryModel = createDirectoryModel(API_ORIGIN, this.accessToken, toParams.type === 'volume' ? `volumes/${toParams.resourceId}` : `apps/${toParams.resourceId}`);
|
||||
}
|
||||
|
||||
this.cwd = toParams.cwd ? `/${toParams.cwd.join('/')}` : '/';
|
||||
parts.forEach((p, i) => {
|
||||
crumbs.push({
|
||||
label: p,
|
||||
action: () => onActivateBreadcrumb('/' + parts.slice(0, i+1).join('/')),
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
onFatalError(errorMessage) {
|
||||
this.fatalError = errorMessage;
|
||||
this.$refs.fatalErrorDialog.open();
|
||||
},
|
||||
onCancelUpload() {
|
||||
if (!this.uploadRequest || !this.uploadRequest.xhr) return;
|
||||
this.uploadRequest.xhr.abort();
|
||||
},
|
||||
// generic dialog focus handler
|
||||
onDialogShow(focusElementId) {
|
||||
setTimeout(() => document.getElementById(focusElementId).focus(), 0);
|
||||
},
|
||||
async onNewFile() {
|
||||
const newFileName = await this.$refs.inputDialog.prompt({
|
||||
message: this.$t('filemanager.newFileDialog.title'),
|
||||
value: '',
|
||||
confirmStyle: 'success',
|
||||
confirmLabel: this.$t('filemanager.newFileDialog.create'),
|
||||
rejectLabel: this.$t('main.dialog.cancel'),
|
||||
modal: false
|
||||
});
|
||||
|
||||
return crumbs;
|
||||
});
|
||||
|
||||
// watch(() => {
|
||||
// if (resourceType.value && resourceId.value) router.push(`/home/${resourceType.value}/${resourceId.value}${cwd.value}`);
|
||||
// loadCwd();
|
||||
// });
|
||||
|
||||
function onFatalError(errorMessage) {
|
||||
fatalError.value = errorMessage;
|
||||
fatalErrorDialog.value.open();
|
||||
}
|
||||
|
||||
function onCancelUpload() {
|
||||
if (!uploadRequest.value || !uploadRequest.value.xhr) return;
|
||||
uploadRequest.value.xhr.abort();
|
||||
}
|
||||
|
||||
async function onNewFile() {
|
||||
const newFileName = await inputDialog.value.prompt({
|
||||
message: t('filemanager.newFileDialog.title'),
|
||||
value: '',
|
||||
confirmStyle: 'success',
|
||||
confirmLabel: t('filemanager.newFileDialog.create'),
|
||||
rejectLabel: t('main.dialog.cancel'),
|
||||
modal: false
|
||||
});
|
||||
|
||||
if (!newFileName) return;
|
||||
|
||||
await directoryModel.newFile(directoryModel.buildFilePath(cwd.value, newFileName));
|
||||
await loadCwd();
|
||||
}
|
||||
|
||||
async function onNewFolder() {
|
||||
const newFolderName = await inputDialog.value.prompt({
|
||||
message: t('filemanager.newDirectoryDialog.title'),
|
||||
value: '',
|
||||
confirmStyle: 'success',
|
||||
confirmLabel: t('filemanager.newFileDialog.create'),
|
||||
rejectLabel: t('main.dialog.cancel'),
|
||||
modal: false
|
||||
});
|
||||
|
||||
if (!newFolderName) return;
|
||||
|
||||
await directoryModel.newFolder(directoryModel.buildFilePath(cwd.value, newFolderName));
|
||||
await loadCwd();
|
||||
}
|
||||
|
||||
function onUploadFile() {
|
||||
fileUploader.value.onUploadFile(cwd.value);
|
||||
}
|
||||
|
||||
function onUploadFolder() {
|
||||
fileUploader.value.onUploadFolder(cwd.value);
|
||||
}
|
||||
|
||||
async function onUploadFinished() {
|
||||
await loadCwd();
|
||||
}
|
||||
|
||||
function onSelectionChanged(items) {
|
||||
activeItem.value = items[0] || null;
|
||||
selectedItems.value = items;
|
||||
}
|
||||
|
||||
function onActivateBreadcrumb(path) {
|
||||
router.push(`/home/${resourceType.value}/${resourceId.value}${sanitize(path)}`);
|
||||
}
|
||||
|
||||
async function onRefresh() {
|
||||
busyRefresh.value = true;
|
||||
await loadCwd();
|
||||
setTimeout(() => { busyRefresh.value = false; }, 500);
|
||||
}
|
||||
|
||||
// either dataTransfer (external drop) or files (internal drag)
|
||||
async function onDrop(targetFolder, dataTransfer, files) {
|
||||
const fullTargetFolder = sanitize(cwd.value + '/' + targetFolder);
|
||||
|
||||
// if dataTransfer is set, we have a file/folder drop from outside
|
||||
if (dataTransfer) {
|
||||
async function getFile(entry) {
|
||||
return new Promise((resolve, reject) => {
|
||||
entry.file(resolve, reject);
|
||||
});
|
||||
}
|
||||
|
||||
if (!newFileName) return;
|
||||
|
||||
await this.directoryModel.newFile(this.directoryModel.buildFilePath(this.cwd, newFileName));
|
||||
await this.loadCwd();
|
||||
},
|
||||
async onNewFolder() {
|
||||
const newFolderName = await this.$refs.inputDialog.prompt({
|
||||
message: this.$t('filemanager.newDirectoryDialog.title'),
|
||||
value: '',
|
||||
confirmStyle: 'success',
|
||||
confirmLabel: this.$t('filemanager.newFileDialog.create'),
|
||||
rejectLabel: this.$t('main.dialog.cancel'),
|
||||
modal: false
|
||||
async function readEntries(dirReader) {
|
||||
return new Promise((resolve, reject) => {
|
||||
dirReader.readEntries(resolve, reject);
|
||||
});
|
||||
}
|
||||
|
||||
if (!newFolderName) return;
|
||||
const fileList = [];
|
||||
async function traverseFileTree(item) {
|
||||
if (item.isFile) {
|
||||
fileList.push(await getFile(item));
|
||||
} else if (item.isDirectory) {
|
||||
// Get folder contents
|
||||
const dirReader = item.createReader();
|
||||
const entries = await readEntries(dirReader);
|
||||
|
||||
await this.directoryModel.newFolder(this.directoryModel.buildFilePath(this.cwd, newFolderName));
|
||||
await this.loadCwd();
|
||||
},
|
||||
onUploadFile() {
|
||||
this.$refs.fileUploader.onUploadFile(this.cwd);
|
||||
},
|
||||
onUploadFolder() {
|
||||
this.$refs.fileUploader.onUploadFolder(this.cwd);
|
||||
},
|
||||
onUploadFinished() {
|
||||
this.loadCwd();
|
||||
},
|
||||
onAppChange(event) {
|
||||
this.$router.push(`/home/${event.value.type}/${event.value.id}`);
|
||||
this.cwd = '/';
|
||||
this.loadResource(event.value);
|
||||
},
|
||||
onSelectionChanged(items) {
|
||||
this.activeItem = items[0] || null;
|
||||
this.selectedItems = items;
|
||||
},
|
||||
onActivateBreadcrumb(path) {
|
||||
this.cwd = sanitize(path);
|
||||
},
|
||||
async onRefresh() {
|
||||
this.busyRefresh = true;
|
||||
await this.loadCwd();
|
||||
setTimeout(() => { this.busyRefresh = false; }, 500);
|
||||
},
|
||||
// either dataTransfer (external drop) or files (internal drag)
|
||||
async onDrop(targetFolder, dataTransfer, files) {
|
||||
const fullTargetFolder = sanitize(this.cwd + '/' + targetFolder);
|
||||
|
||||
// if dataTransfer is set, we have a file/folder drop from outside
|
||||
if (dataTransfer) {
|
||||
|
||||
async function getFile(entry) {
|
||||
return new Promise((resolve, reject) => {
|
||||
entry.file(resolve, reject);
|
||||
});
|
||||
for (const i in entries) {
|
||||
await traverseFileTree(entries[i], item.name);
|
||||
}
|
||||
|
||||
async function readEntries(dirReader) {
|
||||
return new Promise((resolve, reject) => {
|
||||
dirReader.readEntries(resolve, reject);
|
||||
});
|
||||
}
|
||||
|
||||
const fileList = [];
|
||||
async function traverseFileTree(item) {
|
||||
if (item.isFile) {
|
||||
fileList.push(await getFile(item));
|
||||
} else if (item.isDirectory) {
|
||||
// Get folder contents
|
||||
const dirReader = item.createReader();
|
||||
const entries = await readEntries(dirReader);
|
||||
|
||||
for (let i in entries) {
|
||||
await traverseFileTree(entries[i], item.name);
|
||||
}
|
||||
} else {
|
||||
console.log('Skipping uknown file type', item);
|
||||
}
|
||||
}
|
||||
|
||||
// collect all files to upload
|
||||
for (const item of dataTransfer.items) {
|
||||
const entry = item.webkitGetAsEntry();
|
||||
|
||||
if (entry.isFile) {
|
||||
fileList.push(await getFile(entry));
|
||||
} else if (entry.isDirectory) {
|
||||
await traverseFileTree(entry, sanitize(`${this.cwd}/${targetFolder}`));
|
||||
}
|
||||
}
|
||||
|
||||
this.$refs.fileUploader.addFiles(fileList, sanitize(`${this.cwd}/${targetFolder}`));
|
||||
} else {
|
||||
if (!files.length) return;
|
||||
|
||||
window.addEventListener('beforeunload', beforeUnloadListener, { capture: true });
|
||||
this.$refs.pasteInProgressDialog.open();
|
||||
|
||||
// check ctrl for cut/copy
|
||||
await this.directoryModel.paste(fullTargetFolder, 'cut', files);
|
||||
await this.loadCwd();
|
||||
|
||||
window.removeEventListener('beforeunload', beforeUnloadListener, { capture: true });
|
||||
this.$refs.pasteInProgressDialog.close();
|
||||
console.log('Skipping uknown file type', item);
|
||||
}
|
||||
},
|
||||
onItemActivated(item) {
|
||||
if (!item) return;
|
||||
if (item.mimeType === 'inode/symlink') return;
|
||||
}
|
||||
|
||||
if (item.type === 'directory') this.cwd = sanitize(this.cwd + '/' + item.name);
|
||||
else this.$router.push(`/viewer/${this.resourceType}/${this.resourceId}${sanitize(this.cwd + '/' + item.name)}`);
|
||||
},
|
||||
async deleteHandler(files) {
|
||||
if (!files) return;
|
||||
// collect all files to upload
|
||||
for (const item of dataTransfer.items) {
|
||||
const entry = item.webkitGetAsEntry();
|
||||
|
||||
const confirmed = await this.$refs.inputDialog.confirm({
|
||||
message: this.$t('filemanager.removeDialog.reallyDelete'),
|
||||
confirmStyle: 'danger',
|
||||
confirmLabel: this.$t('main.dialog.yes'),
|
||||
rejectLabel: this.$t('main.dialog.no'),
|
||||
modal: false
|
||||
});
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
window.addEventListener('beforeunload', beforeUnloadListener, { capture: true });
|
||||
this.$refs.deleteInProgressDialog.open();
|
||||
|
||||
for (let i in files) {
|
||||
try {
|
||||
await this.directoryModel.remove(this.directoryModel.buildFilePath(this.cwd, files[i].name));
|
||||
} catch (e) {
|
||||
console.error(`Failed to remove file ${files[i].name}:`, e);
|
||||
}
|
||||
if (entry.isFile) {
|
||||
fileList.push(await getFile(entry));
|
||||
} else if (entry.isDirectory) {
|
||||
await traverseFileTree(entry, sanitize(`${cwd.value}/${targetFolder}`));
|
||||
}
|
||||
}
|
||||
|
||||
await this.loadCwd();
|
||||
fileUploader.value.addFiles(fileList, sanitize(`${cwd.value}/${targetFolder}`));
|
||||
} else {
|
||||
if (!files.length) return;
|
||||
|
||||
window.removeEventListener('beforeunload', beforeUnloadListener, { capture: true });
|
||||
this.$refs.deleteInProgressDialog.close();
|
||||
},
|
||||
async renameHandler(file, newName) {
|
||||
if (file.name === newName) return;
|
||||
window.addEventListener('beforeunload', beforeUnloadListener, { capture: true });
|
||||
pasteInProgressDialog.value.open();
|
||||
|
||||
try {
|
||||
await this.directoryModel.rename(this.directoryModel.buildFilePath(this.cwd, file.name), sanitize(this.cwd + '/' + newName));
|
||||
await this.loadCwd();
|
||||
} catch (e) {
|
||||
if (e.status === 409) {
|
||||
const confirmed = await this.$refs.inputDialog.confirm({
|
||||
message: this.$t('filemanager.renameDialog.reallyOverwrite'),
|
||||
confirmStyle: 'danger',
|
||||
confirmLabel: this.$t('main.dialog.yes'),
|
||||
rejectLabel: this.$t('main.dialog.no')
|
||||
});
|
||||
// check ctrl for cut/copy
|
||||
await directoryModel.paste(fullTargetFolder, 'cut', files);
|
||||
await loadCwd();
|
||||
|
||||
if (!confirmed) return;
|
||||
window.removeEventListener('beforeunload', beforeUnloadListener, { capture: true });
|
||||
pasteInProgressDialog.value.close();
|
||||
}
|
||||
}
|
||||
|
||||
await this.directoryModel.rename(this.directoryModel.buildFilePath(this.cwd, file.name), sanitize(this.cwd + '/' + newName), true /* overwrite */);
|
||||
await this.loadCwd();
|
||||
}
|
||||
else console.error(`Failed to rename ${file} to ${newName}`, e);
|
||||
}
|
||||
},
|
||||
async changeOwnerHandler(files, newOwnerUid) {
|
||||
if (!files) return;
|
||||
function onItemActivated(item) {
|
||||
if (!item) return;
|
||||
if (item.mimeType === 'inode/symlink') return;
|
||||
|
||||
for (let i in files) {
|
||||
await this.directoryModel.chown(this.directoryModel.buildFilePath(this.cwd, files[i].name), newOwnerUid);
|
||||
}
|
||||
if (item.type === 'directory') router.push(`/home/${resourceType.value}/${resourceId.value}${sanitize(cwd.value + '/' + item.name)}`);
|
||||
else router.push(`/viewer/${resourceType.value}/${resourceId.value}${sanitize(cwd.value + '/' + item.name)}`);
|
||||
}
|
||||
|
||||
await this.loadCwd();
|
||||
},
|
||||
async pasteHandler(action, files, target) {
|
||||
if (!files || !files.length) return;
|
||||
async function deleteHandler(files) {
|
||||
if (!files) return;
|
||||
|
||||
const targetPath = (target && target.isDirectory) ? sanitize(this.cwd + '/' + target.fileName) : this.cwd;
|
||||
const confirmed = await inputDialog.value.confirm({
|
||||
message: t('filemanager.removeDialog.reallyDelete'),
|
||||
confirmStyle: 'danger',
|
||||
confirmLabel: t('main.dialog.yes'),
|
||||
rejectLabel: t('main.dialog.no'),
|
||||
modal: false
|
||||
});
|
||||
|
||||
window.addEventListener('beforeunload', beforeUnloadListener, { capture: true });
|
||||
this.$refs.pasteInProgressDialog.open();
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
await this.directoryModel.paste(targetPath, action, files);
|
||||
} catch (e) {
|
||||
window.pankow.notify({ type: 'danger', text: e, persistent: true })
|
||||
}
|
||||
window.addEventListener('beforeunload', beforeUnloadListener, { capture: true });
|
||||
deleteInProgressDialog.value.open();
|
||||
|
||||
this.clipboard = {};
|
||||
await this.loadCwd();
|
||||
|
||||
window.removeEventListener('beforeunload', beforeUnloadListener, { capture: true });
|
||||
this.$refs.pasteInProgressDialog.close();
|
||||
},
|
||||
async downloadHandler(file) {
|
||||
await this.directoryModel.download(this.directoryModel.buildFilePath(this.cwd, file.name));
|
||||
},
|
||||
async extractHandler(file) {
|
||||
this.$refs.extractInProgressDialog.open();
|
||||
await this.directoryModel.extract(this.directoryModel.buildFilePath(this.cwd, file.name));
|
||||
await this.loadCwd();
|
||||
this.$refs.extractInProgressDialog.close();
|
||||
},
|
||||
async uploadHandler(targetDir, file, progressHandler) {
|
||||
this.uploadRequest = this.directoryModel.upload(targetDir, file, progressHandler);
|
||||
|
||||
try {
|
||||
await this.uploadRequest;
|
||||
} catch (e) {
|
||||
console.log('Upload cancelled.', e);
|
||||
}
|
||||
|
||||
this.uploadRequest = null;
|
||||
|
||||
await this.loadCwd();
|
||||
},
|
||||
async loadCwd() {
|
||||
this.items = await this.directoryModel.listFiles(this.cwd);
|
||||
|
||||
const tmp = this.cwd.split('/').slice(1);
|
||||
let name = '';
|
||||
if (tmp.length >= 1 && tmp[tmp.length-1]) name = tmp[tmp.length-1];
|
||||
|
||||
this.activeDirectoryItem = {
|
||||
id: name,
|
||||
name: name,
|
||||
type: 'directory',
|
||||
mimeType: 'inode/directory',
|
||||
icon: `${BASE_URL}mime-types/inode-directory.svg`
|
||||
};
|
||||
|
||||
this.busy = false;
|
||||
},
|
||||
async onRestartApp() {
|
||||
if (this.resourceType !== 'app') return;
|
||||
|
||||
const confirmed = await this.$refs.inputDialog.confirm({
|
||||
message: this.$t('filemanager.toolbar.restartApp') + '?',
|
||||
confirmStyle: 'danger',
|
||||
confirmLabel: this.$t('main.dialog.yes'),
|
||||
rejectLabel: this.$t('main.dialog.no')
|
||||
});
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
this.busyRestart = true;
|
||||
|
||||
let error, result;
|
||||
try {
|
||||
result = await fetcher.post(`${API_ORIGIN}/api/v1/apps/${this.resourceId}/restart`, null, { access_token: this.accessToken });
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
if (error || result.status !== 202) {
|
||||
console.error(`Failed to restart app ${this.resourceId}`, error || result.status);
|
||||
return;
|
||||
}
|
||||
|
||||
while(true) {
|
||||
let result;
|
||||
try {
|
||||
result = await fetcher.get(`${API_ORIGIN}/api/v1/apps/${this.resourceId}`, { access_token: this.accessToken });
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch app status.', e);
|
||||
}
|
||||
|
||||
if (result && result.status === 200 && result.body.installationState === ISTATES.INSTALLED) break;
|
||||
|
||||
await sleep(2000);
|
||||
}
|
||||
|
||||
this.busyRestart = false;
|
||||
for (const i in files) {
|
||||
try {
|
||||
await directoryModel.remove(directoryModel.buildFilePath(cwd.value, files[i].name));
|
||||
} catch (e) {
|
||||
console.error(`Failed to remove file ${files[i].name}:`, e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await loadCwd();
|
||||
|
||||
window.removeEventListener('beforeunload', beforeUnloadListener, { capture: true });
|
||||
deleteInProgressDialog.value.close();
|
||||
}
|
||||
|
||||
async function renameHandler(file, newName) {
|
||||
if (file.name === newName) return;
|
||||
|
||||
try {
|
||||
await directoryModel.rename(directoryModel.buildFilePath(cwd.value, file.name), sanitize(cwd.value + '/' + newName));
|
||||
await loadCwd();
|
||||
} catch (e) {
|
||||
if (e.status === 409) {
|
||||
const confirmed = await inputDialog.value.confirm({
|
||||
message: t('filemanager.renameDialog.reallyOverwrite'),
|
||||
confirmStyle: 'danger',
|
||||
confirmLabel: t('main.dialog.yes'),
|
||||
rejectLabel: t('main.dialog.no')
|
||||
});
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
await directoryModel.rename(directoryModel.buildFilePath(cwd.value, file.name), sanitize(cwd.value + '/' + newName), true /* overwrite */);
|
||||
await loadCwd();
|
||||
}
|
||||
else console.error(`Failed to rename ${file} to ${newName}`, e);
|
||||
}
|
||||
}
|
||||
|
||||
async function changeOwnerHandler(files, newOwnerUid) {
|
||||
if (!files) return;
|
||||
|
||||
for (const i in files) {
|
||||
await directoryModel.chown(directoryModel.buildFilePath(cwd.value, files[i].name), newOwnerUid);
|
||||
}
|
||||
|
||||
await loadCwd();
|
||||
}
|
||||
|
||||
async function pasteHandler(action, files, target) {
|
||||
if (!files || !files.length) return;
|
||||
|
||||
const targetPath = (target && target.isDirectory) ? sanitize(cwd.value + '/' + target.fileName) : cwd.value;
|
||||
|
||||
window.addEventListener('beforeunload', beforeUnloadListener, { capture: true });
|
||||
pasteInProgressDialog.value.open();
|
||||
|
||||
try {
|
||||
await directoryModel.paste(targetPath, action, files);
|
||||
} catch (e) {
|
||||
window.pankow.notify({ type: 'danger', text: e, persistent: true });
|
||||
}
|
||||
|
||||
await loadCwd();
|
||||
|
||||
window.removeEventListener('beforeunload', beforeUnloadListener, { capture: true });
|
||||
pasteInProgressDialog.value.close();
|
||||
}
|
||||
|
||||
async function downloadHandler(file) {
|
||||
await directoryModel.download(directoryModel.buildFilePath(cwd.value, file.name));
|
||||
}
|
||||
|
||||
async function extractHandler(file) {
|
||||
extractInProgressDialog.value.open();
|
||||
await directoryModel.extract(directoryModel.buildFilePath(cwd.value, file.name));
|
||||
await loadCwd();
|
||||
extractInProgressDialog.value.close();
|
||||
}
|
||||
|
||||
async function uploadHandler(targetDir, file, progressHandler) {
|
||||
uploadRequest.value = directoryModel.upload(targetDir, file, progressHandler);
|
||||
|
||||
try {
|
||||
await uploadRequest.value;
|
||||
} catch (e) {
|
||||
console.log('Upload cancelled.', e);
|
||||
}
|
||||
|
||||
uploadRequest.value = null;
|
||||
|
||||
await loadCwd();
|
||||
}
|
||||
|
||||
async function loadCwd() {
|
||||
items.value = await directoryModel.listFiles(cwd.value);
|
||||
|
||||
const tmp = cwd.value.split('/').slice(1);
|
||||
let name = '';
|
||||
if (tmp.length >= 1 && tmp[tmp.length-1]) name = tmp[tmp.length-1];
|
||||
|
||||
activeDirectoryItem.value = {
|
||||
id: name,
|
||||
name: name,
|
||||
type: 'directory',
|
||||
mimeType: 'inode/directory',
|
||||
icon: `${BASE_URL}mime-types/inode-directory.svg`
|
||||
};
|
||||
|
||||
busy.value = false;
|
||||
}
|
||||
|
||||
async function onRestartApp() {
|
||||
if (resourceType.value !== 'app') return;
|
||||
|
||||
const confirmed = await inputDialog.value.confirm({
|
||||
message: t('filemanager.toolbar.restartApp') + '?',
|
||||
confirmStyle: 'danger',
|
||||
confirmLabel: t('main.dialog.yes'),
|
||||
rejectLabel: t('main.dialog.no')
|
||||
});
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
busyRestart.value = true;
|
||||
|
||||
let error, result;
|
||||
try {
|
||||
result = await fetcher.post(`${API_ORIGIN}/api/v1/apps/${resourceId.value}/restart`, null, { access_token: accessToken });
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
if (error || result.status !== 202) {
|
||||
console.error(`Failed to restart app ${resourceId.value}`, error || result.status);
|
||||
return;
|
||||
}
|
||||
|
||||
while(true) {
|
||||
let result;
|
||||
try {
|
||||
result = await fetcher.get(`${API_ORIGIN}/api/v1/apps/${resourceId.value}`, { access_token: accessToken });
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch app status.', e);
|
||||
}
|
||||
|
||||
if (result && result.status === 200 && result.body.installationState === ISTATES.INSTALLED) break;
|
||||
|
||||
await sleep(2000);
|
||||
}
|
||||
|
||||
busyRestart.value = false;
|
||||
}
|
||||
|
||||
onBeforeRouteUpdate(async (to) => {
|
||||
if (to.params.type !== 'app' && to.params.type !== 'volume') return onFatalError(`Unknown type ${to.params.type}`);
|
||||
|
||||
if ((to.params.type !== resourceType.value) || (to.params.resourceId !== resourceId.value)) {
|
||||
resourceType.value = to.params.type;
|
||||
resourceId.value = to.params.resourceId;
|
||||
|
||||
directoryModel = createDirectoryModel(API_ORIGIN, accessToken, to.params.type === 'volume' ? `volumes/${to.params.resourceId}` : `apps/${to.params.resourceId}`);
|
||||
}
|
||||
|
||||
cwd.value = to.params.cwd ? `/${to.params.cwd.join('/')}` : '/';
|
||||
|
||||
await loadCwd();
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
busy.value = true;
|
||||
|
||||
const type = route.params.type || 'app';
|
||||
resourceId.value = route.params.resourceId;
|
||||
cwd.value = sanitize('/' + (route.params.cwd ? route.params.cwd.join('/') : '/'));
|
||||
|
||||
if (type === 'app') {
|
||||
let error, result;
|
||||
try {
|
||||
result = await fetcher.get(`${API_ORIGIN}/api/v1/apps/${resourceId.value}`, { access_token: accessToken });
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
if (error || result.status !== 200) {
|
||||
console.error(`Invalid resource ${type} ${resourceId.value}`, error || result.status);
|
||||
return onFatalError(`Invalid resource ${type} ${resourceId.value}`);
|
||||
}
|
||||
|
||||
appLink.value = `https://${result.body.fqdn}`;
|
||||
title.value = `${result.body.label || result.body.fqdn} (${result.body.manifest.title})`;
|
||||
} else if (type === 'volume') {
|
||||
let error, result;
|
||||
try {
|
||||
result = await fetcher.get(`${API_ORIGIN}/api/v1/volumes/${resourceId.value}`, { access_token: accessToken });
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
if (error || result.status !== 200) {
|
||||
console.error(`Invalid resource ${type} ${resourceId.value}`, error || result.status);
|
||||
return onFatalError(`Invalid resource ${type} ${resourceId.value}`);
|
||||
}
|
||||
|
||||
title.value = result.body.name;
|
||||
} else {
|
||||
return onFatalError(`Unsupported type ${type}`);
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await fetcher.get(`${API_ORIGIN}/api/v1/dashboard/config`, { access_token: accessToken });
|
||||
footerContent.value = marked.parse(result.body.footer);
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch Cloudron config.', e);
|
||||
}
|
||||
|
||||
window.document.title = `File Manager - ${title.value}`;
|
||||
|
||||
resourceType.value = type;
|
||||
|
||||
directoryModel = createDirectoryModel(API_ORIGIN, accessToken, type === 'volume' ? `volumes/${resourceId.value}` : `apps/${resourceId.value}`);
|
||||
ownersModel.value = directoryModel.ownersModel;
|
||||
|
||||
loadCwd();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
@@ -508,7 +510,7 @@ export default {
|
||||
<template #header>
|
||||
<TopBar class="navbar">
|
||||
<template #left>
|
||||
<Button icon="fa-solid fa-arrow-rotate-right" :loading="busyRefresh" @click="onRefresh()" secondary plain tool/>
|
||||
<Button icon="fa-solid fa-arrow-rotate-right" :loading="busyRefresh" @click="onRefresh()" secondary plain tool style="margin-right: 10px"/>
|
||||
<Breadcrumb :home="breadcrumbHomeItem" :items="breadcrumbItems" :activate-handler="onActivateBreadcrumb"/>
|
||||
</template>
|
||||
<template #right>
|
||||
@@ -517,10 +519,11 @@ export default {
|
||||
<Button icon="fa-solid fa-upload" :menu="uploadMenuModel">{{ $t('filemanager.toolbar.upload') }}</Button>
|
||||
</ButtonGroup>
|
||||
|
||||
<Button style="margin: 0 20px;" v-tooltip="$t('filemanager.toolbar.restartApp')" secondary tool :loading="busyRestart" icon="fa-solid fa-arrows-rotate" @click="onRestartApp" v-show="resourceType === 'app'"/>
|
||||
|
||||
<ButtonGroup>
|
||||
<Button style="margin-left: 20px;" :title="$t('filemanager.toolbar.restartApp')" secondary tool :loading="busyRestart" icon="fa-solid fa-arrows-rotate" @click="onRestartApp" v-show="resourceType === 'app'"/>
|
||||
<Button :href="'/terminal.html?id=' + resourceId" target="_blank" v-show="resourceType === 'app'" secondary tool icon="fa-solid fa-terminal" :title="$t('terminal.title')" />
|
||||
<Button :href="'/logs.html?appId=' + resourceId" target="_blank" v-show="resourceType === 'app'" secondary tool icon="fa-solid fa-align-left" :title="$t('logs.title')" />
|
||||
<Button :href="'/terminal.html?id=' + resourceId" target="_blank" v-show="resourceType === 'app'" secondary tool icon="fa-solid fa-terminal" v-tooltip="$t('terminal.title')" />
|
||||
<Button :href="'/logs.html?appId=' + resourceId" target="_blank" v-show="resourceType === 'app'" secondary tool icon="fa-solid fa-align-left" v-tooltip="$t('logs.title')" />
|
||||
</ButtonGroup>
|
||||
</template>
|
||||
</TopBar>
|
||||
@@ -597,10 +600,6 @@ export default {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.directory-view {
|
||||
background-color: var(--pankow-color-background);
|
||||
}
|
||||
|
||||
.no-highlight {
|
||||
color: var(--pankow-color-text);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user