UNPKG

@nestjs/platform-fastify

Version:

Run NestJS on Fastify: the HTTP adapter with Fastify's plugins, JSON schemas and serialization.

91 lines (90 loc) • 3.56 kB
import { randomBytes } from 'crypto'; import { createWriteStream, mkdirSync } from 'fs'; import { unlink } from 'fs/promises'; import { tmpdir } from 'os'; import { join } from 'path'; import { finished, pipeline } from 'stream/promises'; function resolveName(resolver, req, file) { return new Promise((resolve, reject) => { const result = resolver(req, file, (err, value) => err ? reject(err) : resolve(value)); // The rejection of an async resolver fails the upload, as a throw does, // rather than going unhandled. if (typeof result?.then === 'function') { result.then(undefined, reject); } }); } const STREAM_DESTROYED_MESSAGE = 'File stream was destroyed'; /** * Rejects once `stream` is destroyed before its end without an error, as * `@fastify/multipart` does when the form is cut off. `pipeline()` never * settles then if the last chunk had already been pushed: it waits for an * 'end' that is never emitted, with the file still open. */ function rejectWhenCutOff(stream) { return new Promise((_resolve, reject) => stream.once('close', () => { if (!stream.readableEnded && !stream.errored) { reject(new Error(STREAM_DESTROYED_MESSAGE)); } })); } /** * Writes each file to disk, with multer's `diskStorage` defaults and file * shape (`destination`, `filename`, `path`, `size`). * * @publicApi */ export function diskStorage(options = {}) { let resolveDestination; if (typeof options.destination === 'string') { const destination = options.destination; mkdirSync(destination, { recursive: true }); resolveDestination = (_req, _file, callback) => callback(null, destination); } else { resolveDestination = options.destination ?? ((_req, _file, callback) => callback(null, tmpdir())); } const resolveFilename = options.filename ?? ((_req, _file, callback) => callback(null, randomBytes(16).toString('hex'))); const store = async (req, file) => { const destination = await resolveName(resolveDestination, req, file); const filename = await resolveName(resolveFilename, req, file); // As in multer, no file is created for a stream destroyed in the meantime. if (file.stream.destroyed) { throw new Error(STREAM_DESTROYED_MESSAGE); } const path = join(destination, filename); // As in multer, `path` is known to the file before it is written. file.path = path; const out = createWriteStream(path); try { await Promise.race([ pipeline(file.stream, out), rejectWhenCutOff(file.stream), ]); } catch (err) { // Removed once closed: a file still being opened would be created // after the unlink. out.destroy(); await finished(out).catch(() => undefined); await unlink(path).catch(() => undefined); throw err; } return { destination, filename, path, size: out.bytesWritten }; }; return { _handleFile(req, file, callback) { store(req, file).then(info => callback(null, info), err => callback(err)); }, _removeFile(_req, file, callback) { const path = file.path; delete file.destination; delete file.filename; delete file.path; unlink(path).then(() => callback(null), err => callback(err)); }, }; }