fetch => axios, some nice upload ux

This commit is contained in:
Roman 2021-03-31 01:48:16 +02:00
parent fb91b9e879
commit 5a169488af
7 changed files with 17343 additions and 283 deletions

@ -469,8 +469,10 @@ namespace Api\File {
return $this->success; return $this->success;
} }
$fileKeys = array_keys($_FILES); $numFilesUploaded = array_sum(array_map(function($fileEntry) {
$numFilesUploaded = count($fileKeys); return is_array($fileEntry["name"]) ? count($fileEntry["name"]) : 1;
}, $_FILES
));
if (!is_null($token)) { if (!is_null($token)) {
@ -511,30 +513,43 @@ namespace Api\File {
} }
$count = $res[0]["count"]; $count = $res[0]["count"];
if ($maxFiles > 0 && $numFilesUploaded > 0 && $numFilesUploaded + $count > $maxFiles) {
return $this->createError("File limit exceeded. Currently uploaded $count / $maxFiles files");
}
if ($maxSize > 0 || !empty($extensions)) {
foreach ($_FILES as $file) {
$name = $file["name"];
if ($maxSize > 0 && $file["size"] > $maxSize) {
return $this->createError("File Size limit of $maxSize bytes exceeded for file $name");
}
$dotPos = strrpos($name, ".");
$ext = ($dotPos !== false ? strtolower(substr($name, $dotPos + 1)) : false);
if (!empty($extensions) && $ext !== false && !in_array($ext, $extensions)) {
return $this->createError("File '$name' has prohibited extension. Allowed extensions: " . implode(",", $extensions));
}
}
}
} else { } else {
$userId = $this->user->getId(); $userId = $this->user->getId();
$maxSize = 0;
$count = 0;
$maxFiles = 0;
$extensions = array();
} }
$maxSize = ($maxSize <= 0 ? $this->getMaxFileSizePHP() : min($maxSize, $this->getMaxFileSizePHP()));
$maxFiles = ($maxFiles <= 0 ? $this->getMaxFiles() : min($maxFiles, $this->getMaxFiles()));
if ($numFilesUploaded === 0) { if ($numFilesUploaded === 0) {
return $this->createError("No file uploaded"); return $this->createError("No file uploaded");
} else if($numFilesUploaded > 1) {
return $this->createError("You can only upload one file at once");
}
if ($maxFiles > 0 && $count + 1 > $maxFiles) {
return $this->createError("File upload limit exceeded. Currently uploaded $count / $maxFiles files");
}
$fileObject = array_shift($_FILES);
$fileName = (is_array($fileObject["name"]) ? $fileObject["name"][0] : $fileObject["name"]);
$fileSize = (is_array($fileObject["size"]) ? $fileObject["size"][0] : $fileObject["size"]);
$tmpPath = (is_array($fileObject["tmp_name"]) ? $fileObject["tmp_name"][0] : $fileObject["tmp_name"]);
$fileError = (is_array($fileObject["error"]) ? $fileObject["error"][0] : $fileObject["error"]);
if ($maxSize > 0 || !empty($extensions)) {
if ($maxSize > 0 && $fileSize > $maxSize) {
return $this->createError("File Size limit of $maxSize bytes exceeded for file $fileName");
}
$dotPos = strrpos($fileName, ".");
$ext = ($dotPos !== false ? strtolower(substr($fileName, $dotPos + 1)) : false);
if (!empty($extensions) && $ext !== false && !in_array($ext, $extensions)) {
return $this->createError("File '$fileName' has prohibited extension. Allowed extensions: " . implode(",", $extensions));
}
} }
$uploadDir = realpath($_SERVER["DOCUMENT_ROOT"] . "/files/uploaded/"); $uploadDir = realpath($_SERVER["DOCUMENT_ROOT"] . "/files/uploaded/");
@ -542,47 +557,40 @@ namespace Api\File {
return $this->createError("Upload directory is not writable"); return $this->createError("Upload directory is not writable");
} }
$fileIds = array(); if (!$tmpPath) {
foreach ($_FILES as $key => $file) { return $this->createError("Error uploading file: $fileError");
$fileName = $file["name"]; }
$tmpPath = $file["tmp_name"];
if (!$tmpPath) {
return $this->createError("Error uploading file: $fileName");
}
$md5Hash = @hash_file('md5', $tmpPath); $md5Hash = @hash_file('md5', $tmpPath);
$sha1Hash = @hash_file('sha1', $tmpPath); $sha1Hash = @hash_file('sha1', $tmpPath);
$filePath = $uploadDir . "/" . $md5Hash . $sha1Hash; $filePath = $uploadDir . "/" . $md5Hash . $sha1Hash;
if (move_uploaded_file($tmpPath, $filePath)) { if (file_exists($filePath) || move_uploaded_file($tmpPath, $filePath)) {
$res = $sql->insert("UserFile", array("name", "directory", "path", "user_id", "parent_id"))
->addRow($fileName, false, $filePath, $userId, $parentId)
->returning("uid")
->execute();
$res = $sql->insert("UserFile", array("name", "directory", "path", "user_id", "parent_id")) if ($res === false) {
->addRow($fileName, false, $filePath, $userId, $parentId) $this->lastError = $sql->getLastError();
->returning("uid") $this->success = false;
->execute(); return false;
if ($res === false) {
$this->lastError = $sql->getLastError();
$this->success = false;
return false;
} else {
$fileIds[] = $sql->getLastInsertId();
}
} else { } else {
return $this->createError("Could not create file: " . $fileName); $fileId = $sql->getLastInsertId();
} }
} else {
return $this->createError("Could not create file: $fileName");
} }
if (!is_null($token)) { if (!is_null($token)) {
$query = $sql->insert("UserFileTokenFile", array("file_id", "token_id")); $res = $sql->insert("UserFileTokenFile", array("file_id", "token_id"))
foreach ($fileIds as $fileId) { ->addRow($fileId, $tokenId)
$query->addRow($fileId, $tokenId); ->execute();
}
$res = $query->execute();
$this->success = ($res !== false); $this->success = ($res !== false);
$this->lastError = $sql->getLastError(); $this->lastError = $sql->getLastError();
} }
$this->result["fileId"] = $fileId;
return $this->success; return $this->success;
} }
} }
@ -651,6 +659,7 @@ namespace Api\File {
} }
} }
// TODO: delete permanently from filesystem
class Delete extends FileAPI { class Delete extends FileAPI {
public function __construct(User $user, bool $externalCall = false) { public function __construct(User $user, bool $externalCall = false) {

File diff suppressed because it is too large Load Diff

@ -3,6 +3,7 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"axios": "^0.21.1",
"moment": "^2.26.0", "moment": "^2.26.0",
"react": "^16.13.1", "react": "^16.13.1",
"react-collapse": "^5.0.1", "react-collapse": "^5.0.1",

@ -1,4 +1,5 @@
import 'babel-polyfill'; import 'babel-polyfill';
import axios from "axios";
export default class API { export default class API {
@ -13,24 +14,18 @@ export default class API {
async apiCall(method, params) { async apiCall(method, params) {
params = params || { }; params = params || { };
const csrf_token = this.csrfToken(); const csrf_token = this.csrfToken();
if (csrf_token) params.csrf_token = csrf_token; if (csrf_token) params.csrf_token = csrf_token;
let response = await fetch("/api/" + method, { let response = await axios.post("/api/" + method, params);
method: 'post', return response.data;
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(params)
});
return await response.json();
} }
async fetchUser() { async fetchUser() {
let response = await fetch("/api/user/info"); let response = await axios.get("/api/user/info");
let data = await response.json(); let data = response.data;
this.user = data["user"]; this.user = data["user"];
this.loggedIn = data["loggedIn"]; this.loggedIn = data["loggedIn"];
return data && data.success && data.loggedIn; return data && data["success"] && data["loggedIn"];
} }
async logout() { async logout() {
@ -73,24 +68,20 @@ export default class API {
return this.apiCall("file/getRestrictions"); return this.apiCall("file/getRestrictions");
} }
async upload(files, token = null, parentId = null) { async upload(file, token = null, parentId = null, onUploadProgress = null) {
const csrf_token = this.csrfToken(); const csrf_token = this.csrfToken();
const fd = new FormData(); const fd = new FormData();
for (let i = 0; i < files.length; i++) { fd.append("file", file);
fd.append('file' + i, files[i]);
}
if (csrf_token) fd.append("csrf_token", csrf_token); if (csrf_token) fd.append("csrf_token", csrf_token);
if (token) fd.append("token", token); if (token) fd.append("token", token);
if (parentId) fd.append("parentId", parentId); if (parentId) fd.append("parentId", parentId);
// send `POST` request let response = await axios.post('/api/file/upload', fd, {
let response = await fetch('/api/file/upload', { headers: { 'Content-Type': 'multipart/form-data' },
method: 'POST', onUploadProgress: onUploadProgress || function () { }
body: fd
}); });
return response.json(); return response.data;
} }
}; };

@ -40,8 +40,7 @@
word-wrap: break-word; word-wrap: break-word;
} }
.uploaded-file > i:nth-child(3) { .uploaded-file > .status-icon {
color: red;
position: absolute; position: absolute;
top: -9px; top: -9px;
right: 25px; right: 25px;

@ -4,62 +4,64 @@ import Dropzone from "react-dropzone";
import Icon from "./icon"; import Icon from "./icon";
import Alert from "./alert"; import Alert from "./alert";
import {Popup} from "./popup"; import {Popup} from "./popup";
import {useState} from "react"; import {useEffect, useState} from "react";
export function FileBrowser(props) { export function FileBrowser(props) {
let files = props.files || { }; let files = props.files || {};
let api = props.api; let api = props.api;
let tokenObj = props.token || { valid: false }; let tokenObj = props.token || {valid: false};
let onSelectFile = props.onSelectFile || function() { }; let onSelectFile = props.onSelectFile || function () { };
let onFetchFiles = props.onFetchFiles || function() { }; let onFetchFiles = props.onFetchFiles || function () { };
let directories = props.directories || {}; let directories = props.directories || {};
let restrictions = props.restrictions || { maxFiles: 0, maxSize: 0, extensions: "" }; let restrictions = props.restrictions || {maxFiles: 0, maxSize: 0, extensions: ""};
let [popup, setPopup] = useState({ visible: false, directoryName: "", directory: 0, type: "upload" }); let [popup, setPopup] = useState({visible: false, directoryName: "", directory: 0, type: "upload"});
let [alerts, setAlerts] = useState( []); let [alerts, setAlerts] = useState([]);
let [filesToUpload, setFilesToUpload] = useState([]); let [filesToUpload, setFilesToUpload] = useState([]);
function svgMiddle(scale=1.0) { function svgMiddle(key, scale = 1.0) {
let width = 48 * scale; let width = 48 * scale;
let height = 64 * scale; let height = 64 * scale;
return <svg width={width} height={height} xmlns="http://www.w3.org/2000/svg"> return <svg key={key} width={width} height={height} xmlns="http://www.w3.org/2000/svg">
<g> <g>
<line y2="0" x2={width/2} y1={height} x1={width/2} strokeWidth="1.5" stroke="#000" fill="none"/> <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"/> <line y2={height / 2} x2={width} y1={height / 2} x1={width / 2} strokeWidth="1.5" stroke="#000"
fill="none"/>
</g> </g>
</svg>; </svg>;
} }
function svgEnd(scale=1.0) { function svgEnd(key, scale = 1.0) {
let width = 48 * scale; let width = 48 * scale;
let height = 64 * scale; let height = 64 * scale;
return <svg width={width} height={height} xmlns="http://www.w3.org/2000/svg"> return <svg key={key} width={width} height={height} xmlns="http://www.w3.org/2000/svg">
<g> <g>
{ /* vertical line */} { /* vertical line */}
<line y2="0" x2={width/2} y1={height/2} x1={width/2} strokeWidth="1.5" stroke="#000" fill="none"/> <line y2="0" x2={width / 2} y1={height / 2} x1={width / 2} strokeWidth="1.5" stroke="#000" fill="none"/>
{ /* horizontal line */} { /* horizontal line */}
<line y2={height/2} x2={width} y1={height/2} 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"/>
</g> </g>
</svg>; </svg>;
} }
function svgLeft(scale=1.0) { function svgLeft(key, scale = 1.0) {
let width = 48 * scale; let width = 48 * scale;
let height = 64 * scale; let height = 64 * scale;
return <svg width={width} height={height} xmlns="http://www.w3.org/2000/svg" style={{}}> return <svg key={key} width={width} height={height} xmlns="http://www.w3.org/2000/svg">
<g> <g>
{ /* vertical line */} { /* vertical line */}
<line y2="0" x2={width/2} y1={height} x1={width/2} strokeWidth="1.5" stroke="#000" fill="none"/> <line y2="0" x2={width / 2} y1={height} x1={width / 2} strokeWidth="1.5" stroke="#000" fill="none"/>
</g> </g>
</svg>; </svg>;
} }
function createFileIcon(mimeType, size="2x") { function createFileIcon(mimeType, size = "2x") {
let icon = ""; let icon = "";
if (mimeType !== null) { if (mimeType) {
mimeType = mimeType.toLowerCase().trim(); mimeType = mimeType.toLowerCase().trim();
let types = ["image", "text", "audio", "video"]; let types = ["image", "text", "audio", "video"];
let languages = ["php", "java", "python", "cpp"]; let languages = ["php", "java", "python", "cpp"];
@ -92,11 +94,11 @@ export function FileBrowser(props) {
icon = "file" + (icon ? ("-" + icon) : icon); icon = "file" + (icon ? ("-" + icon) : icon);
} }
return <Icon icon={icon} type={"far"} className={"p-1 align-middle fa-" + size} /> return <Icon icon={icon} type={"far"} className={"p-1 align-middle fa-" + size}/>
} }
function formatSize(size) { function formatSize(size) {
const suffixes = ["B","KiB","MiB","GiB","TiB"]; const suffixes = ["B", "KiB", "MiB", "GiB", "TiB"];
let i = 0; let i = 0;
for (; i < suffixes.length && size >= 1024; i++) { for (; i < suffixes.length && size >= 1024; i++) {
size /= 1024.0; size /= 1024.0;
@ -109,14 +111,32 @@ export function FileBrowser(props) {
} }
} }
useEffect(() => {
let newFiles = filesToUpload.slice();
for (let fileIndex = 0; fileIndex < newFiles.length; fileIndex++) {
if (typeof newFiles[fileIndex].progress === 'undefined') {
onUpload(fileIndex);
break;
}
}
}, [filesToUpload]);
function canUpload() { function canUpload() {
return api.loggedIn || (tokenObj.valid && tokenObj.type === "upload"); return api.loggedIn || (tokenObj.valid && tokenObj.type === "upload");
} }
function onAddUploadFiles(acceptedFiles) { function onAddUploadFiles(acceptedFiles, rejectedFiles) {
let files = filesToUpload.slice();
files.push(...acceptedFiles); if (rejectedFiles && rejectedFiles.length > 0) {
setFilesToUpload(files); 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);
}
} }
function getSelectedIds(items = null, recursive = true) { function getSelectedIds(items = null, recursive = true) {
@ -158,9 +178,9 @@ export function FileBrowser(props) {
} }
} }
function createFileList(elements, indentation=0) { function createFileList(elements, indentation = 0) {
let rows = []; let rows = [];
let i = 0; let rowIndex = 0;
const scale = 0.45; const scale = 0.45;
const iconSize = "lg"; const iconSize = "lg";
@ -168,7 +188,7 @@ export function FileBrowser(props) {
const values = Object.values(elements); const values = Object.values(elements);
for (const fileElement of values) { for (const fileElement of values) {
let name = fileElement.name; let name = fileElement.name;
let uid = fileElement.uid; let uid = fileElement.uid;
let type = (fileElement.isDirectory ? "Directory" : fileElement.mimeType); let type = (fileElement.isDirectory ? "Directory" : fileElement.mimeType);
let size = (fileElement.isDirectory ? "" : formatSize(fileElement.size)); let size = (fileElement.isDirectory ? "" : formatSize(fileElement.size));
let mimeType = (fileElement.isDirectory ? "application/x-directory" : fileElement.mimeType); let mimeType = (fileElement.isDirectory ? "application/x-directory" : fileElement.mimeType);
@ -176,21 +196,21 @@ export function FileBrowser(props) {
let svg = []; let svg = [];
if (indentation > 0) { if (indentation > 0) {
for (let i = 0; i < indentation - 1; i++) { for (let i = 0; i < indentation - 1; i++) {
svg.push(svgLeft(scale)); svg.push(svgLeft(rowIndex + "-" + i, scale));
} }
if (i === values.length - 1) { if (rowIndex === values.length - 1) {
svg.push(svgEnd(scale)); svg.push(svgEnd(rowIndex + "-end", scale));
} else { } else {
svg.push(svgMiddle(scale)); svg.push(svgMiddle(rowIndex + "-middle", scale));
} }
} }
rows.push( rows.push(
<tr key={"file-" + uid} data-id={uid} className={"file-row"}> <tr key={"file-" + uid} data-id={uid} className={"file-row"}>
<td> <td>
{ svg } {svg}
{ createFileIcon(mimeType, iconSize) } {createFileIcon(mimeType, iconSize)}
</td> </td>
<td> <td>
{fileElement.isDirectory ? name : {fileElement.isDirectory ? name :
@ -210,156 +230,190 @@ export function FileBrowser(props) {
if (fileElement.isDirectory) { if (fileElement.isDirectory) {
rows.push(...createFileList(fileElement.items, indentation + 1)); rows.push(...createFileList(fileElement.items, indentation + 1));
} }
i++; rowIndex++;
} }
return rows; return rows;
} }
let rows = createFileList(files); let rows = createFileList(files);
let selectedIds = getSelectedIds(); let selectedIds = getSelectedIds();
let selectedCount = selectedIds.length; let selectedCount = selectedIds.length;
let uploadZone = <></>; let uploadZone = <></>;
let writePermissions = canUpload(); let writePermissions = canUpload();
let uploadedFiles = []; let uploadedFiles = [];
let alertElements = []; let alertElements = [];
for (let i = 0; i < alerts.length; i++) { for (let i = 0; i < alerts.length; i++) {
const alert = alerts[i]; const alert = alerts[i];
alertElements.push( alertElements.push(
<Alert key={"alert-" + i} {...alert} onClose={() => removeAlert(i)} /> <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
};
}
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 ?
<div className={"progress border border-primary"}>
<div className={"progress-bar progress-bar-striped progress-bar-animated"} role={"progressbar"}
aria-valuenow={progress} aria-valuemin={"0"} aria-valuemax={"100"}
style={{width: progress + "%"}}>
{ progress + "%" }
</div>
</div> : <></>
}
<Icon icon={done ? "check" : "spinner"} className={"status-icon " + (done ? "text-success" : "text-secondary")} />
</span>
); );
} }
let options = []; uploadZone = <>
for (const [uid, dir] of Object.entries(directories)) { <div className={"p-3"}>
options.push( <label><b>Destination Directory:</b></label>
<option key={"option-" + dir} value={uid}>{dir}</option> <select value={popup.directory} className={"form-control"}
); onChange={(e) => onPopupChange(e, "directory")}>
} {options}
</select>
if (writePermissions) { </div>
<Dropzone onDrop={onAddUploadFiles} {...getRestrictions()} >
for(let i = 0; i < filesToUpload.length; i++) {
const file = filesToUpload[i];
uploadedFiles.push(
<span className={"uploaded-file"} key={i}>
{ createFileIcon(file.type, "3x") }
<span>{file.name}</span>
<Icon icon={"times"} onClick={(e) => onRemoveUploadedFile(e, i)}/>
</span>
);
}
uploadZone = <><Dropzone onDrop={onAddUploadFiles}>
{({getRootProps, getInputProps}) => ( {({getRootProps, getInputProps}) => (
<section className={"file-upload-container"}> <section className={"file-upload-container"}>
<div {...getRootProps()}> <div {...getRootProps()}>
<input {...getInputProps()} /> <input {...getInputProps()} />
<p>Drag 'n' drop some files here, or click to select files</p> <p>Drag 'n' drop some files here, or click to select files</p>
{ uploadedFiles.length === 0 ? {uploadedFiles.length === 0 ?
<Icon className={"mx-auto fa-3x text-black-50"} icon={"upload"}/> : <Icon className={"mx-auto fa-3x text-black-50"} icon={"upload"}/> :
<div>{uploadedFiles}</div> <div>{uploadedFiles}</div>
} }
</div> </div>
</section> </section>
)} )}
</Dropzone> </Dropzone>
</>;
}
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>
{ api.loggedIn ?
<button type={"button"} className={"btn btn-info"} onClick={(e) => onPopupOpen("createDirectory")}>
<Icon icon={"plus"} className={"mr-1"}/>
Create Directory
</button> :
<></>
}
{
writePermissions ?
<>
<button type={"button"} className={"btn btn-primary"} disabled={uploadedFiles.length === 0}
onClick={(e) => api.loggedIn ? onPopupOpen("upload") : onUpload()}>
<Icon icon={"upload"} className={"mr-1"}/>
Upload
</button>
<button type={"button"} className={"btn btn-danger"} disabled={selectedCount === 0}
onClick={() => deleteFiles(selectedIds)}>
<Icon icon={"trash"} className={"mr-1"}/>
Delete Selected Files ({selectedCount})
</button>
</>
: <></>
}
</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>
</div>
{ popup.type !== "upload" ?
<div className={"form-group"}>
<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>
</>; </>;
}
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> :
<></>
}
</>
: <></>
}
</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>
</div>
{popup.type !== "upload" ?
<div className={"form-group"}>
<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>
</>;
function onPopupOpen(type) { function onPopupOpen(type) {
setPopup({ ...popup, visible: true, type: type }); setPopup({...popup, visible: true, type: type});
} }
function onPopupClose() { function onPopupClose() {
setPopup({ ...popup, visible: false }); setPopup({...popup, visible: false});
} }
function onPopupChange(e, key) { function onPopupChange(e, key) {
setPopup({ ...popup, [key]: e.target.value }); setPopup({...popup, [key]: e.target.value});
} }
function onPopupButton(btn) { function onPopupButton(btn) {
@ -374,34 +428,40 @@ export function FileBrowser(props) {
fetchFiles(); fetchFiles();
} }
}); });
} else if (popup.type === "upload") {
onUpload();
} }
} }
onPopupClose(); onPopupClose();
} }
function fetchFiles() { function removeUploadedFiles() {
if (tokenObj.valid) { let newFiles = filesToUpload.filter(file => !file.progress || file.progress < 1.0);
api.validateToken(tokenObj.value).then((res) => { if (newFiles.length !== filesToUpload.length) {
if (res) { setFilesToUpload(newFiles);
onFetchFiles(res.files);
} else {
pushAlert(res);
}
});
} else if (api.loggedIn) {
api.listFiles().then((res) => {
if (res) {
onFetchFiles(res.files);
} else {
pushAlert(res);
}
});
} }
} }
function fetchFiles() {
let promise;
if (tokenObj.valid) {
promise = api.validateToken(tokenObj.value);
} else if (api.loggedIn) {
promise = api.listFiles()
} else {
return; // should never happen
}
promise.then((res) => {
if (res) {
onFetchFiles(res.files);
removeUploadedFiles();
} else {
pushAlert(res);
}
});
}
function onRemoveUploadedFile(e, i) { function onRemoveUploadedFile(e, i) {
e.stopPropagation(); e.stopPropagation();
let files = filesToUpload.slice(); let files = filesToUpload.slice();
@ -411,7 +471,7 @@ export function FileBrowser(props) {
function pushAlert(res, title) { function pushAlert(res, title) {
let newAlerts = alerts.slice(); let newAlerts = alerts.slice();
newAlerts.push({ type: "danger", message: res.msg, title: title }); newAlerts.push({type: "danger", message: res.msg, title: title});
setAlerts(newAlerts); setAlerts(newAlerts);
} }
@ -427,21 +487,30 @@ export function FileBrowser(props) {
if (selectedIds && selectedIds.length > 0) { if (selectedIds && selectedIds.length > 0) {
let token = (api.loggedIn ? null : tokenObj.value); let token = (api.loggedIn ? null : tokenObj.value);
api.delete(selectedIds, token).then((res) => { api.delete(selectedIds, token).then((res) => {
if (res.success) { if (res.success) {
fetchFiles(); fetchFiles();
} else { } else {
pushAlert(res); pushAlert(res);
} }
}); });
} }
} }
function onUpload() { 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) {
let token = (api.loggedIn ? null : tokenObj.value); let token = (api.loggedIn ? null : tokenObj.value);
let parentId = ((!api.loggedIn || popup.directory === 0) ? null : popup.directory); let parentId = ((!api.loggedIn || popup.directory === 0) ? null : popup.directory);
api.upload(filesToUpload, token, parentId).then((res) => { const file = filesToUpload[fileIndex];
api.upload(file, token, parentId, (e) => onUploadProgress(e, fileIndex)).then((res) => {
if (res.success) { if (res.success) {
setFilesToUpload([]); // setFilesToUpload([]);
fetchFiles(); fetchFiles();
} else { } else {
pushAlert(res); pushAlert(res);

14
js/files.min.js vendored

File diff suppressed because one or more lines are too long