(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 { LogRequestMiddleware } from "./log";
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 cors from "cors";
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.` });
});
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
*/

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

@ -66,6 +66,22 @@ function DeleteButton({onClick, enabled}) {
</>
}
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}
@ -98,7 +114,7 @@ function PathElement({name, index, onClick}) {
* @returns {JSX.Element}
* @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 <>
<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} />
@ -107,6 +123,9 @@ export default function PathDisplay({path, updatePath, backHome, backArrow, enab
<div className="ml-auto h-full flex items-center">
<DeleteButton onClick={remove} enabled={removeEnable} />
</div>
<div className="h-full flex items-center">
<MoveButton onClick={move} enabled={moveEnabled} />
</div>
<div className="h-full flex items-center">
<CreateButton onClick={create} />
</div>

View File

@ -9,6 +9,7 @@ import ChildrenLoading from "../components/ChildrenLoading.jsx";
import DownloadLoading from "../components/DownloadLoading.jsx";
import Uploader from "../components/Uploader.jsx";
import CreateDirectory from "../components/CreateDirectory.jsx";
import MoveDirectory from "../components/MoveDirectory.jsx";
export default function Dashboard() {
// ---- CONSTANTS ---- //
@ -50,6 +51,8 @@ export default function Dashboard() {
const [editing, setEditing] = useState("");
const [uploading, setUploading] = useState(false);
const [creating, setCreating] = useState(false);
const [moving, setMoving] = useState(false);
// Loading spinners
const [childrenLoading, setChildrenLoading] = useState(false);
@ -118,6 +121,16 @@ export default function Dashboard() {
*/
const closeCreate = () => setCreating(false);
/**
* Hide the moving modal.
*/
const closeMoving = () => setMoving(false);
/**
* Show the moving modal.
*/
const showMoving = () => setMoving(true);
// ---- HANDLERS --- //
/**
@ -225,7 +238,6 @@ export default function Dashboard() {
remove(files).then((data) => {
if (data.code === 201) {
console.log(data);
setSelected([]);
// 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 ---- //
/**
@ -399,9 +426,30 @@ export default function Dashboard() {
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 ---- //
// Update the file list from the server each time an action requires
@ -494,6 +542,7 @@ export default function Dashboard() {
{downloadLoading && <DownloadLoading />}
{creating && <CreateDirectory close={closeCreate} create={createDirectory} />}
{uploading && <Uploader close={toggleUploading} upload={upload} />}
{moving && <MoveDirectory close={closeMoving} move={moveSelected} path={[...path, selected[0]]} />}
{error && <Error error={error} clear={clearError} />}
{
@ -516,6 +565,8 @@ export default function Dashboard() {
create={createDir}
remove={removeSelected}
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">