web-base/fileControlPanel/src/elements/file-browser.js

573 lines
21 KiB
JavaScript
Raw Normal View History

2021-01-10 01:08:03 +01:00
import * as React from "react";
2021-01-10 23:41:00 +01:00
import "./file-browser.css";
import Dropzone from "react-dropzone";
import Icon from "./icon";
2021-01-14 18:32:29 +01:00
import Alert from "./alert";
import {Popup} from "./popup";
2021-03-31 01:48:16 +02:00
import {useEffect, useState} from "react";
2021-03-31 13:15:39 +02:00
import axios from "axios";
2021-01-10 01:08:03 +01:00
2021-01-14 18:32:29 +01:00
export function FileBrowser(props) {
2021-01-10 01:08:03 +01:00
2021-03-31 01:48:16 +02:00
let files = props.files || {};
2021-01-14 18:32:29 +01:00
let api = props.api;
2021-03-31 01:48:16 +02:00
let tokenObj = props.token || {valid: false};
let onSelectFile = props.onSelectFile || function () { };
let onFetchFiles = props.onFetchFiles || function () { };
let directories = props.directories || {};
let restrictions = props.restrictions || {maxFiles: 0, maxSize: 0, extensions: ""};
let [popup, setPopup] = useState({visible: false, directoryName: "", directory: 0, type: "upload"});
let [alerts, setAlerts] = useState([]);
2021-01-14 18:32:29 +01:00
let [filesToUpload, setFilesToUpload] = useState([]);
2021-01-10 01:08:03 +01:00
2021-03-31 01:48:16 +02:00
function svgMiddle(key, scale = 1.0) {
2021-01-12 15:49:41 +01:00
let width = 48 * scale;
let height = 64 * scale;
2021-03-31 01:48:16 +02:00
return <svg key={key} width={width} height={height} xmlns="http://www.w3.org/2000/svg">
2021-01-10 23:41:00 +01:00
<g>
2021-03-31 01:48:16 +02:00
<line y2="0" x2={width / 2} y1={height} x1={width / 2} strokeWidth="1.5" stroke="#000" fill="none"/>
<line y2={height / 2} x2={width} y1={height / 2} x1={width / 2} strokeWidth="1.5" stroke="#000"
fill="none"/>
2021-01-10 23:41:00 +01:00
</g>
</svg>;
}
2021-03-31 01:48:16 +02:00
function svgEnd(key, scale = 1.0) {
2021-01-12 15:49:41 +01:00
let width = 48 * scale;
let height = 64 * scale;
2021-03-31 01:48:16 +02:00
return <svg key={key} width={width} height={height} xmlns="http://www.w3.org/2000/svg">
2021-01-10 23:41:00 +01:00
<g>
{ /* vertical line */}
2021-03-31 01:48:16 +02:00
<line y2="0" x2={width / 2} y1={height / 2} x1={width / 2} strokeWidth="1.5" stroke="#000" fill="none"/>
2021-01-10 23:41:00 +01:00
{ /* horizontal line */}
2021-03-31 01:48:16 +02:00
<line y2={height / 2} x2={width} y1={height / 2} x1={width / 2} strokeWidth="1.5" stroke="#000"
fill="none"/>
2021-01-14 21:45:58 +01:00
</g>
</svg>;
}
2021-03-31 01:48:16 +02:00
function svgLeft(key, scale = 1.0) {
2021-01-14 21:45:58 +01:00
let width = 48 * scale;
let height = 64 * scale;
2021-03-31 01:48:16 +02:00
return <svg key={key} width={width} height={height} xmlns="http://www.w3.org/2000/svg">
2021-01-14 21:45:58 +01:00
<g>
{ /* vertical line */}
2021-03-31 01:48:16 +02:00
<line y2="0" x2={width / 2} y1={height} x1={width / 2} strokeWidth="1.5" stroke="#000" fill="none"/>
2021-01-10 23:41:00 +01:00
</g>
</svg>;
}
2021-03-31 01:48:16 +02:00
function createFileIcon(mimeType, size = "2x") {
2021-01-12 15:49:41 +01:00
let icon = "";
2021-03-31 01:48:16 +02:00
if (mimeType) {
2021-01-12 15:49:41 +01:00
mimeType = mimeType.toLowerCase().trim();
let types = ["image", "text", "audio", "video"];
let languages = ["php", "java", "python", "cpp"];
let archives = ["zip", "tar", "archive"];
let [mainType, subType] = mimeType.split("/");
if (mainType === "text" && languages.find(a => subType.includes(a))) {
icon = "code";
} else if (mainType === "application" && archives.find(a => subType.includes(a))) {
icon = "archive";
} else if (mainType === "application" && subType === "pdf") {
icon = "pdf";
} else if (mainType === "application" && (subType.indexOf("powerpoint") > -1 || subType.indexOf("presentation") > -1)) {
icon = "powerpoint";
} else if (mainType === "application" && (subType.indexOf("word") > -1 || subType.indexOf("opendocument") > -1)) {
icon = "word";
} else if (mainType === "application" && (subType.indexOf("excel") > -1 || subType.indexOf("sheet") > -1)) {
icon = "excel";
} else if (mainType === "application" && subType.indexOf("directory") > -1) {
icon = "folder";
} else if (types.indexOf(mainType) > -1) {
if (mainType === "text") {
icon = "alt";
} else {
icon = mainType;
}
}
}
if (icon !== "folder") {
icon = "file" + (icon ? ("-" + icon) : icon);
}
2021-03-31 13:15:39 +02:00
return <Icon icon={icon} type={"far"} className={"p-1 align-middle file-icon fa-" + size}/>
2021-01-12 15:49:41 +01:00
}
2021-01-14 18:32:29 +01:00
function formatSize(size) {
2021-03-31 01:48:16 +02:00
const suffixes = ["B", "KiB", "MiB", "GiB", "TiB"];
2021-01-10 01:08:03 +01:00
let i = 0;
for (; i < suffixes.length && size >= 1024; i++) {
size /= 1024.0;
}
2021-01-13 15:26:30 +01:00
if (i === 0 || Math.round(size) === size) {
return size + " " + suffixes[i];
} else {
return size.toFixed(1) + " " + suffixes[i];
}
2021-01-10 01:08:03 +01:00
}
2021-03-31 01:48:16 +02:00
useEffect(() => {
let newFiles = filesToUpload.slice();
for (let fileIndex = 0; fileIndex < newFiles.length; fileIndex++) {
if (typeof newFiles[fileIndex].progress === 'undefined') {
onUpload(fileIndex);
break;
}
}
}, [filesToUpload]);
2021-01-14 18:32:29 +01:00
function canUpload() {
return api.loggedIn || (tokenObj.valid && tokenObj.type === "upload");
2021-01-10 23:41:00 +01:00
}
2021-03-31 01:48:16 +02:00
function onAddUploadFiles(acceptedFiles, rejectedFiles) {
if (rejectedFiles && rejectedFiles.length > 0) {
const filenames = rejectedFiles.map(f => f.file.name).join(", ");
pushAlert({msg: "The following files could not be uploaded due to given restrictions: " + filenames }, "Cannot upload file");
}
if (acceptedFiles && acceptedFiles.length > 0) {
let files = filesToUpload.slice();
files.push(...acceptedFiles);
setFilesToUpload(files);
}
2021-01-10 23:41:00 +01:00
}
2021-01-14 18:32:29 +01:00
function getSelectedIds(items = null, recursive = true) {
2021-01-10 23:41:00 +01:00
let ids = [];
2021-01-14 18:32:29 +01:00
items = items || files;
2021-01-10 23:41:00 +01:00
for (const fileItem of Object.values(items)) {
if (fileItem.selected) {
ids.push(fileItem.uid);
}
if (recursive && fileItem.isDirectory) {
2021-01-14 18:32:29 +01:00
ids.push(...getSelectedIds(fileItem.items));
2021-01-10 23:41:00 +01:00
}
}
return ids;
}
2021-03-30 23:00:34 +02:00
// TODO: add more mime type names or use an directory here?
function getTypeName(type) {
if (type.toLowerCase() === "directory") {
return "Directory";
}
switch (type.toLowerCase()) {
case "image/jpeg":
return "JPEG-Image";
case "image/png":
return "PNG-Image";
case "application/pdf":
return "PDF-Document";
case "text/plain":
return "Text-Document"
case "application/x-dosexec":
return "Windows Executable";
case "application/vnd.oasis.opendocument.text":
return "OpenOffice-Document";
default:
return type;
}
}
2021-03-31 01:48:16 +02:00
function createFileList(elements, indentation = 0) {
2021-01-10 01:08:03 +01:00
let rows = [];
2021-03-31 01:48:16 +02:00
let rowIndex = 0;
2021-03-30 23:00:34 +02:00
const scale = 0.45;
const iconSize = "lg";
2021-01-10 23:41:00 +01:00
const values = Object.values(elements);
for (const fileElement of values) {
2021-01-10 01:08:03 +01:00
let name = fileElement.name;
2021-03-31 01:48:16 +02:00
let uid = fileElement.uid;
2021-01-10 23:41:00 +01:00
let type = (fileElement.isDirectory ? "Directory" : fileElement.mimeType);
2021-01-14 18:32:29 +01:00
let size = (fileElement.isDirectory ? "" : formatSize(fileElement.size));
2021-01-12 15:49:41 +01:00
let mimeType = (fileElement.isDirectory ? "application/x-directory" : fileElement.mimeType);
2021-01-14 18:32:29 +01:00
let token = (tokenObj && tokenObj.valid ? "&token=" + tokenObj.value : "");
2021-01-14 21:45:58 +01:00
let svg = [];
2021-01-10 23:41:00 +01:00
if (indentation > 0) {
2021-01-14 21:45:58 +01:00
for (let i = 0; i < indentation - 1; i++) {
2021-03-31 01:48:16 +02:00
svg.push(svgLeft(rowIndex + "-" + i, scale));
2021-01-14 21:45:58 +01:00
}
2021-03-31 01:48:16 +02:00
if (rowIndex === values.length - 1) {
svg.push(svgEnd(rowIndex + "-end", scale));
2021-01-10 23:41:00 +01:00
} else {
2021-03-31 01:48:16 +02:00
svg.push(svgMiddle(rowIndex + "-middle", scale));
2021-01-10 23:41:00 +01:00
}
}
2021-01-10 01:08:03 +01:00
rows.push(
2021-01-10 23:41:00 +01:00
<tr key={"file-" + uid} data-id={uid} className={"file-row"}>
<td>
2021-03-31 01:48:16 +02:00
{svg}
{createFileIcon(mimeType, iconSize)}
2021-01-10 23:41:00 +01:00
</td>
<td>
{fileElement.isDirectory ? name :
<a href={"/api/file/download?id=" + uid + token} download={true}>{name}</a>
}
</td>
2021-03-30 23:00:34 +02:00
<td>{getTypeName(type)}</td>
2021-01-10 23:41:00 +01:00
<td>{size}</td>
<td>
<input type={"checkbox"} checked={!!fileElement.selected}
2021-01-14 18:32:29 +01:00
onChange={(e) => onSelectFile(e, uid)}
2021-01-10 23:41:00 +01:00
/>
</td>
2021-01-10 01:08:03 +01:00
</tr>
);
2021-01-10 23:41:00 +01:00
if (fileElement.isDirectory) {
2021-01-14 18:32:29 +01:00
rows.push(...createFileList(fileElement.items, indentation + 1));
2021-01-10 23:41:00 +01:00
}
2021-03-31 01:48:16 +02:00
rowIndex++;
2021-01-10 23:41:00 +01:00
}
return rows;
}
2021-03-31 01:48:16 +02:00
let rows = createFileList(files);
let selectedIds = getSelectedIds();
let selectedCount = selectedIds.length;
let uploadZone = <></>;
let writePermissions = canUpload();
let uploadedFiles = [];
let alertElements = [];
for (let i = 0; i < alerts.length; i++) {
const alert = alerts[i];
alertElements.push(
<Alert key={"alert-" + i} {...alert} onClose={() => removeAlert(i)}/>
);
}
let options = [];
for (const [uid, dir] of Object.entries(directories)) {
options.push(
<option key={"option-" + dir} value={uid}>{dir}</option>
);
}
function getAllowedExtensions() {
let extensions = restrictions.extensions || "";
return extensions.split(",")
.map(ext => ext.trim())
.map(ext => !ext.startsWith(".") && ext.length > 0 ? "." + ext : ext)
.join(",");
}
function getRestrictions() {
return {
accept: getAllowedExtensions(),
maxFiles: restrictions.maxFiles,
maxSize: restrictions.maxSize
};
}
2021-01-10 23:41:00 +01:00
2021-03-31 13:15:39 +02:00
function onCancelUpload(e, i) {
e.stopPropagation();
e.preventDefault();
const cancelToken = filesToUpload[i].cancelToken;
2021-03-31 13:58:56 +02:00
if (cancelToken && filesToUpload[i].progress < 1) {
2021-03-31 13:15:39 +02:00
cancelToken.cancel("Upload cancelled");
}
2021-03-31 13:58:56 +02:00
let files = filesToUpload.slice();
files.splice(i, 1);
setFilesToUpload(files);
2021-03-31 13:15:39 +02:00
}
2021-03-31 01:48:16 +02:00
if (writePermissions) {
for (let i = 0; i < filesToUpload.length; i++) {
const file = filesToUpload[i];
const progress = Math.round((file.progress ?? 0) * 100);
const done = progress >= 100;
uploadedFiles.push(
<span className={"uploaded-file"} key={i}>
{createFileIcon(file.type, "3x")}
<span>{file.name}</span>
{!done ?
2021-03-31 13:15:39 +02:00
<div className={"progress border border-primary position-relative"}>
2021-03-31 01:48:16 +02:00
<div className={"progress-bar progress-bar-striped progress-bar-animated"} role={"progressbar"}
aria-valuenow={progress} aria-valuemin={"0"} aria-valuemax={"100"}
2021-03-31 13:15:39 +02:00
style={{width: progress + "%"}} />
<span className="justify-content-center d-flex position-absolute w-100" style={{top: "7px"}}>
{ progress + "%" }
</span>
2021-03-31 01:48:16 +02:00
</div> : <></>
}
2021-03-31 13:58:56 +02:00
<Icon icon={done ? (file.success ? "check" : "times") : "spinner"}
className={"status-icon " + (done ? (file.success ? "text-success" : "text-danger") : "text-secondary")} />
<Icon icon={"times"} className={"text-danger cancel-button fa-2x"}
title={"Cancel Upload"} onClick={(e) => onCancelUpload(e, i)}/>
2021-03-31 01:48:16 +02:00
</span>
2021-01-14 21:45:58 +01:00
);
}
2021-03-31 01:48:16 +02:00
uploadZone = <>
<div className={"p-3"}>
2021-03-31 13:58:56 +02:00
<label><b>Upload Directory:</b></label>
2021-03-31 01:48:16 +02:00
<select value={popup.directory} className={"form-control"}
onChange={(e) => onPopupChange(e, "directory")}>
{options}
</select>
</div>
<Dropzone onDrop={onAddUploadFiles} {...getRestrictions()} >
2021-01-10 23:41:00 +01:00
{({getRootProps, getInputProps}) => (
<section className={"file-upload-container"}>
<div {...getRootProps()}>
<input {...getInputProps()} />
<p>Drag 'n' drop some files here, or click to select files</p>
2021-03-31 01:48:16 +02:00
{uploadedFiles.length === 0 ?
2021-01-12 15:49:41 +01:00
<Icon className={"mx-auto fa-3x text-black-50"} icon={"upload"}/> :
<div>{uploadedFiles}</div>
}
2021-01-10 23:41:00 +01:00
</div>
</section>
2021-03-31 01:48:16 +02:00
)}
</Dropzone>
</>;
}
2021-01-10 01:08:03 +01:00
2021-03-31 01:48:16 +02:00
return <>
<h4>
<Icon icon={"sync"} className={"mx-3 clickable small"} onClick={fetchFiles}/>
File Browser
</h4>
<table className={"table data-table file-table"}>
<thead>
<tr>
<th/>
<th>Name</th>
<th>Type</th>
<th>Size</th>
<th/>
</tr>
</thead>
<tbody>
{rows.length > 0 ? rows :
<tr>
<td colSpan={4} className={"text-center text-black-50"}>
No files uploaded yet
</td>
</tr>
}
</tbody>
</table>
<div className={"file-control-buttons"}>
<button type={"button"} className={"btn btn-success"} disabled={selectedCount === 0}
onClick={() => onDownload(selectedIds)}>
<Icon icon={"download"} className={"mr-1"}/>
Download Selected Files ({selectedCount})
</button>
<span/>
{
writePermissions ?
<>
<button type={"button"} className={"btn btn-danger"} disabled={selectedCount === 0}
onClick={() => deleteFiles(selectedIds)}>
<Icon icon={"trash"} className={"mr-1"}/>
Delete Selected Files ({selectedCount})
</button>
{api.loggedIn ?
<button type={"button"} className={"btn btn-info"}
onClick={(e) => onPopupOpen("createDirectory")}>
<Icon icon={"plus"} className={"mr-1"}/>
Create Directory
</button> :
<></>
}
</>
2021-01-10 23:41:00 +01:00
: <></>
2021-03-31 01:48:16 +02:00
}
</div>
{uploadZone}
<div className={"file-browser-restrictions px-4 mb-4"}>
<b>Restrictions:</b>
<span>Max. Files: {restrictions.maxFiles}</span>
<span>Max. Filesize: {formatSize(restrictions.maxSize)}</span>
<span>{restrictions.extensions ? "Allowed extensions: " + restrictions.extensions : "All extensions allowed"}</span>
</div>
<div>
{alertElements}
</div>
<Popup title={"Create Directory"} visible={popup.visible} buttons={["Ok", "Cancel"]} onClose={onPopupClose}
onClick={onPopupButton}>
<div className={"form-group"}>
<label>Destination Directory:</label>
<select value={popup.directory} className={"form-control"}
onChange={(e) => onPopupChange(e, "directory")}>
{options}
</select>
2021-01-12 15:49:41 +01:00
</div>
2021-03-31 01:48:16 +02:00
{popup.type !== "upload" ?
2021-01-14 18:32:29 +01:00
<div className={"form-group"}>
2021-03-31 01:48:16 +02:00
<label>Directory Name</label>
<input type={"text"} className={"form-control"} value={popup.directoryName} maxLength={32}
placeholder={"Enter name…"}
onChange={(e) => onPopupChange(e, "directoryName")}/>
</div> : <></>
}
</Popup>
</>;
2021-01-14 18:32:29 +01:00
2021-01-14 21:45:58 +01:00
function onPopupOpen(type) {
2021-03-31 01:48:16 +02:00
setPopup({...popup, visible: true, type: type});
2021-01-14 18:32:29 +01:00
}
function onPopupClose() {
2021-03-31 01:48:16 +02:00
setPopup({...popup, visible: false});
2021-01-14 18:32:29 +01:00
}
function onPopupChange(e, key) {
2021-03-31 01:48:16 +02:00
setPopup({...popup, [key]: e.target.value});
2021-01-10 01:08:03 +01:00
}
2021-01-10 23:41:00 +01:00
2021-01-14 18:32:29 +01:00
function onPopupButton(btn) {
if (btn === "Ok") {
2021-01-14 21:45:58 +01:00
let parentId = popup.directory === 0 ? null : popup.directory;
if (popup.type === "createDirectory") {
api.createDirectory(popup.directoryName, parentId).then((res) => {
if (!res.success) {
pushAlert(res, "Error creating directory");
} else {
fetchFiles();
}
});
}
2021-01-14 18:32:29 +01:00
}
onPopupClose();
}
2021-03-31 01:48:16 +02:00
function removeUploadedFiles() {
let newFiles = filesToUpload.filter(file => !file.progress || file.progress < 1.0);
if (newFiles.length !== filesToUpload.length) {
setFilesToUpload(newFiles);
}
}
2021-01-14 18:32:29 +01:00
function fetchFiles() {
2021-03-31 01:48:16 +02:00
let promise;
2021-01-14 18:32:29 +01:00
if (tokenObj.valid) {
2021-03-31 01:48:16 +02:00
promise = api.validateToken(tokenObj.value);
2021-01-14 18:32:29 +01:00
} else if (api.loggedIn) {
2021-03-31 01:48:16 +02:00
promise = api.listFiles()
} else {
return; // should never happen
2021-01-12 15:49:41 +01:00
}
2021-03-31 01:48:16 +02:00
promise.then((res) => {
if (res) {
onFetchFiles(res.files);
removeUploadedFiles();
} else {
pushAlert(res);
}
});
2021-01-12 15:49:41 +01:00
}
2021-01-14 18:32:29 +01:00
function pushAlert(res, title) {
let newAlerts = alerts.slice();
2021-03-31 01:48:16 +02:00
newAlerts.push({type: "danger", message: res.msg, title: title});
2021-01-14 18:32:29 +01:00
setAlerts(newAlerts);
}
function removeAlert(i) {
if (i >= 0 && i < alerts.length) {
let newAlerts = alerts.slice();
newAlerts.splice(i, 1);
setAlerts(newAlerts);
}
2021-01-12 15:49:41 +01:00
}
2021-01-14 18:32:29 +01:00
function deleteFiles(selectedIds) {
2021-01-13 01:36:04 +01:00
if (selectedIds && selectedIds.length > 0) {
2021-01-14 18:32:29 +01:00
let token = (api.loggedIn ? null : tokenObj.value);
api.delete(selectedIds, token).then((res) => {
2021-03-31 01:48:16 +02:00
if (res.success) {
fetchFiles();
} else {
pushAlert(res);
}
2021-01-13 01:36:04 +01:00
});
}
2021-01-10 23:41:00 +01:00
}
2021-01-12 15:49:41 +01:00
2021-03-31 01:48:16 +02:00
function onUploadProgress(event, fileIndex) {
if (fileIndex < filesToUpload.length) {
let files = filesToUpload.slice();
files[fileIndex].progress = event.loaded >= event.total ? 1 : event.loaded / event.total;
setFilesToUpload(files);
}
}
function onUpload(fileIndex) {
2021-01-14 18:32:29 +01:00
let token = (api.loggedIn ? null : tokenObj.value);
2021-01-14 21:45:58 +01:00
let parentId = ((!api.loggedIn || popup.directory === 0) ? null : popup.directory);
2021-03-31 01:48:16 +02:00
const file = filesToUpload[fileIndex];
2021-03-31 13:15:39 +02:00
const cancelToken = axios.CancelToken.source();
let newFiles = filesToUpload.slice();
newFiles[fileIndex].cancelToken = cancelToken;
newFiles[fileIndex].progress = 0;
setFilesToUpload(newFiles);
api.upload(file, token, parentId, cancelToken, (e) => onUploadProgress(e, fileIndex)).then((res) => {
2021-03-31 13:58:56 +02:00
let newFiles = filesToUpload.slice();
newFiles[fileIndex].success = res.success;
setFilesToUpload(newFiles);
2021-01-12 15:49:41 +01:00
if (res.success) {
2021-01-14 18:32:29 +01:00
fetchFiles();
2021-01-12 15:49:41 +01:00
} else {
2021-01-14 18:32:29 +01:00
pushAlert(res);
2021-01-12 15:49:41 +01:00
}
2021-03-31 13:15:39 +02:00
}).catch((reason) => {
if (reason && reason.message !== "Upload cancelled") {
2021-03-31 13:58:56 +02:00
pushAlert({ msg: reason }, "Error uploading files");
2021-03-31 13:15:39 +02:00
}
2021-01-12 15:49:41 +01:00
});
}
2021-01-13 01:36:04 +01:00
2021-01-14 18:32:29 +01:00
function onDownload(selectedIds) {
2021-01-13 01:36:04 +01:00
if (selectedIds && selectedIds.length > 0) {
2021-01-14 18:32:29 +01:00
let token = (api.loggedIn ? "" : "&token=" + tokenObj.value);
2021-01-13 01:36:04 +01:00
let ids = selectedIds.map(id => "id[]=" + id).join("&");
let downloadUrl = "/api/file/download?" + ids + token;
fetch(downloadUrl)
.then(response => {
let header = response.headers.get("Content-Disposition") || "";
let fileNameFields = header.split(";").filter(c => c.trim().toLowerCase().startsWith("filename="));
let fileName = null;
if (fileNameFields.length > 0) {
fileName = fileNameFields[0].trim().substr("filename=".length);
} else {
fileName = null;
}
response.blob().then(blob => {
let url = window.URL.createObjectURL(blob);
let a = document.createElement('a');
a.href = url;
if (fileName !== null) a.download = fileName;
a.click();
});
});
}
}
2021-01-10 01:08:03 +01:00
}