(FEATURE): We can now move files! Heck yeah!
All checks were successful
Docker Deploy / build_and_deploy (push) Successful in 1m3s
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:
parent
92e02a821c
commit
f14e2c4781
@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
57
frontend/src/components/MoveDirectory.jsx
Normal file
57
frontend/src/components/MoveDirectory.jsx
Normal 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>
|
||||||
|
</>
|
||||||
|
}
|
||||||
@ -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}
|
* @param name {string}
|
||||||
@ -98,7 +114,7 @@ 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} />
|
||||||
@ -107,6 +123,9 @@ export default function PathDisplay({path, updatePath, backHome, backArrow, enab
|
|||||||
<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">
|
||||||
|
<MoveButton onClick={move} enabled={moveEnabled} />
|
||||||
|
</div>
|
||||||
<div className="h-full flex items-center">
|
<div className="h-full flex items-center">
|
||||||
<CreateButton onClick={create} />
|
<CreateButton onClick={create} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user