Skip to content

Commit

Permalink
Merge pull request #98 from getlarge/fix-consume-fastify-multipart-st…
Browse files Browse the repository at this point in the history
…ream

fix(nestjs-tools-fastify-upload): implement temporary file handling and improve stream processing
  • Loading branch information
getlarge authored Nov 6, 2024
2 parents 194a403 + b076fd9 commit f559743
Show file tree
Hide file tree
Showing 2 changed files with 32 additions and 10 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/node.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ jobs:

strategy:
matrix:
node-version: [18.x, 20.x]
node-version: [18.x, 20.x, 22.x]

services:
rabbitmq:
Expand Down
40 changes: 31 additions & 9 deletions packages/fastify-upload/src/lib/storage/stream-storage.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,57 @@
import { PassThrough, Readable } from 'node:stream';
import { finished } from 'node:stream/promises';
import { randomBytes } from 'node:crypto';
import { createReadStream, createWriteStream } from 'node:fs';
import { rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { normalize, resolve, sep } from 'node:path';
import { Readable } from 'node:stream';
import { finished, pipeline } from 'node:stream/promises';

import { MultipartFile, Storage, StorageFile } from './storage';

export interface StreamStorageFile extends StorageFile {
stream: Readable;
}

const temporaryFileOutput = (filename: string) => {
const safeFileName = normalize(filename).replace(/^(\.\.(\/|\\|$))+/, '');
const fullPath = resolve(tmpdir(), safeFileName);
// Ensure the resolved path starts with the intended storagePath to prevent path traversal
if (!fullPath.startsWith(resolve(tmpdir() + sep))) {
throw new Error('Invalid file path');
}
return fullPath;
};
export class StreamStorage extends Storage<StreamStorageFile> {
handleFile(file: MultipartFile): Promise<StreamStorageFile> {
async handleFile(file: MultipartFile): Promise<StreamStorageFile> {
const { encoding, mimetype, fieldname } = file;
const stream = file.file.pipe(new PassThrough());

// looks like the file.filename type is incorrect, file.filename could be undefined
const filename = file.filename ?? randomBytes(16).toString('hex');
/**
* force the stream to be consumed as required by Fastify and Busboy
* @see https://github.com/fastify/fastify-multipart?tab=readme-ov-file#usage
**/
const output = temporaryFileOutput(filename);
await pipeline(file.file, createWriteStream(output));
const stream = createReadStream(output);
return Promise.resolve({
size: file.file.readableLength,
stream,
encoding,
mimetype,
fieldname,
originalFilename: file.filename,
originalFilename: filename,
});
}

async removeFile(file: StreamStorageFile, force?: boolean): Promise<void> {
if (!force) {
await finished(file.stream);
file.stream.destroy();
return;
}
if (!file.stream.destroyed) {
} else if (!file.stream.destroyed) {
file.stream.destroy();
}
await rm(temporaryFileOutput(file.originalFilename)).catch(() => {
// ignore
});
}
}

0 comments on commit f559743

Please sign in to comment.