Directory

This commit is contained in:
Roman Hergenreder 2021-01-14 21:45:58 +01:00
parent 9337faab97
commit e909e7b221
7 changed files with 127 additions and 66 deletions

@ -181,7 +181,7 @@ namespace Api\File {
$sql = $this->user->getSQL(); $sql = $this->user->getSQL();
$token = $this->getParam("token"); $token = $this->getParam("token");
$res = $sql->select("UserFile.uid", "valid_until", "token_type", $res = $sql->select("UserFile.uid", "valid_until", "token_type",
"maxFiles", "maxSize", "extensions", "name", "path", "directory", "parent_id as parentId") "maxFiles", "maxSize", "extensions", "name", "path", "directory", "UserFile.parent_id as parentId")
->from("UserFileToken") ->from("UserFileToken")
->leftJoin("UserFileTokenFile", "UserFileToken.uid", "token_id") ->leftJoin("UserFileTokenFile", "UserFileToken.uid", "token_id")
->leftJoin("UserFile", "UserFile.uid", "file_id") ->leftJoin("UserFile", "UserFile.uid", "file_id")
@ -203,30 +203,12 @@ namespace Api\File {
); );
$this->result["files"] = $this->createFileList($res); $this->result["files"] = $this->createFileList($res);
/*foreach ($res as $row) {
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"]);
}
$this->result["files"][] = $file;
}
}*/
if ($row["token_type"] === "upload") { if ($row["token_type"] === "upload") {
$this->result["restrictions"] = array( $this->result["restrictions"] = array(
"maxFiles" => $row["maxFiles"] ?? 0, "maxFiles" => $row["maxFiles"] ?? 0,
"maxSize" => $row["maxSize"] ?? 0, "maxSize" => $row["maxSize"] ?? 0,
"extensions" => $row["extensions"] ?? "" "extensions" => $row["extensions"] ?? "",
"parentId" => $row["parentId"] ?? 0
); );
} }
} }
@ -441,7 +423,7 @@ namespace Api\File {
if (!is_null($token)) { if (!is_null($token)) {
$res = $sql->select("uid", "token_type", "maxFiles", "maxSize", "extensions", "user_id") $res = $sql->select("uid", "token_type", "maxFiles", "maxSize", "extensions", "user_id", "parent_id")
->from("UserFileToken") ->from("UserFileToken")
->where(new Compare("token", $token)) ->where(new Compare("token", $token))
->where(new CondNull("valid_until"), new Compare("valid_until", $sql->now(), ">=")) ->where(new CondNull("valid_until"), new Compare("valid_until", $sql->now(), ">="))
@ -458,6 +440,7 @@ namespace Api\File {
return $this->createError("Permission denied (token)"); return $this->createError("Permission denied (token)");
} }
$parentId = $res[0]["parent_id"];
$tokenId = $res[0]["uid"]; $tokenId = $res[0]["uid"];
$maxFiles = $res[0]["maxFiles"] ?? 0; $maxFiles = $res[0]["maxFiles"] ?? 0;
$maxSize = $res[0]["maxSize"] ?? 0; $maxSize = $res[0]["maxSize"] ?? 0;
@ -675,7 +658,8 @@ namespace Api\File {
"maxFiles" => new Parameter("maxFiles", Parameter::TYPE_INT, true, 1), "maxFiles" => new Parameter("maxFiles", Parameter::TYPE_INT, true, 1),
"maxSize" => new Parameter("maxSize", Parameter::TYPE_INT, true, null), "maxSize" => new Parameter("maxSize", Parameter::TYPE_INT, true, null),
"extensions" => new StringType("extensions", 64, true, null), "extensions" => new StringType("extensions", 64, true, null),
"durability" => new Parameter("durability", Parameter::TYPE_INT, true, 60*24*2) "durability" => new Parameter("durability", Parameter::TYPE_INT, true, 60*24*2),
"parentId" => new Parameter("parentId", Parameter::TYPE_INT, true, null)
)); ));
$this->loginRequired = true; $this->loginRequired = true;
$this->csrfTokenRequired = false; $this->csrfTokenRequired = false;
@ -690,6 +674,7 @@ namespace Api\File {
$maxSize = $this->getParam("maxSize"); $maxSize = $this->getParam("maxSize");
$extensions = $this->getParam("extensions"); $extensions = $this->getParam("extensions");
$durability = $this->getParam("durability"); $durability = $this->getParam("durability");
$parentId = $this->getParam("parentId");
if (!is_null($maxFiles) && $maxFiles < 0) { if (!is_null($maxFiles) && $maxFiles < 0) {
return $this->createError("Invalid number of maximum files."); return $this->createError("Invalid number of maximum files.");
@ -715,12 +700,16 @@ namespace Api\File {
$extensions = implode(",", $extensions); $extensions = implode(",", $extensions);
} }
if (!$this->checkDirectory($parentId)) {
return $this->success;
}
$sql = $this->user->getSQL(); $sql = $this->user->getSQL();
$token = generateRandomString(36); $token = generateRandomString(36);
$validUntil = $durability == 0 ? null : (new \DateTime())->modify("+$durability MINUTES"); $validUntil = $durability == 0 ? null : (new \DateTime())->modify("+$durability MINUTES");
$res = $sql->insert("UserFileToken", $res = $sql->insert("UserFileToken",
array("token", "token_type", "maxSize", "maxFiles", "extensions", "valid_until", "user_id")) array("token", "token_type", "maxSize", "maxFiles", "extensions", "valid_until", "user_id", "parent_id"))
->addRow($token, "upload", $maxSize, $maxFiles, $extensions, $validUntil, $this->user->getId()) ->addRow($token, "upload", $maxSize, $maxFiles, $extensions, $validUntil, $this->user->getId(), $parentId)
->returning("uid") ->returning("uid")
->execute(); ->execute();

@ -56,9 +56,11 @@ class file_api extends DatabaseScript {
# upload only: # upload only:
->addInt("maxFiles", true) ->addInt("maxFiles", true)
->addInt("maxSize", true) ->addInt("maxSize", true)
->addInt("parent_id", true)
->addString("extensions", 64, true) ->addString("extensions", 64, true)
->primaryKey("uid") ->primaryKey("uid")
->foreignKey("user_id", "User", "uid", new CascadeStrategy()); ->foreignKey("user_id", "User", "uid", new CascadeStrategy())
->foreignKey("parent_id", "UserFile", "uid", new CascadeStrategy());
$queries[] = $sql->createTable("UserFileTokenFile") $queries[] = $sql->createTable("UserFileTokenFile")
->addInt("file_id") ->addInt("file_id")

@ -8,6 +8,8 @@ export default class API {
} }
csrfToken() { csrfToken() {
console.log(this.loggedIn);
console.log(this.user);
return this.loggedIn ? this.user.session.csrf_token : null; return this.loggedIn ? this.user.session.csrf_token : null;
} }

@ -13,41 +13,46 @@ export function FileBrowser(props) {
let tokenObj = props.token || { valid: false }; let tokenObj = props.token || { valid: false };
let onSelectFile = props.onSelectFile || function() { }; let onSelectFile = props.onSelectFile || function() { };
let onFetchFiles = props.onFetchFiles || function() { }; let onFetchFiles = props.onFetchFiles || function() { };
let directories = props.directories || {};
let [popup, setPopup] = useState({ visible: false, directoryName: "" }); let [popup, setPopup] = useState({ visible: false, directoryName: "", directory: 0, type: "upload" });
let [alerts, setAlerts] = useState( []); let [alerts, setAlerts] = useState( []);
let [filesToUpload, setFilesToUpload] = useState([]); let [filesToUpload, setFilesToUpload] = useState([]);
function svgMiddle(indentation, scale=1.0) {
function svgMiddle(scale=1.0) {
let width = 48 * scale; let width = 48 * scale;
let height = 64 * 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}> return <svg width={width} height={height} xmlns="http://www.w3.org/2000/svg">
<g> <g>
<line strokeLinecap="undefined" strokeLinejoin="undefined" y2="0" x2={width/2} <line y2="0" x2={width/2} y1={height} x1={width/2} strokeWidth="1.5" stroke="#000" fill="none"/>
y1={height} x1={width/2} strokeWidth="1.5" stroke="#000" fill="none"/> <line y2={height/2} x2={width} y1={height/2} 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> </g>
</svg>; </svg>;
} }
function svgEnd(indentation, scale=1.0) { function svgEnd(scale=1.0) {
let width = 48 * scale; let width = 48 * scale;
let height = 64 * 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}> return <svg width={width} height={height} xmlns="http://www.w3.org/2000/svg">
<g> <g>
{ /* vertical line */} { /* vertical line */}
<line strokeLinecap="undefined" strokeLinejoin="undefined" y2="0" x2={width/2} <line y2="0" x2={width/2} y1={height/2} x1={width/2} strokeWidth="1.5" stroke="#000" fill="none"/>
y1={height/2} x1={width/2} strokeWidth="1.5" stroke="#000" fill="none"/>
{ /* horizontal line */} { /* horizontal line */}
<line strokeLinecap="undefined" strokeLinejoin="undefined" y2={height/2} x2={width} <line y2={height/2} x2={width} y1={height/2} x1={width/2} strokeWidth="1.5" stroke="#000" fill="none"/>
y1={height/2} x1={width/2} fillOpacity="null" strokeOpacity="null" strokeWidth="1.5" </g>
stroke="#000" fill="none"/> </svg>;
}
function svgLeft(scale=1.0) {
let width = 48 * scale;
let height = 64 * scale;
return <svg width={width} height={height} xmlns="http://www.w3.org/2000/svg" style={{}}>
<g>
{ /* vertical line */}
<line y2="0" x2={width/2} y1={height} x1={width/2} strokeWidth="1.5" stroke="#000" fill="none"/>
</g> </g>
</svg>; </svg>;
} }
@ -140,12 +145,16 @@ export function FileBrowser(props) {
let size = (fileElement.isDirectory ? "" : formatSize(fileElement.size)); let size = (fileElement.isDirectory ? "" : formatSize(fileElement.size));
let mimeType = (fileElement.isDirectory ? "application/x-directory" : fileElement.mimeType); let mimeType = (fileElement.isDirectory ? "application/x-directory" : fileElement.mimeType);
let token = (tokenObj && tokenObj.valid ? "&token=" + tokenObj.value : ""); let token = (tokenObj && tokenObj.valid ? "&token=" + tokenObj.value : "");
let svg = <></>; let svg = [];
if (indentation > 0) { if (indentation > 0) {
for (let i = 0; i < indentation - 1; i++) {
svg.push(svgLeft(0.75));
}
if (i === values.length - 1) { if (i === values.length - 1) {
svg = svgEnd(indentation, 0.75); svg.push(svgEnd(0.75));
} else { } else {
svg = svgMiddle(indentation, 0.75); svg.push(svgMiddle(0.75));
} }
} }
@ -193,6 +202,13 @@ export function FileBrowser(props) {
); );
} }
let options = [];
for (const [uid, dir] of Object.entries(directories)) {
options.push(
<option key={"option-" + dir} value={uid}>{dir}</option>
);
}
if (writePermissions) { if (writePermissions) {
for(let i = 0; i < filesToUpload.length; i++) { for(let i = 0; i < filesToUpload.length; i++) {
@ -255,7 +271,7 @@ export function FileBrowser(props) {
Download Selected Files ({selectedCount}) Download Selected Files ({selectedCount})
</button> </button>
{ api.loggedIn ? { api.loggedIn ?
<button type={"button"} className={"btn btn-info"} onClick={onPopupOpen}> <button type={"button"} className={"btn btn-info"} onClick={(e) => onPopupOpen("createDirectory")}>
<Icon icon={"plus"} className={"mr-1"}/> <Icon icon={"plus"} className={"mr-1"}/>
Create Directory Create Directory
</button> : </button> :
@ -265,7 +281,7 @@ export function FileBrowser(props) {
writePermissions ? writePermissions ?
<> <>
<button type={"button"} className={"btn btn-primary"} disabled={uploadedFiles.length === 0} <button type={"button"} className={"btn btn-primary"} disabled={uploadedFiles.length === 0}
onClick={onUpload}> onClick={(e) => api.loggedIn ? onPopupOpen("upload") : onUpload()}>
<Icon icon={"upload"} className={"mr-1"}/> <Icon icon={"upload"} className={"mr-1"}/>
Upload Upload
</button> </button>
@ -284,15 +300,24 @@ export function FileBrowser(props) {
</div> </div>
<Popup title={"Create Directory"} visible={popup.visible} buttons={["Ok","Cancel"]} onClose={onPopupClose} onClick={onPopupButton}> <Popup title={"Create Directory"} visible={popup.visible} buttons={["Ok","Cancel"]} onClose={onPopupClose} onClick={onPopupButton}>
<div className={"form-group"}> <div className={"form-group"}>
<label>Directory Name</label> <label>Destination Directory:</label>
<input type={"text"} className={"form-control"} value={popup.directoryName} maxLength={32} placeholder={"Enter name…"} <select value={popup.directory} className={"form-control"}
onChange={(e) => onPopupChange(e, "directoryName")}/> onChange={(e) => onPopupChange(e, "directory")}>
{ options }
</select>
</div> </div>
{ popup.type !== "upload" ?
<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> </Popup>
</>; </>;
function onPopupOpen() { function onPopupOpen(type) {
setPopup({ ...popup, visible: true }); setPopup({ ...popup, visible: true, type: type });
} }
function onPopupClose() { function onPopupClose() {
@ -306,13 +331,18 @@ export function FileBrowser(props) {
function onPopupButton(btn) { function onPopupButton(btn) {
if (btn === "Ok") { if (btn === "Ok") {
api.createDirectory(popup.directoryName, null).then((res) => { let parentId = popup.directory === 0 ? null : popup.directory;
if (!res.success) { if (popup.type === "createDirectory") {
pushAlert(res, "Error creating directory"); api.createDirectory(popup.directoryName, parentId).then((res) => {
} else { if (!res.success) {
fetchFiles(); pushAlert(res, "Error creating directory");
} } else {
}) fetchFiles();
}
});
} else if (popup.type === "upload") {
onUpload();
}
} }
onPopupClose(); onPopupClose();
@ -374,7 +404,8 @@ export function FileBrowser(props) {
function onUpload() { function onUpload() {
let token = (api.loggedIn ? null : tokenObj.value); let token = (api.loggedIn ? null : tokenObj.value);
api.upload(filesToUpload, token).then((res) => { let parentId = ((!api.loggedIn || popup.directory === 0) ? null : popup.directory);
api.upload(filesToUpload, token, parentId).then((res) => {
if (res.success) { if (res.success) {
setFilesToUpload([]); setFilesToUpload([]);
fetchFiles(); fetchFiles();

@ -9,6 +9,7 @@ export function TokenList(props) {
let api = props.api; let api = props.api;
let selectedFiles = props.selectedFiles || []; let selectedFiles = props.selectedFiles || [];
let directories = props.directories || {};
let [tokens, setTokens] = useState(null); let [tokens, setTokens] = useState(null);
let [alerts, setAlerts] = useState([]); let [alerts, setAlerts] = useState([]);
@ -19,7 +20,8 @@ export function TokenList(props) {
maxSize: 0, maxSize: 0,
extensions: "", extensions: "",
durability: 24 * 60 * 2, durability: 24 * 60 * 2,
visible: false visible: false,
directory: 0
}); });
function fetchTokens() { function fetchTokens() {
@ -72,6 +74,13 @@ export function TokenList(props) {
); );
} }
let options = [];
for (const [uid, dir] of Object.entries(directories)) {
options.push(
<option key={"option-" + dir} value={uid}>{dir}</option>
);
}
return <> return <>
<h4> <h4>
<Icon icon={"sync"} className={"mx-3 clickable small"} onClick={fetchTokens}/> <Icon icon={"sync"} className={"mx-3 clickable small"} onClick={fetchTokens}/>
@ -128,6 +137,13 @@ export function TokenList(props) {
</div> </div>
{popup.tokenType === "upload" ? {popup.tokenType === "upload" ?
<> <>
<div className={"form-group"}>
<label>Destination Directory:</label>
<select value={popup.directory} className={"form-control"}
onChange={(e) => onPopupChange(e, "directory")}>
{ options }
</select>
</div>
<b>Upload Restrictions:</b> <b>Upload Restrictions:</b>
<div className={"form-group"}> <div className={"form-group"}>
<label>Max. Files (0 = unlimited):</label> <label>Max. Files (0 = unlimited):</label>
@ -212,7 +228,8 @@ export function TokenList(props) {
} }
}); });
} else if (popup.tokenType === "upload") { } else if (popup.tokenType === "upload") {
api.createUploadToken(durability, null, popup.maxFiles, popup.maxSize, popup.extensions).then((res) => { let parentId = popup.directory === 0 ? null : popup.directory;
api.createUploadToken(durability, parentId, popup.maxFiles, popup.maxSize, popup.extensions).then((res) => {
if (!res.success) { if (!res.success) {
pushAlert(res, "Error creating token"); pushAlert(res, "Error creating token");
} else { } else {

@ -24,6 +24,24 @@ class FileControlPanel extends React.Component {
this.setState({ ...this.state, files: files }); this.setState({ ...this.state, files: files });
} }
getDirectories(prefix = "/", items = null) {
let directories = { }
items = items || this.state.files;
if (prefix === "/") {
directories[0] = "/";
}
for(const fileItem of Object.values(items)) {
if (fileItem.isDirectory) {
let path = prefix + (prefix.length > 1 ? "/" : "") + fileItem.name;
directories[fileItem.uid] = path;
directories = Object.assign(directories, {...this.getDirectories(path, fileItem.items)});
}
}
return directories;
}
getSelectedIds(items = null, recursive = true) { getSelectedIds(items = null, recursive = true) {
let ids = []; let ids = [];
items = items || this.state.files; items = items || this.state.files;
@ -90,6 +108,7 @@ class FileControlPanel extends React.Component {
this.setState({ ...this.state, validatingToken: true, errorMessage: "" }); this.setState({ ...this.state, validatingToken: true, errorMessage: "" });
token = this.state.token.value; token = this.state.token.value;
} }
this.api.validateToken(token).then((res) => { this.api.validateToken(token).then((res) => {
let newState = { ...this.state, loaded: true, validatingToken: false }; let newState = { ...this.state, loaded: true, validatingToken: false };
if (res.success) { if (res.success) {
@ -143,10 +162,11 @@ class FileControlPanel extends React.Component {
return <>Loading <Icon icon={"spinner"} /></>; return <>Loading <Icon icon={"spinner"} /></>;
} else if (this.api.loggedIn || this.state.token.valid) { } else if (this.api.loggedIn || this.state.token.valid) {
let selectedIds = this.getSelectedIds(); let selectedIds = this.getSelectedIds();
let directories = this.getDirectories();
let tokenList = (this.api.loggedIn) ? let tokenList = (this.api.loggedIn) ?
<div className={"row"}> <div className={"row"}>
<div className={"col-lg-8 col-md-10 col-sm-12 mx-auto"}> <div className={"col-lg-8 col-md-10 col-sm-12 mx-auto"}>
<TokenList api={this.api} selectedFiles={selectedIds} /> <TokenList api={this.api} selectedFiles={selectedIds} directories={directories} />
</div> </div>
</div> : </div> :
<></>; <></>;
@ -156,7 +176,7 @@ class FileControlPanel extends React.Component {
<div className={"row"}> <div className={"row"}>
<div className={"col-lg-8 col-md-10 col-sm-12 mx-auto"}> <div className={"col-lg-8 col-md-10 col-sm-12 mx-auto"}>
<h2>File Control Panel</h2> <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} directories={directories}
onSelectFile={this.onSelectFile.bind(this)} onSelectFile={this.onSelectFile.bind(this)}
onFetchFiles={this.onFetchFiles.bind(this)}/> onFetchFiles={this.onFetchFiles.bind(this)}/>
</div> </div>

2
js/files.min.js vendored

File diff suppressed because one or more lines are too long