Files
myDrive/backend/services/chunk-service/utils/getFileData.ts
T

216 lines
5.6 KiB
TypeScript

import { EventEmitter } from "stream";
import { UserInterface } from "../../../models/user-model";
import e, { 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 { FileInterface } from "../../../models/file-model";
import NotAuthorizedError from "../../../utils/NotAuthorizedError";
import sanitizeFilename from "../../../utils/sanitizeFilename";
const fileDB = new FileDB();
const storageActions = getStorageActions();
const activeStreams = new Map<
string,
{
readStream: NodeJS.ReadableStream;
decipherStream: NodeJS.ReadableStream;
file: FileInterface;
}
>();
const getFileAndRemoveActiveStream = async (
fileID: string,
userID: string,
isVideoStream: boolean
) => {
const cachedFileData = activeStreams.get(fileID);
if (!cachedFileData || !isVideoStream) {
const file = await fileDB.getFileInfo(fileID, userID);
if (!file) {
throw new NotFoundError("File not found");
}
if (file.metadata.owner !== userID) {
throw new NotAuthorizedError("Not owner of file");
}
return file;
} else {
const { file, readStream, decipherStream } = cachedFileData;
try {
activeStreams.delete(fileID);
// @ts-ignore
readStream.destroy();
// @ts-ignore
decipherStream.destroy();
} catch (e) {
console.log("Error destroying streams", e);
}
return file;
}
};
const proccessData = (
res: Response,
fileID: string,
user: UserInterface,
rangeIV?: Buffer,
range?: {
start: number;
end: number;
fixedStart: number;
fixedEnd: number;
skip: number;
chunksize: number;
}
) => {
const eventEmitter = new EventEmitter();
const processFile = async () => {
try {
const currentFile = await getFileAndRemoveActiveStream(
fileID,
user._id.toString(),
!!range
);
if (!currentFile) throw new NotFoundError("Download File Not Found");
const password = user.getEncryptionKey();
if (!password) throw new ForbiddenError("Invalid Encryption Key");
const IV = rangeIV || currentFile.metadata.IV;
const readStreamParams = createGenericParams({
filePath: currentFile.metadata.filePath,
Key: currentFile.metadata.s3ID,
});
let readStream: NodeJS.ReadableStream;
if (range) {
readStream = storageActions.createReadStreamWithRange(
readStreamParams,
range.fixedStart,
range.fixedEnd
);
} else {
readStream = storageActions.createReadStream(readStreamParams);
}
const CIPHER_KEY = crypto.createHash("sha256").update(password).digest();
const decipher = crypto.createDecipheriv("aes256", CIPHER_KEY, IV);
if (range) {
decipher.setAutoPadding(false);
}
decipher.on("error", (e: Error) => {
eventEmitter.emit("error", e);
});
readStream.on("error", (e: Error) => {
eventEmitter.emit("error", e);
});
if (!!range) {
activeStreams.set(fileID, {
readStream,
decipherStream: decipher,
file: currentFile,
});
}
res.on("error", (e: Error) => {
eventEmitter.emit("error", e);
});
if (!range) {
const sanatizedFilename = sanitizeFilename(currentFile.filename);
const encodedFilename = encodeURIComponent(sanatizedFilename);
res.set("Content-Type", "binary/octet-stream");
res.set(
"Content-Disposition",
`attachment; filename="${sanatizedFilename}"; filename*=UTF-8''${encodedFilename}`
);
res.set("Content-Length", currentFile.metadata.size.toString());
}
if (range) {
let bytesSent = 0;
decipher.on("data", (data: Buffer) => {
if (bytesSent === 0 && range.skip > 0) {
const neededData = data.slice(range.skip, data.length);
res.write(neededData);
bytesSent += neededData.length;
} else if (bytesSent + data.length > range.chunksize) {
const currentDataLength = bytesSent + data.length;
const difference = currentDataLength - range.chunksize;
const neededData = data.slice(0, data.length - difference);
res.write(neededData);
bytesSent += neededData.length;
} else {
res.write(data);
bytesSent += data.length;
}
});
decipher.on("finish", () => {
res.end();
eventEmitter.emit("finish");
});
readStream.pipe(decipher);
} else {
readStream
.pipe(decipher)
.pipe(res)
.on("finish", () => {
eventEmitter.emit("finish");
});
}
} catch (e) {
eventEmitter.emit("error", e);
}
};
processFile();
return eventEmitter;
};
const getFileData = (
res: Response,
fileID: string,
user: UserInterface,
rangeIV?: Buffer,
range?: {
start: number;
end: number;
fixedStart: number;
fixedEnd: number;
skip: number;
chunksize: number;
}
) => {
return new Promise((resolve, reject) => {
const eventEmitter = proccessData(res, fileID, user, rangeIV, range);
eventEmitter.on("finish", (data) => {
resolve(data);
});
eventEmitter.on("error", (e) => {
reject(e);
});
});
};
export default getFileData;