fc-store
Version:
File chunking, and storage with MongoDB
247 lines (217 loc) • 7.39 kB
JavaScript
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;