FEAT: Downloading as a zip is working really well! some minor bug fixes are needed but that's about it!
This commit is contained in:
parent
a60396545d
commit
f9d16ddce9
896
backend/package-lock.json
generated
896
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -12,10 +12,12 @@
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"archiver": "^7.0.1",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.21.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/archiver": "^6.0.3",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/node": "^22.13.9",
|
||||
|
||||
57
backend/src/download.ts
Normal file
57
backend/src/download.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
import archiver from "archiver";
|
||||
|
||||
/**
|
||||
* Append a file from the path to the archiver. Errors will be bubbled out
|
||||
* @param filePath Target path
|
||||
* @param archive The archiver
|
||||
* @constructor
|
||||
*/
|
||||
export function appendFileToArchive(filePath: string, archive: archiver.Archiver): void {
|
||||
try {
|
||||
const fileName = path.basename(filePath);
|
||||
archive.append(fs.createReadStream(filePath), {name: fileName});
|
||||
} catch (error) {
|
||||
throw new Error(`Error appending file to archive: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively append directories to
|
||||
* @param filePath
|
||||
* @param archiveRelPath
|
||||
* @param archive
|
||||
*/
|
||||
export function appendDirectoryToArchive(filePath: string, archiveRelPath: string = "", archive: archiver.Archiver): void {
|
||||
try {
|
||||
const files = fs.readdirSync(filePath);
|
||||
const relative = path.relative(path.dirname(filePath), filePath);
|
||||
|
||||
// Skip hidden folders, for now this is enabled
|
||||
// TODO: Implement a selector for hidden folders
|
||||
if (relative.startsWith(".")) {
|
||||
return
|
||||
}
|
||||
|
||||
files.forEach((file: string): void => {
|
||||
const fullPath: string = path.join(filePath, file);
|
||||
const relPath: string = path.join(archiveRelPath, file);
|
||||
|
||||
// Might need to skip symbolic links
|
||||
|
||||
// If it's file, append it the normal way, use the relative path as the name
|
||||
if (!fs.statSync(fullPath).isDirectory()) {
|
||||
archive.append(fs.createReadStream(fullPath), {name: relPath});
|
||||
|
||||
// Otherwise, call self with a new relative path
|
||||
} else {
|
||||
appendDirectoryToArchive(fullPath, relPath, archive);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
} catch (error) {
|
||||
throw new Error(`Error appending directory to archive: ${error}`);
|
||||
}
|
||||
}
|
||||
@ -5,6 +5,9 @@ import {LogRequestMiddleware} from "./log";
|
||||
import * as fs from "node:fs";
|
||||
import {entry} from "./entry";
|
||||
import cors from "cors";
|
||||
import archiver from "archiver";
|
||||
import {appendDirectoryToArchive, appendFileToArchive} from "./download";
|
||||
import path from "node:path";
|
||||
|
||||
/**
|
||||
* App details
|
||||
@ -27,6 +30,7 @@ APP.use(cors(corsOptions));
|
||||
* Apply middleware, this must be done before the routes are created.
|
||||
*/
|
||||
APP.use(LogRequestMiddleware);
|
||||
APP.use(express.json());
|
||||
|
||||
/**
|
||||
* Create routes for modular routing
|
||||
@ -53,6 +57,9 @@ v1.get("/", (req: Request, res: Response): void => {
|
||||
res.send("Hello world!");
|
||||
});
|
||||
|
||||
/**
|
||||
* Get the children of a directory provided in the path query.
|
||||
*/
|
||||
v1.get("/children", (req: Request, res: Response): void => {
|
||||
// Get the path, if it was not provided, use the root
|
||||
const path: string = (req.query.path || ROOT) as string;
|
||||
@ -82,6 +89,54 @@ v1.get("/children", (req: Request, res: Response): void => {
|
||||
res.status(200).json(children);
|
||||
});
|
||||
|
||||
/**
|
||||
* Down a group of files provided in the body
|
||||
*/
|
||||
v1.post("/download", (req: Request, res: Response): void => {
|
||||
// Get the files from the body
|
||||
const {filePaths} = req.body;
|
||||
|
||||
// Validate the path array
|
||||
if (!filePaths || !Array.isArray(filePaths) || filePaths.length === 0) {
|
||||
res.status(400).send({error: 'Invalid file paths provided.'});
|
||||
return;
|
||||
}
|
||||
|
||||
const archive: archiver.Archiver = archiver('zip', {
|
||||
zlib: {level: 9}, // Compression leve
|
||||
});
|
||||
|
||||
// Set the file headers
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=donwloads.zip');
|
||||
res.setHeader('Content-Type', 'application/zip');
|
||||
|
||||
archive.pipe(res);
|
||||
|
||||
// Add each file to the archive
|
||||
filePaths.forEach((filePath): void => {
|
||||
// This works for files, but for directories, we need to read the files
|
||||
|
||||
try {
|
||||
const stats = fs.statSync(filePath);
|
||||
if (!stats.isDirectory()) {
|
||||
appendFileToArchive(filePath, archive);
|
||||
} else {
|
||||
// Call this with the name to include the name of the directory
|
||||
appendDirectoryToArchive(filePath, path.basename(filePath), archive);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error adding file to zip: ${err}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Return errors
|
||||
archive.on('error', (err): void => {
|
||||
res.status(500).send({error: err.message});
|
||||
});
|
||||
|
||||
archive.finalize();
|
||||
});
|
||||
|
||||
/**
|
||||
* Apply the routes to the server
|
||||
*/
|
||||
|
||||
@ -23,13 +23,12 @@ function DirectoryIcon() {
|
||||
*
|
||||
* @param name {{name: string, path: string, directory: boolean}}
|
||||
* @param showHidden {boolean}
|
||||
* @param key {number}
|
||||
* @param appendPath {function(string)}
|
||||
* @param toggleSelected {function(string)}
|
||||
* @returns {{}}
|
||||
* @constructor
|
||||
*/
|
||||
export default function Directory({entry, showHidden, key, appendPath, toggleSelected}) {
|
||||
export default function Directory({entry, showHidden, appendPath, toggleSelected}) {
|
||||
const [selected, setSelected] = useState(false);
|
||||
|
||||
const handleClick = () => {
|
||||
@ -59,7 +58,7 @@ export default function Directory({entry, showHidden, key, appendPath, toggleSel
|
||||
<div className="w-full flex bg-gray-100 peer-checked:bg-blue-300 hover:bg-gray-300 peer-hover:bg-gray-300">
|
||||
<button className="flex items-center" onClick={handleClick}>
|
||||
{entry.directory ? <DirectoryIcon/> : <FileIcon/>}
|
||||
<p className="p-2 hover:underline hover:text-blue-400" key={key}>{entry.name}</p>
|
||||
<p className="p-2 hover:underline hover:text-blue-400">{entry.name}</p>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@ import Directory from "./Directory.jsx";
|
||||
export default function DirectoryList({dirs, showHidden, appendPath, toggleSelected}) {
|
||||
return (
|
||||
<>
|
||||
{dirs.map((dir, idx) => <Directory entry={dir} showHidden={showHidden} key={idx} appendPath={appendPath}
|
||||
{dirs.map((dir) => <Directory entry={dir} showHidden={showHidden} appendPath={appendPath}
|
||||
toggleSelected={toggleSelected}/>)}
|
||||
</>
|
||||
)
|
||||
|
||||
@ -18,12 +18,13 @@ function MainIcon() {
|
||||
|
||||
/**
|
||||
* Download button
|
||||
* @param downloadFiles {function}
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
function DownloadButton() {
|
||||
function DownloadButton({downloadFiles}) {
|
||||
return (
|
||||
<button className="text-black" title="Download files">
|
||||
<button className="text-black" title="Download files" onClick={downloadFiles}>
|
||||
<svg className="hover:bg-gray-300 mx-1 transition-colors duration-200 p-1.5 rounded-full h-8"
|
||||
viewBox="0 0 24 24" fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
@ -139,7 +140,7 @@ function SearchBar() {
|
||||
)
|
||||
}
|
||||
|
||||
export default function Navbar() {
|
||||
export default function Navbar({downloadFiles}) {
|
||||
return <nav className="absolute w-full p-2 flex items-center border-b-1 border-gray-400 bg-gray-100">
|
||||
<MainIcon/>
|
||||
|
||||
@ -147,7 +148,7 @@ export default function Navbar() {
|
||||
<SearchBar/>
|
||||
|
||||
<div className="min-h-fit ml-auto flex">
|
||||
<DownloadButton/>
|
||||
<DownloadButton downloadFiles={downloadFiles}/>
|
||||
<UploadButton/>
|
||||
<InfoButton/>
|
||||
<LogoutButton/>
|
||||
|
||||
@ -26,6 +26,8 @@ export default function Dashboard() {
|
||||
setFiles(data);
|
||||
});
|
||||
|
||||
setSelected([]);
|
||||
|
||||
}, [path]);
|
||||
|
||||
/**
|
||||
@ -90,17 +92,47 @@ export default function Dashboard() {
|
||||
}
|
||||
};
|
||||
|
||||
// Example of getting absolute paths, for download
|
||||
useEffect(() => {
|
||||
console.log(selected);
|
||||
selected.forEach((file) => {
|
||||
console.log(`/${path.join("/")}/${file}`);
|
||||
/**
|
||||
* Callback function for when the download button is clicked.
|
||||
*/
|
||||
const downloadFiles = () => {
|
||||
const download = async (paths) => {
|
||||
try {
|
||||
const resp = await fetch("http://localhost:5000/v1/download", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({filePaths: paths}),
|
||||
});
|
||||
}, [selected]);
|
||||
if (!resp.ok) {
|
||||
throw new Error(`HTTP error! status: ${resp.status}`);
|
||||
}
|
||||
|
||||
const blob = await resp.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = "downloads.zip";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch (err) {
|
||||
console.error(`Download error: ${err}`);
|
||||
}
|
||||
};
|
||||
const targets = [];
|
||||
selected.forEach((file) => {
|
||||
targets.push(`/${path.join("/")}/${file}`);
|
||||
});
|
||||
console.log(targets);
|
||||
download(targets);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full min-h-screen h-screen pb-8">
|
||||
<Navbar/>
|
||||
<Navbar downloadFiles={downloadFiles}/>
|
||||
<div className="h-full w-full flex flex-col items-center justify-center pb-8">
|
||||
|
||||
<PathDisplay path={path} updatePath={updatePath} backHome={backHome} backArrow={backArrow}/>
|
||||
|
||||
Reference in New Issue
Block a user