started zip download changes
This commit is contained in:
@@ -596,7 +596,7 @@ class FileController {
|
||||
if (!req.user) {
|
||||
return;
|
||||
}
|
||||
let responseSent = false;
|
||||
|
||||
try {
|
||||
const user = req.user;
|
||||
const fileID = req.params.id;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user