FileAPI + Frontend

This commit is contained in:
Roman Hergenreder 2021-01-10 01:08:03 +01:00
parent 9d52ec1832
commit 191afa06fb
14 changed files with 1044 additions and 145 deletions

2
.gitignore vendored

@ -1,2 +0,0 @@
fileControlPanel/public/

@ -100,7 +100,7 @@ namespace Api\File {
$res = $sql->select("UserFile.uid", "valid_until", "token_type", "maxFiles", "maxSize", "extensions", "name", "path", "directory") $res = $sql->select("UserFile.uid", "valid_until", "token_type", "maxFiles", "maxSize", "extensions", "name", "path", "directory")
->from("UserFileToken") ->from("UserFileToken")
->leftJoin("UserFileTokenFile", "UserFileToken.uid", "token_id") ->leftJoin("UserFileTokenFile", "UserFileToken.uid", "token_id")
->innerJoin("UserFile", "UserFile.uid", "file_id") ->leftJoin("UserFile", "UserFile.uid", "file_id")
->where(new Compare("token", $token)) ->where(new Compare("token", $token))
->where(new Compare("valid_until", $sql->now(), ">")) ->where(new Compare("valid_until", $sql->now(), ">"))
->execute(); ->execute();
@ -120,20 +120,22 @@ namespace Api\File {
$this->result["files"] = array(); $this->result["files"] = array();
foreach ($res as $row) { foreach ($res as $row) {
$file = array( if ($row["uid"]) {
"isDirectory" => $row["directory"], $file = array(
"name" => $row["name"], "isDirectory" => $row["directory"],
"uid" => $row["uid"] "name" => $row["name"],
); "uid" => $row["uid"]
);
if ($file["isDirectory"]) { if ($file["isDirectory"]) {
$file["items"] = array(); $file["items"] = array();
} else { } else {
$file["size"] = @filesize($row["path"]); $file["size"] = @filesize($row["path"]);
$file["mimeType"] = @mime_content_type($row["path"]); $file["mimeType"] = @mime_content_type($row["path"]);
}
$this->result["files"][] = $file;
} }
$this->result["files"][] = $file;
} }
if ($row["token_type"] === "upload") { if ($row["token_type"] === "upload") {
@ -314,6 +316,8 @@ namespace Api\File {
unset($row["maxSize"]); unset($row["maxSize"]);
unset($row["extensions"]); unset($row["extensions"]);
} }
unset($row["token_type"]);
$row["type"] = $tokenType;
$this->result["tokens"][] = $row; $this->result["tokens"][] = $row;
} }
} }
@ -464,8 +468,12 @@ namespace Api\File {
foreach ($_FILES as $key => $file) { foreach ($_FILES as $key => $file) {
$fileName = $file["name"]; $fileName = $file["name"];
$tmpPath = $file["tmp_name"]; $tmpPath = $file["tmp_name"];
$md5Hash = hash_file('md5', $tmpPath); if (!$tmpPath) {
$sha1Hash = hash_file('sha1', $tmpPath); return $this->createError("Error uploading file: $fileName");
}
$md5Hash = @hash_file('md5', $tmpPath);
$sha1Hash = @hash_file('sha1', $tmpPath);
$filePath = $uploadDir . "/" . $md5Hash . $sha1Hash; $filePath = $uploadDir . "/" . $md5Hash . $sha1Hash;
if (move_uploaded_file($tmpPath, $filePath)) { if (move_uploaded_file($tmpPath, $filePath)) {
$res = $sql->insert("UserFile", array("name", "directory", "path", "user_id", "parent_id")) $res = $sql->insert("UserFile", array("name", "directory", "path", "user_id", "parent_id"))

@ -17,6 +17,7 @@ namespace Documents {
namespace Documents\Files { namespace Documents\Files {
use Elements\Head; use Elements\Head;
use Elements\Script;
use Elements\SimpleBody; use Elements\SimpleBody;
class FilesHead extends Head { class FilesHead extends Head {
@ -54,6 +55,7 @@ namespace Documents\Files {
protected function getContent() { protected function getContent() {
$html = "<noscript>" . $this->createErrorText("Javascript is required for this site to render.") . "</noscript>"; $html = "<noscript>" . $this->createErrorText("Javascript is required for this site to render.") . "</noscript>";
$html .= "<div id=\"root\"></div>"; $html .= "<div id=\"root\"></div>";
$html .= new Script(Script::MIME_TEXT_JAVASCRIPT, Script::FILES);
return $html; return $html;
} }
} }

@ -11,6 +11,7 @@ class Script extends StaticView {
const INSTALL = "/js/install.js"; const INSTALL = "/js/install.js";
const BOOTSTRAP = "/js/bootstrap.bundle.min.js"; const BOOTSTRAP = "/js/bootstrap.bundle.min.js";
const ACCOUNT = "/js/account.js"; const ACCOUNT = "/js/account.js";
const FILES = "/js/files.min.js";
private string $type; private string $type;
private string $content; private string $content;

@ -10,6 +10,7 @@
# production # production
/build /build
public/
# misc # misc
.DS_Store .DS_Store

File diff suppressed because it is too large Load Diff

@ -36,6 +36,8 @@
"@babel/preset-env": "^7.10.2", "@babel/preset-env": "^7.10.2",
"@babel/preset-react": "^7.10.1", "@babel/preset-react": "^7.10.1",
"babel-loader": "^8.1.0", "babel-loader": "^8.1.0",
"babel-polyfill": "^6.26.0" "babel-polyfill": "^6.26.0",
"webpack": "^4.42.0",
"webpack-cli": "^4.3.1"
} }
} }

@ -13,7 +13,9 @@ export default class API {
async apiCall(method, params) { async apiCall(method, params) {
params = params || { }; params = params || { };
params.csrf_token = this.csrfToken();
const csrf_token = this.csrfToken();
if (csrf_token) params.csrf_token = csrf_token;
let response = await fetch("/api/" + method, { let response = await fetch("/api/" + method, {
method: 'post', method: 'post',
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
@ -34,4 +36,16 @@ export default class API {
async logout() { async logout() {
return this.apiCall("user/logout"); return this.apiCall("user/logout");
} }
validateToken(token) {
return this.apiCall("file/validateToken", { token: token });
}
listFiles() {
return this.apiCall("file/listFiles");
}
listTokens() {
return this.apiCall("file/listTokens");
}
}; };

@ -0,0 +1,60 @@
import * as React from "react";
export class FileBrowser extends React.Component {
constructor(props) {
super(props);
this.state = {
files: props.files,
}
}
formatSize(size) {
const suffixes = ["B","KiB","MiB","GiB","TiB"];
let i = 0;
for (; i < suffixes.length && size >= 1024; i++) {
size /= 1024.0;
}
return size.toFixed(1) + " " + suffixes[i];
}
render() {
let rows = [];
for (const [uid, fileElement] of Object.entries(this.state.files)) {
let name = fileElement.name;
let type = (fileElement.directory ? "Directory" : fileElement.mimeType);
let size = (fileElement.directory ? "" : fileElement.size)
// let iconUrl = (fileElement.directory ? "/img/icon/")
let iconUrl = "";
rows.push(
<tr key={"file-" + uid}>
<td><img src={iconUrl} alt={"[Icon]"} /></td>
<td>{name}</td>
<td>{type}</td>
<td>{this.formatSize(size)}</td>
</tr>
);
}
return <>
<h4>File Browser</h4>
<table className={"table"}>
<thead>
<tr>
<th/>
<th>Name</th>
<th>Type</th>
<th>Size</th>
</tr>
</thead>
<tbody>
{ rows }
</tbody>
</table>
</>;
}
}

@ -0,0 +1,24 @@
import * as React from "react";
export default function Icon(props) {
let classes = props.className || [];
classes = Array.isArray(classes) ? classes : classes.toString().split(" ");
let type = props.type || "fas";
let icon = props.icon;
classes.push(type);
classes.push("fa-" + icon);
if (icon === "spinner" || icon === "circle-notch") {
classes.push("fa-spin");
}
let newProps = {...props, className: classes.join(" ") };
delete newProps["type"];
delete newProps["icon"];
return (
<i {...newProps} />
);
}

@ -0,0 +1,49 @@
import * as React from "react";
export class TokenList extends React.Component {
constructor(props) {
super(props);
this.state = {
api: props.api,
tokens: null
}
}
render() {
let rows = [];
if (this.state.tokens === null) {
this.state.api.listTokens().then((res) => {
this.setState({ ...this.state, tokens: res.tokens });
});
} else {
for (const token of this.state.tokens) {
rows.push(
<tr key={"token-" + token.uid}>
<td>{token.token}</td>
<td>{token.type}</td>
<td>{token.valid_until}</td>
</tr>
);
}
}
return <>
<h4>Tokens</h4>
<table className={"table"}>
<thead>
<tr>
<th>Token</th>
<th>Type</th>
<th>Valid Until</th>
</tr>
</thead>
<tbody>
{ rows }
</tbody>
</table>
</>;
}
}

@ -1,8 +1,9 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import API from "./api"; import API from "./api";
import Icon from "./elements/icon";
import {FileBrowser} from "./elements/file-browser";
import {TokenList} from "./elements/token-list";
class FileControlPanel extends React.Component { class FileControlPanel extends React.Component {
@ -10,25 +11,94 @@ class FileControlPanel extends React.Component {
super(props); super(props);
this.api = new API(); this.api = new API();
this.state = { this.state = {
loaded: false loaded: false,
validatingToken: false,
errorMessage: "",
user: { },
token: { valid: false, value: "", validUntil: null, type: null },
files: [],
}; };
} }
render() { onValidateToken() {
this.setState({ ...this.state, validatingToken: true, errorMessage: "" })
if (!this.state.loaded) { this.api.validateToken(this.state.token.value).then((res) => {
this.api.fetchUser().then(() => { if (res.success) {
this.setState({ ...this.state, loaded: true }); this.setState({ ...this.state, validatingToken: false,
}); token: {
} else if (this.state.user.loggedIn) { ...this.state.token,
valid: true,
} else { validUntil: res.token.valid_util,
type: res.token.type
} },
files: res.files
return <></>; });
} else {
this.setState({ ...this.state, validatingToken: false, errorMessage: res.msg });
}
});
} }
onUpdateToken(e) {
this.setState({ ...this.state, token: { ...this.state.token, value: e.target.value } });
}
render() {
const self = this;
const errorMessageShown = !!this.state.errorMessage;
if (!this.state.loaded) {
this.api.fetchUser().then((isLoggedIn) => {
console.log(`api.fetchUser => ${isLoggedIn}`);
if (isLoggedIn) {
this.api.listFiles().then((res) => {
console.log(`api.listFiles => ${res.success}`);
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) ?
<div className={"row"}>
<div className={"col-lg-6 col-md-8 col-sm-10 col-xs-12 mx-auto"}>
<TokenList api={this.api} />
</div>
</div> :
<></>;
return <div className={"container mt-4"}>
<div className={"row"}>
<div className={"col-lg-6 col-md-8 col-sm-10 col-xs-12 mx-auto"}>
<h2>File Control Panel</h2>
<FileBrowser files={this.state.files}/>
</div>
</div>
{ tokenList }
</div>;
} else {
return <div className={"container mt-4"}>
<div className={"row"}>
<div className={"col-lg-6 col-md-8 col-sm-10 col-xs-12 mx-auto"}>
<h2>File Control Panel</h2>
<form onSubmit={(e) => e.preventDefault()}>
<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}>
{ this.state.validatingToken ? <>Validating <Icon icon={"spinner"}/></> : "Submit" }
</button>
</form>
<div className={"alert alert-danger mt-2"} hidden={!errorMessageShown}>
{ this.state.errorMessage }
</div>
</div>
</div>
</div>;
}
}
} }
ReactDOM.render( ReactDOM.render(

2
files/uploaded/.gitignore vendored Normal file

@ -0,0 +1,2 @@
*
!.htaccess

30
js/files.min.js vendored Normal file

File diff suppressed because one or more lines are too long