diff --git a/backend/controllers/file-controller.ts b/backend/controllers/file-controller.ts index 41805f3..27e2457 100644 --- a/backend/controllers/file-controller.ts +++ b/backend/controllers/file-controller.ts @@ -56,47 +56,21 @@ class FileController { } }; - getFullThumbnail = async (req: RequestTypeFullUser, res: Response) => { + getFullThumbnail = async ( + req: RequestTypeFullUser, + res: Response, + next: NextFunction + ) => { if (!req.user) { return; } - let responseSent = false; try { const user = req.user; const fileID = req.params.id; - const { decipher, readStream, file } = - await this.chunkService.getFullThumbnail(user, fileID); - - readStream.on("error", (e: Error) => { - console.log("Get full thumbnail read stream error", e); - if (!responseSent) { - responseSent = true; - res.status(500).send("Server error getting full thumbnail"); - } - }); - - decipher.on("error", (e: Error) => { - console.log("Get full thumbnail decipher error", e); - if (!responseSent) { - responseSent = true; - res.status(500).send("Server error gettingfull thumbnail"); - } - }); - - res.set("Content-Type", "binary/octet-stream"); - res.set( - "Content-Disposition", - 'attachment; filename="' + file.filename + '"' - ); - res.set("Content-Length", file.metadata.size.toString()); - - readStream.pipe(decipher).pipe(res); + await this.chunkService.getFullThumbnail(user, fileID, res); } catch (e: unknown) { - if (e instanceof Error) { - console.log("\nGet Thumbnail Full Error File Route:", e.message); - } - res.status(500).send("Server error getting image"); + next(e); } }; @@ -121,35 +95,18 @@ class FileController { } }; - getPublicDownload = async (req: RequestType, res: Response) => { - let responseSent = false; + getPublicDownload = async ( + req: RequestType, + res: Response, + next: NextFunction + ) => { try { const ID = req.params.id; const tempToken = req.params.tempToken; - const { readStream, decipher, file } = - await this.chunkService.getPublicDownload(ID, tempToken, res); - - readStream.on("error", (e: Error) => { - console.log("read stream error", e); - }); - - decipher.on("error", (e: Error) => { - console.log("decipher stream error", e); - }); - - res.set("Content-Type", "binary/octet-stream"); - res.set( - "Content-Disposition", - 'attachment; filename="' + file.filename + '"' - ); - res.set("Content-Length", file.metadata.size.toString()); - - readStream.pipe(decipher).pipe(res); + await this.chunkService.getPublicDownload(ID, tempToken, res); } catch (e: unknown) { - if (e instanceof Error) { - console.log("\nGet Public Download Error File Route:", e.message); - } + next(e); } }; @@ -397,65 +354,23 @@ class FileController { } }; - streamVideo = async (req: RequestTypeFullUser, res: Response) => { + streamVideo = async ( + req: RequestTypeFullUser, + res: Response, + next: NextFunction + ) => { if (!req.user) { return; } - let responseSent = false; try { const user = req.user; const fileID = req.params.id; const headers = req.headers; - const { decipher, readStream, head } = - await this.chunkService.streamVideo(user, fileID, headers); - - const cleanUp = () => { - if (readStream) readStream.destroy(); - if (decipher) decipher.end(); - }; - - const handleError = (e: Error) => { - console.log("stream video read stream error", e); - cleanUp(); - }; - - readStream.on("error", handleError); - - decipher.on("error", handleError); - - readStream.on("end", () => { - if (!responseSent) { - responseSent = true; - res.end(); - } - cleanUp(); - }); - - readStream.on("close", () => { - cleanUp(); - }); - - decipher.on("end", () => { - if (!responseSent) { - responseSent = true; - res.end(); - } - cleanUp(); - }); - - decipher.on("close", () => { - cleanUp(); - }); - - res.writeHead(206, head); - - readStream.pipe(decipher).pipe(res); + await this.chunkService.streamVideo(user, fileID, headers, res); } catch (e: unknown) { - if (e instanceof Error) { - console.log("\nStream Video Error File Route:", e.message); - } + next(e); } }; diff --git a/backend/services/chunk-service/chunk-service.ts b/backend/services/chunk-service/chunk-service.ts index 860ce70..d5d9ab1 100644 --- a/backend/services/chunk-service/chunk-service.ts +++ b/backend/services/chunk-service/chunk-service.ts @@ -26,6 +26,7 @@ import getFolderBusboyData from "./utils/getFolderUploadBusboyData"; import { getStorageActions } from "./actions/helper-actions"; import getThumbnailData from "./utils/getThumbnailData"; import getFileData from "./utils/getFileData"; +import getPublicFileData from "./utils/getPublicFileData"; const fileDB = new FileDB(); const folderDB = new FolderDB(); @@ -352,38 +353,20 @@ class StorageService { return bufferData; }; - getFullThumbnail = async (user: UserInterface, fileID: string) => { - const userID = user._id; - - const file = await fileDB.getFileInfo(fileID, userID.toString()); - - if (!file) throw new NotFoundError("File Thumbnail Not Found"); - - const password = user.getEncryptionKey(); - - const IV = file.metadata.IV; - - if (!password) throw new ForbiddenError("Invalid Encryption Key"); - - const readStreamParams = createGenericParams({ - filePath: file.metadata.filePath, - Key: file.metadata.s3ID, - }); - - const readStream = storageActions.createReadStream(readStreamParams); - - const CIPHER_KEY = crypto.createHash("sha256").update(password).digest(); - - const decipher = crypto.createDecipheriv("aes256", CIPHER_KEY, IV); - - return { - readStream, - decipher, - file, - }; + getFullThumbnail = async ( + user: UserInterface, + fileID: string, + res: Response + ) => { + await getFileData(res, fileID, user); }; - streamVideo = async (user: UserInterface, fileID: string, headers: any) => { + streamVideo = async ( + user: UserInterface, + fileID: string, + headers: any, + res: Response + ) => { const userID = user._id; const currentFile = await fileDB.getFileInfo(fileID, userID.toString()); @@ -397,20 +380,17 @@ class StorageService { const range = headers.range; const parts = range.replace(/bytes=/, "").split("-"); - let start = parseInt(parts[0], 10); - let end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1; - const IV = currentFile.metadata.IV; + const start = parseInt(parts[0], 10); + const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1; const chunksize = end - start + 1; - let head = { + const head = { "Content-Range": "bytes " + start + "-" + end + "/" + fileSize, "Accept-Ranges": "bytes", "Content-Length": chunksize, "Content-Type": "video/mp4", }; - let currentIV = IV; - let fixedStart = 0; let fixedEnd = currentFile.length % 16 === 0 @@ -428,75 +408,13 @@ class StorageService { fixedStart = 0; } - const readStreamParams = createGenericParams({ - filePath: currentFile.metadata.filePath, - Key: currentFile.metadata.s3ID, - }); + res.writeHead(206, head); - if (fixedStart !== 0 && start !== 0) { - currentIV = (await storageActions.getPrevIV( - readStreamParams, - fixedStart - 16 - )) as Buffer; - } - - const readStream = storageActions.createReadStreamWithRange( - readStreamParams, - fixedStart, - fixedEnd - ); - - const CIPHER_KEY = crypto.createHash("sha256").update(password).digest(); - - const decipher = crypto.createDecipheriv("aes256", CIPHER_KEY, currentIV); - - decipher.setAutoPadding(false); - - return { - readStream, - decipher, - file: currentFile, - head, - }; + await getFileData(res, fileID, user, { start: fixedStart, end: fixedEnd }); }; getPublicDownload = async (fileID: string, tempToken: any, res: Response) => { - const file = await fileDB.getPublicFile(fileID); - - if (!file || !file.metadata.link || file.metadata.link !== tempToken) { - throw new NotAuthorizedError("File Not Public"); - } - - if (file.metadata.linkType === "one") { - await fileDB.removeOneTimePublicLink(fileID); - } - - const user = await userDB.getUserInfo(file.metadata.owner); - - if (!user) throw new NotFoundError("User Not Found"); - - const password = user.getEncryptionKey(); - - if (!password) throw new ForbiddenError("Invalid Encryption Key"); - - const IV = file.metadata.IV; - - const readStreamParams = createGenericParams({ - filePath: file.metadata.filePath, - Key: file.metadata.s3ID, - }); - - const readStream = storageActions.createReadStream(readStreamParams); - - const CIPHER_KEY = crypto.createHash("sha256").update(password).digest(); - - const decipher = crypto.createDecipheriv("aes256", CIPHER_KEY, IV); - - return { - readStream, - decipher, - file: file, - }; + await getPublicFileData(res, fileID, tempToken); }; deleteMulti = async ( diff --git a/backend/services/chunk-service/utils/getFileData.ts b/backend/services/chunk-service/utils/getFileData.ts index d45c901..d0dfb7f 100644 --- a/backend/services/chunk-service/utils/getFileData.ts +++ b/backend/services/chunk-service/utils/getFileData.ts @@ -12,7 +12,12 @@ const fileDB = new FileDB(); const storageActions = getStorageActions(); -const proccessData = (res: Response, fileID: string, user: UserInterface) => { +const proccessData = ( + res: Response, + fileID: string, + user: UserInterface, + range?: { start: number; end: number } +) => { const eventEmitter = new EventEmitter(); const processFile = async () => { @@ -32,12 +37,27 @@ const proccessData = (res: Response, fileID: string, user: UserInterface) => { Key: currentFile.metadata.s3ID, }); - const readStream = storageActions.createReadStream(readStreamParams); + let readStream; + + if (range) { + readStream = storageActions.createReadStreamWithRange( + readStreamParams, + range.start, + range.end + ); + } else { + readStream = storageActions.createReadStream(readStreamParams); + } const CIPHER_KEY = crypto.createHash("sha256").update(password).digest(); const decipher = crypto.createDecipheriv("aes256", CIPHER_KEY, IV); + if (range) { + // TODO: Check if we need this for video streaming + decipher.setAutoPadding(false); + } + decipher.on("error", (e: Error) => { eventEmitter.emit("error", e); }); @@ -50,12 +70,14 @@ const proccessData = (res: Response, fileID: string, user: UserInterface) => { eventEmitter.emit("error", e); }); - res.set("Content-Type", "binary/octet-stream"); - res.set( - "Content-Disposition", - 'attachment; filename="' + currentFile.filename + '"' - ); - res.set("Content-Length", currentFile.metadata.size.toString()); + if (!range) { + res.set("Content-Type", "binary/octet-stream"); + res.set( + "Content-Disposition", + 'attachment; filename="' + currentFile.filename + '"' + ); + res.set("Content-Length", currentFile.metadata.size.toString()); + } readStream .pipe(decipher) @@ -73,9 +95,14 @@ const proccessData = (res: Response, fileID: string, user: UserInterface) => { return eventEmitter; }; -const getFileData = (res: Response, fileID: string, user: UserInterface) => { +const getFileData = ( + res: Response, + fileID: string, + user: UserInterface, + range?: { start: number; end: number } +) => { return new Promise((resolve, reject) => { - const eventEmitter = proccessData(res, fileID, user); + const eventEmitter = proccessData(res, fileID, user, range); eventEmitter.on("finish", (data) => { resolve(data); }); diff --git a/backend/services/chunk-service/utils/getPublicFileData.ts b/backend/services/chunk-service/utils/getPublicFileData.ts new file mode 100644 index 0000000..b815c5a --- /dev/null +++ b/backend/services/chunk-service/utils/getPublicFileData.ts @@ -0,0 +1,105 @@ +import { EventEmitter } from "stream"; +import { UserInterface } from "../../../models/user-model"; +import { Response } from "express"; +import ForbiddenError from "../../../utils/ForbiddenError"; +import NotFoundError from "../../../utils/NotFoundError"; +import crypto from "crypto"; +import { createGenericParams } from "./storageHelper"; +import { getStorageActions } from "../actions/helper-actions"; +import FileDB from "../../../db/mongoDB/fileDB"; +import NotAuthorizedError from "../../../utils/NotAuthorizedError"; +import UserDB from "../../../db/mongoDB/userDB"; + +const fileDB = new FileDB(); +const userDB = new UserDB(); + +const storageActions = getStorageActions(); + +const proccessData = (res: Response, fileID: string, tempToken: string) => { + const eventEmitter = new EventEmitter(); + + const processFile = async () => { + try { + const file = await fileDB.getPublicFile(fileID); + + if (!file || !file.metadata.link || file.metadata.link !== tempToken) { + throw new NotAuthorizedError("File Not Public"); + } + + if (file.metadata.linkType === "one") { + await fileDB.removeOneTimePublicLink(fileID); + } + + const user = await userDB.getUserInfo(file.metadata.owner); + + if (!user) throw new NotFoundError("User Not Found"); + + const password = user.getEncryptionKey(); + + if (!password) throw new ForbiddenError("Invalid Encryption Key"); + + const IV = file.metadata.IV; + + const readStreamParams = createGenericParams({ + filePath: file.metadata.filePath, + Key: file.metadata.s3ID, + }); + + const readStream = storageActions.createReadStream(readStreamParams); + + const CIPHER_KEY = crypto.createHash("sha256").update(password).digest(); + + const decipher = crypto.createDecipheriv("aes256", CIPHER_KEY, IV); + + decipher.on("error", (e: Error) => { + eventEmitter.emit("error", e); + }); + + readStream.on("error", (e: Error) => { + eventEmitter.emit("error", e); + }); + + res.on("error", (e: Error) => { + eventEmitter.emit("error", e); + }); + + res.set("Content-Type", "binary/octet-stream"); + res.set( + "Content-Disposition", + 'attachment; filename="' + file.filename + '"' + ); + res.set("Content-Length", file.metadata.size.toString()); + + readStream + .pipe(decipher) + .pipe(res) + .on("finish", () => { + eventEmitter.emit("finish"); + }); + } catch (e) { + eventEmitter.emit("error", e); + } + }; + + processFile(); + + return eventEmitter; +}; + +const getPublicFileData = ( + res: Response, + fileID: string, + tempToken: string +) => { + return new Promise((resolve, reject) => { + const eventEmitter = proccessData(res, fileID, tempToken); + eventEmitter.on("finish", (data) => { + resolve(data); + }); + eventEmitter.on("error", (e) => { + reject(e); + }); + }); +}; + +export default getPublicFileData;