@remix-run/multipart-parser 0.11.0 → 0.13.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/LICENSE +1 -1
- package/README.md +39 -45
- package/dist/{multipart-parser.d.ts → index.d.ts} +1 -1
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/lib/buffer-search.d.ts.map +1 -1
- package/dist/lib/buffer-search.js +51 -0
- package/dist/lib/multipart-request.d.ts.map +1 -1
- package/dist/lib/multipart-request.js +46 -0
- package/dist/lib/multipart.d.ts.map +1 -1
- package/dist/lib/multipart.js +320 -0
- package/dist/lib/multipart.node.d.ts.map +1 -1
- package/dist/lib/multipart.node.js +60 -0
- package/dist/lib/read-stream.js +16 -0
- package/dist/{multipart-parser.node.d.ts → node.d.ts} +2 -2
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +4 -0
- package/package.json +13 -24
- package/src/{multipart-parser.ts → index.ts} +3 -3
- package/src/lib/buffer-search.ts +25 -25
- package/src/lib/multipart-request.ts +11 -11
- package/src/lib/multipart.node.ts +13 -13
- package/src/lib/multipart.ts +123 -123
- package/src/lib/read-stream.ts +5 -5
- package/src/{multipart-parser.node.ts → node.ts} +9 -4
- package/dist/multipart-parser.cjs +0 -2021
- package/dist/multipart-parser.cjs.map +0 -7
- package/dist/multipart-parser.d.ts.map +0 -1
- package/dist/multipart-parser.js +0 -1985
- package/dist/multipart-parser.js.map +0 -7
- package/dist/multipart-parser.node.cjs +0 -2027
- package/dist/multipart-parser.node.cjs.map +0 -7
- package/dist/multipart-parser.node.d.ts.map +0 -1
- package/dist/multipart-parser.node.js +0 -1991
- package/dist/multipart-parser.node.js.map +0 -7
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
- Convenient `MultipartPart` API with `arrayBuffer`, `bytes`, `text`, `size`, and metadata access
|
|
20
20
|
- Built-in file size limiting to prevent abuse
|
|
21
21
|
- First-class Node.js support with native `http.IncomingMessage` compatibility
|
|
22
|
-
- [
|
|
22
|
+
- [Demos for every major runtime](https://github.com/remix-run/remix/tree/main/packages/multipart-parser/demos)
|
|
23
23
|
|
|
24
24
|
## Installation
|
|
25
25
|
|
|
@@ -29,41 +29,35 @@ Install from [npm](https://www.npmjs.com/):
|
|
|
29
29
|
npm install @remix-run/multipart-parser
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
Or install from [JSR](https://jsr.io/):
|
|
33
|
-
|
|
34
|
-
```sh
|
|
35
|
-
deno add @remix-run/multipart-parser
|
|
36
|
-
```
|
|
37
|
-
|
|
38
32
|
## Usage
|
|
39
33
|
|
|
40
34
|
The most common use case for `multipart-parser` is handling file uploads when you're building a web server. For this case, the `parseMultipartRequest` function is your friend. It automatically validates the request is `multipart/form-data`, extracts the multipart boundary from the `Content-Type` header, parses all fields and files in the `request.body` stream, and gives each one to you as a `MultipartPart` object with a rich API for accessing its metadata and content.
|
|
41
35
|
|
|
42
36
|
```ts
|
|
43
|
-
import { MultipartParseError, parseMultipartRequest } from '@remix-run/multipart-parser'
|
|
37
|
+
import { MultipartParseError, parseMultipartRequest } from '@remix-run/multipart-parser'
|
|
44
38
|
|
|
45
39
|
async function handleRequest(request: Request): void {
|
|
46
40
|
try {
|
|
47
41
|
for await (let part of parseMultipartRequest(request)) {
|
|
48
42
|
if (part.isFile) {
|
|
49
43
|
// Access file data in multiple formats
|
|
50
|
-
let buffer = part.arrayBuffer
|
|
51
|
-
console.log(`File received: ${part.filename} (${buffer.byteLength} bytes)`)
|
|
52
|
-
console.log(`Content type: ${part.mediaType}`)
|
|
53
|
-
console.log(`Field name: ${part.name}`)
|
|
44
|
+
let buffer = part.arrayBuffer // ArrayBuffer
|
|
45
|
+
console.log(`File received: ${part.filename} (${buffer.byteLength} bytes)`)
|
|
46
|
+
console.log(`Content type: ${part.mediaType}`)
|
|
47
|
+
console.log(`Field name: ${part.name}`)
|
|
54
48
|
|
|
55
49
|
// Save to disk, upload to cloud storage, etc.
|
|
56
|
-
await saveFile(part.filename, part.bytes)
|
|
50
|
+
await saveFile(part.filename, part.bytes)
|
|
57
51
|
} else {
|
|
58
|
-
let text = part.text
|
|
59
|
-
console.log(`Field received: ${part.name} = ${JSON.stringify(text)}`)
|
|
52
|
+
let text = part.text // string
|
|
53
|
+
console.log(`Field received: ${part.name} = ${JSON.stringify(text)}`)
|
|
60
54
|
}
|
|
61
55
|
}
|
|
62
56
|
} catch (error) {
|
|
63
57
|
if (error instanceof MultipartParseError) {
|
|
64
|
-
console.error('Failed to parse multipart request:', error.message)
|
|
58
|
+
console.error('Failed to parse multipart request:', error.message)
|
|
65
59
|
} else {
|
|
66
|
-
console.error('An unexpected error occurred:', error)
|
|
60
|
+
console.error('An unexpected error occurred:', error)
|
|
67
61
|
}
|
|
68
62
|
}
|
|
69
63
|
}
|
|
@@ -78,10 +72,10 @@ import {
|
|
|
78
72
|
MultipartParseError,
|
|
79
73
|
MaxFileSizeExceededError,
|
|
80
74
|
parseMultipartRequest,
|
|
81
|
-
} from '@remix-run/multipart-parser/node'
|
|
75
|
+
} from '@remix-run/multipart-parser/node'
|
|
82
76
|
|
|
83
|
-
const oneMb = Math.pow(2, 20)
|
|
84
|
-
const maxFileSize = 10 * oneMb
|
|
77
|
+
const oneMb = Math.pow(2, 20)
|
|
78
|
+
const maxFileSize = 10 * oneMb
|
|
85
79
|
|
|
86
80
|
async function handleRequest(request: Request): Promise<Response> {
|
|
87
81
|
try {
|
|
@@ -90,12 +84,12 @@ async function handleRequest(request: Request): Promise<Response> {
|
|
|
90
84
|
}
|
|
91
85
|
} catch (error) {
|
|
92
86
|
if (error instanceof MaxFileSizeExceededError) {
|
|
93
|
-
return new Response('File size limit exceeded', { status: 413 })
|
|
87
|
+
return new Response('File size limit exceeded', { status: 413 })
|
|
94
88
|
} else if (error instanceof MultipartParseError) {
|
|
95
|
-
return new Response('Failed to parse multipart request', { status: 400 })
|
|
89
|
+
return new Response('Failed to parse multipart request', { status: 400 })
|
|
96
90
|
} else {
|
|
97
|
-
console.error(error)
|
|
98
|
-
return new Response('Internal Server Error', { status: 500 })
|
|
91
|
+
console.error(error)
|
|
92
|
+
return new Response('Internal Server Error', { status: 500 })
|
|
99
93
|
}
|
|
100
94
|
}
|
|
101
95
|
}
|
|
@@ -108,8 +102,8 @@ The main module (`import from "@remix-run/multipart-parser"`) assumes you're wor
|
|
|
108
102
|
If however you're building a server for Node.js that relies on node-specific APIs like `http.IncomingMessage`, `stream.Readable`, and `buffer.Buffer` (ala Express or `http.createServer`), `multipart-parser` ships with an additional module that works directly with these APIs.
|
|
109
103
|
|
|
110
104
|
```ts
|
|
111
|
-
import * as http from 'node:http'
|
|
112
|
-
import { MultipartParseError, parseMultipartRequest } from '@remix-run/multipart-parser/node'
|
|
105
|
+
import * as http from 'node:http'
|
|
106
|
+
import { MultipartParseError, parseMultipartRequest } from '@remix-run/multipart-parser/node'
|
|
113
107
|
|
|
114
108
|
let server = http.createServer(async (req, res) => {
|
|
115
109
|
try {
|
|
@@ -118,14 +112,14 @@ let server = http.createServer(async (req, res) => {
|
|
|
118
112
|
}
|
|
119
113
|
} catch (error) {
|
|
120
114
|
if (error instanceof MultipartParseError) {
|
|
121
|
-
console.error('Failed to parse multipart request:', error.message)
|
|
115
|
+
console.error('Failed to parse multipart request:', error.message)
|
|
122
116
|
} else {
|
|
123
|
-
console.error('An unexpected error occurred:', error)
|
|
117
|
+
console.error('An unexpected error occurred:', error)
|
|
124
118
|
}
|
|
125
119
|
}
|
|
126
|
-
})
|
|
120
|
+
})
|
|
127
121
|
|
|
128
|
-
server.listen(8080)
|
|
122
|
+
server.listen(8080)
|
|
129
123
|
```
|
|
130
124
|
|
|
131
125
|
## Low-level API
|
|
@@ -133,10 +127,10 @@ server.listen(8080);
|
|
|
133
127
|
If you're working directly with multipart boundaries and buffers/streams of multipart data that are not necessarily part of a request, `multipart-parser` provides a low-level `parseMultipart()` API that you can use directly:
|
|
134
128
|
|
|
135
129
|
```ts
|
|
136
|
-
import { parseMultipart } from '@remix-run/multipart-parser'
|
|
130
|
+
import { parseMultipart } from '@remix-run/multipart-parser'
|
|
137
131
|
|
|
138
|
-
let message = new Uint8Array(/* ... */)
|
|
139
|
-
let boundary = '----WebKitFormBoundary56eac3x'
|
|
132
|
+
let message = new Uint8Array(/* ... */)
|
|
133
|
+
let boundary = '----WebKitFormBoundary56eac3x'
|
|
140
134
|
|
|
141
135
|
for (let part of parseMultipart(message, { boundary })) {
|
|
142
136
|
// ...
|
|
@@ -146,24 +140,24 @@ for (let part of parseMultipart(message, { boundary })) {
|
|
|
146
140
|
In addition, the `parseMultipartStream` function provides an `async` generator interface for multipart data in a `ReadableStream`:
|
|
147
141
|
|
|
148
142
|
```ts
|
|
149
|
-
import { parseMultipartStream } from '@remix-run/multipart-parser'
|
|
143
|
+
import { parseMultipartStream } from '@remix-run/multipart-parser'
|
|
150
144
|
|
|
151
|
-
let message = new ReadableStream(/* ... */)
|
|
152
|
-
let boundary = '----WebKitFormBoundary56eac3x'
|
|
145
|
+
let message = new ReadableStream(/* ... */)
|
|
146
|
+
let boundary = '----WebKitFormBoundary56eac3x'
|
|
153
147
|
|
|
154
148
|
for await (let part of parseMultipartStream(message, { boundary })) {
|
|
155
149
|
// ...
|
|
156
150
|
}
|
|
157
151
|
```
|
|
158
152
|
|
|
159
|
-
##
|
|
153
|
+
## Demos
|
|
160
154
|
|
|
161
|
-
The [`
|
|
155
|
+
The [`demos` directory](https://github.com/remix-run/remix/tree/main/packages/multipart-parser/demos) contains a few working demos of how you can use this library:
|
|
162
156
|
|
|
163
|
-
- [`
|
|
164
|
-
- [`
|
|
165
|
-
- [`
|
|
166
|
-
- [`
|
|
157
|
+
- [`demos/bun`](https://github.com/remix-run/remix/tree/main/packages/multipart-parser/demos/bun) - using multipart-parser in Bun
|
|
158
|
+
- [`demos/cf-workers`](https://github.com/remix-run/remix/tree/main/packages/multipart-parser/demos/cf-workers) - using multipart-parser in a Cloudflare Worker and storing file uploads in R2
|
|
159
|
+
- [`demos/deno`](https://github.com/remix-run/remix/tree/main/packages/multipart-parser/demos/deno) - using multipart-parser in Deno
|
|
160
|
+
- [`demos/node`](https://github.com/remix-run/remix/tree/main/packages/multipart-parser/demos/node) - using multipart-parser in Node.js
|
|
167
161
|
|
|
168
162
|
## Benchmark
|
|
169
163
|
|
|
@@ -223,8 +217,8 @@ Deno 2.3.6
|
|
|
223
217
|
|
|
224
218
|
## Related Packages
|
|
225
219
|
|
|
226
|
-
- [`form-data-parser`](https://github.com/remix-run/remix/tree/
|
|
227
|
-
- [`headers`](https://github.com/remix-run/remix/tree/
|
|
220
|
+
- [`form-data-parser`](https://github.com/remix-run/remix/tree/main/packages/form-data-parser) - Uses `multipart-parser` internally to parse multipart requests and generate `FileUpload`s for storage
|
|
221
|
+
- [`headers`](https://github.com/remix-run/remix/tree/main/packages/headers) - Used internally to parse HTTP headers and get metadata (filename, content type) for each `MultipartPart`
|
|
228
222
|
|
|
229
223
|
## Credits
|
|
230
224
|
|
|
@@ -232,4 +226,4 @@ Thanks to Jacob Ebey who gave me several code reviews on this project prior to p
|
|
|
232
226
|
|
|
233
227
|
## License
|
|
234
228
|
|
|
235
|
-
See [LICENSE](https://github.com/remix-run/remix/blob/
|
|
229
|
+
See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export type { ParseMultipartOptions, MultipartParserOptions } from './lib/multipart.ts';
|
|
2
2
|
export { MultipartParseError, MaxHeaderSizeExceededError, MaxFileSizeExceededError, parseMultipart, parseMultipartStream, MultipartParser, MultipartPart, } from './lib/multipart.ts';
|
|
3
3
|
export { getMultipartBoundary, isMultipartRequest, parseMultipartRequest, } from './lib/multipart-request.ts';
|
|
4
|
-
//# sourceMappingURL=
|
|
4
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAA;AACvF,OAAO,EACL,mBAAmB,EACnB,0BAA0B,EAC1B,wBAAwB,EACxB,cAAc,EACd,oBAAoB,EACpB,eAAe,EACf,aAAa,GACd,MAAM,oBAAoB,CAAA;AAE3B,OAAO,EACL,oBAAoB,EACpB,kBAAkB,EAClB,qBAAqB,GACtB,MAAM,4BAA4B,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export { MultipartParseError, MaxHeaderSizeExceededError, MaxFileSizeExceededError, parseMultipart, parseMultipartStream, MultipartParser, MultipartPart, } from "./lib/multipart.js";
|
|
2
|
+
export { getMultipartBoundary, isMultipartRequest, parseMultipartRequest, } from "./lib/multipart-request.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"buffer-search.d.ts","sourceRoot":"","sources":["../../src/lib/buffer-search.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,cAAc;IAC7B,CAAC,QAAQ,EAAE,UAAU,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"buffer-search.d.ts","sourceRoot":"","sources":["../../src/lib/buffer-search.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,cAAc;IAC7B,CAAC,QAAQ,EAAE,UAAU,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;CAC/C;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,CA+B5D;AAED,MAAM,WAAW,yBAAyB;IACxC,CAAC,QAAQ,EAAE,UAAU,GAAG,MAAM,CAAA;CAC/B;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,yBAAyB,CAyBlF"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export function createSearch(pattern) {
|
|
2
|
+
let needle = new TextEncoder().encode(pattern);
|
|
3
|
+
let search;
|
|
4
|
+
if ('Buffer' in globalThis && !('Bun' in globalThis || 'Deno' in globalThis)) {
|
|
5
|
+
// Use the built-in Buffer.indexOf method on Node.js for better perf.
|
|
6
|
+
search = (haystack, start = 0) => Buffer.prototype.indexOf.call(haystack, needle, start);
|
|
7
|
+
}
|
|
8
|
+
else {
|
|
9
|
+
let needleEnd = needle.length - 1;
|
|
10
|
+
let skipTable = new Uint8Array(256).fill(needle.length);
|
|
11
|
+
for (let i = 0; i < needleEnd; ++i) {
|
|
12
|
+
skipTable[needle[i]] = needleEnd - i;
|
|
13
|
+
}
|
|
14
|
+
search = (haystack, start = 0) => {
|
|
15
|
+
let haystackLength = haystack.length;
|
|
16
|
+
let i = start + needleEnd;
|
|
17
|
+
while (i < haystackLength) {
|
|
18
|
+
for (let j = needleEnd, k = i; j >= 0 && haystack[k] === needle[j]; --j, --k) {
|
|
19
|
+
if (j === 0)
|
|
20
|
+
return k;
|
|
21
|
+
}
|
|
22
|
+
i += skipTable[haystack[i]];
|
|
23
|
+
}
|
|
24
|
+
return -1;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return search;
|
|
28
|
+
}
|
|
29
|
+
export function createPartialTailSearch(pattern) {
|
|
30
|
+
let needle = new TextEncoder().encode(pattern);
|
|
31
|
+
let byteIndexes = {};
|
|
32
|
+
for (let i = 0; i < needle.length; ++i) {
|
|
33
|
+
let byte = needle[i];
|
|
34
|
+
if (byteIndexes[byte] === undefined)
|
|
35
|
+
byteIndexes[byte] = [];
|
|
36
|
+
byteIndexes[byte].push(i);
|
|
37
|
+
}
|
|
38
|
+
return function (haystack) {
|
|
39
|
+
let haystackEnd = haystack.length - 1;
|
|
40
|
+
if (haystack[haystackEnd] in byteIndexes) {
|
|
41
|
+
let indexes = byteIndexes[haystack[haystackEnd]];
|
|
42
|
+
for (let i = indexes.length - 1; i >= 0; --i) {
|
|
43
|
+
for (let j = indexes[i], k = haystackEnd; j >= 0 && haystack[k] === needle[j]; --j, --k) {
|
|
44
|
+
if (j === 0)
|
|
45
|
+
return k;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return -1;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"multipart-request.d.ts","sourceRoot":"","sources":["../../src/lib/multipart-request.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,aAAa,EAAE,MAAM,gBAAgB,
|
|
1
|
+
{"version":3,"file":"multipart-request.d.ts","sourceRoot":"","sources":["../../src/lib/multipart-request.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAG3E;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGvE;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAG5D;AAED;;;;;;;GAOG;AACH,wBAAuB,qBAAqB,CAC1C,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE,sBAAsB,GAC/B,cAAc,CAAC,aAAa,EAAE,IAAI,EAAE,OAAO,CAAC,CAkB9C"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { MultipartParseError, parseMultipartStream } from "./multipart.js";
|
|
2
|
+
/**
|
|
3
|
+
* Extracts the boundary string from a `multipart/*` content type.
|
|
4
|
+
*
|
|
5
|
+
* @param contentType The `Content-Type` header value from the request
|
|
6
|
+
* @return The boundary string if found, or null if not present
|
|
7
|
+
*/
|
|
8
|
+
export function getMultipartBoundary(contentType) {
|
|
9
|
+
let match = /boundary=(?:"([^"]+)"|([^;]+))/i.exec(contentType);
|
|
10
|
+
return match ? (match[1] ?? match[2]) : null;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Returns true if the given request contains multipart data.
|
|
14
|
+
*
|
|
15
|
+
* @param request The `Request` object to check
|
|
16
|
+
* @return `true` if the request is a multipart request, `false` otherwise
|
|
17
|
+
*/
|
|
18
|
+
export function isMultipartRequest(request) {
|
|
19
|
+
let contentType = request.headers.get('Content-Type');
|
|
20
|
+
return contentType != null && contentType.startsWith('multipart/');
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Parse a multipart [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) and yield each part as
|
|
24
|
+
* a `MultipartPart` object. Useful in HTTP server contexts for handling incoming `multipart/*` requests.
|
|
25
|
+
*
|
|
26
|
+
* @param request The `Request` object containing multipart data
|
|
27
|
+
* @param options Optional parser options, such as `maxHeaderSize` and `maxFileSize`
|
|
28
|
+
* @return An async generator yielding `MultipartPart` objects
|
|
29
|
+
*/
|
|
30
|
+
export async function* parseMultipartRequest(request, options) {
|
|
31
|
+
if (!isMultipartRequest(request)) {
|
|
32
|
+
throw new MultipartParseError('Request is not a multipart request');
|
|
33
|
+
}
|
|
34
|
+
if (!request.body) {
|
|
35
|
+
throw new MultipartParseError('Request body is empty');
|
|
36
|
+
}
|
|
37
|
+
let boundary = getMultipartBoundary(request.headers.get('Content-Type'));
|
|
38
|
+
if (!boundary) {
|
|
39
|
+
throw new MultipartParseError('Invalid Content-Type header: missing boundary');
|
|
40
|
+
}
|
|
41
|
+
yield* parseMultipartStream(request.body, {
|
|
42
|
+
boundary,
|
|
43
|
+
maxHeaderSize: options?.maxHeaderSize,
|
|
44
|
+
maxFileSize: options?.maxFileSize,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"multipart.d.ts","sourceRoot":"","sources":["../../src/lib/multipart.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,oBAAoB,
|
|
1
|
+
{"version":3,"file":"multipart.d.ts","sourceRoot":"","sources":["../../src/lib/multipart.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,oBAAoB,CAAA;AAMxC;;GAEG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,OAAO,EAAE,MAAM;CAI5B;AAED;;GAEG;AACH,qBAAa,0BAA2B,SAAQ,mBAAmB;gBACrD,aAAa,EAAE,MAAM;CAIlC;AAED;;GAEG;AACH,qBAAa,wBAAyB,SAAQ,mBAAmB;gBACnD,WAAW,EAAE,MAAM;CAIhC;AAED,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED;;;;;;;;;GASG;AACH,wBAAiB,cAAc,CAC7B,OAAO,EAAE,UAAU,GAAG,QAAQ,CAAC,UAAU,CAAC,EAC1C,OAAO,EAAE,qBAAqB,GAC7B,SAAS,CAAC,aAAa,EAAE,IAAI,EAAE,OAAO,CAAC,CAmBzC;AAED;;;;;;;;;GASG;AACH,wBAAuB,oBAAoB,CACzC,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,EAClC,OAAO,EAAE,qBAAqB,GAC7B,cAAc,CAAC,aAAa,EAAE,IAAI,EAAE,OAAO,CAAC,CAe9C;AAED,MAAM,MAAM,sBAAsB,GAAG,IAAI,CAAC,qBAAqB,EAAE,UAAU,CAAC,CAAA;AAa5E;;GAEG;AACH,qBAAa,eAAe;;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;gBAahB,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,sBAAsB;IAY9D;;;;;OAKG;IACF,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,SAAS,CAAC,aAAa,EAAE,IAAI,EAAE,OAAO,CAAC;IA0HlE;;;;;;;OAOG;IACH,MAAM,IAAI,IAAI;CAKf;AAID;;GAEG;AACH,qBAAa,aAAa;;IACxB;;OAEG;IACH,QAAQ,CAAC,OAAO,EAAE,UAAU,EAAE,CAAA;gBAKlB,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,UAAU,EAAE;IAKrD;;OAEG;IACH,IAAI,WAAW,IAAI,WAAW,CAE7B;IAED;;;OAGG;IACH,IAAI,KAAK,IAAI,UAAU,CAUtB;IAED;;OAEG;IACH,IAAI,OAAO,IAAI,OAAO,CAMrB;IAED;;OAEG;IACH,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED;;OAEG;IACH,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED;;OAEG;IACH,IAAI,QAAQ,IAAI,MAAM,GAAG,SAAS,CAEjC;IAED;;OAEG;IACH,IAAI,SAAS,IAAI,MAAM,GAAG,SAAS,CAElC;IAED;;OAEG;IACH,IAAI,IAAI,IAAI,MAAM,GAAG,SAAS,CAE7B;IAED;;OAEG;IACH,IAAI,IAAI,IAAI,MAAM,CAQjB;IAED;;;;;OAKG;IACH,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF"}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import Headers from '@remix-run/headers';
|
|
2
|
+
import { readStream } from "./read-stream.js";
|
|
3
|
+
import { createSearch, createPartialTailSearch } from "./buffer-search.js";
|
|
4
|
+
/**
|
|
5
|
+
* The base class for errors thrown by the multipart parser.
|
|
6
|
+
*/
|
|
7
|
+
export class MultipartParseError extends Error {
|
|
8
|
+
constructor(message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'MultipartParseError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* An error thrown when the maximum allowed size of a header is exceeded.
|
|
15
|
+
*/
|
|
16
|
+
export class MaxHeaderSizeExceededError extends MultipartParseError {
|
|
17
|
+
constructor(maxHeaderSize) {
|
|
18
|
+
super(`Multipart header size exceeds maximum allowed size of ${maxHeaderSize} bytes`);
|
|
19
|
+
this.name = 'MaxHeaderSizeExceededError';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* An error thrown when the maximum allowed size of a file is exceeded.
|
|
24
|
+
*/
|
|
25
|
+
export class MaxFileSizeExceededError extends MultipartParseError {
|
|
26
|
+
constructor(maxFileSize) {
|
|
27
|
+
super(`File size exceeds maximum allowed size of ${maxFileSize} bytes`);
|
|
28
|
+
this.name = 'MaxFileSizeExceededError';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Parse a `multipart/*` message from a buffer/iterable and yield each part as a `MultipartPart` object.
|
|
33
|
+
*
|
|
34
|
+
* Note: This is a low-level API that requires manual handling of the content and boundary. If you're
|
|
35
|
+
* building a web server, consider using `parseMultipartRequest(request)` instead.
|
|
36
|
+
*
|
|
37
|
+
* @param message The multipart message as a `Uint8Array` or an iterable of `Uint8Array` chunks
|
|
38
|
+
* @param options Options for the parser
|
|
39
|
+
* @return A generator that yields `MultipartPart` objects
|
|
40
|
+
*/
|
|
41
|
+
export function* parseMultipart(message, options) {
|
|
42
|
+
let parser = new MultipartParser(options.boundary, {
|
|
43
|
+
maxHeaderSize: options.maxHeaderSize,
|
|
44
|
+
maxFileSize: options.maxFileSize,
|
|
45
|
+
});
|
|
46
|
+
if (message instanceof Uint8Array) {
|
|
47
|
+
if (message.length === 0) {
|
|
48
|
+
return; // No data to parse
|
|
49
|
+
}
|
|
50
|
+
yield* parser.write(message);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
for (let chunk of message) {
|
|
54
|
+
yield* parser.write(chunk);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
parser.finish();
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Parse a `multipart/*` message stream and yield each part as a `MultipartPart` object.
|
|
61
|
+
*
|
|
62
|
+
* Note: This is a low-level API that requires manual handling of the content and boundary. If you're
|
|
63
|
+
* building a web server, consider using `parseMultipartRequest(request)` instead.
|
|
64
|
+
*
|
|
65
|
+
* @param stream A stream containing multipart data as a `ReadableStream<Uint8Array>`
|
|
66
|
+
* @param options Options for the parser
|
|
67
|
+
* @return An async generator that yields `MultipartPart` objects
|
|
68
|
+
*/
|
|
69
|
+
export async function* parseMultipartStream(stream, options) {
|
|
70
|
+
let parser = new MultipartParser(options.boundary, {
|
|
71
|
+
maxHeaderSize: options.maxHeaderSize,
|
|
72
|
+
maxFileSize: options.maxFileSize,
|
|
73
|
+
});
|
|
74
|
+
for await (let chunk of readStream(stream)) {
|
|
75
|
+
if (chunk.length === 0) {
|
|
76
|
+
continue; // No data to parse
|
|
77
|
+
}
|
|
78
|
+
yield* parser.write(chunk);
|
|
79
|
+
}
|
|
80
|
+
parser.finish();
|
|
81
|
+
}
|
|
82
|
+
const MultipartParserStateStart = 0;
|
|
83
|
+
const MultipartParserStateAfterBoundary = 1;
|
|
84
|
+
const MultipartParserStateHeader = 2;
|
|
85
|
+
const MultipartParserStateBody = 3;
|
|
86
|
+
const MultipartParserStateDone = 4;
|
|
87
|
+
const findDoubleNewline = createSearch('\r\n\r\n');
|
|
88
|
+
const oneKb = 1024;
|
|
89
|
+
const oneMb = 1024 * oneKb;
|
|
90
|
+
/**
|
|
91
|
+
* A streaming parser for `multipart/*` HTTP messages.
|
|
92
|
+
*/
|
|
93
|
+
export class MultipartParser {
|
|
94
|
+
boundary;
|
|
95
|
+
maxHeaderSize;
|
|
96
|
+
maxFileSize;
|
|
97
|
+
#findOpeningBoundary;
|
|
98
|
+
#openingBoundaryLength;
|
|
99
|
+
#findBoundary;
|
|
100
|
+
#findPartialTailBoundary;
|
|
101
|
+
#boundaryLength;
|
|
102
|
+
#state = MultipartParserStateStart;
|
|
103
|
+
#buffer = null;
|
|
104
|
+
#currentPart = null;
|
|
105
|
+
#contentLength = 0;
|
|
106
|
+
constructor(boundary, options) {
|
|
107
|
+
this.boundary = boundary;
|
|
108
|
+
this.maxHeaderSize = options?.maxHeaderSize ?? 8 * oneKb;
|
|
109
|
+
this.maxFileSize = options?.maxFileSize ?? 2 * oneMb;
|
|
110
|
+
this.#findOpeningBoundary = createSearch(`--${boundary}`);
|
|
111
|
+
this.#openingBoundaryLength = 2 + boundary.length; // length of '--' + boundary
|
|
112
|
+
this.#findBoundary = createSearch(`\r\n--${boundary}`);
|
|
113
|
+
this.#findPartialTailBoundary = createPartialTailSearch(`\r\n--${boundary}`);
|
|
114
|
+
this.#boundaryLength = 4 + boundary.length; // length of '\r\n--' + boundary
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Write a chunk of data to the parser.
|
|
118
|
+
*
|
|
119
|
+
* @param chunk A chunk of data to write to the parser
|
|
120
|
+
* @return A generator yielding `MultipartPart` objects as they are parsed
|
|
121
|
+
*/
|
|
122
|
+
*write(chunk) {
|
|
123
|
+
if (this.#state === MultipartParserStateDone) {
|
|
124
|
+
throw new MultipartParseError('Unexpected data after end of stream');
|
|
125
|
+
}
|
|
126
|
+
let index = 0;
|
|
127
|
+
let chunkLength = chunk.length;
|
|
128
|
+
if (this.#buffer !== null) {
|
|
129
|
+
let newChunk = new Uint8Array(this.#buffer.length + chunkLength);
|
|
130
|
+
newChunk.set(this.#buffer, 0);
|
|
131
|
+
newChunk.set(chunk, this.#buffer.length);
|
|
132
|
+
chunk = newChunk;
|
|
133
|
+
chunkLength = chunk.length;
|
|
134
|
+
this.#buffer = null;
|
|
135
|
+
}
|
|
136
|
+
while (true) {
|
|
137
|
+
if (this.#state === MultipartParserStateBody) {
|
|
138
|
+
if (chunkLength - index < this.#boundaryLength) {
|
|
139
|
+
this.#buffer = chunk.subarray(index);
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
let boundaryIndex = this.#findBoundary(chunk, index);
|
|
143
|
+
if (boundaryIndex === -1) {
|
|
144
|
+
// No boundary found, but there may be a partial match at the end of the chunk.
|
|
145
|
+
let partialTailIndex = this.#findPartialTailBoundary(chunk);
|
|
146
|
+
if (partialTailIndex === -1) {
|
|
147
|
+
this.#append(index === 0 ? chunk : chunk.subarray(index));
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
this.#append(chunk.subarray(index, partialTailIndex));
|
|
151
|
+
this.#buffer = chunk.subarray(partialTailIndex);
|
|
152
|
+
}
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
this.#append(chunk.subarray(index, boundaryIndex));
|
|
156
|
+
yield this.#currentPart;
|
|
157
|
+
index = boundaryIndex + this.#boundaryLength;
|
|
158
|
+
this.#state = MultipartParserStateAfterBoundary;
|
|
159
|
+
}
|
|
160
|
+
if (this.#state === MultipartParserStateAfterBoundary) {
|
|
161
|
+
if (chunkLength - index < 2) {
|
|
162
|
+
this.#buffer = chunk.subarray(index);
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
if (chunk[index] === 45 && chunk[index + 1] === 45) {
|
|
166
|
+
this.#state = MultipartParserStateDone;
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
index += 2; // Skip \r\n after boundary
|
|
170
|
+
this.#state = MultipartParserStateHeader;
|
|
171
|
+
}
|
|
172
|
+
if (this.#state === MultipartParserStateHeader) {
|
|
173
|
+
if (chunkLength - index < 4) {
|
|
174
|
+
this.#buffer = chunk.subarray(index);
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
let headerEndIndex = findDoubleNewline(chunk, index);
|
|
178
|
+
if (headerEndIndex === -1) {
|
|
179
|
+
if (chunkLength - index > this.maxHeaderSize) {
|
|
180
|
+
throw new MaxHeaderSizeExceededError(this.maxHeaderSize);
|
|
181
|
+
}
|
|
182
|
+
this.#buffer = chunk.subarray(index);
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
if (headerEndIndex - index > this.maxHeaderSize) {
|
|
186
|
+
throw new MaxHeaderSizeExceededError(this.maxHeaderSize);
|
|
187
|
+
}
|
|
188
|
+
this.#currentPart = new MultipartPart(chunk.subarray(index, headerEndIndex), []);
|
|
189
|
+
this.#contentLength = 0;
|
|
190
|
+
index = headerEndIndex + 4; // Skip header + \r\n\r\n
|
|
191
|
+
this.#state = MultipartParserStateBody;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (this.#state === MultipartParserStateStart) {
|
|
195
|
+
if (chunkLength < this.#openingBoundaryLength) {
|
|
196
|
+
this.#buffer = chunk;
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
if (this.#findOpeningBoundary(chunk) !== 0) {
|
|
200
|
+
throw new MultipartParseError('Invalid multipart stream: missing initial boundary');
|
|
201
|
+
}
|
|
202
|
+
index = this.#openingBoundaryLength;
|
|
203
|
+
this.#state = MultipartParserStateAfterBoundary;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
#append(chunk) {
|
|
208
|
+
if (this.#contentLength + chunk.length > this.maxFileSize) {
|
|
209
|
+
throw new MaxFileSizeExceededError(this.maxFileSize);
|
|
210
|
+
}
|
|
211
|
+
this.#currentPart.content.push(chunk);
|
|
212
|
+
this.#contentLength += chunk.length;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Should be called after all data has been written to the parser.
|
|
216
|
+
*
|
|
217
|
+
* Note: This will throw if the multipart message is incomplete or
|
|
218
|
+
* wasn't properly terminated.
|
|
219
|
+
*
|
|
220
|
+
* @return void
|
|
221
|
+
*/
|
|
222
|
+
finish() {
|
|
223
|
+
if (this.#state !== MultipartParserStateDone) {
|
|
224
|
+
throw new MultipartParseError('Multipart stream not finished');
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const decoder = new TextDecoder('utf-8', { fatal: true });
|
|
229
|
+
/**
|
|
230
|
+
* A part of a `multipart/*` HTTP message.
|
|
231
|
+
*/
|
|
232
|
+
export class MultipartPart {
|
|
233
|
+
/**
|
|
234
|
+
* The raw content of this part as an array of `Uint8Array` chunks.
|
|
235
|
+
*/
|
|
236
|
+
content;
|
|
237
|
+
#header;
|
|
238
|
+
#headers;
|
|
239
|
+
constructor(header, content) {
|
|
240
|
+
this.#header = header;
|
|
241
|
+
this.content = content;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* The content of this part as an `ArrayBuffer`.
|
|
245
|
+
*/
|
|
246
|
+
get arrayBuffer() {
|
|
247
|
+
return this.bytes.buffer;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* The content of this part as a single `Uint8Array`. In `multipart/form-data` messages, this is useful
|
|
251
|
+
* for reading the value of files that were uploaded using `<input type="file">` fields.
|
|
252
|
+
*/
|
|
253
|
+
get bytes() {
|
|
254
|
+
let buffer = new Uint8Array(this.size);
|
|
255
|
+
let offset = 0;
|
|
256
|
+
for (let chunk of this.content) {
|
|
257
|
+
buffer.set(chunk, offset);
|
|
258
|
+
offset += chunk.length;
|
|
259
|
+
}
|
|
260
|
+
return buffer;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* The headers associated with this part.
|
|
264
|
+
*/
|
|
265
|
+
get headers() {
|
|
266
|
+
if (!this.#headers) {
|
|
267
|
+
this.#headers = new Headers(decoder.decode(this.#header));
|
|
268
|
+
}
|
|
269
|
+
return this.#headers;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* True if this part originated from a file upload.
|
|
273
|
+
*/
|
|
274
|
+
get isFile() {
|
|
275
|
+
return this.filename !== undefined || this.mediaType === 'application/octet-stream';
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* True if this part originated from a text input field in a form submission.
|
|
279
|
+
*/
|
|
280
|
+
get isText() {
|
|
281
|
+
return !this.isFile;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* The filename of the part, if it is a file upload.
|
|
285
|
+
*/
|
|
286
|
+
get filename() {
|
|
287
|
+
return this.headers.contentDisposition.preferredFilename;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* The media type of the part.
|
|
291
|
+
*/
|
|
292
|
+
get mediaType() {
|
|
293
|
+
return this.headers.contentType.mediaType;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* The name of the part, usually the `name` of the field in the `<form>` that submitted the request.
|
|
297
|
+
*/
|
|
298
|
+
get name() {
|
|
299
|
+
return this.headers.contentDisposition.name;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* The size of the content in bytes.
|
|
303
|
+
*/
|
|
304
|
+
get size() {
|
|
305
|
+
let size = 0;
|
|
306
|
+
for (let chunk of this.content) {
|
|
307
|
+
size += chunk.length;
|
|
308
|
+
}
|
|
309
|
+
return size;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* The content of this part as a string. In `multipart/form-data` messages, this is useful for
|
|
313
|
+
* reading the value of parts that originated from `<input type="text">` fields.
|
|
314
|
+
*
|
|
315
|
+
* Note: Do not use this for binary data, use `part.bytes` or `part.arrayBuffer` instead.
|
|
316
|
+
*/
|
|
317
|
+
get text() {
|
|
318
|
+
return decoder.decode(this.bytes);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"multipart.node.d.ts","sourceRoot":"","sources":["../../src/lib/multipart.node.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,IAAI,MAAM,WAAW,
|
|
1
|
+
{"version":3,"file":"multipart.node.d.ts","sourceRoot":"","sources":["../../src/lib/multipart.node.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,IAAI,MAAM,WAAW,CAAA;AACtC,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAEtC,OAAO,KAAK,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAQlG;;;;;;;;;GASG;AACH,wBAAiB,cAAc,CAC7B,OAAO,EAAE,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,EAClC,OAAO,EAAE,qBAAqB,GAC7B,SAAS,CAAC,aAAa,EAAE,IAAI,EAAE,OAAO,CAAC,CAEzC;AAED;;;;;;;;;GASG;AACH,wBAAuB,oBAAoB,CACzC,MAAM,EAAE,QAAQ,EAChB,OAAO,EAAE,qBAAqB,GAC7B,cAAc,CAAC,aAAa,EAAE,IAAI,EAAE,OAAO,CAAC,CAE9C;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,GAAG,OAAO,CAGrE;AAED;;;;;;GAMG;AACH,wBAAuB,qBAAqB,CAC1C,GAAG,EAAE,IAAI,CAAC,eAAe,EACzB,OAAO,CAAC,EAAE,sBAAsB,GAC/B,cAAc,CAAC,aAAa,EAAE,IAAI,EAAE,OAAO,CAAC,CAe9C"}
|