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

@ -57,6 +57,18 @@ export default class API {
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) {
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;
}
.token-table td:nth-child(4) > i {
padding-left: 10px;
}
.file-table td:nth-child(n+3), .file-table th:nth-child(n+3) {
text-align: center;
}

@ -2,22 +2,23 @@ import * as React from "react";
import "./file-browser.css";
import Dropzone from "react-dropzone";
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) {
super(props);
let files = props.files || { };
let api = props.api;
let tokenObj = props.token || { valid: false };
let onSelectFile = props.onSelectFile || function() { };
let onFetchFiles = props.onFetchFiles || function() { };
this.state = {
api: props.api,
files: props.files,
token: props.token,
filesToUpload: [],
alerts: []
}
}
let [popup, setPopup] = useState({ visible: false, directoryName: "" });
let [alerts, setAlerts] = useState( []);
let [filesToUpload, setFilesToUpload] = useState([]);
svgMiddle(indentation, scale=1.0) {
function svgMiddle(indentation, scale=1.0) {
let width = 48 * scale;
let height = 64 * scale;
let style = (indentation > 1 ? { marginLeft: ((indentation-1)*width) + "px" } : {});
@ -33,7 +34,7 @@ export class FileBrowser extends React.Component {
</svg>;
}
svgEnd(indentation, scale=1.0) {
function svgEnd(indentation, scale=1.0) {
let width = 48 * scale;
let height = 64 * scale;
let style = (indentation > 1 ? { marginLeft: ((indentation-1)*width) + "px" } : {});
@ -51,7 +52,7 @@ export class FileBrowser extends React.Component {
</svg>;
}
createFileIcon(mimeType, size=2) {
function createFileIcon(mimeType, size=2) {
let icon = "";
if (mimeType !== null) {
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"} />
}
formatSize(size) {
function formatSize(size) {
const suffixes = ["B","KiB","MiB","GiB","TiB"];
let i = 0;
for (; i < suffixes.length && size >= 1024; i++) {
@ -103,78 +104,32 @@ export class FileBrowser extends React.Component {
}
}
canUpload() {
return this.state.api.loggedIn || (this.state.token.valid && this.state.token.type === "upload");
function canUpload() {
return api.loggedIn || (tokenObj.valid && tokenObj.type === "upload");
}
onAddUploadFiles(acceptedFiles) {
let files = this.state.filesToUpload.slice();
function onAddUploadFiles(acceptedFiles) {
let files = filesToUpload.slice();
files.push(...acceptedFiles);
this.setState({ ...this.state, filesToUpload: files });
setFilesToUpload(files);
}
getSelectedIds(items = null, recursive = true) {
function getSelectedIds(items = null, recursive = true) {
let ids = [];
items = items || this.state.files;
items = items || 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));
ids.push(...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;
}
createFileList(elements, indentation=0) {
function createFileList(elements, indentation=0) {
let rows = [];
let i = 0;
const values = Object.values(elements);
@ -182,15 +137,15 @@ export class FileBrowser extends React.Component {
let name = fileElement.name;
let uid = fileElement.uid;
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 token = (this.state.token && this.state.token.valid ? "&token=" + this.state.token.value : "");
let token = (tokenObj && tokenObj.valid ? "&token=" + tokenObj.value : "");
let svg = <></>;
if (indentation > 0) {
if (i === values.length - 1) {
svg = this.svgEnd(indentation, 0.75);
svg = svgEnd(indentation, 0.75);
} 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"}>
<td>
{ svg }
{ this.createFileIcon(mimeType) }
{ createFileIcon(mimeType) }
</td>
<td>
{fileElement.isDirectory ? name :
@ -209,53 +164,49 @@ export class FileBrowser extends React.Component {
<td>{size}</td>
<td>
<input type={"checkbox"} checked={!!fileElement.selected}
onChange={(e) => this.onSelectFile(e, uid)}
onChange={(e) => onSelectFile(e, uid)}
/>
</td>
</tr>
);
if (fileElement.isDirectory) {
rows.push(...this.createFileList(fileElement.items, indentation + 1));
rows.push(...createFileList(fileElement.items, indentation + 1));
}
i++;
}
return rows;
}
render() {
let rows = this.createFileList(this.state.files);
let selectedIds = this.getSelectedIds();
let rows = createFileList(files);
let selectedIds = getSelectedIds();
let selectedCount = selectedIds.length;
let uploadZone = <></>;
let writePermissions = this.canUpload();
let writePermissions = canUpload();
let uploadedFiles = [];
let alerts = [];
let alertElements = [];
let i = 0;
for (const alert of this.state.alerts) {
alerts.push(
<div key={"alert-" + i++} className={"alert alert-" + alert.type}>
{ alert.text }
</div>
for (let i = 0; i < alerts.length; i++) {
const alert = alerts[i];
alertElements.push(
<Alert key={"alert-" + i} {...alert} onClose={() => removeAlert(i)} />
);
}
if (writePermissions) {
for(let i = 0; i < this.state.filesToUpload.length; i++) {
const file = this.state.filesToUpload[i];
for(let i = 0; i < filesToUpload.length; i++) {
const file = filesToUpload[i];
uploadedFiles.push(
<span className={"uploaded-file"} key={i}>
{ this.createFileIcon(file.type, 3) }
{ createFileIcon(file.type, 3) }
<span>{file.name}</span>
<Icon icon={"times"} onClick={(e) => this.onRemoveUploadedFile(e, i)}/>
<Icon icon={"times"} onClick={(e) => onRemoveUploadedFile(e, i)}/>
</span>
);
}
uploadZone = <><Dropzone onDrop={this.onAddUploadFiles.bind(this)}>
uploadZone = <><Dropzone onDrop={onAddUploadFiles}>
{({getRootProps, getInputProps}) => (
<section className={"file-upload-container"}>
<div {...getRootProps()}>
@ -273,7 +224,10 @@ export class FileBrowser extends React.Component {
}
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"}>
<thead>
<tr>
@ -285,17 +239,23 @@ export class FileBrowser extends React.Component {
</tr>
</thead>
<tbody>
{ rows }
{ 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={() => this.onDownload(selectedIds)}>
onClick={() => onDownload(selectedIds)}>
<Icon icon={"download"} className={"mr-1"}/>
Download Selected Files ({selectedCount})
</button>
{ this.state.api.loggedIn ?
<button type={"button"} className={"btn btn-info"}>
{ api.loggedIn ?
<button type={"button"} className={"btn btn-info"} onClick={onPopupOpen}>
<Icon icon={"plus"} className={"mr-1"}/>
Create Directory
</button> :
@ -305,12 +265,12 @@ export class FileBrowser extends React.Component {
writePermissions ?
<>
<button type={"button"} className={"btn btn-primary"} disabled={uploadedFiles.length === 0}
onClick={this.onUpload.bind(this)}>
onClick={onUpload}>
<Icon icon={"upload"} className={"mr-1"}/>
Upload
</button>
<button type={"button"} className={"btn btn-danger"} disabled={selectedCount === 0}
onClick={() => this.deleteFiles(selectedIds)}>
onClick={() => deleteFiles(selectedIds)}>
<Icon icon={"trash"} className={"mr-1"}/>
Delete Selected Files ({selectedCount})
</button>
@ -320,72 +280,113 @@ export class FileBrowser extends React.Component {
</div>
{ uploadZone }
<div>
{ alerts }
{ alertElements }
</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() {
if (this.state.token.valid) {
this.state.api.validateToken(this.state.token.value).then((res) => {
if (res) {
this.setState({ ...this.state, files: res.files });
function onPopupClose() {
setPopup({ ...popup, visible: false });
}
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 {
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) {
this.state.api.listFiles().then((res) => {
} else if (api.loggedIn) {
api.listFiles().then((res) => {
if (res) {
this.setState({ ...this.state, files: res.files });
onFetchFiles(res.files);
} else {
this.pushAlert(res);
pushAlert(res);
}
});
}
}
onRemoveUploadedFile(e, i) {
function onRemoveUploadedFile(e, i) {
e.stopPropagation();
let files = this.state.filesToUpload.slice();
let files = filesToUpload.slice();
files.splice(i, 1);
this.setState({ ...this.state, filesToUpload: files });
setFilesToUpload(files);
}
pushAlert(res) {
let newAlerts = this.state.alerts.slice();
newAlerts.push({ type: "danger", text: res.msg });
this.setState({ ...this.state, alerts: newAlerts });
function pushAlert(res, title) {
let newAlerts = alerts.slice();
newAlerts.push({ type: "danger", message: res.msg, title: title });
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) {
let token = (this.state.api.loggedIn ? null : this.state.token.value);
this.state.api.delete(selectedIds, token).then((res) => {
let token = (api.loggedIn ? null : tokenObj.value);
api.delete(selectedIds, token).then((res) => {
if (res.success) {
this.fetchFiles();
fetchFiles();
} else {
this.pushAlert(res);
pushAlert(res);
}
});
}
}
onUpload() {
let token = (this.state.api.loggedIn ? null : this.state.token.value);
this.state.api.upload(this.state.filesToUpload, token).then((res) => {
function onUpload() {
let token = (api.loggedIn ? null : tokenObj.value);
api.upload(filesToUpload, token).then((res) => {
if (res.success) {
this.setState({ ...this.state, filesToUpload: [] })
this.fetchFiles();
setFilesToUpload([]);
fetchFiles();
} else {
this.pushAlert(res);
pushAlert(res);
}
});
}
onDownload(selectedIds) {
function onDownload(selectedIds) {
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 downloadUrl = "/api/file/download?" + ids + token;
fetch(downloadUrl)

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

@ -1,31 +1,49 @@
import * as React from "react";
import Icon from "./icon";
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) {
super(props);
let api = props.api;
let selectedFiles = props.selectedFiles || [];
this.state = {
api: props.api,
tokens: null,
alerts: []
let [tokens, setTokens] = useState(null);
let [alerts, setAlerts] = useState([]);
let [hideRevoked, setHideRevoked] = useState(true);
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 = [];
if (this.state.tokens === null) {
this.state.api.listTokens().then((res) => {
this.setState({ ...this.state, tokens: res.tokens });
});
if (tokens === null) {
fetchTokens();
} else {
for (const token of this.state.tokens) {
for (const token of tokens) {
const validUntil = token.valid_until;
const revoked = validUntil !== null && moment(validUntil).isSameOrBefore(new Date());
if (revoked && hideRevoked) {
continue;
}
const timeStr = (validUntil === null ? "Forever" : moment(validUntil).format("Do MMM YYYY, HH:mm"));
rows.push(
@ -35,26 +53,36 @@ export class TokenList extends React.Component {
<td>{timeStr}</td>
<td>
<Icon icon={"times"} className={"clickable text-" + (revoked ? "secondary" : "danger")}
onClick={() => (revoked ? null : this.onRevokeToken(token.token) )}
disabled={revoked} />
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>
);
}
}
let alerts = [];
let i = 0;
for (const alert of this.state.alerts) {
alerts.push(
<div key={"alert-" + i++} className={"alert alert-" + alert.type}>
{ alert.text }
</div>
let alertElements = [];
for (let i = 0; i < alerts.length; i++) {
const alert = alerts[i];
alertElements.push(
<Alert key={"alert-" + i} {...alert} onClose={() => removeAlert(i)}/>
);
}
return <>
<h4>Tokens</h4>
<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>
@ -65,41 +93,144 @@ export class TokenList extends React.Component {
</tr>
</thead>
<tbody>
{ rows }
{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={this.onCreateToken.bind(this)}>
<button type={"button"} className={"btn btn-success m-2"} onClick={onPopupOpen}>
<Icon icon={"plus"} className={"mr-1"}/>
Create Token
</button>
</div>
<div>
{ alerts }
{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);
}
onRevokeToken(token) {
this.state.api.revokeToken(token).then((res) => {
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) {
let newTokens = this.state.tokens.slice();
let newTokens = tokens.slice();
for (const tokenObj of newTokens) {
if (tokenObj.token === token) {
tokenObj.valid_until = moment();
break;
}
}
this.setState({ ...this.state, tokens: newTokens });
setTokens(newTokens);
} else {
let newAlerts = this.state.alerts.slice();
newAlerts.push({ type: "danger", text: res.msg });
this.setState({ ...this.state, alerts: newAlerts });
pushAlert(res, "Error revoking token");
}
});
}
onCreateToken() {
Popup.alert('I am alert, nice to meet you');
function onPopupOpen() {
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: "",
user: { },
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) {
if (token === null) {
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 token = pathName.substr(start, end);
if (token) {
// this.setState({ ...this.state, loaded: true, token: { ...this.state.token, value: token } });
this.onValidateToken(token);
checkUser = false;
}
@ -78,10 +142,11 @@ class FileControlPanel extends React.Component {
return <>Loading <Icon icon={"spinner"} /></>;
} else if (this.api.loggedIn || this.state.token.valid) {
let selectedIds = this.getSelectedIds();
let tokenList = (this.api.loggedIn) ?
<div className={"row"}>
<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> :
<></>;
@ -91,7 +156,9 @@ class FileControlPanel extends React.Component {
<div className={"row"}>
<div className={"col-lg-8 col-md-10 col-sm-12 mx-auto"}>
<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>
{ tokenList }

14
js/files.min.js vendored

File diff suppressed because one or more lines are too long