Browse Source

FileAPI + Frontend

Roman Hergenreder 3 years ago
parent
commit
191afa06fb

+ 0 - 2
.gitignore

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

+ 23 - 15
core/Api/FileAPI.class.php

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

+ 2 - 0
core/Documents/Files.class.php

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

+ 1 - 0
core/Elements/Script.class.php

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

+ 1 - 0
fileControlPanel/.gitignore

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

File diff suppressed because it is too large
+ 741 - 112
fileControlPanel/package-lock.json


+ 3 - 1
fileControlPanel/package.json

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

+ 15 - 1
fileControlPanel/src/api.js

@@ -13,7 +13,9 @@ export default class API {
 
     async apiCall(method, 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, {
             method: 'post',
             headers: {'Content-Type': 'application/json'},
@@ -34,4 +36,16 @@ export default class API {
     async 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 - 0
fileControlPanel/src/elements/file-browser.js

@@ -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 - 0
fileControlPanel/src/elements/icon.js

@@ -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 - 0
fileControlPanel/src/elements/token-list.js

@@ -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>
+        </>;
+    }
+}

+ 80 - 10
fileControlPanel/src/index.js

@@ -1,8 +1,9 @@
 import React from 'react';
 import ReactDOM from 'react-dom';
 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 {
 
@@ -10,25 +11,94 @@ class FileControlPanel extends React.Component {
         super(props);
         this.api = new API();
         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() {
+        const self = this;
+        const errorMessageShown = !!this.state.errorMessage;
 
         if (!this.state.loaded) {
-            this.api.fetchUser().then(() => {
-                this.setState({ ...this.state, loaded: true });
+            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 });
+                }
             });
-        } else if (this.state.user.loggedIn) {
+            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>;
         }
-
-        return <></>;
     }
-
 }
 
 ReactDOM.render(

+ 2 - 0
files/uploaded/.gitignore

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

File diff suppressed because it is too large
+ 0 - 0
js/files.min.js


Some files were not shown because too many files changed in this diff