@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
JavaScript
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));
},
};
}