(FEATURE): We can now move files! Heck yeah!
All checks were successful
Docker Deploy / build_and_deploy (push) Successful in 1m3s

Hope this works well, didn't test much
This commit is contained in:
Hayden Hargreaves 2025-05-29 22:35:17 -07:00
parent 92e02a821c
commit f14e2c4781
4 changed files with 181 additions and 18 deletions

View File

@ -3,7 +3,7 @@ import { Healthcheck } from "./healthcheck";
import { printEndpoints, validateHash } from "./utils"; import { printEndpoints, validateHash } from "./utils";
import { LogRequestMiddleware } from "./log"; import { LogRequestMiddleware } from "./log";
import * as fs from "node:fs"; import * as fs from "node:fs";
import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs"; import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync, existsSync } from "node:fs";
import { entry } from "./entry"; import { entry } from "./entry";
import cors from "cors"; import cors from "cors";
import archiver from "archiver"; import archiver from "archiver";
@ -362,6 +362,42 @@ v1.post("/remove", (req: Request, res: Response): void => {
res.status(201).json({ code: 201, message: `Deleted ${files.length} files.` }); res.status(201).json({ code: 201, message: `Deleted ${files.length} files.` });
}); });
v1.post("/move", (req: Request, res: Response): void => {
// Get the array of paths and the root
const { oldPath, newPath } = req.body;
try {
// Get path of new path
const newDirectory = path.dirname(newPath);
// Ensure the directory exists
if (!existsSync(newDirectory)) {
mkdirSync(newDirectory, { recursive: true });
console.log(`Created directory: ${newDirectory}`);
}
// Move the file
renameSync(oldPath, newPath);
console.log(`File moved from ${oldPath} to ${newPath}`);
res.status(200).json({ code: 200 });
} catch (err: any) {
console.error("Error moving file:", err);
// You might want to send a more specific error message based on 'err.code'
if (err.code === 'EXDEV') {
// This specific error means moving across different file systems.
// Synchronous fallback would be more complex (read/write stream synchronously),
// which is why async is preferred here. For synchronous, you'd generally
// just fail or implement a blocking copy/delete.
res.status(500).json({ code: 500, error: "Error: Cannot move across different file systems synchronously. Try copying and deleting." });
} else if (err.code === 'ENOENT') {
res.status(404).json({ code: 404, error: "Error: Source file or destination path not found." });
} else {
res.status(500).json({ code: 500, error: `Failed to move file: ${err.message}` });
}
}
});
/** /**
* Apply the routes to the server * Apply the routes to the server
*/ */

View File

@ -0,0 +1,57 @@
import { useState, useEffect } from "react";
/**
* Move a file or directory.
* @constructor
*/
export default function MoveDirectory({ close, move, path }) {
const [dirName, setDirName] = useState("");
useEffect(() => {
setDirName("/" + path.join("/"));
}, [path]);
const moveDirectory = () => move(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">
{/* Blured backdrop */}
<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-9/10 lg:w-2/5 border-1 border-gray-400">
<h2 className="text-2xl font-semibold mb-2 text-blue-400">
Move/Rename Directory
</h2>
<p className="text-sm">
Move a file or directory from the current directory to a new location. This can be used to rename files
if the base directory remains the same.
</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={moveDirectory}
title="Move 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">
Move
</button>
</div>
</div>
</div>
</>
}

View File

@ -6,7 +6,7 @@
* @returns {JSX.Element} * @returns {JSX.Element}
* @constructor * @constructor
*/ */
function HomeButton({onClick, enabled}) { function HomeButton({ onClick, enabled }) {
return <> return <>
<button <button
onClick={onClick} onClick={onClick}
@ -16,27 +16,27 @@ function HomeButton({onClick, enabled}) {
viewBox="0 0 16 16" viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="currentColor"> fill="currentColor">
<path d="M1 6V15H6V11C6 9.89543 6.89543 9 8 9C9.10457 9 10 9.89543 10 11V15H15V6L8 0L1 6Z"/> <path d="M1 6V15H6V11C6 9.89543 6.89543 9 8 9C9.10457 9 10 9.89543 10 11V15H15V6L8 0L1 6Z" />
</svg> </svg>
</button> </button>
</> </>
} }
function BackButton({onClick, enabled}) { function BackButton({ onClick, enabled }) {
return <> return <>
<button onClick={onClick} <button onClick={onClick}
disabled={!enabled} disabled={!enabled}
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"> 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="currentColor" xmlns="http://www.w3.org/2000/svg"> <svg className="h-5" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path <path
d="M4 10L3.29289 10.7071L2.58579 10L3.29289 9.29289L4 10ZM21 18C21 18.5523 20.5523 19 20 19C19.4477 19 19 18.5523 19 18L21 18ZM8.29289 15.7071L3.29289 10.7071L4.70711 9.29289L9.70711 14.2929L8.29289 15.7071ZM3.29289 9.29289L8.29289 4.29289L9.70711 5.70711L4.70711 10.7071L3.29289 9.29289ZM4 9L14 9L14 11L4 11L4 9ZM21 16L21 18L19 18L19 16L21 16ZM14 9C17.866 9 21 12.134 21 16L19 16C19 13.2386 16.7614 11 14 11L14 9Z"/> d="M4 10L3.29289 10.7071L2.58579 10L3.29289 9.29289L4 10ZM21 18C21 18.5523 20.5523 19 20 19C19.4477 19 19 18.5523 19 18L21 18ZM8.29289 15.7071L3.29289 10.7071L4.70711 9.29289L9.70711 14.2929L8.29289 15.7071ZM3.29289 9.29289L8.29289 4.29289L9.70711 5.70711L4.70711 10.7071L3.29289 9.29289ZM4 9L14 9L14 11L4 11L4 9ZM21 16L21 18L19 18L19 16L21 16ZM14 9C17.866 9 21 12.134 21 16L19 16C19 13.2386 16.7614 11 14 11L14 9Z" />
</svg> </svg>
</button> </button>
</> </>
} }
function CreateButton({onClick}) { function CreateButton({ onClick }) {
return <> return <>
<button onClick={onClick} <button onClick={onClick}
title="Create a file/directory" title="Create a file/directory"
@ -45,13 +45,13 @@ function CreateButton({onClick}) {
xmlns="http://www.w3.org/2000/svg"> xmlns="http://www.w3.org/2000/svg">
<path <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" 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"/> stroke="#000000" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg> </svg>
</button> </button>
</> </>
} }
function DeleteButton({onClick, enabled}) { function DeleteButton({ onClick, enabled }) {
return <> return <>
<button onClick={onClick} <button onClick={onClick}
disabled={!enabled} disabled={!enabled}
@ -60,12 +60,28 @@ function DeleteButton({onClick, enabled}) {
<svg className="h-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg className="h-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path <path
d="M18 6L17.1991 18.0129C17.129 19.065 17.0939 19.5911 16.8667 19.99C16.6666 20.3412 16.3648 20.6235 16.0011 20.7998C15.588 21 15.0607 21 14.0062 21H9.99377C8.93927 21 8.41202 21 7.99889 20.7998C7.63517 20.6235 7.33339 20.3412 7.13332 19.99C6.90607 19.5911 6.871 19.065 6.80086 18.0129L6 6M4 6H20M16 6L15.7294 5.18807C15.4671 4.40125 15.3359 4.00784 15.0927 3.71698C14.8779 3.46013 14.6021 3.26132 14.2905 3.13878C13.9376 3 13.523 3 12.6936 3H11.3064C10.477 3 10.0624 3 9.70951 3.13878C9.39792 3.26132 9.12208 3.46013 8.90729 3.71698C8.66405 4.00784 8.53292 4.40125 8.27064 5.18807L8 6" d="M18 6L17.1991 18.0129C17.129 19.065 17.0939 19.5911 16.8667 19.99C16.6666 20.3412 16.3648 20.6235 16.0011 20.7998C15.588 21 15.0607 21 14.0062 21H9.99377C8.93927 21 8.41202 21 7.99889 20.7998C7.63517 20.6235 7.33339 20.3412 7.13332 19.99C6.90607 19.5911 6.871 19.065 6.80086 18.0129L6 6M4 6H20M16 6L15.7294 5.18807C15.4671 4.40125 15.3359 4.00784 15.0927 3.71698C14.8779 3.46013 14.6021 3.26132 14.2905 3.13878C13.9376 3 13.523 3 12.6936 3H11.3064C10.477 3 10.0624 3 9.70951 3.13878C9.39792 3.26132 9.12208 3.46013 8.90729 3.71698C8.66405 4.00784 8.53292 4.40125 8.27064 5.18807L8 6"
stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/> stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg> </svg>
</button> </button>
</> </>
} }
function MoveButton({ onClick, enabled }) {
return <>
<button onClick={onClick}
disabled={!enabled}
title="Moved a selected 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="M12 21V3M12 21L14 19M12 21L10 19M12 3L14 5M12 3L10 5M21 12H3M21 12L19 10M21 12L19 14M3 12L5 10M3 12L5 14"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</button>
</>
}
/** /**
* *
* @param name {string} * @param name {string}
@ -74,7 +90,7 @@ function DeleteButton({onClick, enabled}) {
* @returns {JSX.Element} * @returns {JSX.Element}
* @constructor * @constructor
*/ */
function PathElement({name, index, onClick}) { function PathElement({ name, index, onClick }) {
const handleClick = () => onClick(index); const handleClick = () => onClick(index);
return <> return <>
<button onClick={handleClick}> <button onClick={handleClick}>
@ -98,17 +114,20 @@ function PathElement({name, index, onClick}) {
* @returns {JSX.Element} * @returns {JSX.Element}
* @constructor * @constructor
*/ */
export default function PathDisplay({path, updatePath, backHome, backArrow, enabled, create, remove, removeEnable}) { export default function PathDisplay({ path, updatePath, backHome, backArrow, enabled, create, remove, removeEnable, move, moveEnabled }) {
return <> return <>
<div className="w-9/10 lg:w-2/3 mt-8 border-b-1 border-gray-400 bg-white flex items-center truncate"> <div className="w-9/10 lg:w-2/3 mt-8 border-b-1 border-gray-400 bg-white flex items-center truncate">
<HomeButton onClick={backHome} enabled={enabled}/> <HomeButton onClick={backHome} enabled={enabled} />
<BackButton onClick={backArrow} enabled={enabled}/> <BackButton onClick={backArrow} enabled={enabled} />
{path.map((seg, idx) => <PathElement name={seg} key={idx} index={idx} onClick={updatePath}/>)} {path.map((seg, idx) => <PathElement name={seg} key={idx} index={idx} onClick={updatePath} />)}
<div className="ml-auto h-full flex items-center"> <div className="ml-auto h-full flex items-center">
<DeleteButton onClick={remove} enabled={removeEnable}/> <DeleteButton onClick={remove} enabled={removeEnable} />
</div> </div>
<div className="h-full flex items-center"> <div className="h-full flex items-center">
<CreateButton onClick={create}/> <MoveButton onClick={move} enabled={moveEnabled} />
</div>
<div className="h-full flex items-center">
<CreateButton onClick={create} />
</div> </div>
</div> </div>
</> </>

View File

@ -9,6 +9,7 @@ import ChildrenLoading from "../components/ChildrenLoading.jsx";
import DownloadLoading from "../components/DownloadLoading.jsx"; import DownloadLoading from "../components/DownloadLoading.jsx";
import Uploader from "../components/Uploader.jsx"; import Uploader from "../components/Uploader.jsx";
import CreateDirectory from "../components/CreateDirectory.jsx"; import CreateDirectory from "../components/CreateDirectory.jsx";
import MoveDirectory from "../components/MoveDirectory.jsx";
export default function Dashboard() { export default function Dashboard() {
// ---- CONSTANTS ---- // // ---- CONSTANTS ---- //
@ -50,6 +51,8 @@ export default function Dashboard() {
const [editing, setEditing] = useState(""); const [editing, setEditing] = useState("");
const [uploading, setUploading] = useState(false); const [uploading, setUploading] = useState(false);
const [creating, setCreating] = useState(false); const [creating, setCreating] = useState(false);
const [moving, setMoving] = useState(false);
// Loading spinners // Loading spinners
const [childrenLoading, setChildrenLoading] = useState(false); const [childrenLoading, setChildrenLoading] = useState(false);
@ -118,6 +121,16 @@ export default function Dashboard() {
*/ */
const closeCreate = () => setCreating(false); const closeCreate = () => setCreating(false);
/**
* Hide the moving modal.
*/
const closeMoving = () => setMoving(false);
/**
* Show the moving modal.
*/
const showMoving = () => setMoving(true);
// ---- HANDLERS --- // // ---- HANDLERS --- //
/** /**
@ -225,7 +238,6 @@ export default function Dashboard() {
remove(files).then((data) => { remove(files).then((data) => {
if (data.code === 201) { if (data.code === 201) {
console.log(data);
setSelected([]); setSelected([]);
// Fetch the new files & deselect everything // Fetch the new files & deselect everything
@ -236,6 +248,21 @@ export default function Dashboard() {
}); });
}; };
const moveSelected = (newPath) => {
const oldPath = "/" + [...path, selected].join("/");
console.log("@oldPath", oldPath);
console.log("@newPath", newPath);
move(oldPath, newPath).then((data) => {
if (data.code === 200) {
setMoving(false);
setSelected([]);
fetchFiles();
}
});
};
// ---- SERVER FUNCTIONS ---- // // ---- SERVER FUNCTIONS ---- //
/** /**
@ -399,9 +426,30 @@ export default function Dashboard() {
return data; return data;
} }
return await resp.json(); const json = await resp.json();
return json;
}; };
const move = async (oldPath, newPath) => {
const resp = await fetch(`${backendUrl}/v1/move`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"authorization": `Bearer ${token}`,
},
body: JSON.stringify({ oldPath, newPath })
});
if (!resp.ok) {
const data = await resp.json()
setError(data.error);
return data;
}
const json = await resp.json();
return json;
}
// ---- EFFECTS ---- // // ---- EFFECTS ---- //
// Update the file list from the server each time an action requires // Update the file list from the server each time an action requires
@ -494,6 +542,7 @@ export default function Dashboard() {
{downloadLoading && <DownloadLoading />} {downloadLoading && <DownloadLoading />}
{creating && <CreateDirectory close={closeCreate} create={createDirectory} />} {creating && <CreateDirectory close={closeCreate} create={createDirectory} />}
{uploading && <Uploader close={toggleUploading} upload={upload} />} {uploading && <Uploader close={toggleUploading} upload={upload} />}
{moving && <MoveDirectory close={closeMoving} move={moveSelected} path={[...path, selected[0]]} />}
{error && <Error error={error} clear={clearError} />} {error && <Error error={error} clear={clearError} />}
{ {
@ -516,6 +565,8 @@ export default function Dashboard() {
create={createDir} create={createDir}
remove={removeSelected} remove={removeSelected}
removeEnable={selected.length > 0} removeEnable={selected.length > 0}
move={showMoving}
moveEnabled={selected.length === 1}
/> />
<div className="w-9/10 lg:w-2/3 h-5/6 overflow-y-auto border-1 border-gray-300"> <div className="w-9/10 lg:w-2/3 h-5/6 overflow-y-auto border-1 border-gray-300">