started zip download changes

This commit is contained in:
subnub
2024-11-05 21:25:38 -05:00
parent 29a4a8027b
commit 43151731f4
9 changed files with 284 additions and 19 deletions
+1 -1
View File
@@ -596,7 +596,7 @@ class FileController {
if (!req.user) {
return;
}
let responseSent = false;
try {
const user = req.user;
const fileID = req.params.id;
+34
View File
@@ -209,6 +209,40 @@ class FolderController {
}
};
downloadZip = async (req: RequestType, res: Response, next: NextFunction) => {
if (!req.user) {
return;
}
try {
const userID = req.user._id;
const folderIDs = (req.query.folderIDs as string[]) || [];
const fileIDs = (req.query.fileIDs as string[]) || [];
console.log("folderIDs", folderIDs);
const { archive } = await chunkService.downloadZip(
userID,
folderIDs,
fileIDs
);
archive.on("error", (e: Error) => {
console.log("archive error", e);
});
res.set("Content-Type", "application/zip");
res.set(
"Content-Disposition",
`attachment; filename="myDrive-${new Date().toISOString()}.zip"`
);
archive.pipe(res);
} catch (e) {
next(e);
}
};
getMoveFolderList = async (
req: RequestType,
res: Response,
+2
View File
@@ -39,6 +39,8 @@ router.get(
folderController.getMoveFolderList
);
router.get("/folder-service/download-zip", auth, folderController.downloadZip);
// PATCH
router.patch(
@@ -20,6 +20,7 @@ import { Readable } from "stream";
import ThumbnailDB from "../../db/mongoDB/thumbnailDB";
import UserDB from "../../db/mongoDB/userDB";
import fixEndChunkLength from "./utils/fixEndChunkLength";
import archiver from "archiver";
const fileDB = new FileDB();
const folderDB = new FolderDB();
@@ -156,6 +157,176 @@ class StorageService {
};
};
downloadZip = async (
userID: string,
folderIDs: string[],
fileIDs: string[]
) => {
const archive = archiver("zip", {
zlib: { level: 9 },
});
const user = await userDB.getUserInfo(userID);
if (!user) throw new NotFoundError("User Not Found");
const password = user.getEncryptionKey();
if (!password) throw new ForbiddenError("Invalid Encryption Key");
const parentInfoMap = new Map<string, { name: string }>();
const previouslyUsedFileNames = new Map<string, number>();
for (const folderID of folderIDs) {
const folder = await folderDB.getFolderInfo(folderID, userID);
if (!folder) throw new NotFoundError("Folder Info Not Found Error");
const parentList = [...folder.parentList, folder._id];
const files = await fileDB.getFileListByIncludedParent(
userID,
parentList.toString()
);
for (const file of files) {
const fileParent = await folderDB.getFolderInfo(
file.metadata.parent,
userID
);
if (!fileParent) throw new NotFoundError("File Parent Not Found Error");
let directory = "";
const parentSplit = file.metadata.parentList.split(",");
for (const parent of parentSplit) {
if (parent === "/") continue;
if (!parentInfoMap.has(parent)) {
const parentFolder = await folderDB.getFolderInfo(parent, userID);
if (!parentFolder)
throw new NotFoundError("Parent Folder Not Found Error");
parentInfoMap.set(parent, {
name: parentFolder.name,
});
directory += parentFolder.name + "/";
} else {
const savedName = parentInfoMap.get(parent)!.name;
directory += savedName + "/";
}
}
if (
previouslyUsedFileNames.has(
`${file.metadata.parent}/${file.filename}`
)
) {
const counter = previouslyUsedFileNames.get(
`${file.metadata.parent}/${file.filename}`
)!;
const extensionSplit = file.filename.split(".");
const extension = extensionSplit[extensionSplit.length - 1];
const filenameWithoutExtension = extensionSplit
.slice(0, -1)
.join(".");
directory += `${filenameWithoutExtension}-${counter}${
extension ? `.${extension}` : ""
}`;
previouslyUsedFileNames.set(
`${file.metadata.parent}/${file.filename}`,
+counter + 1
);
} else {
directory += file.filename;
previouslyUsedFileNames.set(
`${file.metadata.parent}/${file.filename}`,
1
);
}
const IV = file.metadata.IV;
const readStreamParams = createGenericParams({
filePath: file.metadata.filePath,
Key: file.metadata.s3ID,
});
const readStream = storageActions.createReadStream(readStreamParams);
readStream.on("error", (e: Error) => {
console.log("read stream error", e);
});
const CIPHER_KEY = crypto
.createHash("sha256")
.update(password)
.digest();
const decipher = crypto.createDecipheriv("aes256", CIPHER_KEY, IV);
decipher.on("error", (e: Error) => {
console.log("decipher stream error", e);
});
archive.append(readStream.pipe(decipher), { name: directory });
}
}
for (const fileID of fileIDs) {
const file = await fileDB.getFileInfo(fileID, userID);
if (!file) throw new NotFoundError("File Info Not Found Error");
const IV = file.metadata.IV;
const readStreamParams = createGenericParams({
filePath: file.metadata.filePath,
Key: file.metadata.s3ID,
});
const readStream = storageActions.createReadStream(readStreamParams);
readStream.on("error", (e: Error) => {
console.log("read stream error", e);
});
const CIPHER_KEY = crypto.createHash("sha256").update(password).digest();
const decipher = crypto.createDecipheriv("aes256", CIPHER_KEY, IV);
decipher.on("error", (e: Error) => {
console.log("decipher stream error", e);
});
if (previouslyUsedFileNames.has(file.filename)) {
const counter = previouslyUsedFileNames.get(file.filename)!;
const extensionSplit = file.filename.split(".");
const extension = extensionSplit[extensionSplit.length - 1];
const filenameWithoutExtension = extensionSplit.slice(0, -1).join(".");
archive.append(readStream.pipe(decipher), {
name: `${filenameWithoutExtension}-${counter}${
extension ? `.${extension}` : ""
}`,
});
previouslyUsedFileNames.set(file.filename, +counter + 1);
} else {
archive.append(readStream.pipe(decipher), { name: file.filename });
previouslyUsedFileNames.set(file.filename, 1);
}
}
archive.finalize();
return { archive };
};
getThumbnail = async (user: UserInterface, id: string) => {
const password = user.getEncryptionKey();
@@ -1,11 +1,9 @@
import Folder, { FolderInterface } from "../../models/folder-model";
import InternalServerError from "../../utils/InternalServerError";
import NotFoundError from "../../utils/NotFoundError";
import FileDB from "../../db/mongoDB/fileDB";
import FolderDB from "../../db/mongoDB/folderDB";
import sortBySwitch from "../../utils/sortBySwitchFolder";
import { UserInterface } from "../../models/user-model";
import { FolderListQueryType } from "../../types/folder-types";
import UserDB from "../../db/mongoDB/userDB";
type userAccessType = {
_id: string;
@@ -16,6 +14,7 @@ type userAccessType = {
const fileDB = new FileDB();
const folderDB = new FolderDB();
const userDB = new UserDB();
class FolderService {
createFolder = async (userID: string, name: string, parent: string) => {
+2
View File
@@ -65,6 +65,7 @@
"@types/uuid": "^7.0.2",
"@types/validator": "^13.0.0",
"@vitejs/plugin-react": "^4.3.0",
"archiver": "^7.0.1",
"autoprefixer": "^10.4.19",
"aws-sdk": "^2.657.0",
"axios": "^1.7.2",
@@ -122,6 +123,7 @@
},
"devDependencies": {
"@eslint/js": "^9.6.0",
"@types/archiver": "^6.0.3",
"@types/bytes": "^3.1.4",
"@types/fluent-ffmpeg": "^2.1.24",
"@types/lodash": "^4.17.5",
+26
View File
@@ -1,5 +1,6 @@
import { QueryFunctionContext } from "react-query";
import axios from "../axiosInterceptor";
import { getUserToken } from "./user";
interface QueryKeyParams {
parent: string;
@@ -61,6 +62,31 @@ export const getMoveFolderListAPI = async ({
return response.data;
};
export const downloadZIPAPI = async (
folderIDs: string[],
fileIDs: string[]
) => {
await getUserToken();
// TODO: Change this
let url = `http://localhost:5173/api/folder-service/download-zip?`;
for (const folderID of folderIDs) {
url += `folderIDs[]=${folderID}&`;
}
for (const fileID of fileIDs) {
url += `fileIDs[]=${fileID}&`;
}
const link = document.createElement("a");
document.body.appendChild(link);
link.href = url;
link.setAttribute("type", "hidden");
link.setAttribute("download", "true");
link.click();
};
// POST
export const createFolderAPI = async (name: string, parent?: string) => {
+3 -1
View File
@@ -10,6 +10,7 @@ import { useFilesClient, useQuickFilesClient } from "../../hooks/files";
import { useFoldersClient } from "../../hooks/folders";
import {
deleteFolderAPI,
downloadZIPAPI,
renameFolder,
restoreFolderAPI,
trashFolderAPI,
@@ -255,6 +256,7 @@ const ContextMenu: React.FC<ContextMenuProps> = memo((props) => {
const downloadItem = () => {
closeContext();
if (file) downloadFileAPI(file._id);
if (folder) downloadZIPAPI([folder._id], []);
};
const selectItemMultiSelect = () => {
@@ -326,7 +328,7 @@ const ContextMenu: React.FC<ContextMenuProps> = memo((props) => {
<p className="ml-2.5 text-sm">Share</p>
</div>
)}
{!folderMode && !isTrash && (
{!isTrash && (
<div
onClick={downloadItem}
className="text-gray-primary flex flex-row p-4 hover:bg-white-hover hover:text-primary"
@@ -18,6 +18,10 @@ import {
import RestoreIcon from "../../icons/RestoreIcon";
import { useUtils } from "../../hooks/utils";
import { toast } from "react-toastify";
import DownloadIcon from "../../icons/DownloadIcon";
import { downloadZIPAPI } from "../../api/foldersAPI";
import CloseIcon from "../../icons/CloseIcon";
import { useLocation } from "react-router-dom";
const MultiSelectBar: React.FC = () => {
const dispatch = useAppDispatch();
@@ -37,13 +41,19 @@ const MultiSelectBar: React.FC = () => {
const { isTrash, isMedia } = useUtils();
const location = useLocation();
// useEffect(() => {
// if (ignoreFirstMount.current) {
// ignoreFirstMount.current = false;
// } else {
// closeMultiSelect();
// }
// }, [isTrash]);
useEffect(() => {
if (ignoreFirstMount.current) {
ignoreFirstMount.current = false;
} else {
closeMultiSelect();
}
}, [isTrash]);
closeMultiSelect();
}, [location.pathname]);
const closeMultiSelect = useCallback(() => {
dispatch(resetMultiSelect());
@@ -126,6 +136,22 @@ const MultiSelectBar: React.FC = () => {
dispatch(setMoveModal({ type: "multi-select", file: null, folder: null }));
};
const downloadItems = () => {
const folders = [];
const files = [];
for (const key of Object.keys(multiSelectMap)) {
const item = multiSelectMap[key];
if (item.type === "folder") {
folders.push(item.id);
} else {
files.push(item.id);
}
}
downloadZIPAPI(folders, files);
};
if (!multiSelectMode) return <div></div>;
return (
@@ -133,37 +159,40 @@ const MultiSelectBar: React.FC = () => {
<div className="border border-[#ebe9f9] bg-[#ebe9f9] rounded-full p-2 px-5 text-black text-sm mb-4 max-w-[600px] w-full mt-4 min-w-[300px] shadow-lg">
<div className="flex flex-row items-center justify-between">
<div className="flex flex-row items-center">
<img
className="w-[22px] h-[22px] cursor-pointer"
src="/images/close_icon.png"
<CloseIcon
className="w-5 h-5 cursor-pointer hover:text-primary"
onClick={closeMultiSelect}
/>
<p className="ml-4">{multiSelectCount} selected</p>
<p className="ml-4 select-none">{multiSelectCount} selected</p>
</div>
<div className="flex flex-row items-center">
{!isTrash && (
<React.Fragment>
<TrashIcon
className="ml-4 cursor-pointer w-5 h-5"
className="ml-4 cursor-pointer w-5 h-5 hover:text-primary"
onClick={trashItems}
/>
{!isMedia && (
<Moveicon
className="ml-4 cursor-pointer w-5 h-5"
className="ml-4 cursor-pointer w-5 h-5 hover:text-primary"
onClick={moveItems}
/>
)}
<DownloadIcon
className="ml-4 cursor-pointer w-5 h-5 hover:text-primary"
onClick={downloadItems}
/>
</React.Fragment>
)}
{isTrash && (
<React.Fragment>
<RestoreIcon
className="ml-4 cursor-pointer w-5 h-5"
className="ml-4 cursor-pointer w-5 h-5 hover:text-primary"
onClick={restoreItems}
/>
<TrashIcon
className="ml-4 cursor-pointer text-red-500 w-5 h-5"
className="ml-4 cursor-pointer text-red-500 w-5 h-5 hover:text-red-700"
onClick={deleteItems}
/>
</React.Fragment>