FileAPI + Frontend
This commit is contained in:
parent
9d52ec1832
commit
191afa06fb
2
.gitignore
vendored
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,6 +120,7 @@ namespace Api\File {
|
|||||||
|
|
||||||
$this->result["files"] = array();
|
$this->result["files"] = array();
|
||||||
foreach ($res as $row) {
|
foreach ($res as $row) {
|
||||||
|
if ($row["uid"]) {
|
||||||
$file = array(
|
$file = array(
|
||||||
"isDirectory" => $row["directory"],
|
"isDirectory" => $row["directory"],
|
||||||
"name" => $row["name"],
|
"name" => $row["name"],
|
||||||
@ -135,6 +136,7 @@ namespace Api\File {
|
|||||||
|
|
||||||
$this->result["files"][] = $file;
|
$this->result["files"][] = $file;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($row["token_type"] === "upload") {
|
if ($row["token_type"] === "upload") {
|
||||||
$this->result["restrictions"] = array(
|
$this->result["restrictions"] = array(
|
||||||
@ -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;
|
||||||
|
1
fileControlPanel/.gitignore
vendored
1
fileControlPanel/.gitignore
vendored
@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
# production
|
# production
|
||||||
/build
|
/build
|
||||||
|
public/
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
858
fileControlPanel/package-lock.json
generated
858
fileControlPanel/package-lock.json
generated
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");
|
||||||
|
}
|
||||||
};
|
};
|
60
fileControlPanel/src/elements/file-browser.js
Normal file
60
fileControlPanel/src/elements/file-browser.js
Normal file
@ -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>
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
}
|
24
fileControlPanel/src/elements/icon.js
Normal file
24
fileControlPanel/src/elements/icon.js
Normal file
@ -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} />
|
||||||
|
);
|
||||||
|
}
|
49
fileControlPanel/src/elements/token-list.js
Normal file
49
fileControlPanel/src/elements/token-list.js
Normal file
@ -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: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onValidateToken() {
|
||||||
|
this.setState({ ...this.state, validatingToken: true, errorMessage: "" })
|
||||||
|
this.api.validateToken(this.state.token.value).then((res) => {
|
||||||
|
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
|
||||||
|
});
|
||||||
|
} 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() {
|
render() {
|
||||||
|
const self = this;
|
||||||
|
const errorMessageShown = !!this.state.errorMessage;
|
||||||
|
|
||||||
if (!this.state.loaded) {
|
if (!this.state.loaded) {
|
||||||
this.api.fetchUser().then(() => {
|
this.api.fetchUser().then((isLoggedIn) => {
|
||||||
this.setState({ ...this.state, loaded: true });
|
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 if (this.state.user.loggedIn) {
|
|
||||||
|
|
||||||
} else {
|
} 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 <></>;
|
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
2
files/uploaded/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.htaccess
|
30
js/files.min.js
vendored
Normal file
30
js/files.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user