File Frontend + Array type Backend
This commit is contained in:
@@ -49,7 +49,32 @@ export default class API {
|
||||
return this.apiCall("file/listTokens");
|
||||
}
|
||||
|
||||
delete(id) {
|
||||
return this.apiCall("file/delete", { id: id })
|
||||
delete(id, token=null) {
|
||||
return this.apiCall("file/delete", { id: id, token: token });
|
||||
}
|
||||
|
||||
revokeToken(token) {
|
||||
return this.apiCall("file/revokeToken", { token: token });
|
||||
}
|
||||
|
||||
async upload(files, token = null, parentId = null) {
|
||||
const csrf_token = this.csrfToken();
|
||||
|
||||
const fd = new FormData();
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
fd.append('file' + i, files[i]);
|
||||
}
|
||||
|
||||
if (csrf_token) fd.append("csrf_token", csrf_token);
|
||||
if (token) fd.append("token", token);
|
||||
if (parentId) fd.append("parentId", parentId);
|
||||
|
||||
// send `POST` request
|
||||
let response = await fetch('/api/file/upload', {
|
||||
method: 'POST',
|
||||
body: fd
|
||||
});
|
||||
|
||||
return response.json();
|
||||
}
|
||||
};
|
||||
@@ -31,6 +31,7 @@
|
||||
.uploaded-file {
|
||||
max-width: 120px;
|
||||
position: relative;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.uploaded-file > img {
|
||||
@@ -44,10 +45,25 @@
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.uploaded-file > i {
|
||||
.uploaded-file > i:nth-child(3) {
|
||||
color: red;
|
||||
position: absolute;
|
||||
top: -9px;
|
||||
right: 25px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.clickable { cursor: pointer; }
|
||||
.token-revoked td { text-decoration: line-through; }
|
||||
|
||||
.token-table td:not(:first-child), .token-table th:not(:first-child) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.file-table td:nth-child(n+3), .file-table th:nth-child(n+3) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.file-table tr, .file-table td {
|
||||
height: 40px;
|
||||
}
|
||||
@@ -13,37 +13,82 @@ export class FileBrowser extends React.Component {
|
||||
files: props.files,
|
||||
token: props.token,
|
||||
filesToUpload: [],
|
||||
alerts: []
|
||||
}
|
||||
}
|
||||
|
||||
svgMiddle(indentation, size=64) {
|
||||
let style = (indentation > 1 ? { marginLeft: ((indentation-1)*size) + "px" } : {});
|
||||
return <svg width={size} height={size} xmlns="http://www.w3.org/2000/svg" style={style}>
|
||||
svgMiddle(indentation, scale=1.0) {
|
||||
let width = 48 * scale;
|
||||
let height = 64 * scale;
|
||||
let style = (indentation > 1 ? { marginLeft: ((indentation-1)*width) + "px" } : {});
|
||||
|
||||
return <svg width={width} height={height} xmlns="http://www.w3.org/2000/svg" style={style}>
|
||||
<g>
|
||||
<line strokeLinecap="undefined" strokeLinejoin="undefined" y2="0" x2={size/2}
|
||||
y1={size} x1={size/2} strokeWidth="1.5" stroke="#000" fill="none"/>
|
||||
<line strokeLinecap="undefined" strokeLinejoin="undefined" y2={size/2} x2={size}
|
||||
y1={size/2} x1={size/2} fillOpacity="null" strokeOpacity="null" strokeWidth="1.5"
|
||||
<line strokeLinecap="undefined" strokeLinejoin="undefined" y2="0" x2={width/2}
|
||||
y1={height} x1={width/2} strokeWidth="1.5" stroke="#000" fill="none"/>
|
||||
<line strokeLinecap="undefined" strokeLinejoin="undefined" y2={height/2} x2={width}
|
||||
y1={height/2} x1={width/2} fillOpacity="null" strokeOpacity="null" strokeWidth="1.5"
|
||||
stroke="#000" fill="none"/>
|
||||
</g>
|
||||
</svg>;
|
||||
}
|
||||
|
||||
svgEnd(indentation, size=64) {
|
||||
let style = (indentation > 1 ? { marginLeft: ((indentation-1)*size) + "px" } : {});
|
||||
return <svg width={size} height={size} xmlns="http://www.w3.org/2000/svg" style={style}>
|
||||
svgEnd(indentation, scale=1.0) {
|
||||
let width = 48 * scale;
|
||||
let height = 64 * scale;
|
||||
let style = (indentation > 1 ? { marginLeft: ((indentation-1)*width) + "px" } : {});
|
||||
|
||||
return <svg width={width} height={height} xmlns="http://www.w3.org/2000/svg" style={style}>
|
||||
<g>
|
||||
{ /* vertical line */}
|
||||
<line strokeLinecap="undefined" strokeLinejoin="undefined" y2="0" x2={size/2}
|
||||
y1={size/2} x1={size/2} strokeWidth="1.5" stroke="#000" fill="none"/>
|
||||
<line strokeLinecap="undefined" strokeLinejoin="undefined" y2="0" x2={width/2}
|
||||
y1={height/2} x1={width/2} strokeWidth="1.5" stroke="#000" fill="none"/>
|
||||
{ /* horizontal line */}
|
||||
<line strokeLinecap="undefined" strokeLinejoin="undefined" y2={size/2} x2={size}
|
||||
y1={size/2} x1={size/2} fillOpacity="null" strokeOpacity="null" strokeWidth="1.5"
|
||||
<line strokeLinecap="undefined" strokeLinejoin="undefined" y2={height/2} x2={width}
|
||||
y1={height/2} x1={width/2} fillOpacity="null" strokeOpacity="null" strokeWidth="1.5"
|
||||
stroke="#000" fill="none"/>
|
||||
</g>
|
||||
</svg>;
|
||||
}
|
||||
|
||||
createFileIcon(mimeType, size=2) {
|
||||
let icon = "";
|
||||
if (mimeType !== null) {
|
||||
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);
|
||||
}
|
||||
|
||||
return <Icon icon={icon} type={"far"} className={"p-1 align-middle fa-" + size + "x"} />
|
||||
}
|
||||
|
||||
formatSize(size) {
|
||||
const suffixes = ["B","KiB","MiB","GiB","TiB"];
|
||||
let i = 0;
|
||||
@@ -134,15 +179,14 @@ export class FileBrowser extends React.Component {
|
||||
let uid = fileElement.uid;
|
||||
let type = (fileElement.isDirectory ? "Directory" : fileElement.mimeType);
|
||||
let size = (fileElement.isDirectory ? "" : this.formatSize(fileElement.size));
|
||||
// let iconUrl = (fileElement.directory ? "/img/icon/")
|
||||
let iconUrl = "";
|
||||
let token = (this.state.token && this.state.token.valid ? "&token=" + this.token.state.value : "");
|
||||
let mimeType = (fileElement.isDirectory ? "application/x-directory" : fileElement.mimeType);
|
||||
let token = (this.state.token && this.state.token.valid ? "&token=" + this.state.token.value : "");
|
||||
let svg = <></>;
|
||||
if (indentation > 0) {
|
||||
if (i === values.length - 1) {
|
||||
svg = this.svgEnd(indentation, 48);
|
||||
svg = this.svgEnd(indentation, 0.75);
|
||||
} else {
|
||||
svg = this.svgMiddle(indentation, 48);
|
||||
svg = this.svgMiddle(indentation, 0.75);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,7 +194,7 @@ export class FileBrowser extends React.Component {
|
||||
<tr key={"file-" + uid} data-id={uid} className={"file-row"}>
|
||||
<td>
|
||||
{ svg }
|
||||
<img src={iconUrl} alt={"[Icon]"} />
|
||||
{ this.createFileIcon(mimeType) }
|
||||
</td>
|
||||
<td>
|
||||
{fileElement.isDirectory ? name :
|
||||
@@ -183,6 +227,16 @@ export class FileBrowser extends React.Component {
|
||||
let uploadZone = <></>;
|
||||
let writePermissions = this.canUpload();
|
||||
let uploadedFiles = [];
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
if (writePermissions) {
|
||||
|
||||
@@ -190,7 +244,7 @@ export class FileBrowser extends React.Component {
|
||||
const file = this.state.filesToUpload[i];
|
||||
uploadedFiles.push(
|
||||
<span className={"uploaded-file"} key={i}>
|
||||
<img />
|
||||
{ this.createFileIcon(file.type, 3) }
|
||||
<span>{file.name}</span>
|
||||
<Icon icon={"times"} onClick={(e) => this.onRemoveUploadedFile(e, i)}/>
|
||||
</span>
|
||||
@@ -203,7 +257,10 @@ export class FileBrowser extends React.Component {
|
||||
<div {...getRootProps()}>
|
||||
<input {...getInputProps()} />
|
||||
<p>Drag 'n' drop some files here, or click to select files</p>
|
||||
{ uploadedFiles.length === 0 ? <Icon className={"mx-auto fa-3x text-black-50"} icon={"upload"}/> : <div>{uploadedFiles}</div> }
|
||||
{ uploadedFiles.length === 0 ?
|
||||
<Icon className={"mx-auto fa-3x text-black-50"} icon={"upload"}/> :
|
||||
<div>{uploadedFiles}</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
@@ -213,7 +270,7 @@ export class FileBrowser extends React.Component {
|
||||
|
||||
return <>
|
||||
<h4>File Browser</h4>
|
||||
<table className={"table"}>
|
||||
<table className={"table data-table file-table"}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th/>
|
||||
@@ -242,11 +299,13 @@ export class FileBrowser extends React.Component {
|
||||
{
|
||||
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)}>
|
||||
<Icon icon={"upload"} className={"mr-1"}/>
|
||||
Upload
|
||||
</button>
|
||||
<button type={"button"} className={"btn btn-danger"} disabled={selectedCount === 0} onClick={(e) => this.deleteFiles(selectedIds)}>
|
||||
<button type={"button"} className={"btn btn-danger"} disabled={selectedCount === 0}
|
||||
onClick={(e) => this.deleteFiles(selectedIds)}>
|
||||
<Icon icon={"trash"} className={"mr-1"}/>
|
||||
Delete Selected Files ({selectedCount})
|
||||
</button>
|
||||
@@ -254,11 +313,33 @@ export class FileBrowser extends React.Component {
|
||||
: <></>
|
||||
}
|
||||
</div>
|
||||
|
||||
{ uploadZone }
|
||||
<div>
|
||||
{ alerts }
|
||||
</div>
|
||||
</>;
|
||||
}
|
||||
|
||||
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 });
|
||||
} else {
|
||||
this.pushAlert(res);
|
||||
}
|
||||
});
|
||||
} else if (this.state.api.loggedIn) {
|
||||
this.state.api.listFiles().then((res) => {
|
||||
if (res) {
|
||||
this.setState({ ...this.state, files: res.files });
|
||||
} else {
|
||||
this.pushAlert(res);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onRemoveUploadedFile(e, i) {
|
||||
e.stopPropagation();
|
||||
let files = this.state.filesToUpload.slice();
|
||||
@@ -266,11 +347,32 @@ export class FileBrowser extends React.Component {
|
||||
this.setState({ ...this.state, filesToUpload: files });
|
||||
}
|
||||
|
||||
pushAlert(res) {
|
||||
let newAlerts = this.state.alerts.slice();
|
||||
newAlerts.push({ type: "danger", text: res.msg });
|
||||
this.setState({ ...this.state, alerts: newAlerts });
|
||||
}
|
||||
|
||||
deleteFiles(selectedIds) {
|
||||
// TODO: delete files
|
||||
this.state.api.delete(selectedIds).then((res) => {
|
||||
let token = (this.state.api.loggedIn ? null : this.state.token.value);
|
||||
this.state.api.delete(selectedIds, token).then((res) => {
|
||||
if (res.success) {
|
||||
this.fetchFiles();
|
||||
} else {
|
||||
this.pushAlert(res);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onUpload() {
|
||||
let token = (this.state.api.loggedIn ? null : this.state.token.value);
|
||||
this.state.api.upload(this.state.filesToUpload, token).then((res) => {
|
||||
if (res.success) {
|
||||
this.setState({ ...this.state, filesToUpload: [] })
|
||||
this.fetchFiles();
|
||||
} else {
|
||||
this.pushAlert(res);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as React from "react";
|
||||
import Icon from "./icon";
|
||||
import moment from "moment";
|
||||
|
||||
export class TokenList extends React.Component {
|
||||
|
||||
@@ -8,7 +9,8 @@ export class TokenList extends React.Component {
|
||||
|
||||
this.state = {
|
||||
api: props.api,
|
||||
tokens: null
|
||||
tokens: null,
|
||||
alerts: []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,20 +23,35 @@ export class TokenList extends React.Component {
|
||||
});
|
||||
} else {
|
||||
for (const token of this.state.tokens) {
|
||||
const revoked = moment(token.valid_until).isSameOrBefore(new Date());
|
||||
rows.push(
|
||||
<tr key={"token-" + token.uid}>
|
||||
<tr key={"token-" + token.uid} className={revoked ? "token-revoked" : ""}>
|
||||
<td>{token.token}</td>
|
||||
<td>{token.type}</td>
|
||||
<td>{token.valid_until}</td>
|
||||
<td><Icon icon={"times"} className={"text-danger"}/></td>
|
||||
<td>{moment(token.valid_until).format("Do MMM YYYY, HH:mm")}</td>
|
||||
<td>
|
||||
<Icon icon={"times"} className={"clickable text-" + (revoked ? "secondary" : "danger")}
|
||||
onClick={() => (revoked ? null : this.onRevokeToken(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>
|
||||
);
|
||||
}
|
||||
|
||||
return <>
|
||||
<h4>Tokens</h4>
|
||||
<table className={"table"}>
|
||||
<table className={"table token-table"}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Token</th>
|
||||
@@ -53,6 +70,28 @@ export class TokenList extends React.Component {
|
||||
Create Token
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
{ alerts }
|
||||
</div>
|
||||
</>;
|
||||
}
|
||||
|
||||
onRevokeToken(token) {
|
||||
this.state.api.revokeToken(token).then((res) => {
|
||||
if (res.success) {
|
||||
let newTokens = this.state.tokens.slice();
|
||||
for (const tokenObj of newTokens) {
|
||||
if (tokenObj.token === token) {
|
||||
tokenObj.valid_until = moment();
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.setState({ ...this.state, tokens: newTokens });
|
||||
} else {
|
||||
let newAlerts = this.state.alerts.slice();
|
||||
newAlerts.push({ type: "danger", text: res.msg });
|
||||
this.setState({ ...this.state, alerts: newAlerts });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -20,22 +20,24 @@ class FileControlPanel extends React.Component {
|
||||
};
|
||||
}
|
||||
|
||||
onValidateToken() {
|
||||
this.setState({ ...this.state, validatingToken: true, errorMessage: "" })
|
||||
this.api.validateToken(this.state.token.value).then((res) => {
|
||||
onValidateToken(token = null) {
|
||||
if (token === null) {
|
||||
this.setState({ ...this.state, validatingToken: true, errorMessage: "" });
|
||||
token = this.state.token.value;
|
||||
}
|
||||
this.api.validateToken(token).then((res) => {
|
||||
let newState = { ...this.state, loaded: true, validatingToken: false };
|
||||
if (res.success) {
|
||||
this.setState({ ...this.state, validatingToken: false,
|
||||
token: {
|
||||
...this.state.token,
|
||||
valid: true,
|
||||
validUntil: res.token.valid_util,
|
||||
type: res.token.type
|
||||
},
|
||||
files: res.files
|
||||
});
|
||||
newState.token = { ...this.state.token, valid: true, validUntil: res.token.valid_until, type: res.token.type };
|
||||
if (!newState.token.value) {
|
||||
newState.token.value = token;
|
||||
}
|
||||
newState.files = res.files;
|
||||
} else {
|
||||
this.setState({ ...this.state, validatingToken: false, errorMessage: res.msg });
|
||||
newState.errorMessage = res.msg;
|
||||
}
|
||||
|
||||
this.setState(newState);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -48,15 +50,32 @@ class FileControlPanel extends React.Component {
|
||||
const errorMessageShown = !!this.state.errorMessage;
|
||||
|
||||
if (!this.state.loaded) {
|
||||
this.api.fetchUser().then((isLoggedIn) => {
|
||||
if (isLoggedIn) {
|
||||
this.api.listFiles().then((res) => {
|
||||
this.setState({ ...this.state, loaded: true, user: this.api.user, files: res.files });
|
||||
});
|
||||
} else {
|
||||
this.setState({ ...this.state, loaded: true, user: this.api.user });
|
||||
|
||||
let checkUser = true;
|
||||
let pathName = window.location.pathname;
|
||||
if (pathName.length > 1) {
|
||||
let end = (pathName.endsWith("/") ? pathName.length - 2 : pathName.length - 1);
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (checkUser) {
|
||||
this.api.fetchUser().then((isLoggedIn) => {
|
||||
if (isLoggedIn) {
|
||||
this.api.listFiles().then((res) => {
|
||||
this.setState({ ...this.state, loaded: true, user: this.api.user, files: res.files });
|
||||
});
|
||||
} else {
|
||||
this.setState({ ...this.state, loaded: true, user: this.api.user });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return <>Loading… <Icon icon={"spinner"} /></>;
|
||||
} else if (this.api.loggedIn || this.state.token.valid) {
|
||||
let tokenList = (this.api.loggedIn) ?
|
||||
@@ -85,7 +104,7 @@ class FileControlPanel extends React.Component {
|
||||
<label htmlFor={"token"}>Enter a file token to download or upload files</label>
|
||||
<input type={"text"} className={"form-control"} name={"token"} placeholder={"Enter token…"} maxLength={36}
|
||||
value={this.state.token.value} onChange={(e) => self.onUpdateToken(e)}/>
|
||||
<button className={"btn btn-success mt-2"} onClick={this.onValidateToken.bind(this)} disabled={this.state.validatingToken}>
|
||||
<button className={"btn btn-success mt-2"} onClick={() => this.onValidateToken()} disabled={this.state.validatingToken}>
|
||||
{ this.state.validatingToken ? <>Validating… <Icon icon={"spinner"}/></> : "Submit" }
|
||||
</button>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user