FEAT: Creation of files and directories!

This commit is contained in:
Hayden Hargreaves 2025-03-10 22:01:53 -07:00
parent 07952933b2
commit 0466a9c33a
5 changed files with 161 additions and 9 deletions

View File

@ -12,7 +12,7 @@ import {verifyToken} from "./authenicate";
import jwt from "jsonwebtoken";
import {config} from "dotenv";
import Multer from "multer";
import {readFileSync, rmSync, writeFileSync} from "fs";
import {mkdirSync, readFileSync, rmSync, writeFileSync} from "fs";
/**
* App details
@ -207,7 +207,7 @@ v1.post("/update", (req: Request, res: Response): void => {
try {
fs.writeFileSync(path, content);
res.status(204);
res.status(200).json({code: 200, message: "Success"});
} catch (error) {
res.status(500).json({code: 500, error})
}
@ -242,7 +242,6 @@ v1.post("/upload", upload.array("files"), (req: Request, res: Response) => {
// Directory to upload the files to
const cwd: string[] = JSON.parse(req.body.path);
const files = (req as any).files as UploadedFile[];
files.forEach((file) => {
try {
@ -272,6 +271,38 @@ v1.post("/upload", upload.array("files"), (req: Request, res: Response) => {
res.status(200).json({code: 200, message: "Success"});
});
v1.post("/create", (req: Request, res: Response): void => {
// Generate the path to create
const {cwd, name} = req.body;
try {
const newPath: string = path.join("/", ...cwd, name);
if (name.endsWith("/")) {
mkdirSync(newPath, {mode: "644"})
} else {
console.log("NOT DIR");
writeFileSync(newPath, "", {mode: "644"})
}
} catch (error: any) {
if (error.code === 'EACCES') {
res.status(403).json({code: 403, error: "Permission denied."}); // Specific error
return;
} else if (error.code === 'ENOSPC') {
res.status(507).json({code: 507, error: "Insufficient storage."}); // Specific error
return;
} else if (error instanceof TypeError) {
res.status(400).json({code: 400, error: "Invalid data type."}); // Example of instance check
return;
} else {
res.status(500).json({code: 500, error: "Error processing directory."}); // Generic error
return;
}
}
res.status(201).json({code: 201, message: "Success"});
});
/**
* Apply the routes to the server
*/

View File

@ -0,0 +1,55 @@
import {useState} from "react";
/**
* Create a file or directory, end with a / for a directory.
* @constructor
*/
export default function CreateDirectory({close, create}) {
const [dirName, setDirName] = useState("");
const createDirectory = () => {
create(dirName);
}
const updateDirName = (e) => {
setDirName(e.target.value);
}
const closeWithoutSaving = () => {
setDirName("");
close();
}
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="fixed -inset-10 bg-black opacity-50 blur-lg"></div>
<div className="relative z-50 bg-white p-8 rounded-lg shadow-lg w-2/5 border-1 border-gray-400">
<h2 className="text-2xl font-semibold mb-2 text-blue-400">Create a Directory</h2>
<p className="text-sm">
Create a file or directory in the current directory. To create a directory, include a <span
className="font-mono text-black bg-gray-300 p-1 rounded-md">/</span> at the end of the name.
Otherwise, the entry created will be a file.
</p>
<input className="border-b-2 border-blue-400 w-full p-2 my-4" type="text" name="directoryName"
value={dirName}
onInput={updateDirName}
placeholder="Directory name"/>
<div className="flex justify-end">
<button
onClick={closeWithoutSaving}
title="Close without creating"
className="bg-red-500 hover:bg-red-600 duration-100 text-white text-sm font-semibold py-1.5 px-3 mt-2 mx-2 rounded hover:cursor-pointer">
Close
</button>
<button
onClick={createDirectory}
title="Create directory"
className="bg-blue-400 hover:bg-blue-500 duration-100 text-white text-sm font-semibold py-1.5 px-3 mt-2 rounded hover:cursor-pointer">
Create
</button>
</div>
</div>
</div>
);
}

View File

@ -34,6 +34,22 @@ function BackButton({onClick, enabled}) {
)
}
function CreateButton({onClick}) {
return (
<button onClick={onClick}
title="Create a file/directory"
className="hover:bg-gray-200 p-1.5 mr-1 rounded-full transition-colors duration-150 disabled:text-gray-500 disabled:hover:bg-red-300 disabled:cursor-not-allowed text-black">
<svg className="h-5" viewBox="0 0 24 24" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M13 3H8.2C7.0799 3 6.51984 3 6.09202 3.21799C5.71569 3.40973 5.40973 3.71569 5.21799 4.09202C5 4.51984 5 5.0799 5 6.2V17.8C5 18.9201 5 19.4802 5.21799 19.908C5.40973 20.2843 5.71569 20.5903 6.09202 20.782C6.51984 21 7.0799 21 8.2 21H10M13 3L19 9M13 3V7.4C13 7.96005 13 8.24008 13.109 8.45399C13.2049 8.64215 13.3578 8.79513 13.546 8.89101C13.7599 9 14.0399 9 14.6 9H19M19 9V10M14 21L16.025 20.595C16.2015 20.5597 16.2898 20.542 16.3721 20.5097C16.4452 20.4811 16.5147 20.4439 16.579 20.399C16.6516 20.3484 16.7152 20.2848 16.8426 20.1574L21 16C21.5523 15.4477 21.5523 14.5523 21 14C20.4477 13.4477 19.5523 13.4477 19 14L14.8426 18.1574C14.7152 18.2848 14.6516 18.3484 14.601 18.421C14.5561 18.4853 14.5189 18.5548 14.4903 18.6279C14.458 18.7102 14.4403 18.7985 14.405 18.975L14 21Z"
stroke="#000000" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</button>
)
}
/**
*
* @param name {string}
@ -56,16 +72,20 @@ function PathElement({name, index, onClick}) {
* @param backHome {function}
* @param backArrow {function}
* @param enabled {boolean}
* @param create {function}
* @returns {JSX.Element}
* @constructor
*/
export default function PathDisplay({path, updatePath, backHome, backArrow, enabled}) {
export default function PathDisplay({path, updatePath, backHome, backArrow, enabled, create}) {
return (
<div
className="w-2/3 mt-8 border-b-1 border-gray-400 bg-white flex items-center truncate">
<HomeButton onClick={backHome} enabled={enabled}/>
<BackButton onClick={backArrow} enabled={enabled}/>
{path.map((seg, idx) => <PathElement name={seg} key={idx} index={idx} onClick={updatePath}/>)}
<div className="ml-auto h-full flex items-center">
<CreateButton onClick={create}/>
</div>
</div>
)
}

View File

@ -107,7 +107,7 @@ export default function Uploader({close, upload}) {
</button>
<button
onClick={uploadFiles}
title="Upload times"
title="Upload files"
className="bg-blue-400 hover:bg-blue-500 duration-100 text-white text-sm font-semibold py-1.5 px-3 mt-2 rounded hover:cursor-pointer">
Upload
</button>

View File

@ -8,6 +8,7 @@ import Editor from "../components/Editor.jsx";
import ChildrenLoading from "../components/ChildrenLoading.jsx";
import DownloadLoading from "../components/DownloadLoading.jsx";
import Uploader from "../components/Uploader.jsx";
import CreateDirectory from "../components/CreateDirectory.jsx";
export default function Dashboard() {
// Store the default path
@ -30,6 +31,7 @@ export default function Dashboard() {
const [error, setError] = useState(null);
const [editing, setEditing] = useState("");
const [uploading, setUploading] = useState(false);
const [creating, setCreating] = useState(false);
const [childrenLoading, setChildrenLoading] = useState(false);
const [downloadLoading, setDownloadLoading] = useState(false);
const [contentLoading, setContentLoading] = useState(false);
@ -84,7 +86,7 @@ export default function Dashboard() {
setSelected([]);
}, [path, uploading]);
}, [path, uploading, creating]);
// Redirect if the user isn't logged in, otherwise update the state.
@ -239,8 +241,10 @@ export default function Dashboard() {
// Send request to server to update the file. This will return nothing
// so no need for any promise handling.
updateContent(editing, newContent).finally(() => {
updateContent(editing, newContent).then((data) => {
if (data.code === 200) {
setEditing("");
}
});
};
@ -339,11 +343,53 @@ export default function Dashboard() {
});
};
const createDir = () => {
setCreating(true);
};
const closeCreate = () => {
setCreating(false);
};
/**
* Create a directory or file in the backend
* @param name {string} Name of new directory/file
*/
const createDirectory = (name) => {
const create = async (name, cwd) => {
const resp = await fetch(`${backendUrl}/v1/create`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"authorization": `Bearer ${token}`,
},
body: JSON.stringify({name, cwd})
});
if (!resp.ok) {
const data = await resp.json()
setError(data.error);
return data;
}
return await resp.json();
};
create(name, path).then((data) => {
if (data.code === 201) {
setCreating(false);
}
}).catch((error) => {
setError(error);
});
};
return (
<div className="w-full min-h-screen h-screen pb-8">
<Navbar downloadFiles={downloadFiles} uploadFiles={toggleUploading}/>
<div className="h-full w-full flex flex-col items-center justify-center pb-8">
{downloadLoading && <DownloadLoading/>}
{creating && <CreateDirectory close={closeCreate} create={createDirectory}/>}
{uploading && <Uploader close={toggleUploading} upload={upload}/>}
{error && <Error error={error} clear={clearError}/>}
@ -352,7 +398,7 @@ export default function Dashboard() {
loading={contentLoading}/>}
<PathDisplay path={path} updatePath={updatePath} backHome={backHome} backArrow={backArrow}
enabled={path.length > defaultPath.length}/>
enabled={path.length > defaultPath.length} create={createDir}/>
<div className="w-2/3 h-5/6 overflow-y-auto border-1 border-gray-300">
{childrenLoading && <ChildrenLoading/>}
<DirectoryList dirs={files} showHidden={showHidden} appendPath={appendPath}