File Control frontend

This commit is contained in:
Roman Hergenreder 2021-01-14 18:32:29 +01:00
parent 48a3932451
commit 9337faab97
9 changed files with 487 additions and 239 deletions

@ -115,7 +115,7 @@ namespace Api {
$files = array(); $files = array();
foreach ($res as $row) { foreach ($res as $row) {
if ($row["uid"] === null) continue; if ($row["uid"] === null) continue;
$fileId = (string)$row["uid"]; $fileId = $row["uid"];
$parentId = $row["parentId"]; $parentId = $row["parentId"];
$fileName = $row["name"]; $fileName = $row["name"];
$isDirectory = $row["directory"]; $isDirectory = $row["directory"];
@ -377,7 +377,6 @@ namespace Api\File {
'parentId' => new Parameter('parentId', Parameter::TYPE_INT, true, null) 'parentId' => new Parameter('parentId', Parameter::TYPE_INT, true, null)
)); ));
$this->loginRequired = true; $this->loginRequired = true;
$this->csrfTokenRequired = false;
} }
public function execute($values = array()) { public function execute($values = array()) {

@ -57,6 +57,18 @@ export default class API {
return this.apiCall("file/revokeToken", { token: token }); return this.apiCall("file/revokeToken", { token: token });
} }
createDownloadToken(durability, files) {
return this.apiCall("file/createDownloadToken", { files: files, durability: durability });
}
createUploadToken(durability, parentId=null, maxFiles=0, maxSize=0, extensions = "") {
return this.apiCall("file/createUploadToken", { parentId: parentId, durability: durability, maxFiles: maxFiles, maxSize: maxSize, extensions: extensions });
}
createDirectory(name, parentId = null) {
return this.apiCall("file/createDirectory", { name: name, parentId: parentId });
}
async upload(files, token = null, parentId = null) { async upload(files, token = null, parentId = null) {
const csrf_token = this.csrfToken(); const csrf_token = this.csrfToken();

@ -0,0 +1,25 @@
import Icon from "./icon";
import React from "react";
export default function Alert(props) {
const onClose = props.onClose || null;
const title = props.title || "Untitled Alert";
const message = props.message || "Alert message";
const type = props.type || "danger";
let icon = "ban";
if (type === "warning") {
icon = "exclamation-triangle";
} else if(type === "success") {
icon = "check";
}
return (
<div className={"alert alert-" + type + " alert-dismissible"}>
{onClose ? <button type="button" className={"close"} data-dismiss={"alert"} aria-hidden={"true"} onClick={onClose}>×</button> : null}
<h5><Icon icon={icon} className={"icon"} /> {title}</h5>
{message}
</div>
)
}

@ -60,6 +60,10 @@
text-align: center; text-align: center;
} }
.token-table td:nth-child(4) > i {
padding-left: 10px;
}
.file-table td:nth-child(n+3), .file-table th:nth-child(n+3) { .file-table td:nth-child(n+3), .file-table th:nth-child(n+3) {
text-align: center; text-align: center;
} }

@ -2,22 +2,23 @@ import * as React from "react";
import "./file-browser.css"; import "./file-browser.css";
import Dropzone from "react-dropzone"; import Dropzone from "react-dropzone";
import Icon from "./icon"; import Icon from "./icon";
import Alert from "./alert";
import {Popup} from "./popup";
import {useState} from "react";
export class FileBrowser extends React.Component { export function FileBrowser(props) {
constructor(props) { let files = props.files || { };
super(props); let api = props.api;
let tokenObj = props.token || { valid: false };
let onSelectFile = props.onSelectFile || function() { };
let onFetchFiles = props.onFetchFiles || function() { };
this.state = { let [popup, setPopup] = useState({ visible: false, directoryName: "" });
api: props.api, let [alerts, setAlerts] = useState( []);
files: props.files, let [filesToUpload, setFilesToUpload] = useState([]);
token: props.token,
filesToUpload: [],
alerts: []
}
}
svgMiddle(indentation, scale=1.0) { function svgMiddle(indentation, scale=1.0) {
let width = 48 * scale; let width = 48 * scale;
let height = 64 * scale; let height = 64 * scale;
let style = (indentation > 1 ? { marginLeft: ((indentation-1)*width) + "px" } : {}); let style = (indentation > 1 ? { marginLeft: ((indentation-1)*width) + "px" } : {});
@ -33,7 +34,7 @@ export class FileBrowser extends React.Component {
</svg>; </svg>;
} }
svgEnd(indentation, scale=1.0) { function svgEnd(indentation, scale=1.0) {
let width = 48 * scale; let width = 48 * scale;
let height = 64 * scale; let height = 64 * scale;
let style = (indentation > 1 ? { marginLeft: ((indentation-1)*width) + "px" } : {}); let style = (indentation > 1 ? { marginLeft: ((indentation-1)*width) + "px" } : {});
@ -51,7 +52,7 @@ export class FileBrowser extends React.Component {
</svg>; </svg>;
} }
createFileIcon(mimeType, size=2) { function createFileIcon(mimeType, size=2) {
let icon = ""; let icon = "";
if (mimeType !== null) { if (mimeType !== null) {
mimeType = mimeType.toLowerCase().trim(); mimeType = mimeType.toLowerCase().trim();
@ -89,7 +90,7 @@ export class FileBrowser extends React.Component {
return <Icon icon={icon} type={"far"} className={"p-1 align-middle fa-" + size + "x"} /> return <Icon icon={icon} type={"far"} className={"p-1 align-middle fa-" + size + "x"} />
} }
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++) {
@ -103,78 +104,32 @@ export class FileBrowser extends React.Component {
} }
} }
canUpload() { function canUpload() {
return this.state.api.loggedIn || (this.state.token.valid && this.state.token.type === "upload"); return api.loggedIn || (tokenObj.valid && tokenObj.type === "upload");
} }
onAddUploadFiles(acceptedFiles) { function onAddUploadFiles(acceptedFiles) {
let files = this.state.filesToUpload.slice(); let files = filesToUpload.slice();
files.push(...acceptedFiles); files.push(...acceptedFiles);
this.setState({ ...this.state, filesToUpload: files }); setFilesToUpload(files);
} }
getSelectedIds(items = null, recursive = true) { function getSelectedIds(items = null, recursive = true) {
let ids = []; let ids = [];
items = items || this.state.files; items = items || files;
for (const fileItem of Object.values(items)) { for (const fileItem of Object.values(items)) {
if (fileItem.selected) { if (fileItem.selected) {
ids.push(fileItem.uid); ids.push(fileItem.uid);
} }
if (recursive && fileItem.isDirectory) { if (recursive && fileItem.isDirectory) {
ids.push(...this.getSelectedIds(fileItem.items)); ids.push(...getSelectedIds(fileItem.items));
} }
} }
return ids; return ids;
} }
onSelectAll(selected, items) { function createFileList(elements, indentation=0) {
for (const fileElement of Object.values(items)) {
fileElement.selected = selected;
if (fileElement.isDirectory) {
this.onSelectAll(selected, fileElement.items);
}
}
}
onSelectFile(e, uid, items=null) {
let found = false;
let updatedFiles = (items === null) ? {...this.state.files} : items;
if (updatedFiles.hasOwnProperty(uid)) {
let fileElement = updatedFiles[uid];
found = true;
fileElement.selected = e.target.checked;
if (fileElement.isDirectory) {
this.onSelectAll(fileElement.selected, fileElement.items);
}
} else {
for (const fileElement of Object.values(updatedFiles)) {
if (fileElement.isDirectory) {
if (this.onSelectFile(e, uid, fileElement.items)) {
if (!e.target.checked) {
fileElement.selected = false;
} else if (this.getSelectedIds(fileElement.items, false).length === Object.values(fileElement.items).length) {
fileElement.selected = true;
}
found = true;
break;
}
}
}
}
if (items === null) {
this.setState({
...this.state,
files: updatedFiles
});
}
return found;
}
createFileList(elements, indentation=0) {
let rows = []; let rows = [];
let i = 0; let i = 0;
const values = Object.values(elements); const values = Object.values(elements);
@ -182,15 +137,15 @@ export class FileBrowser extends React.Component {
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 ? "" : this.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);
let token = (this.state.token && this.state.token.valid ? "&token=" + this.state.token.value : ""); let token = (tokenObj && tokenObj.valid ? "&token=" + tokenObj.value : "");
let svg = <></>; let svg = <></>;
if (indentation > 0) { if (indentation > 0) {
if (i === values.length - 1) { if (i === values.length - 1) {
svg = this.svgEnd(indentation, 0.75); svg = svgEnd(indentation, 0.75);
} else { } else {
svg = this.svgMiddle(indentation, 0.75); svg = svgMiddle(indentation, 0.75);
} }
} }
@ -198,7 +153,7 @@ export class FileBrowser extends React.Component {
<tr key={"file-" + uid} data-id={uid} className={"file-row"}> <tr key={"file-" + uid} data-id={uid} className={"file-row"}>
<td> <td>
{ svg } { svg }
{ this.createFileIcon(mimeType) } { createFileIcon(mimeType) }
</td> </td>
<td> <td>
{fileElement.isDirectory ? name : {fileElement.isDirectory ? name :
@ -209,53 +164,49 @@ export class FileBrowser extends React.Component {
<td>{size}</td> <td>{size}</td>
<td> <td>
<input type={"checkbox"} checked={!!fileElement.selected} <input type={"checkbox"} checked={!!fileElement.selected}
onChange={(e) => this.onSelectFile(e, uid)} onChange={(e) => onSelectFile(e, uid)}
/> />
</td> </td>
</tr> </tr>
); );
if (fileElement.isDirectory) { if (fileElement.isDirectory) {
rows.push(...this.createFileList(fileElement.items, indentation + 1)); rows.push(...createFileList(fileElement.items, indentation + 1));
} }
i++; i++;
} }
return rows; return rows;
} }
render() { let rows = createFileList(files);
let selectedIds = getSelectedIds();
let rows = this.createFileList(this.state.files);
let selectedIds = this.getSelectedIds();
let selectedCount = selectedIds.length; let selectedCount = selectedIds.length;
let uploadZone = <></>; let uploadZone = <></>;
let writePermissions = this.canUpload(); let writePermissions = canUpload();
let uploadedFiles = []; let uploadedFiles = [];
let alerts = []; let alertElements = [];
let i = 0; for (let i = 0; i < alerts.length; i++) {
for (const alert of this.state.alerts) { const alert = alerts[i];
alerts.push( alertElements.push(
<div key={"alert-" + i++} className={"alert alert-" + alert.type}> <Alert key={"alert-" + i} {...alert} onClose={() => removeAlert(i)} />
{ alert.text }
</div>
); );
} }
if (writePermissions) { if (writePermissions) {
for(let i = 0; i < this.state.filesToUpload.length; i++) { for(let i = 0; i < filesToUpload.length; i++) {
const file = this.state.filesToUpload[i]; const file = filesToUpload[i];
uploadedFiles.push( uploadedFiles.push(
<span className={"uploaded-file"} key={i}> <span className={"uploaded-file"} key={i}>
{ this.createFileIcon(file.type, 3) } { createFileIcon(file.type, 3) }
<span>{file.name}</span> <span>{file.name}</span>
<Icon icon={"times"} onClick={(e) => this.onRemoveUploadedFile(e, i)}/> <Icon icon={"times"} onClick={(e) => onRemoveUploadedFile(e, i)}/>
</span> </span>
); );
} }
uploadZone = <><Dropzone onDrop={this.onAddUploadFiles.bind(this)}> uploadZone = <><Dropzone onDrop={onAddUploadFiles}>
{({getRootProps, getInputProps}) => ( {({getRootProps, getInputProps}) => (
<section className={"file-upload-container"}> <section className={"file-upload-container"}>
<div {...getRootProps()}> <div {...getRootProps()}>
@ -273,7 +224,10 @@ export class FileBrowser extends React.Component {
} }
return <> return <>
<h4>File Browser</h4> <h4>
<Icon icon={"sync"} className={"mx-3 clickable small"} onClick={fetchFiles}/>
File Browser
</h4>
<table className={"table data-table file-table"}> <table className={"table data-table file-table"}>
<thead> <thead>
<tr> <tr>
@ -285,17 +239,23 @@ export class FileBrowser extends React.Component {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{ rows } { rows.length > 0 ? rows :
<tr>
<td colSpan={4} className={"text-center text-black-50"}>
No files uploaded yet
</td>
</tr>
}
</tbody> </tbody>
</table> </table>
<div className={"file-control-buttons"}> <div className={"file-control-buttons"}>
<button type={"button"} className={"btn btn-success"} disabled={selectedCount === 0} <button type={"button"} className={"btn btn-success"} disabled={selectedCount === 0}
onClick={() => this.onDownload(selectedIds)}> onClick={() => onDownload(selectedIds)}>
<Icon icon={"download"} className={"mr-1"}/> <Icon icon={"download"} className={"mr-1"}/>
Download Selected Files ({selectedCount}) Download Selected Files ({selectedCount})
</button> </button>
{ this.state.api.loggedIn ? { api.loggedIn ?
<button type={"button"} className={"btn btn-info"}> <button type={"button"} className={"btn btn-info"} onClick={onPopupOpen}>
<Icon icon={"plus"} className={"mr-1"}/> <Icon icon={"plus"} className={"mr-1"}/>
Create Directory Create Directory
</button> : </button> :
@ -305,12 +265,12 @@ export class FileBrowser extends React.Component {
writePermissions ? writePermissions ?
<> <>
<button type={"button"} className={"btn btn-primary"} disabled={uploadedFiles.length === 0} <button type={"button"} className={"btn btn-primary"} disabled={uploadedFiles.length === 0}
onClick={this.onUpload.bind(this)}> onClick={onUpload}>
<Icon icon={"upload"} className={"mr-1"}/> <Icon icon={"upload"} className={"mr-1"}/>
Upload Upload
</button> </button>
<button type={"button"} className={"btn btn-danger"} disabled={selectedCount === 0} <button type={"button"} className={"btn btn-danger"} disabled={selectedCount === 0}
onClick={() => this.deleteFiles(selectedIds)}> onClick={() => deleteFiles(selectedIds)}>
<Icon icon={"trash"} className={"mr-1"}/> <Icon icon={"trash"} className={"mr-1"}/>
Delete Selected Files ({selectedCount}) Delete Selected Files ({selectedCount})
</button> </button>
@ -320,72 +280,113 @@ export class FileBrowser extends React.Component {
</div> </div>
{ uploadZone } { uploadZone }
<div> <div>
{ alerts } { alertElements }
</div> </div>
<Popup title={"Create Directory"} visible={popup.visible} buttons={["Ok","Cancel"]} onClose={onPopupClose} onClick={onPopupButton}>
<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() {
setPopup({ ...popup, visible: true });
} }
fetchFiles() { function onPopupClose() {
if (this.state.token.valid) { setPopup({ ...popup, visible: false });
this.state.api.validateToken(this.state.token.value).then((res) => { }
if (res) {
this.setState({ ...this.state, files: res.files }); function onPopupChange(e, key) {
setPopup({ ...popup, [key]: e.target.value });
}
function onPopupButton(btn) {
if (btn === "Ok") {
api.createDirectory(popup.directoryName, null).then((res) => {
if (!res.success) {
pushAlert(res, "Error creating directory");
} else { } else {
this.pushAlert(res); fetchFiles();
}
})
}
onPopupClose();
}
function fetchFiles() {
if (tokenObj.valid) {
api.validateToken(tokenObj.value).then((res) => {
if (res) {
onFetchFiles(res.files);
} else {
pushAlert(res);
} }
}); });
} else if (this.state.api.loggedIn) { } else if (api.loggedIn) {
this.state.api.listFiles().then((res) => { api.listFiles().then((res) => {
if (res) { if (res) {
this.setState({ ...this.state, files: res.files }); onFetchFiles(res.files);
} else { } else {
this.pushAlert(res); pushAlert(res);
} }
}); });
} }
} }
onRemoveUploadedFile(e, i) { function onRemoveUploadedFile(e, i) {
e.stopPropagation(); e.stopPropagation();
let files = this.state.filesToUpload.slice(); let files = filesToUpload.slice();
files.splice(i, 1); files.splice(i, 1);
this.setState({ ...this.state, filesToUpload: files }); setFilesToUpload(files);
} }
pushAlert(res) { function pushAlert(res, title) {
let newAlerts = this.state.alerts.slice(); let newAlerts = alerts.slice();
newAlerts.push({ type: "danger", text: res.msg }); newAlerts.push({ type: "danger", message: res.msg, title: title });
this.setState({ ...this.state, alerts: newAlerts }); setAlerts(newAlerts);
} }
deleteFiles(selectedIds) { function removeAlert(i) {
if (i >= 0 && i < alerts.length) {
let newAlerts = alerts.slice();
newAlerts.splice(i, 1);
setAlerts(newAlerts);
}
}
function deleteFiles(selectedIds) {
if (selectedIds && selectedIds.length > 0) { if (selectedIds && selectedIds.length > 0) {
let token = (this.state.api.loggedIn ? null : this.state.token.value); let token = (api.loggedIn ? null : tokenObj.value);
this.state.api.delete(selectedIds, token).then((res) => { api.delete(selectedIds, token).then((res) => {
if (res.success) { if (res.success) {
this.fetchFiles(); fetchFiles();
} else { } else {
this.pushAlert(res); pushAlert(res);
} }
}); });
} }
} }
onUpload() { function onUpload() {
let token = (this.state.api.loggedIn ? null : this.state.token.value); let token = (api.loggedIn ? null : tokenObj.value);
this.state.api.upload(this.state.filesToUpload, token).then((res) => { api.upload(filesToUpload, token).then((res) => {
if (res.success) { if (res.success) {
this.setState({ ...this.state, filesToUpload: [] }) setFilesToUpload([]);
this.fetchFiles(); fetchFiles();
} else { } else {
this.pushAlert(res); pushAlert(res);
} }
}); });
} }
onDownload(selectedIds) { function onDownload(selectedIds) {
if (selectedIds && selectedIds.length > 0) { if (selectedIds && selectedIds.length > 0) {
let token = (this.state.api.loggedIn ? "" : "&token=" + this.state.token.value); let token = (api.loggedIn ? "" : "&token=" + tokenObj.value);
let ids = selectedIds.map(id => "id[]=" + id).join("&"); let ids = selectedIds.map(id => "id[]=" + id).join("&");
let downloadUrl = "/api/file/download?" + ids + token; let downloadUrl = "/api/file/download?" + ids + token;
fetch(downloadUrl) fetch(downloadUrl)

@ -1,37 +1,46 @@
import React from 'react'; import React from 'react';
class Popup extends React.Component { export function Popup(props) {
constructor(props) { let buttonNames = props.buttons || ["Ok", "Cancel"];
super(props); let onClick = props.onClick || function () { };
this.state = { let visible = !!props.visible;
title: props.title || "Title", let title = props.title || "Popup Title";
content: props.content || "Content", let onClose = props.onClose || function() { };
buttons: props.buttons || ["Ok", "Cancel"]
} let buttons = [];
const colors = ["primary", "secondary", "success", "warning", "danger"];
for (let i = 0; i < buttonNames.length; i++) {
let name = buttonNames[i];
let color = colors[i % colors.length];
buttons.push(
<button key={"btn-" + i} type={"button"} className={"btn btn-" + color} data-dismiss={"modal"}
onClick={() => onClick(name)}>
{name}
</button>
);
} }
return <>
render() { <div className={"modal fade" + (visible ? " show" : "")} tabIndex="-1" role="dialog" style={{display: (visible) ? "block" : "none"}}>
return <div className="modal" tabIndex="-1" role="dialog">
<div className="modal-dialog" role="document"> <div className="modal-dialog" role="document">
<div className="modal-content"> <div className="modal-content">
<div className="modal-header"> <div className="modal-header">
<h5 className="modal-title">{this.state.title}</h5> <h5 className="modal-title">{title}</h5>
<button type="button" className="close" data-dismiss="modal" aria-label="Close"> <button type="button" className="close" aria-label="Close" onClick={onClose}>
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>
<div className="modal-body"> <div className="modal-body">
<p>Modal body text goes here.</p> {props.children}
</div> </div>
<div className="modal-footer"> <div className="modal-footer">
<button type="button" className="btn btn-secondary" data-dismiss="modal">Close</button> {buttons}
<button type="button" className="btn btn-primary">Save changes</button>
</div> </div>
</div> </div>
</div> </div>
</div>; </div>
} {visible ? <div className={"modal-backdrop fade show"}/> : <></>}
</>;
} }

@ -1,105 +1,236 @@
import * as React from "react"; import * as React from "react";
import Icon from "./icon"; import Icon from "./icon";
import moment from "moment"; import moment from "moment";
import Popup from "react-popup"; import {Popup} from "./popup";
import Alert from "./alert";
import {useState} from "react";
export class TokenList extends React.Component { export function TokenList(props) {
constructor(props) { let api = props.api;
super(props); let selectedFiles = props.selectedFiles || [];
this.state = { let [tokens, setTokens] = useState(null);
api: props.api, let [alerts, setAlerts] = useState([]);
tokens: null, let [hideRevoked, setHideRevoked] = useState(true);
alerts: [] let [popup, setPopup] = useState({
} tokenType: "download",
maxFiles: 0,
maxSize: 0,
extensions: "",
durability: 24 * 60 * 2,
visible: false
});
function fetchTokens() {
api.listTokens().then((res) => {
if (res) {
setTokens(res.tokens);
} else {
pushAlert(res, "Error fetching tokens");
}
});
} }
render() {
let rows = []; let rows = [];
if (this.state.tokens === null) { if (tokens === null) {
this.state.api.listTokens().then((res) => { fetchTokens();
this.setState({ ...this.state, tokens: res.tokens }); } else {
}); for (const token of tokens) {
} else { const validUntil = token.valid_until;
for (const token of this.state.tokens) { const revoked = validUntil !== null && moment(validUntil).isSameOrBefore(new Date());
const validUntil = token.valid_until; if (revoked && hideRevoked) {
const revoked = validUntil !== null && moment(validUntil).isSameOrBefore(new Date()); continue;
const timeStr = (validUntil === null ? "Forever" : moment(validUntil).format("Do MMM YYYY, HH:mm"));
rows.push(
<tr key={"token-" + token.uid} className={revoked ? "token-revoked" : ""}>
<td>{token.token}</td>
<td>{token.type}</td>
<td>{timeStr}</td>
<td>
<Icon icon={"times"} className={"clickable text-" + (revoked ? "secondary" : "danger")}
onClick={() => (revoked ? null : this.onRevokeToken(token.token) )}
disabled={revoked} />
</td>
</tr>
);
} }
}
let alerts = []; const timeStr = (validUntil === null ? "Forever" : moment(validUntil).format("Do MMM YYYY, HH:mm"));
let i = 0;
for (const alert of this.state.alerts) { rows.push(
alerts.push( <tr key={"token-" + token.uid} className={revoked ? "token-revoked" : ""}>
<div key={"alert-" + i++} className={"alert alert-" + alert.type}> <td>{token.token}</td>
{ alert.text } <td>{token.type}</td>
</div> <td>{timeStr}</td>
<td>
<Icon icon={"times"} className={"clickable text-" + (revoked ? "secondary" : "danger")}
onClick={() => (revoked ? null : onRevokeToken(token.token))}
disabled={revoked}/>
<Icon icon={"save"} className={"clickable text-" + (revoked ? "secondary" : "info")}
onClick={() => (revoked ? null : onCopyToken(token.token))}
disabled={revoked}/>
</td>
</tr>
); );
} }
return <>
<h4>Tokens</h4>
<table className={"table token-table"}>
<thead>
<tr>
<th>Token</th>
<th>Type</th>
<th>Valid Until</th>
<th/>
</tr>
</thead>
<tbody>
{ rows }
</tbody>
</table>
<div>
<button type={"button"} className={"btn btn-success m-2"} onClick={this.onCreateToken.bind(this)}>
<Icon icon={"plus"} className={"mr-1"}/>
Create Token
</button>
</div>
<div>
{ alerts }
</div>
</>;
} }
onRevokeToken(token) { let alertElements = [];
this.state.api.revokeToken(token).then((res) => { for (let i = 0; i < alerts.length; i++) {
const alert = alerts[i];
alertElements.push(
<Alert key={"alert-" + i} {...alert} onClose={() => removeAlert(i)}/>
);
}
return <>
<h4>
<Icon icon={"sync"} className={"mx-3 clickable small"} onClick={fetchTokens}/>
Tokens
</h4>
<div className={"form-check p-3 ml-3"}>
<input type={"checkbox"} checked={hideRevoked} name={"hide-revoked"}
className={"form-check-input"} style={{marginTop: "0.2rem"}}
onChange={(e) => setHideRevoked(e.target.checked)}/>
<label htmlFor={"hide-revoked"} className={"form-check-label pl-2"}>Hide revoked</label>
</div>
<table className={"table token-table"}>
<thead>
<tr>
<th>Token</th>
<th>Type</th>
<th>Valid Until</th>
<th/>
</tr>
</thead>
<tbody>
{rows.length > 0 ? rows :
<tr>
<td colSpan={4} className={"text-center text-black-50"}>
No active tokens connected with this account
</td>
</tr>
}
</tbody>
</table>
<div>
<button type={"button"} className={"btn btn-success m-2"} onClick={onPopupOpen}>
<Icon icon={"plus"} className={"mr-1"}/>
Create Token
</button>
</div>
<div>
{alertElements}
</div>
<Popup title={"Create Token"} visible={popup.visible} buttons={["Ok", "Cancel"]}
onClose={onPopupClose} onClick={onPopupButton}>
<div className={"form-group"}>
<label>Token Durability in minutes (0 = forever):</label>
<input type={"number"} min={0} className={"form-control"}
value={popup.durability} onChange={(e) => onPopupChange(e, "durability")}/>
</div>
<div className="form-group">
<label>Token Type:</label>
<select value={popup.tokenType} className={"form-control"}
onChange={(e) => onPopupChange(e, "tokenType")}>
<option value={"upload"}>Upload</option>
<option value={"download"}>Download</option>
</select>
</div>
{popup.tokenType === "upload" ?
<>
<b>Upload Restrictions:</b>
<div className={"form-group"}>
<label>Max. Files (0 = unlimited):</label>
<input type={"number"} min={0} max={25} className={"form-control"}
value={popup.maxFiles}
onChange={(e) => onPopupChange(e, "maxFiles")}/>
</div>
<div className={"form-group"}>
<label>Max. Size per file in MB (0 = unlimited):</label>
<input type={"number"} min={0} max={10} className={"form-control"}
value={popup.maxSize} onChange={(e) => onPopupChange(e, "maxSize")}/>
</div>
<div className={"form-group"}>
<label>Allowed Extensions:</label>
<input type={"text"} placeholder={"(no restrictions)"} maxLength={256}
className={"form-control"}
value={popup.extensions}
onChange={(e) => onPopupChange(e, "extensions")}/>
</div>
</> :
<></>
}
</Popup>
</>;
function pushAlert(res, title) {
let newAlerts = alerts.slice();
newAlerts.push({type: "danger", message: res.msg, title: title});
setAlerts(newAlerts);
}
function removeAlert(i) {
if (i >= 0 && i < alerts.length) {
let newAlerts = alerts.slice();
newAlerts.splice(i, 1);
setAlerts(newAlerts);
}
}
function onRevokeToken(token) {
api.revokeToken(token).then((res) => {
if (res.success) { if (res.success) {
let newTokens = this.state.tokens.slice(); let newTokens = tokens.slice();
for (const tokenObj of newTokens) { for (const tokenObj of newTokens) {
if (tokenObj.token === token) { if (tokenObj.token === token) {
tokenObj.valid_until = moment(); tokenObj.valid_until = moment();
break; break;
} }
} }
this.setState({ ...this.state, tokens: newTokens }); setTokens(newTokens);
} else { } else {
let newAlerts = this.state.alerts.slice(); pushAlert(res, "Error revoking token");
newAlerts.push({ type: "danger", text: res.msg });
this.setState({ ...this.state, alerts: newAlerts });
} }
}); });
} }
onCreateToken() { function onPopupOpen() {
Popup.alert('I am alert, nice to meet you'); setPopup({...popup, visible: true});
}
function onPopupClose() {
setPopup({...popup, visible: false});
}
function onPopupChange(e, key) {
setPopup({...popup, [key]: e.target.value});
}
function onPopupButton(btn) {
if (btn === "Ok") {
let durability = popup.durability;
let validUntil = (durability === 0 ? null : moment().add(durability, "hours").format("YYYY-MM-DD HH:mm:ss"));
if (popup.tokenType === "download") {
api.createDownloadToken(durability, selectedFiles).then((res) => {
if (!res.success) {
pushAlert(res, "Error creating token");
} else {
let newTokens = tokens.slice();
newTokens.push({token: res.token, valid_until: validUntil, type: "download"});
setTokens(newTokens);
}
});
} else if (popup.tokenType === "upload") {
api.createUploadToken(durability, null, popup.maxFiles, popup.maxSize, popup.extensions).then((res) => {
if (!res.success) {
pushAlert(res, "Error creating token");
} else {
let newTokens = tokens.slice();
newTokens.push({uid: res.tokenId, token: res.token, valid_until: validUntil, type: "upload"});
setTokens(newTokens);
}
});
}
}
onPopupClose();
}
function onCopyToken(token) {
let url = window.location.href;
if (!url.endsWith("/")) url += "/";
url += token;
navigator.clipboard.writeText(url);
} }
} }

@ -16,10 +16,75 @@ class FileControlPanel extends React.Component {
errorMessage: "", errorMessage: "",
user: { }, user: { },
token: { valid: false, value: "", validUntil: null, type: null }, token: { valid: false, value: "", validUntil: null, type: null },
files: [], files: {}
}; };
} }
onFetchFiles(files) {
this.setState({ ...this.state, files: files });
}
getSelectedIds(items = null, recursive = true) {
let ids = [];
items = items || this.state.files;
for (const fileItem of Object.values(items)) {
if (fileItem.selected) {
ids.push(fileItem.uid);
}
if (recursive && fileItem.isDirectory) {
ids.push(...this.getSelectedIds(fileItem.items));
}
}
return ids;
}
onSelectAll(selected, items) {
for (const fileElement of Object.values(items)) {
fileElement.selected = selected;
if (fileElement.isDirectory) {
this.onSelectAll(selected, fileElement.items);
}
}
}
onSelectFile(e, uid, items=null) {
let found = false;
let updatedFiles = (items === null) ? {...this.state.files} : items;
if (updatedFiles.hasOwnProperty(uid)) {
let fileElement = updatedFiles[uid];
found = true;
fileElement.selected = e.target.checked;
if (fileElement.isDirectory) {
this.onSelectAll(fileElement.selected, fileElement.items);
}
} else {
for (const fileElement of Object.values(updatedFiles)) {
if (fileElement.isDirectory) {
if (this.onSelectFile(e, uid, fileElement.items)) {
if (!e.target.checked) {
fileElement.selected = false;
} else if (this.getSelectedIds(fileElement.items, false).length === Object.values(fileElement.items).length) {
fileElement.selected = true;
}
found = true;
break;
}
}
}
}
if (items === null) {
this.setState({
...this.state,
files: updatedFiles
});
}
return found;
}
onValidateToken(token = null) { onValidateToken(token = null) {
if (token === null) { if (token === null) {
this.setState({ ...this.state, validatingToken: true, errorMessage: "" }); this.setState({ ...this.state, validatingToken: true, errorMessage: "" });
@ -58,7 +123,6 @@ class FileControlPanel extends React.Component {
let start = (pathName.startsWith("/files/") ? ("/files/").length : 1); let start = (pathName.startsWith("/files/") ? ("/files/").length : 1);
let token = pathName.substr(start, end); let token = pathName.substr(start, end);
if (token) { if (token) {
// this.setState({ ...this.state, loaded: true, token: { ...this.state.token, value: token } });
this.onValidateToken(token); this.onValidateToken(token);
checkUser = false; checkUser = false;
} }
@ -78,10 +142,11 @@ class FileControlPanel extends React.Component {
return <>Loading <Icon icon={"spinner"} /></>; return <>Loading <Icon icon={"spinner"} /></>;
} else if (this.api.loggedIn || this.state.token.valid) { } else if (this.api.loggedIn || this.state.token.valid) {
let selectedIds = this.getSelectedIds();
let tokenList = (this.api.loggedIn) ? let tokenList = (this.api.loggedIn) ?
<div className={"row"}> <div className={"row"}>
<div className={"col-lg-8 col-md-10 col-sm-12 mx-auto"}> <div className={"col-lg-8 col-md-10 col-sm-12 mx-auto"}>
<TokenList api={this.api} /> <TokenList api={this.api} selectedFiles={selectedIds} />
</div> </div>
</div> : </div> :
<></>; <></>;
@ -91,7 +156,9 @@ class FileControlPanel extends React.Component {
<div className={"row"}> <div className={"row"}>
<div className={"col-lg-8 col-md-10 col-sm-12 mx-auto"}> <div className={"col-lg-8 col-md-10 col-sm-12 mx-auto"}>
<h2>File Control Panel</h2> <h2>File Control Panel</h2>
<FileBrowser files={this.state.files} token={this.state.token} api={this.api} /> <FileBrowser files={this.state.files} token={this.state.token} api={this.api}
onSelectFile={this.onSelectFile.bind(this)}
onFetchFiles={this.onFetchFiles.bind(this)}/>
</div> </div>
</div> </div>
{ tokenList } { tokenList }

14
js/files.min.js vendored

File diff suppressed because one or more lines are too long