457 lines
14 KiB
Vue
457 lines
14 KiB
Vue
<template>
|
|
<MainLayout>
|
|
<template #dialogs>
|
|
<Dialog v-model:visible="fatalError" modal header="Error" :closable="false" :closeOnEscape="false">
|
|
<p>{{ fatalError }}</p>
|
|
</Dialog>
|
|
|
|
<!-- have to use v-model instead of : bind - https://github.com/primefaces/primevue/issues/815 -->
|
|
<Dialog v-model:visible="newFileDialog.visible" modal :style="{ width: '50vw' }" @show="onDialogShow('newFileDialogNameInput')">
|
|
<template #header>
|
|
<label class="dialog-header" for="newFileDialogNameInput">New file name</label>
|
|
</template>
|
|
<template #default>
|
|
<form @submit="onNewFileDialogSubmit" @submit.prevent>
|
|
<InputText class="dialog-single-input" id="newFileDialogNameInput" v-model="newFileDialog.name" :disabled="newFileDialog.busy" required/>
|
|
<Button class="dialog-single-input-submit" type="submit" label="Create" :loading="newFileDialog.busy" :disabled="newFileDialog.busy || !newFileDialog.name"/>
|
|
</form>
|
|
</template>
|
|
</Dialog>
|
|
<Dialog v-model:visible="newFolderDialog.visible" modal :style="{ width: '50vw' }" @show="onDialogShow('newFolderDialogNameInput')">
|
|
<template #header>
|
|
<label class="dialog-header" for="newFolderDialogNameInput">New folder name</label>
|
|
</template>
|
|
<template #default>
|
|
<form @submit="onNewFolderDialogSubmit" @submit.prevent>
|
|
<InputText class="dialog-single-input" id="newFolderDialogNameInput" v-model="newFolderDialog.name" :disabled="newFolderDialog.busy" required/>
|
|
<Button class="dialog-single-input-submit" type="submit" label="Create" :loading="newFolderDialog.busy" :disabled="newFolderDialog.busy || !newFolderDialog.name"/>
|
|
</form>
|
|
</template>
|
|
</Dialog>
|
|
</template>
|
|
<template #header>
|
|
<TopBar class="navbar">
|
|
<template #left>
|
|
<Button icon="pi pi-chevron-left" @click="onGoUp()" text :disabled="cwd === '/'" style="margin-right: 5px;"/>
|
|
<PathBreadcrumbs :path="cwd" :activate-handler="onActivateBreadcrumb"/>
|
|
</template>
|
|
<template #right>
|
|
<Button type="button" label="New" icon="pi pi-plus" @click="onCreateMenu" aria-haspopup="true" aria-controls="create_menu" style="margin-right: 10px" />
|
|
<Menu ref="createMenu" id="create_menu" :model="createMenuModel" :popup="true" />
|
|
<Button type="button" label="Upload" icon="pi pi-upload" @click="onUploadMenu" aria-haspopup="true" aria-controls="upload_menu" style="margin-right: 10px" />
|
|
<Menu ref="uploadMenu" id="upload_menu" :model="uploadMenuModel" :popup="true" />
|
|
</template>
|
|
</TopBar>
|
|
</template>
|
|
<template #body>
|
|
<div class="main-view">
|
|
<div class="main-view-col">
|
|
<DirectoryView
|
|
:show-owner="true"
|
|
:show-size="true"
|
|
:show-modified="true"
|
|
@selection-changed="onSelectionChanged"
|
|
@item-activated="onItemActivated"
|
|
:delete-handler="deleteHandler"
|
|
:rename-handler="renameHandler"
|
|
:change-owner-handler="changeOwnerHandler"
|
|
:copy-handler="copyHandler"
|
|
:cut-handler="cutHandler"
|
|
:paste-handler="pasteHandler"
|
|
:new-file-handler="onNewFile"
|
|
:new-folder-handler="onNewFolder"
|
|
:upload-file-handler="onUploadFile"
|
|
:upload-folder-handler="onUploadFolder"
|
|
:drop-handler="onDrop"
|
|
:items="items"
|
|
:clipboard="clipboard"
|
|
:owners-model="ownersModel"
|
|
/>
|
|
</div>
|
|
<div class="main-view-col" style="max-width: 300px;">
|
|
<PreviewPanel :item="activeItem || activeDirectoryItem"/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<template #footer>
|
|
<FileUploader
|
|
ref="fileUploader"
|
|
:upload-handler="uploadHandler"
|
|
@finished="onUploadFinished"
|
|
/>
|
|
<BottomBar />
|
|
</template>
|
|
</MainLayout>
|
|
</template>
|
|
|
|
<script>
|
|
|
|
import superagent from 'superagent';
|
|
|
|
import Button from 'primevue/button';
|
|
import Dialog from 'primevue/dialog';
|
|
import InputText from 'primevue/inputtext';
|
|
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 PreviewPanel from '../components/PreviewPanel.vue';
|
|
import { createDirectoryModel } from '../models/DirectoryModel.js';
|
|
|
|
const API_ORIGIN = import.meta.env.VITE_API_ORIGIN ? 'https://' + import.meta.env.VITE_API_ORIGIN : '';
|
|
const BASE_URL = import.meta.env.BASE_URL || '/';
|
|
|
|
export default {
|
|
name: 'Home',
|
|
components: {
|
|
BottomBar,
|
|
Button,
|
|
Dialog,
|
|
DirectoryView,
|
|
FileUploader,
|
|
InputText,
|
|
MainLayout,
|
|
Menu,
|
|
PathBreadcrumbs,
|
|
PreviewPanel,
|
|
TopBar
|
|
},
|
|
data() {
|
|
return {
|
|
cwd: '/',
|
|
fatalError: false,
|
|
activeItem: null,
|
|
activeDirectoryItem: {},
|
|
items: [],
|
|
selectedItems: [],
|
|
clipboard: {
|
|
action: '', // copy or cut
|
|
files: []
|
|
},
|
|
accessToken: localStorage.token,
|
|
apiOrigin: API_ORIGIN || '',
|
|
title: 'Cloudron',
|
|
resourceType: '',
|
|
resourceId: '',
|
|
visible: true,
|
|
newFileDialog: {
|
|
visible: false,
|
|
busy: false,
|
|
name: ''
|
|
},
|
|
newFolderDialog: {
|
|
visible: false,
|
|
busy: false,
|
|
name: ''
|
|
},
|
|
ownersModel: [{
|
|
uid: 0,
|
|
label: 'root'
|
|
}, {
|
|
uid: 33,
|
|
label: 'www-data'
|
|
}, {
|
|
uid: 1000,
|
|
label: 'cloudron'
|
|
}, {
|
|
uid: 1001,
|
|
label: 'git'
|
|
}],
|
|
// contextMenuModel will have activeItem attached if any command() is called
|
|
createMenuModel: [{
|
|
label: 'File',
|
|
icon: 'pi pi-file',
|
|
command: this.onNewFile
|
|
}, {
|
|
label: 'Folder',
|
|
icon: 'pi pi-folder',
|
|
command: this.onNewFolder
|
|
}],
|
|
uploadMenuModel: [{
|
|
label: 'File',
|
|
icon: 'pi pi-file',
|
|
command: this.onUploadFile
|
|
}, {
|
|
label: 'Folder',
|
|
icon: 'pi pi-folder',
|
|
command: this.onUploadFolder
|
|
}]
|
|
};
|
|
},
|
|
watch: {
|
|
cwd(newCwd, oldCwd) {
|
|
if (this.resourceType && this.resourceId) this.$router.push(`/home/${this.resourceType}/${this.resourceId}${this.cwd}`);
|
|
this.loadCwd();
|
|
}
|
|
},
|
|
methods: {
|
|
onCreateMenu(event) {
|
|
this.$refs.createMenu.toggle(event);
|
|
},
|
|
onUploadMenu(event) {
|
|
this.$refs.uploadMenu.toggle(event);
|
|
},
|
|
// generic dialog focus handler
|
|
onDialogShow(focusElementId) {
|
|
setTimeout(() => document.getElementById(focusElementId).focus(), 0);
|
|
},
|
|
onNewFile() {
|
|
this.newFileDialog.busy = false;
|
|
this.newFileDialog.name = '';
|
|
this.newFileDialog.visible = true;
|
|
},
|
|
async onNewFileDialogSubmit() {
|
|
this.newFileDialog.busy = true;
|
|
await this.directoryModel.newFile(buildFilePath(this.cwd, this.newFileDialog.name), this.newFileDialog.name);
|
|
await this.loadCwd();
|
|
this.newFileDialog.visible = false;
|
|
},
|
|
onNewFolder() {
|
|
this.newFolderDialog.busy = false;
|
|
this.newFolderDialog.name = '';
|
|
this.newFolderDialog.visible = true;
|
|
},
|
|
async onNewFolderDialogSubmit() {
|
|
this.newFolderDialog.busy = true;
|
|
await this.directoryModel.newFolder(buildFilePath(this.cwd, this.newFolderDialog.name));
|
|
await this.loadCwd();
|
|
this.newFolderDialog.visible = false;
|
|
},
|
|
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);
|
|
},
|
|
onGoUp() {
|
|
this.cwd = sanitize(this.cwd.split('/').slice(0, -1).join('/'));
|
|
},
|
|
async onDrop(targetFolder, dataTransfer) {
|
|
const fullTargetFolder = sanitize(this.cwd + '/' + targetFolder);
|
|
|
|
// figure if a folder was dropped on a modern browser, in this case the first would have to be a directory
|
|
let folderItem;
|
|
try {
|
|
folderItem = dataTransfer.items[0].webkitGetAsEntry();
|
|
if (folderItem.isFile) return this.$refs.fileUploader.addFiles(dataTransfer.files, fullTargetFolder, false);
|
|
} catch (e) {
|
|
return this.$refs.fileUploader.addFiles(dataTransfer.files, fullTargetFolder, false);
|
|
}
|
|
|
|
// if we got here we have a folder drop and a modern browser
|
|
// now traverse the folder tree and create a file list
|
|
var that = this;
|
|
function traverseFileTree(item, path) {
|
|
if (item.isFile) {
|
|
item.file(function (file) {
|
|
that.$refs.fileUploader.addFiles([file], sanitize(`${that.cwd}/${targetFolder}`), false);
|
|
});
|
|
} else if (item.isDirectory) {
|
|
// Get folder contents
|
|
var dirReader = item.createReader();
|
|
dirReader.readEntries(function (entries) {
|
|
for (let i in entries) {
|
|
traverseFileTree(entries[i], item.name);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
traverseFileTree(folderItem, '');
|
|
},
|
|
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;
|
|
|
|
for (let i in files) {
|
|
await this.directoryModel.remove(buildFilePath(this.cwd, files[i].name));
|
|
}
|
|
|
|
await this.loadCwd();
|
|
},
|
|
async renameHandler(file, newName) {
|
|
await this.directoryModel.rename(buildFilePath(this.cwd, file.name), sanitize(this.cwd + '/' + newName));
|
|
await this.loadCwd();
|
|
},
|
|
async changeOwnerHandler(files, newOwnerUid) {
|
|
if (!files) return;
|
|
|
|
for (let i in files) {
|
|
await this.directoryModel.chown(buildFilePath(this.cwd, files[i].name), newOwnerUid);
|
|
}
|
|
|
|
await this.loadCwd();
|
|
},
|
|
async copyHandler(files) {
|
|
if (!files) return;
|
|
|
|
this.clipboard = {
|
|
action: 'copy',
|
|
files
|
|
};
|
|
},
|
|
async cutHandler(files) {
|
|
if (!files) return;
|
|
|
|
this.clipboard = {
|
|
action: 'cut',
|
|
files
|
|
};
|
|
},
|
|
async pasteHandler(target) {
|
|
if (!this.clipboard.files || !this.clipboard.files.length) return;
|
|
|
|
const targetPath = target ? sanitize(this.cwd + '/' + target.fileName) : this.cwd;
|
|
|
|
await this.directoryModel.paste(targetPath, this.clipboard.action, this.clipboard.files);
|
|
this.clipboard = {};
|
|
await this.loadCwd();
|
|
},
|
|
async uploadHandler(targetDir, file, progressHandler) {
|
|
await this.directoryModel.upload(targetDir, file, progressHandler);
|
|
await this.loadCwd();
|
|
},
|
|
async loadCwd() {
|
|
this.items = await this.directoryModel.listFiles(this.cwd);
|
|
|
|
const tmp = this.cwd.split('/').slice(1);
|
|
let name = this.title;
|
|
if (tmp.length > 1) name = tmp[tmp.length-2];
|
|
|
|
this.activeDirectoryItem = {
|
|
id: name,
|
|
name: name,
|
|
type: 'directory',
|
|
mimeType: 'inode/directory',
|
|
icon: `${BASE_URL}mime-types/inode-directory.svg`
|
|
};
|
|
}
|
|
},
|
|
async mounted() {
|
|
useConfirm();
|
|
|
|
const type = this.$route.params.type || 'app';
|
|
const resourceId = this.$route.params.resourceId;
|
|
const cwd = this.$route.params.cwd;
|
|
|
|
if (type === 'app') {
|
|
let error, result;
|
|
try {
|
|
result = await superagent.get(`${this.apiOrigin}/api/v1/apps/${resourceId}`).query({ access_token: this.accessToken });
|
|
} catch (e) {
|
|
error = e;
|
|
}
|
|
|
|
if (error || result.statusCode !== 200) {
|
|
console.error(`Invalid resource ${type} ${resourceId}`, error || result.statusCode);
|
|
this.fatalError = `Invalid resource ${type} ${resourceId}`;
|
|
return;
|
|
}
|
|
|
|
this.title = result.body.label || result.body.fqdn;
|
|
} else if (type === 'volume') {
|
|
let error, result;
|
|
try {
|
|
result = await superagent.get(`${this.apiOrigin}/api/v1/volumes/${resourceId}`).query({ access_token: this.accessToken });
|
|
} catch (e) {
|
|
error = e;
|
|
}
|
|
|
|
if (error || result.statusCode !== 200) {
|
|
console.error(`Invalid resource ${type} ${resourceId}`, error || result.statusCode);
|
|
this.fatalError = `Invalid resource ${type} ${resourceId}`;
|
|
return;
|
|
}
|
|
|
|
this.title = result.body.name;
|
|
} else {
|
|
this.fatalError = `Unsupported type ${type}`;
|
|
return;
|
|
}
|
|
|
|
window.document.title = this.title + ' - File Manager';
|
|
|
|
this.cwd = sanitize('/' + (cwd ? cwd.join('/') : '/'));
|
|
this.resourceType = type;
|
|
this.resourceId = resourceId;
|
|
|
|
this.directoryModel = createDirectoryModel(this.apiOrigin, this.accessToken, type === 'volume' ? `volumes/${resourceId}` : `apps/${resourceId}`);
|
|
this.loadCwd();
|
|
|
|
this.$watch(() => this.$route.params, (toParams, previousParams) => {
|
|
if (toParams.type !== 'app' && toParams.type !== 'volume') {
|
|
this.fatalError = `Unknown type ${toParams.type}`;
|
|
return;
|
|
}
|
|
|
|
if ((toParams.type !== this.resourceType) || (toParams.resourceId !== this.resourceId)) {
|
|
this.resourceType = toParams.type;
|
|
this.resourceId = toParams.resourceId;
|
|
|
|
this.directoryModel = createDirectoryModel(this.apiOrigin, this.accessToken, toParams.type === 'volume' ? `volumes/${toParams.resourceId}` : `apps/${toParams.resourceId}`);
|
|
}
|
|
|
|
this.cwd = toParams.cwd ? `/${toParams.cwd.join('/')}` : '/';
|
|
});
|
|
}
|
|
};
|
|
|
|
</script>
|
|
|
|
<style scoped>
|
|
|
|
.main-view {
|
|
flex-grow: 1;
|
|
overflow: hidden;
|
|
height: 100%;
|
|
display: flex;
|
|
padding: 0 10px
|
|
}
|
|
|
|
.main-view-col {
|
|
overflow: auto;
|
|
flex-grow: 1;
|
|
}
|
|
|
|
.dialog-header {
|
|
font-weight: 600;
|
|
font-size: 1.25rem;
|
|
}
|
|
|
|
.dialog-single-input {
|
|
display: block;
|
|
width: 100%;
|
|
margin-top: 5px;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.dialog-single-input-submit {
|
|
margin-top: 5px;
|
|
}
|
|
|
|
</style>
|