UNPKG

fc-store

Version:

File chunking, and storage with MongoDB

247 lines (217 loc) 7.39 kB
const fs = require("fs"); const path = require("path"); const { Writable, pipeline } = require("stream"); const util = require("util"); const pipelineAsync = util.promisify(pipeline); const crypto = require("crypto"); class FileChunk { /** * * @param {MongoClient()} db driver connected to your mongodb database * @param {string} firstBucketName name of bucket that stores the files meta * @param {string} secondBucketName name of bucket that stores the files data */ #bucket1; #bucket2; constructor(db, firstBucketName, secondBucketName) { this.#bucket1 = db.collection(firstBucketName); this.#bucket2 = db.collection(secondBucketName); } /** * * @param {string} filepath path to the targeted file * @param {string} filename optional * @param {string} userPassword * @param {number} [number_per_kb=1] * @returns {<Promise>}fileId */ disassembleFileFromPath(filepath, number_per_kb = 1, userPassword, filename) { return new Promise(async (resolve, reject) => { try { if (!fs.existsSync(filepath)) throw new Error("File doesn't exists"); const algorithm = "aes-256-cbc"; const salt = crypto.randomBytes(32); const iv = crypto.randomBytes(16); const password = userPassword || "@dis_file_023_#tag_Me"; let fileId; let fileIdExist = true; while (fileIdExist) { fileId = crypto.randomBytes(20).toString("hex"); fileIdExist = await this.#bucket1.findOne({ fileId }); } const meta = path.parse(filepath); const key = await new Promise((resolve, reject) => { crypto.scrypt(password, salt, 32, (err, key) => { if (err) reject(err); else resolve(key); }); }); await this.#bucket1.insertOne({ fileId, date: new Date(), ext: meta.ext, filename: filename || meta.name, meta: salt.toString("base64"), code: iv.toString("base64"), }); const readStream = fs.createReadStream(filepath, { highWaterMark: 1024 * number_per_kb, }); const crypt = crypto.createCipheriv(algorithm, key, iv); let index = 1; const dbWritable = new Writable({ objectMode: true, write: async (chunk, _, cb) => { try { await this.#bucket2.insertOne({ fileId, data: chunk.toString("base64"), index: index++, }); cb(); } catch (error) { cb(error); } }, }); await pipelineAsync(readStream, crypt, dbWritable); resolve(fileId); } catch (error) { reject(error); } }); } /** * * @param {string} fileId * @param {string} dir * @param {string} userPassword * @param {number} [number_per_kb=1] * @returns {<Promise>} response details */ reassembleFileFromPath( fileId, number_per_kb = 1, dir, filenameChoice, userPassword ) { let fileOutput; return new Promise(async (resolve, reject) => { try { const fileMeta = await this.#bucket1.findOne({ fileId }); if (!fileMeta) { throw new Error("FileId doesn't exists"); } if (dir && filenameChoice) { fileOutput = path.join(dir, `${filenameChoice}${fileMeta.ext}`); } else if (dir && !filenameChoice) { fileOutput = path.join(dir, `${fileMeta.filename}${fileMeta.ext}`); } else if (!dir && filenameChoice) { fileOutput = path.join( process.cwd(), `${filenameChoice}${fileMeta.ext}` ); } else { fileOutput = path.join( process.cwd(), `${fileMeta.filename}${fileMeta.ext}` ); } const algorithm = "aes-256-cbc"; const salt = Buffer.from(fileMeta.meta, "base64"); const iv = Buffer.from(fileMeta.code, "base64"); const password = userPassword || "@dis_file_023_#tag_Me"; const key = await new Promise((resolve, reject) => { crypto.scrypt(password, salt, 32, (err, key) => { if (err) { reject(err); } else resolve(key); }); }); const writeStream = fs.createWriteStream(fileOutput, { highWaterMark: 1024 * number_per_kb, }); const decryption = crypto.createDecipheriv(algorithm, key, iv); const chunks = await this.#bucket2 .find({ fileId }) .sort({ index: 1 }) .toArray(); if (chunks.length < 1) { throw new Error("Chunk missing"); } for (const chunk of chunks) { const encryptedData = Buffer.from(chunk.data, "base64"); const decryptedChunk = decryption.update(encryptedData); await new Promise((resolve, reject) => { writeStream.write(decryptedChunk, (err) => { if (err) reject(err); else resolve(); }); }); } const finalChunk = decryption.final(); if (finalChunk.length > 0) { await new Promise((resolve, reject) => { writeStream.write(finalChunk, (err) => { if (err) reject(err); else resolve(); }); }); } await new Promise((resolve, reject) => { writeStream.end((err) => { if (err) reject(err); else resolve(); }); }); resolve({ success: true, outputPath: fileOutput, filename: `${fileMeta.filename}${fileMeta.ext}`, }); } catch (error) { if (fileOutput && fs.existsSync(fileOutput)) { fs.unlinkSync(fileOutput); } reject(`Decryption failed: ${error.message}`); } }); } /** * if you don't have your fileId, use the filename * @param {string} fileId * @param {string} filename * @returns {Promise} */ deleteFile(fileId, filename) { return new Promise(async (resolve, reject) => { try { if (!fileId && !filename) { throw new Error("Either fileId or filename must be provided."); } const query = fileId ? { fileId } : { filename }; const fileDoc = await this.#bucket1.findOne(query); if (!fileDoc) { throw new Error("No matching records found to delete"); } const result1 = await this.#bucket1.deleteOne({ fileId: fileDoc.fileId, }); const result2 = await this.#bucket2.deleteMany({ fileId: fileDoc.fileId, }); if (result1.deletedCount > 0 && result2.deletedCount > 0) { resolve( `File deleted successfully. Removed ${result2.deletedCount} chunks.` ); } else { throw new Error("Deletion failed"); } } catch (error) { reject(`Delete operation failed: ${error.message}`); } }); } } module.exports = FileChunk;