imp(middleware-multipart): validate provided set of files
MODIFY
packages/@integration/src/effects/static.effects.ts
packages/@integration/src/effects/static.effects.ts
import * as fs from 'fs';
import * as path from 'path';
-import { promisify } from 'util';
import { r, combineRoutes, use } from '@marblejs/core';
import { requestValidator$, t } from '@marblejs/middleware-io';
import { multipart$, StreamHandler } from '@marblejs/middleware-multipart';
import { readFile } from '@marblejs/core/dist/+internal';
+import { of } from 'rxjs';
import { map, mergeMap } from 'rxjs/operators';
-const fsExists = promisify(fs.exists);
-const fsMakeDir = promisify(fs.mkdir);
-
const STATIC_PATH = path.resolve(__dirname, '../../../../assets');
const TMP_PATH = path.resolve(__dirname, '../../../../tmp');
-const streamFileTo: StreamHandler = ({ file, fieldname }) =>
- fsExists(TMP_PATH)
- .then(exists => !exists
- ? fsMakeDir(TMP_PATH)
- : Promise.resolve())
- .then(() => {
- const destination = path.join(TMP_PATH, path.basename(fieldname));
+const streamFileTo = (basePath: string): StreamHandler => {
+ if (!fs.existsSync(basePath)) { fs.mkdirSync(basePath); }
+
+ return ({ file, fieldname }) => {
+ const destination = path.join(basePath, path.basename(fieldname));
file.pipe(fs.createWriteStream(destination));
- return { destination };
- });
+ return of({ destination });
+ };
+}
const getFileValidator$ = requestValidator$({
params: t.type({ dir: t.string })
@@ -32,8 +28,16 @@ const postFile$ = r.pipe(
r.matchPath('/upload'),
r.matchType('POST'),
r.useEffect(req$ => req$.pipe(
- use(multipart$({ stream: streamFileTo })),
- map(req => ({ body: req.file }))
+ use(multipart$({
+ files: ['image_1'],
+ stream: streamFileTo(TMP_PATH),
+ })),
+ map(req => ({
+ body: {
+ file: req.file,
+ body: req.body,
+ },
+ }))
)));
const getFileStream$ = r.pipe(

MODIFY
packages/middleware-multipart/src/index.ts
packages/middleware-multipart/src/index.ts
export * from './multipart.middleware';
-export { StreamHandler } from './multipart.parser';
+export * from './multipart.interface';
+export { StreamHandler, ParserOpts } from './multipart.parser';

MODIFY
packages/middleware-multipart/src/multipart.interface.ts
packages/middleware-multipart/src/multipart.interface.ts
@@ -8,6 +8,6 @@ export interface File {
fieldname: string;
}
-export interface WithFile {
- file: Record<string, File>;
+export interface WithFile<T extends string = string> {
+ file: Record<T, File>;
}

MODIFY
packages/middleware-multipart/src/multipart.middleware.ts
packages/middleware-multipart/src/multipart.middleware.ts
@@ -5,10 +5,10 @@ import { parseMultipart, ParserOpts } from './multipart.parser';
import { shouldParseMultipart } from './multipart.util';
import { WithFile } from './multipart.interface';
-export const multipart$ = (opts: ParserOpts = {}) => <T extends HttpRequest>(req$: Observable<T>) =>
+export const multipart$ = <File extends string>(opts: ParserOpts = {}) => <T extends HttpRequest>(req$: Observable<T>) =>
req$.pipe(
mergeMap(req => shouldParseMultipart(req)
? parseMultipart(opts)(req)
: of(req)),
- map(req => req as T & WithFile),
+ map(req => req as T & WithFile<File>),
);

MODIFY
packages/middleware-multipart/src/multipart.parser.ts
packages/middleware-multipart/src/multipart.parser.ts
import { HttpRequest, HttpError, HttpStatus } from '@marblejs/core';
import { isNonNullable } from '@marblejs/core/dist/+internal/utils';
import { fromReadableStream } from '@marblejs/core/dist/+internal/observable';
-import { fromEvent, Observable, of, throwError, merge } from 'rxjs';
+import { fromEvent, Observable, of, throwError, merge, iif } from 'rxjs';
import { mapTo, map, mergeMap, takeUntil, toArray, tap, catchError, buffer, mergeMapTo, take, ignoreElements } from 'rxjs/operators';
import * as Busboy from 'busboy';
import { Readable } from 'stream';
@@ -18,11 +18,19 @@ type FileData = {
mimetype: string;
};
+interface StreamHandlerOutput {
+ destination: any;
+ size?: number;
+}
+
export interface StreamHandler {
- (opts: FileData): Promise<{ destination: any }> | Observable<{ destination: any }>;
+ (opts: FileData):
+ | Promise<StreamHandlerOutput>
+ | Observable<StreamHandlerOutput>;
}
export interface ParserOpts {
+ files?: string[];
stream?: StreamHandler;
maxFileSize?: number;
maxFileCount?: number;
@@ -41,15 +49,21 @@ const parseFile = (req: HttpRequest) => (opts: ParserOpts) => (event$: Observabl
event$.pipe(
takeUntil(finish$),
map(([ fieldname, file, filename, encoding, mimetype ]) => ({ fieldname, file, filename, encoding, mimetype })),
- mergeMap((data) => merge(fileSizeLimit(data, opts.maxFileSize), of(data))),
+ mergeMap(data => iif(
+ () => !!opts.files ? !opts.files.includes(data.fieldname) : false,
+ throwError(new HttpError(`File "${data.fieldname}" is not acceptable`, HttpStatus.PRECONDITION_FAILED)),
+ of(data),
+ )),
+ mergeMap(data => merge(fileSizeLimit(data, opts.maxFileSize), of(data))),
mergeMap(data => isNonNullable(opts.stream)
? of(data).pipe(
mergeMap(opts.stream),
catchError(error => throwError(new HttpError(error.message, HttpStatus.INTERNAL_SERVER_ERROR))),
- tap(({ destination }) => {
+ tap(({ destination, size }) => {
req.file = {
...req.file || {},
[data.fieldname]: {
+ size,
destination,
encoding: data.encoding,
mimetype: data.mimetype,
@@ -69,12 +83,12 @@ const parseFile = (req: HttpRequest) => (opts: ParserOpts) => (event$: Observabl
req.file = {
...req.file || {},
[data.fieldname]: {
+ size: Buffer.byteLength(buffer),
buffer,
encoding: data.encoding,
mimetype: data.mimetype,
filename: data.filename,
fieldname: data.fieldname,
- size: Buffer.byteLength(buffer),
},
};
}),

imp(middleware-multipart): validate provided set of files