UNPKG

mosquito-transport

Version:

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

472 lines (417 loc) 18.1 kB
import { join } from "path"; import { STORAGE_DIRS } from "../../helpers/values"; import { access, appendFile, constants, open, readdir, readFile, rename, rm, stat, unlink, writeFile } from 'fs/promises'; import { createHash } from "crypto"; import { PassThrough } from "stream"; import { Scoped } from "../../helpers/variables"; import { createReadStream, createWriteStream } from "fs"; import { availableMemory } from "process"; import { ensureDir, normalizeRoute } from "../../helpers/utils"; export const getSource = async (path, projectName) => { path = strapPath(path); const { FILES, HASH_LINK, HASH_FILE } = STORAGE_DIRS(projectName); const [mainPath, hashPath] = await Promise.all([ fileExists(join(FILES, path)), fileExists(join(HASH_LINK, path)) ]); if (mainPath) return { source: mainPath }; if (hashPath) { try { const hashValue = await readFile(hashPath, 'utf8'); return { source: join(HASH_FILE, hashValue), hashValue }; } catch (error) { console.error(error); } } return {}; }; export const streamWritableSource = (path, makeHash, projectName, callback) => { path = strapPath(path); const stream = new PassThrough(); const start = () => new Promise(async (resolve, reject) => { let writeReadyCallback; const writeReadyPromise = new Promise((resolve, reject) => { writeReadyCallback = err => { if (err) reject(); else resolve(); }; }); try { const { HASH_FILE, FILES, HASH_GROUPING, HASH_LINK, PENDING_HASH_LOG } = STORAGE_DIRS(projectName); if (makeHash) { const thisSource = getSource(path, projectName); const sessionID = `${Date.now()}${++Scoped.AbsoluteIterator}`; const removeLogFlag = async (deleteStage) => { try { if (deleteStage) await unlink(fileWriter.path); } catch (error) { console.error(error); setTimeout(() => { unlink(fileWriter.path); }, 500); } const handler = await openIO(PENDING_HASH_LOG); try { const logValue = (await handler.read('utf8')).split('\n').filter(v => v !== sessionID && v).join('\n').trim(); await handler.write(logValue, 'utf8'); } catch (error) { console.error(error); } await handler.close(); }; /** @type {import('fs').WriteStream} */ let fileWriter; let bufferSize = 0; let residueExecutions = []; let resolvedData; const hasher = createHash('sha256'); stream.on('data', chunk => { const execute = () => { fileWriter.write(chunk); hasher.update(chunk); bufferSize += chunk.length; } if (residueExecutions) { residueExecutions.push(execute); } else execute(); }); stream.on('error', async err => { await writeReadyPromise; fileWriter.destroy(new Error(err)); removeLogFlag(true); }); stream.on('end', async () => { await writeReadyPromise; try { fileWriter.end(); const { hashValue } = await thisSource; const hashResult = encodeURIComponent(hasher.digest().toString('base64')); const hashPath = join(HASH_FILE, hashResult); const saveHash = async () => { await Promise.all([ writeFile(await ensureDir(join(HASH_LINK, path)), hashResult, 'utf8'), openIO(await ensureDir(join(HASH_GROUPING, hashResult))).then(async handler => { try { const newList = [ ...new Set([ ...(await handler.read('utf8')).split('\n'), encodeURIComponent(path) ]) ].filter(v => v).join('\n'); await handler.write(newList, 'utf8'); } catch (error) { console.error(error); } handler.close(); }) ]); }; if (await fileExists(hashPath)) { let uri; if (hashResult !== hashValue) { uri = await new Promise(async (resolve, reject) => { try { let bufferOffset = 0; let wasSame = true; let writtenPath; while (bufferOffset < bufferSize) { // avoid overflowing the system's memory const BUFFERING_LIMIT = Math.round(availableMemory() / 3); const readSize = Math.min(BUFFERING_LIMIT, bufferSize - bufferOffset); const [incomingFile, restedFile] = await Promise.all([ readFileSection(fileWriter.path, bufferOffset, bufferOffset + readSize), readFileSection(hashPath, bufferOffset, bufferOffset + readSize) ]); if (!incomingFile.equals(restedFile)) { wasSame = false; await new Promise(async (resolve, reject) => { const writer = createWriteStream(await ensureDir(join(FILES, path))); writtenPath = writer.path; writer.on('finish', resolve).on('error', reject); createReadStream(fileWriter.path) .pipe(writer); }); break; } bufferOffset += readSize; } try { await deleteSource(path, projectName, wasSame ? 'main' : 'hash'); } catch (_) { } if (wasSame) await saveHash(); resolve(wasSame ? hashPath : writtenPath); } catch (error) { reject(error); } }); } else uri = hashPath; await removeLogFlag(true); resolvedData = uri; fileWriter.end(); } else { try { await deleteSource(path, projectName); } catch (_) { } await rename(fileWriter.path, hashPath); await saveHash(); await removeLogFlag(); resolvedData = hashPath; fileWriter.end(); } } catch (error) { await removeLogFlag(true); fileWriter.destroy(new Error(error)); } }); await openIO(await ensureDir(PENDING_HASH_LOG)).then(async handler => { try { await handler.append(`\n${sessionID}`, 'utf8'); } catch (error) { console.error(error); } await handler.close(); }); fileWriter = createWriteStream(await ensureDir(join(HASH_FILE, sessionID))); fileWriter.on('finish', () => { resolve(resolvedData); }); fileWriter.on('error', (err) => { reject(err); }); residueExecutions.forEach(e => e()); residueExecutions = undefined; writeReadyCallback(); } else { /** @type {import('fs').WriteStream} */ let writable; let residueBuffers = []; stream.on('data', buf => { if (residueBuffers) residueBuffers.push(buf); else writable.write(buf); }); stream.on('error', async (err) => { await writeReadyPromise; writable.destroy(err); }); stream.on('end', async () => { await writeReadyPromise; try { await deleteSource(path, projectName, 'hash'); } catch (_) { } writable.end(); }); writable = createWriteStream(await ensureDir(join(FILES, path))); writable.on('finish', () => { resolve(writable.path); }); writable.on('error', (err) => { reject(err); }); residueBuffers.forEach(buf => { writable.write(buf); }); residueBuffers = undefined; writeReadyCallback(); } } catch (error) { reject(error); writeReadyCallback(error || new Error('')); } }); start().then(uri => { callback(undefined, uri); }).catch(err => { callback(err); stream.destroy(err); }); return stream; }; export const streamReadableSource = (path, projectName) => { path = strapPath(path); const stream = new PassThrough(); getSource(path, projectName).then(result => { if (result?.source) { createReadStream(result.source).pipe(stream); } else { stream.destroy(new Error(`ENOENT: no such file or directory, open '${path}'`)); } }); return stream; }; export const deleteDir = async (path, projectName) => { path = strapPath(path); const { FILES, HASH_LINK } = STORAGE_DIRS(projectName); const [mainDeletion, hashDeletion] = await Promise.all( [FILES, HASH_LINK].map(async (dir, isHash) => { dir = join(dir, path); try { if (isHash) { const recursiveDeletion = async (dir, trailingPath) => { const fileListing = await readdir(dir); await Promise.allSettled( fileListing.map(async p => { const thisPath = join(dir, p); const dest = join(trailingPath, p); if ((await stat(thisPath)).isDirectory()) { await recursiveDeletion(thisPath, dest); } else await deleteSource(dest, projectName, 'hash'); }) ); } await recursiveDeletion(dir, path); } await rm(dir, { recursive: true, force: true }); } catch (error) { return error; } }) ); if (hashDeletion && mainDeletion) throw mainDeletion; }; export const deleteSource = async (path, projectName, deletion) => { path = strapPath(path); const { FILES, HASH_LINK, HASH_FILE, HASH_GROUPING } = STORAGE_DIRS(projectName); const [mainDeletion, hashDeletion] = await Promise.all([ (async () => { try { if (![undefined, 'main'].includes(deletion)) return; await unlink(join(FILES, path)); } catch (error) { return error; } })(), (async () => { try { if (![undefined, 'hash'].includes(deletion)) return; const hashPath = join(HASH_LINK, path); const hashValue = await readFile(hashPath, 'utf8'); try { const groupingPath = join(HASH_GROUPING, hashValue); const groupingHandle = await openIO(groupingPath); try { const pathURI = encodeURIComponent(path); const groupingList = ( await groupingHandle.read('utf8') ).split('\n').filter(v => v !== pathURI && v); if (groupingList.length) { await groupingHandle.write(groupingList.join('\n'), 'utf8'); } else { await Promise.allSettled([groupingPath, join(HASH_FILE, hashValue)].map(p => unlink(p))); } } catch (error) { console.error(error); } await groupingHandle.close(); } catch (err) { console.error(err); } await unlink(hashPath); } catch (error) { return error; } })() ]); if (deletion) { if (hashDeletion || mainDeletion) throw mainDeletion || hashDeletion; } else if (hashDeletion && mainDeletion) throw mainDeletion; }; export const writeBuffer = (path, buffer, projectName, makeHash) => new Promise((resolve, reject) => { path = strapPath(path); streamWritableSource(path, makeHash, projectName, (err, result) => { if (err) reject(err); else resolve(result); }).end(buffer); }); export const readBuffer = (path, projectName) => new Promise((resolve, reject) => { path = strapPath(path); let buffer = []; streamReadableSource(path, projectName) .on('data', buf => { buffer.push(buf); }) .on('error', reject) .on('end', () => { resolve(Buffer.concat(buffer)); }); }); const readFileSection = async (path, start, end) => { path = strapPath(path); const length = end - start; const buffer = Buffer.alloc(length); // Create a buffer to hold the specific section const handle = await open(path, 'r'); try { const { bytesRead } = await handle.read({ buffer, length, offset: 0, position: start }); handle.close(); return buffer.subarray(0, bytesRead); } catch (error) { handle.close(); throw error; } }; const strapPath = path => `/${normalizeRoute(path)}`; const openIO = async (path, strictReader) => { let callback; const thisPromise = new Promise(resolve => { callback = () => { resolve(); if (thisPromise === Scoped.SequentialIO[path]) delete Scoped.SequentialIO[path]; }; }); const lastPromise = Scoped.SequentialIO[path]; Scoped.SequentialIO[path] = thisPromise; if (lastPromise) await lastPromise; try { return { close: callback, read: async (encoding) => { try { const r = await readFile(path, encoding); return r; } catch (error) { if (strictReader) throw error; return ''; } }, write: (data, encoding) => writeFile(path, data, encoding), append: (data, encoding) => appendFile(path, data, encoding) }; } catch (error) { callback(); throw error; } }; const fileExists = async (path) => { path = strapPath(path); try { await access(path, constants.F_OK); return path; } catch (_) { return false; } }; export const cleanupPendingHashes = async (projectName) => { const { PENDING_HASH_LOG, HASH_FILE } = STORAGE_DIRS(projectName); try { let residueError; const handler = await openIO(PENDING_HASH_LOG, true); try { await Promise.all( (await handler.read('utf8')).split('\n').map(async v => { if (v) { try { await unlink(join(HASH_FILE, v)); } catch (_) { } } }) ); await handler.write('', 'utf8'); } catch (error) { residueError = { error }; } await handler.close(); if (residueError) throw residueError.error; } catch (error) { // console.error('cleanupPendingHashes err:', error); } };