@naturalcycles/backend-lib 9.63.1 → 9.65.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server/eventLoop.util.d.ts +5 -4
- package/dist/server/eventLoop.util.js +14 -2
- package/dist/upload/getUploadedFiles.d.ts +43 -0
- package/dist/upload/getUploadedFiles.js +94 -0
- package/package.json +5 -2
- package/src/server/eventLoop.util.ts +28 -9
- package/src/upload/getUploadedFiles.ts +140 -0
|
@@ -5,6 +5,11 @@ import type { Integer, NonNegativeInteger, NumberOfMilliseconds, NumberOfPercent
|
|
|
5
5
|
* Also, monitors GC performance.
|
|
6
6
|
* Once per `measureInterval` sends a callback with stats.
|
|
7
7
|
*
|
|
8
|
+
* On Node >=24.19 (or >=26.5) delays are sampled once per event loop iteration
|
|
9
|
+
* (`samplePerIteration`), so stats reflect true per-iteration delay:
|
|
10
|
+
* expect p50/mean near 0 on a healthy service, instead of the ~`resolution` baseline
|
|
11
|
+
* reported by older interval-based sampling.
|
|
12
|
+
*
|
|
8
13
|
* @experimental
|
|
9
14
|
*/
|
|
10
15
|
export declare class EventLoopMonitor {
|
|
@@ -22,10 +27,6 @@ export declare class EventLoopMonitor {
|
|
|
22
27
|
stop(): void;
|
|
23
28
|
}
|
|
24
29
|
export interface EventLoopMonitorCfg {
|
|
25
|
-
/**
|
|
26
|
-
* Defaults to 20.
|
|
27
|
-
*/
|
|
28
|
-
resolution?: NumberOfMilliseconds;
|
|
29
30
|
/**
|
|
30
31
|
* Defaults to 60_000 ms
|
|
31
32
|
*/
|
|
@@ -21,12 +21,24 @@ import { monitorEventLoopDelay, performance, PerformanceObserver } from 'node:pe
|
|
|
21
21
|
* Also, monitors GC performance.
|
|
22
22
|
* Once per `measureInterval` sends a callback with stats.
|
|
23
23
|
*
|
|
24
|
+
* On Node >=24.19 (or >=26.5) delays are sampled once per event loop iteration
|
|
25
|
+
* (`samplePerIteration`), so stats reflect true per-iteration delay:
|
|
26
|
+
* expect p50/mean near 0 on a healthy service, instead of the ~`resolution` baseline
|
|
27
|
+
* reported by older interval-based sampling.
|
|
28
|
+
*
|
|
24
29
|
* @experimental
|
|
25
30
|
*/
|
|
26
31
|
export class EventLoopMonitor {
|
|
27
32
|
constructor(cfg = {}) {
|
|
28
|
-
const {
|
|
29
|
-
|
|
33
|
+
const { measureInterval = 60_000 } = cfg;
|
|
34
|
+
const eldOptions = {
|
|
35
|
+
// Node >=24.19 / >=26.5: sample once per event loop iteration (uv_prepare/uv_check hooks),
|
|
36
|
+
// which measures actual per-iteration delay instead of timer jitter.
|
|
37
|
+
// More accurate (no `resolution` offset baked into every percentile) and cheaper.
|
|
38
|
+
// Older Node silently ignores this option and falls back to interval-based sampling.
|
|
39
|
+
samplePerIteration: true,
|
|
40
|
+
};
|
|
41
|
+
this.eld = monitorEventLoopDelay(eldOptions);
|
|
30
42
|
this.eld.enable();
|
|
31
43
|
this.lastElu = performance.eventLoopUtilization();
|
|
32
44
|
this.po = new PerformanceObserver(list => {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { NumberOfBytes } from '@naturalcycles/js-lib/types';
|
|
2
|
+
import type { BackendRequest } from '../server/server.model.js';
|
|
3
|
+
/**
|
|
4
|
+
* Parses a `multipart/form-data` request body (on top of `busboy`), populates `req.body` with the
|
|
5
|
+
* form fields, and returns the requested files keyed by their form field name:
|
|
6
|
+
*
|
|
7
|
+
* const { csv } = await getUploadedFiles(req, { files: ['csv'] })
|
|
8
|
+
* const { csv, avatar } = await getUploadedFiles(req, {
|
|
9
|
+
* files: ['csv'], // required -> UploadedFile
|
|
10
|
+
* optionalFiles: ['avatar'], // optional -> UploadedFile | undefined
|
|
11
|
+
* })
|
|
12
|
+
*
|
|
13
|
+
* `files` are required: each is asserted to be present and non-empty (throws 400 otherwise).
|
|
14
|
+
* `optionalFiles` are returned as `UploadedFile | undefined` — `undefined` when the field is
|
|
15
|
+
* absent or empty (e.g. a blank file input), so "optional" is genuinely optional.
|
|
16
|
+
*
|
|
17
|
+
* Throws 413 as soon as ANY uploaded file exceeds `maxFileSize` bytes (default: 10 MB), aborting
|
|
18
|
+
* the parse immediately without buffering the rest of the body.
|
|
19
|
+
*
|
|
20
|
+
* Meant to be called from inside a request handler, AFTER authentication — so an
|
|
21
|
+
* unauthenticated/unauthorized request is rejected before its (potentially large) multipart body
|
|
22
|
+
* is buffered into memory.
|
|
23
|
+
*/
|
|
24
|
+
export declare function getUploadedFiles<const FILENAMES extends string = never, const OPTIONAL_FILENAMES extends string = never>(req: BackendRequest, opt: {
|
|
25
|
+
files?: readonly FILENAMES[];
|
|
26
|
+
optionalFiles?: readonly OPTIONAL_FILENAMES[];
|
|
27
|
+
maxFileSize?: NumberOfBytes;
|
|
28
|
+
}): Promise<Record<FILENAMES, UploadedFile> & Record<OPTIONAL_FILENAMES, UploadedFile | undefined>>;
|
|
29
|
+
/**
|
|
30
|
+
* A single uploaded file, buffered in memory.
|
|
31
|
+
*/
|
|
32
|
+
export interface UploadedFile {
|
|
33
|
+
/** Original filename from the client. */
|
|
34
|
+
name: string;
|
|
35
|
+
/** Content-Type of the file part. */
|
|
36
|
+
mimeType: string;
|
|
37
|
+
/** Content-Transfer-Encoding of the file part. */
|
|
38
|
+
encoding: string;
|
|
39
|
+
/** File contents. */
|
|
40
|
+
data: Buffer;
|
|
41
|
+
/** Size of `data` in bytes. */
|
|
42
|
+
size: NumberOfBytes;
|
|
43
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { _assert, AppError } from '@naturalcycles/js-lib/error';
|
|
2
|
+
import busboy from 'busboy';
|
|
3
|
+
/**
|
|
4
|
+
* Parses a `multipart/form-data` request body (on top of `busboy`), populates `req.body` with the
|
|
5
|
+
* form fields, and returns the requested files keyed by their form field name:
|
|
6
|
+
*
|
|
7
|
+
* const { csv } = await getUploadedFiles(req, { files: ['csv'] })
|
|
8
|
+
* const { csv, avatar } = await getUploadedFiles(req, {
|
|
9
|
+
* files: ['csv'], // required -> UploadedFile
|
|
10
|
+
* optionalFiles: ['avatar'], // optional -> UploadedFile | undefined
|
|
11
|
+
* })
|
|
12
|
+
*
|
|
13
|
+
* `files` are required: each is asserted to be present and non-empty (throws 400 otherwise).
|
|
14
|
+
* `optionalFiles` are returned as `UploadedFile | undefined` — `undefined` when the field is
|
|
15
|
+
* absent or empty (e.g. a blank file input), so "optional" is genuinely optional.
|
|
16
|
+
*
|
|
17
|
+
* Throws 413 as soon as ANY uploaded file exceeds `maxFileSize` bytes (default: 10 MB), aborting
|
|
18
|
+
* the parse immediately without buffering the rest of the body.
|
|
19
|
+
*
|
|
20
|
+
* Meant to be called from inside a request handler, AFTER authentication — so an
|
|
21
|
+
* unauthenticated/unauthorized request is rejected before its (potentially large) multipart body
|
|
22
|
+
* is buffered into memory.
|
|
23
|
+
*/
|
|
24
|
+
export async function getUploadedFiles(req, opt) {
|
|
25
|
+
const { files: requiredNames = [], optionalFiles = [], maxFileSize = 10 * MB } = opt;
|
|
26
|
+
const uploadedFiles = await parseMultipart(req, maxFileSize);
|
|
27
|
+
const result = {};
|
|
28
|
+
for (const name of requiredNames) {
|
|
29
|
+
const file = uploadedFiles[name];
|
|
30
|
+
_assert(file, `Uploaded file "${name}" is missing`, { backendResponseStatusCode: 400 });
|
|
31
|
+
_assert(file.size > 0, `Uploaded file "${name}" is empty`, { backendResponseStatusCode: 400 });
|
|
32
|
+
result[name] = file;
|
|
33
|
+
}
|
|
34
|
+
for (const name of optionalFiles) {
|
|
35
|
+
const file = uploadedFiles[name];
|
|
36
|
+
// Absent or empty (e.g. a blank file input) is treated as "not provided".
|
|
37
|
+
result[name] = file && file.size > 0 ? file : undefined;
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Streams the request through busboy, buffering each file into memory and collecting the form
|
|
43
|
+
* fields onto `req.body`. Resolves with the uploaded files keyed by their form field name.
|
|
44
|
+
*
|
|
45
|
+
* Rejects (aborting the parse) the moment a file exceeds `maxFileSize`.
|
|
46
|
+
*/
|
|
47
|
+
async function parseMultipart(req, maxFileSize) {
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
const files = {};
|
|
50
|
+
const body = {};
|
|
51
|
+
let bb;
|
|
52
|
+
try {
|
|
53
|
+
bb = busboy({ headers: req.headers, limits: { fileSize: maxFileSize } });
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// Not a multipart request (or malformed Content-Type) — no files to parse
|
|
57
|
+
resolve(files);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
bb.on('field', (name, value) => {
|
|
61
|
+
body[name] = value;
|
|
62
|
+
});
|
|
63
|
+
bb.on('file', (name, stream, info) => {
|
|
64
|
+
const chunks = [];
|
|
65
|
+
stream.on('data', (chunk) => chunks.push(chunk));
|
|
66
|
+
// Abort as soon as the size limit is hit: stop feeding busboy and reject right away,
|
|
67
|
+
// rather than buffering the whole over-limit body and reporting it at the end.
|
|
68
|
+
stream.on('limit', () => {
|
|
69
|
+
req.unpipe(bb);
|
|
70
|
+
req.resume(); // drain the remainder so the connection can close cleanly
|
|
71
|
+
reject(new AppError(`Uploaded file "${name}" exceeds the size limit of ${maxFileSize} bytes`, {
|
|
72
|
+
backendResponseStatusCode: 413,
|
|
73
|
+
}));
|
|
74
|
+
});
|
|
75
|
+
stream.on('close', () => {
|
|
76
|
+
const data = Buffer.concat(chunks);
|
|
77
|
+
files[name] = {
|
|
78
|
+
name: info.filename,
|
|
79
|
+
mimeType: info.mimeType,
|
|
80
|
+
encoding: info.encoding,
|
|
81
|
+
data,
|
|
82
|
+
size: data.length,
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
bb.on('error', reject);
|
|
87
|
+
bb.on('close', () => {
|
|
88
|
+
req.body = body;
|
|
89
|
+
resolve(files);
|
|
90
|
+
});
|
|
91
|
+
req.pipe(bb);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
const MB = 1024 * 1024;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@naturalcycles/backend-lib",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "9.
|
|
4
|
+
"version": "9.65.0",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@naturalcycles/db-lib": "^10",
|
|
7
7
|
"@naturalcycles/js-lib": "^15",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"@types/on-finished": "^2",
|
|
15
15
|
"@types/on-headers": "^1",
|
|
16
16
|
"@types/vary": "^1",
|
|
17
|
+
"busboy": "^1",
|
|
17
18
|
"compressible": "^2",
|
|
18
19
|
"cookie-parser": "^1",
|
|
19
20
|
"cors": "^2",
|
|
@@ -43,7 +44,8 @@
|
|
|
43
44
|
},
|
|
44
45
|
"devDependencies": {
|
|
45
46
|
"@sentry/node-core": "^10",
|
|
46
|
-
"
|
|
47
|
+
"@types/busboy": "^1",
|
|
48
|
+
"typescript": "^7",
|
|
47
49
|
"fastify": "^5",
|
|
48
50
|
"@naturalcycles/dev-lib": "0.0.0"
|
|
49
51
|
},
|
|
@@ -55,6 +57,7 @@
|
|
|
55
57
|
"./deploy": "./dist/deploy/index.js",
|
|
56
58
|
"./deploy/*.js": "./dist/deploy/*.js",
|
|
57
59
|
"./express/*.js": "./dist/express/*.js",
|
|
60
|
+
"./upload": "./dist/upload/getUploadedFiles.js",
|
|
58
61
|
"./validateRequest": "./dist/validation/ajv/validateRequest.js",
|
|
59
62
|
"./onFinished": "./dist/onFinished.js",
|
|
60
63
|
"./testing": "./dist/testing/index.js"
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
EventLoopMonitorOptions,
|
|
3
|
+
EventLoopUtilization,
|
|
4
|
+
IntervalHistogram,
|
|
5
|
+
} from 'node:perf_hooks'
|
|
2
6
|
import { monitorEventLoopDelay, performance, PerformanceObserver } from 'node:perf_hooks'
|
|
3
7
|
import type {
|
|
4
8
|
Integer,
|
|
@@ -30,13 +34,25 @@ import type {
|
|
|
30
34
|
* Also, monitors GC performance.
|
|
31
35
|
* Once per `measureInterval` sends a callback with stats.
|
|
32
36
|
*
|
|
37
|
+
* On Node >=24.19 (or >=26.5) delays are sampled once per event loop iteration
|
|
38
|
+
* (`samplePerIteration`), so stats reflect true per-iteration delay:
|
|
39
|
+
* expect p50/mean near 0 on a healthy service, instead of the ~`resolution` baseline
|
|
40
|
+
* reported by older interval-based sampling.
|
|
41
|
+
*
|
|
33
42
|
* @experimental
|
|
34
43
|
*/
|
|
35
44
|
export class EventLoopMonitor {
|
|
36
45
|
constructor(cfg: EventLoopMonitorCfg = {}) {
|
|
37
|
-
const {
|
|
38
|
-
|
|
39
|
-
|
|
46
|
+
const { measureInterval = 60_000 } = cfg
|
|
47
|
+
|
|
48
|
+
const eldOptions: EventLoopMonitorOptions2 = {
|
|
49
|
+
// Node >=24.19 / >=26.5: sample once per event loop iteration (uv_prepare/uv_check hooks),
|
|
50
|
+
// which measures actual per-iteration delay instead of timer jitter.
|
|
51
|
+
// More accurate (no `resolution` offset baked into every percentile) and cheaper.
|
|
52
|
+
// Older Node silently ignores this option and falls back to interval-based sampling.
|
|
53
|
+
samplePerIteration: true,
|
|
54
|
+
}
|
|
55
|
+
this.eld = monitorEventLoopDelay(eldOptions)
|
|
40
56
|
this.eld.enable()
|
|
41
57
|
|
|
42
58
|
this.lastElu = performance.eventLoopUtilization()
|
|
@@ -107,11 +123,6 @@ export class EventLoopMonitor {
|
|
|
107
123
|
}
|
|
108
124
|
|
|
109
125
|
export interface EventLoopMonitorCfg {
|
|
110
|
-
/**
|
|
111
|
-
* Defaults to 20.
|
|
112
|
-
*/
|
|
113
|
-
resolution?: NumberOfMilliseconds
|
|
114
|
-
|
|
115
126
|
/**
|
|
116
127
|
* Defaults to 60_000 ms
|
|
117
128
|
*/
|
|
@@ -156,3 +167,11 @@ export interface EventLoopStats {
|
|
|
156
167
|
*/
|
|
157
168
|
gcCPU: Integer
|
|
158
169
|
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Extends the Node types with `samplePerIteration` (added in Node 24.19 / 26.5).
|
|
173
|
+
* Remove once @types/node declares it.
|
|
174
|
+
*/
|
|
175
|
+
interface EventLoopMonitorOptions2 extends EventLoopMonitorOptions {
|
|
176
|
+
samplePerIteration?: boolean
|
|
177
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { _assert, AppError } from '@naturalcycles/js-lib/error'
|
|
2
|
+
import type { NumberOfBytes } from '@naturalcycles/js-lib/types'
|
|
3
|
+
import busboy from 'busboy'
|
|
4
|
+
import type { Busboy, FileInfo } from 'busboy'
|
|
5
|
+
import type { BackendRequest } from '../server/server.model.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Parses a `multipart/form-data` request body (on top of `busboy`), populates `req.body` with the
|
|
9
|
+
* form fields, and returns the requested files keyed by their form field name:
|
|
10
|
+
*
|
|
11
|
+
* const { csv } = await getUploadedFiles(req, { files: ['csv'] })
|
|
12
|
+
* const { csv, avatar } = await getUploadedFiles(req, {
|
|
13
|
+
* files: ['csv'], // required -> UploadedFile
|
|
14
|
+
* optionalFiles: ['avatar'], // optional -> UploadedFile | undefined
|
|
15
|
+
* })
|
|
16
|
+
*
|
|
17
|
+
* `files` are required: each is asserted to be present and non-empty (throws 400 otherwise).
|
|
18
|
+
* `optionalFiles` are returned as `UploadedFile | undefined` — `undefined` when the field is
|
|
19
|
+
* absent or empty (e.g. a blank file input), so "optional" is genuinely optional.
|
|
20
|
+
*
|
|
21
|
+
* Throws 413 as soon as ANY uploaded file exceeds `maxFileSize` bytes (default: 10 MB), aborting
|
|
22
|
+
* the parse immediately without buffering the rest of the body.
|
|
23
|
+
*
|
|
24
|
+
* Meant to be called from inside a request handler, AFTER authentication — so an
|
|
25
|
+
* unauthenticated/unauthorized request is rejected before its (potentially large) multipart body
|
|
26
|
+
* is buffered into memory.
|
|
27
|
+
*/
|
|
28
|
+
export async function getUploadedFiles<
|
|
29
|
+
const FILENAMES extends string = never,
|
|
30
|
+
const OPTIONAL_FILENAMES extends string = never,
|
|
31
|
+
>(
|
|
32
|
+
req: BackendRequest,
|
|
33
|
+
opt: {
|
|
34
|
+
files?: readonly FILENAMES[]
|
|
35
|
+
optionalFiles?: readonly OPTIONAL_FILENAMES[]
|
|
36
|
+
maxFileSize?: NumberOfBytes
|
|
37
|
+
},
|
|
38
|
+
): Promise<Record<FILENAMES, UploadedFile> & Record<OPTIONAL_FILENAMES, UploadedFile | undefined>> {
|
|
39
|
+
const { files: requiredNames = [], optionalFiles = [], maxFileSize = 10 * MB } = opt
|
|
40
|
+
|
|
41
|
+
const uploadedFiles = await parseMultipart(req, maxFileSize)
|
|
42
|
+
const result: Record<string, UploadedFile | undefined> = {}
|
|
43
|
+
|
|
44
|
+
for (const name of requiredNames) {
|
|
45
|
+
const file = uploadedFiles[name]
|
|
46
|
+
_assert(file, `Uploaded file "${name}" is missing`, { backendResponseStatusCode: 400 })
|
|
47
|
+
_assert(file.size > 0, `Uploaded file "${name}" is empty`, { backendResponseStatusCode: 400 })
|
|
48
|
+
result[name] = file
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const name of optionalFiles) {
|
|
52
|
+
const file = uploadedFiles[name]
|
|
53
|
+
// Absent or empty (e.g. a blank file input) is treated as "not provided".
|
|
54
|
+
result[name] = file && file.size > 0 ? file : undefined
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return result as Record<FILENAMES, UploadedFile> &
|
|
58
|
+
Record<OPTIONAL_FILENAMES, UploadedFile | undefined>
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Streams the request through busboy, buffering each file into memory and collecting the form
|
|
63
|
+
* fields onto `req.body`. Resolves with the uploaded files keyed by their form field name.
|
|
64
|
+
*
|
|
65
|
+
* Rejects (aborting the parse) the moment a file exceeds `maxFileSize`.
|
|
66
|
+
*/
|
|
67
|
+
async function parseMultipart(
|
|
68
|
+
req: BackendRequest,
|
|
69
|
+
maxFileSize: NumberOfBytes,
|
|
70
|
+
): Promise<Record<string, UploadedFile>> {
|
|
71
|
+
return new Promise((resolve, reject) => {
|
|
72
|
+
const files: Record<string, UploadedFile> = {}
|
|
73
|
+
const body: Record<string, string> = {}
|
|
74
|
+
|
|
75
|
+
let bb: Busboy
|
|
76
|
+
try {
|
|
77
|
+
bb = busboy({ headers: req.headers, limits: { fileSize: maxFileSize } })
|
|
78
|
+
} catch {
|
|
79
|
+
// Not a multipart request (or malformed Content-Type) — no files to parse
|
|
80
|
+
resolve(files)
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
bb.on('field', (name, value) => {
|
|
85
|
+
body[name] = value
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
bb.on('file', (name: string, stream, info: FileInfo) => {
|
|
89
|
+
const chunks: Buffer[] = []
|
|
90
|
+
stream.on('data', (chunk: Buffer) => chunks.push(chunk))
|
|
91
|
+
// Abort as soon as the size limit is hit: stop feeding busboy and reject right away,
|
|
92
|
+
// rather than buffering the whole over-limit body and reporting it at the end.
|
|
93
|
+
stream.on('limit', () => {
|
|
94
|
+
req.unpipe(bb)
|
|
95
|
+
req.resume() // drain the remainder so the connection can close cleanly
|
|
96
|
+
reject(
|
|
97
|
+
new AppError(`Uploaded file "${name}" exceeds the size limit of ${maxFileSize} bytes`, {
|
|
98
|
+
backendResponseStatusCode: 413,
|
|
99
|
+
}),
|
|
100
|
+
)
|
|
101
|
+
})
|
|
102
|
+
stream.on('close', () => {
|
|
103
|
+
const data = Buffer.concat(chunks)
|
|
104
|
+
files[name] = {
|
|
105
|
+
name: info.filename,
|
|
106
|
+
mimeType: info.mimeType,
|
|
107
|
+
encoding: info.encoding,
|
|
108
|
+
data,
|
|
109
|
+
size: data.length,
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
bb.on('error', reject)
|
|
115
|
+
bb.on('close', () => {
|
|
116
|
+
req.body = body
|
|
117
|
+
resolve(files)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
req.pipe(bb)
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const MB = 1024 * 1024
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* A single uploaded file, buffered in memory.
|
|
128
|
+
*/
|
|
129
|
+
export interface UploadedFile {
|
|
130
|
+
/** Original filename from the client. */
|
|
131
|
+
name: string
|
|
132
|
+
/** Content-Type of the file part. */
|
|
133
|
+
mimeType: string
|
|
134
|
+
/** Content-Transfer-Encoding of the file part. */
|
|
135
|
+
encoding: string
|
|
136
|
+
/** File contents. */
|
|
137
|
+
data: Buffer
|
|
138
|
+
/** Size of `data` in bytes. */
|
|
139
|
+
size: NumberOfBytes
|
|
140
|
+
}
|