From 43151731f410e1afbf83f48b70a6ade40cf8e673 Mon Sep 17 00:00:00 2001 From: subnub Date: Tue, 5 Nov 2024 21:25:38 -0500 Subject: [PATCH] started zip download changes --- backend/controllers/file-controller.ts | 2 +- backend/controllers/folder-controller.ts | 34 ++++ backend/express-routers/folder-router.ts | 2 + .../services/chunk-service/chunk-service.ts | 171 ++++++++++++++++++ .../services/folder-service/folder-service.ts | 5 +- package.json | 2 + src/api/foldersAPI.ts | 26 +++ src/components/ContextMenu/ContextMenu.tsx | 4 +- .../MultiSelectBar/MultiSelectBar.tsx | 57 ++++-- 9 files changed, 284 insertions(+), 19 deletions(-) diff --git a/backend/controllers/file-controller.ts b/backend/controllers/file-controller.ts index e79b65c..5b318ea 100644 --- a/backend/controllers/file-controller.ts +++ b/backend/controllers/file-controller.ts @@ -596,7 +596,7 @@ class FileController { if (!req.user) { return; } - let responseSent = false; + try { const user = req.user; const fileID = req.params.id; diff --git a/backend/controllers/folder-controller.ts b/backend/controllers/folder-controller.ts index ddc572a..e9feadf 100644 --- a/backend/controllers/folder-controller.ts +++ b/backend/controllers/folder-controller.ts @@ -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, diff --git a/backend/express-routers/folder-router.ts b/backend/express-routers/folder-router.ts index c6178f4..b6d71ad 100644 --- a/backend/express-routers/folder-router.ts +++ b/backend/express-routers/folder-router.ts @@ -39,6 +39,8 @@ router.get( folderController.getMoveFolderList ); +router.get("/folder-service/download-zip", auth, folderController.downloadZip); + // PATCH router.patch( diff --git a/backend/services/chunk-service/chunk-service.ts b/backend/services/chunk-service/chunk-service.ts index f2fa668..c9718eb 100644 --- a/backend/services/chunk-service/chunk-service.ts +++ b/backend/services/chunk-service/chunk-service.ts @@ -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(); + const previouslyUsedFileNames = new Map(); + + 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(); diff --git a/backend/services/folder-service/folder-service.ts b/backend/services/folder-service/folder-service.ts index a86ed25..44ebcf1 100644 --- a/backend/services/folder-service/folder-service.ts +++ b/backend/services/folder-service/folder-service.ts @@ -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) => { diff --git a/package.json b/package.json index 0244bf6..85641ca 100755 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/api/foldersAPI.ts b/src/api/foldersAPI.ts index 6f33fba..1a3b726 100644 --- a/src/api/foldersAPI.ts +++ b/src/api/foldersAPI.ts @@ -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) => { diff --git a/src/components/ContextMenu/ContextMenu.tsx b/src/components/ContextMenu/ContextMenu.tsx index 32c4d86..7c3ea88 100644 --- a/src/components/ContextMenu/ContextMenu.tsx +++ b/src/components/ContextMenu/ContextMenu.tsx @@ -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 = 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 = memo((props) => {

Share

)} - {!folderMode && !isTrash && ( + {!isTrash && (
{ 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
; return ( @@ -133,37 +159,40 @@ const MultiSelectBar: React.FC = () => {
- -

{multiSelectCount} selected

+

{multiSelectCount} selected

{!isTrash && ( {!isMedia && ( )} + )} {isTrash && (