UNPKG

mosquito-transport

Version:

Quickly spawn server infrastructure along robust authentication, database, storage, and cross-platform compatibility

195 lines (174 loc) 8.04 kB
import { BLOCKS_IDENTIFIERS, decryptData, resolvePath } from "./utils.js"; import { MongoClient } from "mongodb"; import { deserialize } from 'mongodb/lib/bson.js'; import { mkdir } from "fs/promises"; import { createWriteStream } from "fs"; import { ReadableBit } from "@deflexable/bit-stream"; import { dirname } from "path"; export const installBackup = (config) => new Promise((callResolve, callReject) => { /** * @type {{stream: import('stream').Readable}} */ const { password, storage, stream, onMongodbOption } = { ...config }; const streamingBit = new ReadableBit(); let steadyPromise; const INIT_BLOCKS = { database: { dbUrl: undefined, dbName: undefined, collection: undefined }, storage: { path: undefined, file: undefined }, headers: undefined }; const lastBlocks = { ...INIT_BLOCKS }; const installionStats = { database: {}, totalWrittenDocuments: 0, totalWrittenFiles: 0 }; /** * @type {{[key: string]: MongoClient}} */ const mongodbInstances = {}; const dbUrlMap = {}; let bitIndex = 0; const handleChunk = async (chunk) => { try { const thisElem = password ? decryptData(chunk, password) : chunk; const thisHeader = !(bitIndex++ % 2) && thisElem.toString('utf8'); if (thisHeader) { lastBlocks.headers = thisHeader; } else { const BLOCK_ID = `${bitIndex}`; if (lastBlocks.headers === undefined) throw `no blocks identifier at block_id (${BLOCK_ID})`; const prevHeader = lastBlocks.headers; if (prevHeader === BLOCKS_IDENTIFIERS.DB_URL) { lastBlocks.database = { dbUrl: thisElem.toString('utf8') }; if (lastBlocks.storage.path) throw `(${BLOCKS_IDENTIFIERS.DB_URL}) block should come first before (${BLOCKS_IDENTIFIERS.STORAGE_FILE_PATH})`; } else if (prevHeader === BLOCKS_IDENTIFIERS.DB_NAME) { lastBlocks.database.dbName = thisElem.toString('utf8'); } else if (prevHeader === BLOCKS_IDENTIFIERS.COLLECTION) { lastBlocks.database.collection = thisElem.toString('utf8'); } else if (prevHeader === BLOCKS_IDENTIFIERS.DOCUMENT) { const { collection, dbName, dbUrl } = lastBlocks.database; if (typeof dbUrl !== 'string' || !dbUrl.trim()) throw `no previous ${BLOCKS_IDENTIFIERS.DB_URL} was registered at block_id ${BLOCK_ID}`; if (typeof dbName !== 'string' || !dbName.trim()) throw `no previous ${BLOCKS_IDENTIFIERS.DB_NAME} was registered at block_id ${BLOCK_ID}`; if (typeof collection !== 'string' || !collection.trim()) throw `no previous ${BLOCKS_IDENTIFIERS.COLLECTION} was registered at block_id ${BLOCK_ID}`; if (!mongodbInstances[dbUrl]) { const mongoHandle = onMongodbOption?.(dbUrl); const isInstance = mongoHandle instanceof MongoClient; const { url, ...dbOptions } = isInstance ? {} : { ...mongoHandle }; mongodbInstances[dbUrl] = isInstance ? mongoHandle : new MongoClient(url || dbUrl, { ...dbOptions }); dbUrlMap[dbUrl] = url || dbUrl; installionStats.database[url || dbUrl] = {}; } const thisUrl = dbUrlMap[dbUrl]; if (installionStats.database[thisUrl][dbName]) { ++installionStats.database[thisUrl][dbName]; } else installionStats.database[thisUrl][dbName] = 1; const { _id, ...docRest } = deserialize(thisElem, { bsonRegExp: true, promoteLongs: false, promoteValues: false }); if (!_id) throw `invalid doc found in block_id ${BLOCK_ID}`; await mongodbInstances[dbUrl].db(dbName).collection(collection).replaceOne( { _id }, { ...docRest }, { upsert: true } ); ++installionStats.totalWrittenDocuments; } else { lastBlocks.database = INIT_BLOCKS.database; if (prevHeader === BLOCKS_IDENTIFIERS.STORAGE_DIRECTORY) { const path = thisElem.toString('utf8'); lastBlocks.storage = INIT_BLOCKS.storage; try { await mkdir(resolvePath(storage, path), { force: true, recursive: true }); } catch (_) { } } else if (prevHeader === BLOCKS_IDENTIFIERS.STORAGE_FILE_PATH) { const path = resolvePath(storage, thisElem.toString('utf8')); if (lastBlocks.storage.file) { lastBlocks.storage.file.end(); } lastBlocks.storage = { path }; } else if (prevHeader === BLOCKS_IDENTIFIERS.STORAGE_FILE) { if (typeof lastBlocks.storage.path !== 'string') throw `no previous ${BLOCKS_IDENTIFIERS.STORAGE_FILE_PATH} was registered at block_id ${BLOCK_ID}`; if (lastBlocks.storage.file) { lastBlocks.storage.file.write(thisElem); } else { try { await mkdir(dirname(lastBlocks.storage.path), { force: true, recursive: true }); } catch (_) { } const writeStream = createWriteStream(lastBlocks.storage.path); writeStream.write(thisElem); lastBlocks.storage.file = writeStream; ++installionStats.totalWrittenFiles; }; } else throw `unknown block identifier "${prevHeader}" at block_id ${BLOCK_ID}`; } lastBlocks.headers = undefined; } } catch (error) { streamingBit.destroy(error); throw error; } } let lastBitID = 0, hasEnded, lastResolvedBitID; const resolveInstall = () => { callResolve(installionStats); if (lastBlocks.storage.file) { lastBlocks.storage.file.end(); lastBlocks.storage.file = undefined; } } streamingBit.on('data', chunk => { const thisPromise = steadyPromise; const bitID = ++lastBitID; steadyPromise = new Promise(async (resolve, reject) => { try { await thisPromise; await handleChunk(chunk); resolve(); if (hasEnded && lastBitID === bitID) resolveInstall(); lastResolvedBitID = bitID; } catch (error) { reject(error); } }); }); streamingBit.on('end', () => { if (lastResolvedBitID === lastBitID) { resolveInstall(); } else hasEnded = true; }); streamingBit.on('error', err => { callReject(err); if (lastBlocks.storage.file) { lastBlocks.storage.file.end(); lastBlocks.storage.file = undefined; } }); stream.pipe(streamingBit); });