Skip to content

Commit 24576ca

Browse files
authored
Add support for Blob (#43)
2 parents b6152f4 + 9e213dc commit 24576ca

File tree

5 files changed

+158
-27
lines changed

5 files changed

+158
-27
lines changed

README.md

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,17 +46,14 @@ http.createServer(async (req, res) => {
4646
if (req.method === "POST" && req.headers["content-type"]) {
4747

4848
// get the request body
49-
const chunks: Uint8Array[] = [];
49+
const body: Uint8Array[] = [];
5050
for await (const chunk of req)
51-
chunks.push(chunk);
52-
const body = Buffer.concat(chunks);
53-
54-
// create a multipart component to hold the content-type header (which includes the boundary) and the body
55-
const component = new Component({
56-
"Content-Type": req.headers["content-type"]
57-
}, body);
58-
// parse multipart from the component
59-
const multipart = Multipart.part(component);
51+
body.push(chunk);
52+
53+
// create a blob to hold the Content-Type header (which includes the boundary) and the body
54+
const blob = new Blob(body, {type: req.headers["content-type"]});
55+
// parse multipart from the blob
56+
const multipart = await Multipart.blob(blob);
6057
console.log(multipart);
6158

6259
res.end("ok");

src/Component.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,28 @@ export class Component implements Part {
4343
}
4444

4545
/**
46-
* Create a Component from a {@link File}. If file media type is available,
46+
* Create a Component from a {@link !File}. If file media type is available,
4747
* it will be set in the `Content-Type` header. The file's contents will be used as the part's body.
4848
*
4949
* This method might be slow if a large file is provided as the file contents need to be read.
5050
*
5151
* @param file File instance to create the component from
52+
* @deprecated Use {@link Component.blob}.
5253
*/
5354
public static async file(file: File) {
54-
return new Component(file.type.length > 0 ? {"Content-Type": file.type} : {}, await file.arrayBuffer());
55+
return await Component.blob(file);
56+
}
57+
58+
/**
59+
* Create a Component from a {@link !Blob}. If blob media type is available,
60+
* it will be set in the `Content-Type` header. The blob's contents will be used as the part's body.
61+
*
62+
* This method might be slow if a large file is provided as the blob contents need to be read.
63+
*
64+
* @param blob Blob to create the component from
65+
*/
66+
public static async blob(blob: Blob) {
67+
return new Component(blob.type.length > 0 ? {"Content-Type": blob.type} : {}, await blob.arrayBuffer());
5568
}
5669

5770
public bytes(): Uint8Array {
@@ -67,4 +80,11 @@ export class Component implements Part {
6780
result.push(this.body);
6881
return Multipart.combineArrays(result);
6982
}
83+
84+
/**
85+
* A Blob representation of this component. Headers will be lost.
86+
*/
87+
public blob(): Blob {
88+
return new Blob([this.body], {type: this.headers.get("Content-Type") ?? undefined});
89+
}
7090
}

src/Multipart.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,19 @@ export class Multipart implements Part {
231231
return Multipart.parseBody(part.bytes(), new TextEncoder().encode(boundary), mediaType ?? void 0);
232232
}
233233

234+
/**
235+
* Create Multipart from a {@link !Blob}. The boundary and media type are determined from the blob's type.
236+
* @param blob The blob
237+
* @throws {@link !SyntaxError} If the `Content-Type` header is missing or does not include a boundary
238+
*/
239+
public static async blob(blob: Blob): Promise<Multipart> {
240+
const type = blob.type;
241+
if (type === "") throw new SyntaxError("Blob is missing Content-Type header");
242+
const {mediaType, boundary} = Multipart.parseContentType(type);
243+
if (boundary === null) throw new SyntaxError("Missing boundary in Content-Type header of blob");
244+
return Multipart.parseBody(new Uint8Array(await blob.arrayBuffer()), new TextEncoder().encode(boundary), mediaType ?? void 0);
245+
}
246+
234247
/**
235248
* Create Multipart from {@link FormData}.
236249
* This method might be slow if the form data contains large files.
@@ -244,7 +257,7 @@ export class Multipart implements Part {
244257
for (const [key, value] of formData.entries()) {
245258
if (typeof value === "string") parts.push(new Component({"Content-Disposition": `form-data; name="${key}"`}, new TextEncoder().encode(value)));
246259
else {
247-
const part = await Component.file(value);
260+
const part = await Component.blob(value);
248261
part.headers.set("Content-Disposition", `form-data; name="${key}"; filename="${value.name}"`);
249262
parts.push(part);
250263
}
@@ -439,6 +452,16 @@ export class Multipart implements Part {
439452
return Multipart.combineArrays(result);
440453
}
441454

455+
/**
456+
* Create Blob from this multipart.
457+
*
458+
* @throws {@link !RangeError} If the multipart boundary is invalid. A valid boundary is 1 to 70 characters long,
459+
* does not end with space, and may only contain: A-Z a-z 0-9 '()+_,-./:=? and space.
460+
*/
461+
public blob(): Blob {
462+
return new Blob([this.bytes()], {type: this.headers.get("content-type") ?? undefined});
463+
}
464+
442465
private static boundaryShouldBeQuoted(boundary: Uint8Array): boolean {
443466
for (const byte of boundary) {
444467
if (

test/Component.test.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {expect} from "chai";
22
import {Multipart, Component} from "../dist/index.js";
3+
import {describe} from "mocha";
34

45
describe("Component", () => {
56

@@ -78,6 +79,22 @@ describe("Component", () => {
7879
});
7980
});
8081

82+
describe("blob", () => {
83+
it("should create Component from Blob with type", async () => {
84+
const blob = new Blob([new Uint8Array([1, 2, 3])], {type: "text/plain"});
85+
const component = await Component.blob(blob);
86+
expect(component.headers.get("Content-Type")).to.equal("text/plain");
87+
expect(component.body).to.deep.equal(new Uint8Array([1, 2, 3]));
88+
});
89+
90+
it ("should create Component from Blob without type", async () => {
91+
const blob = new Blob([new Uint8Array([1, 2, 3])]);
92+
const component = await Component.blob(blob);
93+
expect(component.headers.get("Content-Type")).to.equal(null);
94+
expect(component.body).to.deep.equal(new Uint8Array([1, 2, 3]));
95+
});
96+
});
97+
8198
describe("#bytes", () => {
8299
it("should return the bytes of a Component with headers and body", () => {
83100
const headersInit = {"Content-Type": "text/plain", "Content-Length": "3"};
@@ -96,4 +113,23 @@ describe("Component", () => {
96113
expect(new TextDecoder().decode(bytes)).to.equal(expected);
97114
});
98115
});
116+
117+
describe("#blob", () => {
118+
it("should return the Blob of a Component with headers and body", async () => {
119+
const headersInit = {"Content-Type": "text/plain", "Content-Length": "3"};
120+
const body = new Uint8Array([1, 2, 3]);
121+
const component = new Component(headersInit, body);
122+
const blob = component.blob();
123+
expect(blob.type).to.equal("text/plain");
124+
expect(await blob.bytes()).to.deep.equal(body);
125+
});
126+
127+
it("should return the Blob of a Component with only headers", async () => {
128+
const headersInit = {"Content-Type": "text/plain", "Content-Length": "3"};
129+
const component = new Component(headersInit);
130+
const blob = component.blob();
131+
expect(blob.type).to.equal("text/plain");
132+
expect(await blob.bytes()).to.deep.equal(new Uint8Array(0));
133+
});
134+
});
99135
});

test/Multipart.test.js

Lines changed: 69 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import { Multipart, Component} from "../dist/index.js";
2-
import { expect } from "chai";
1+
import {Multipart, Component} from "../dist/index.js";
2+
import {expect} from "chai";
3+
import {describe} from "mocha";
34

45
describe("Multipart", function () {
56
describe("constructor", function () {
67
it("should initialize with default boundary and mediaType", function () {
7-
const component = new Component({ "content-type": "text/plain" }, new TextEncoder().encode("foo bar"));
8+
const component = new Component({"content-type": "text/plain"}, new TextEncoder().encode("foo bar"));
89
const multipart = new Multipart([component]);
910

1011
expect(multipart.boundary).to.be.an.instanceof(Uint8Array);
@@ -14,7 +15,7 @@ describe("Multipart", function () {
1415
it("should accept a custom boundary and mediaType", function () {
1516
const boundary = "my-custom-boundary";
1617
const mediaType = "Multipart/form-data";
17-
const component = new Component({ "x-foo": "bar" }, new TextEncoder().encode("custom content"));
18+
const component = new Component({"x-foo": "bar"}, new TextEncoder().encode("custom content"));
1819
const multipart = new Multipart([component], boundary, mediaType);
1920

2021
expect(new TextDecoder().decode(multipart.boundary)).to.equal(boundary);
@@ -32,8 +33,8 @@ describe("Multipart", function () {
3233
describe("parse", function () {
3334
it("should parse Multipart data correctly", function () {
3435
const boundary = "my-boundary";
35-
const component1 = new Component({ "x-foo": "bar" }, new TextEncoder().encode("Component1 content"));
36-
const component2 = new Component({ "content-type": "text/plain" }, new TextEncoder().encode("Component2 content"));
36+
const component1 = new Component({"x-foo": "bar"}, new TextEncoder().encode("Component1 content"));
37+
const component2 = new Component({"content-type": "text/plain"}, new TextEncoder().encode("Component2 content"));
3738
const multipart = new Multipart([component1, component2], boundary);
3839

3940
const multipartBytes = multipart.bytes();
@@ -90,10 +91,10 @@ describe("Multipart", function () {
9091

9192
it("should handle nested multiparts", function () {
9293
const components = [
93-
new Component({ "x-foo": "bar" }, new TextEncoder().encode("foo bar")),
94+
new Component({"x-foo": "bar"}, new TextEncoder().encode("foo bar")),
9495
new Multipart([
95-
new Component({ "content-type": "text/plain" }, new TextEncoder().encode("nested Component 1")),
96-
new Component({ "content-type": "application/json" }, new TextEncoder().encode(JSON.stringify({ foo: "bar" })))
96+
new Component({"content-type": "text/plain"}, new TextEncoder().encode("nested Component 1")),
97+
new Component({"content-type": "application/json"}, new TextEncoder().encode(JSON.stringify({foo: "bar"})))
9798
], "inner-boundary")
9899
];
99100
const multipart = new Multipart(components, "outer-boundary");
@@ -112,7 +113,7 @@ describe("Multipart", function () {
112113
expect(parsedInnerMultipart.parts[0].headers.get("content-type")).to.equal("text/plain");
113114
expect(new TextDecoder().decode(parsedInnerMultipart.parts[0].body)).to.equal("nested Component 1");
114115
expect(parsedInnerMultipart.parts[1].headers.get("content-type")).to.equal("application/json");
115-
expect(new TextDecoder().decode(parsedInnerMultipart.parts[1].body)).to.equal(JSON.stringify({ foo: "bar" }));
116+
expect(new TextDecoder().decode(parsedInnerMultipart.parts[1].body)).to.equal(JSON.stringify({foo: "bar"}));
116117
});
117118

118119
it("should handle malformed Multipart data", function () {
@@ -217,6 +218,47 @@ describe("Multipart", function () {
217218
});
218219
});
219220

221+
describe("part", function () {
222+
it("should create Multipart from Part", function () {
223+
const multipart = new Multipart([
224+
new Component({"content-type": "text/plain", "x-foo": "bar"}, new TextEncoder().encode("foo bar")),
225+
new Component({}, new TextEncoder().encode("test content"))
226+
]);
227+
const part = new Component({"Content-Type": multipart.headers.get("content-type")}, multipart.bytes());
228+
229+
const parsedMultipart = Multipart.part(part);
230+
expect(parsedMultipart).to.be.an.instanceof(Multipart);
231+
expect(parsedMultipart.parts.length).to.equal(2);
232+
expect(parsedMultipart.parts[0].headers.get("content-type")).to.equal("text/plain");
233+
expect(parsedMultipart.parts[0].headers.get("x-foo")).to.equal("bar");
234+
expect(new TextDecoder().decode(parsedMultipart.parts[0].body)).to.equal("foo bar");
235+
expect(parsedMultipart.parts[1].headers.get("content-type")).to.equal(null);
236+
expect(new TextDecoder().decode(parsedMultipart.parts[1].body)).to.equal("test content");
237+
});
238+
});
239+
240+
describe("blob", async function () {
241+
it("should create Multipart from Blob with type", async function () {
242+
const boundary = "example-boundary";
243+
const component1 = new Component({"x-foo": "bar"}, new TextEncoder().encode("Component1 content"));
244+
const component2 = new Component({"content-type": "text/plain"}, new TextEncoder().encode("Component2 content"));
245+
const multipart = new Multipart([component1, component2], boundary);
246+
247+
const blob = multipart.blob();
248+
const parsedMultipart = await Multipart.blob(blob);
249+
250+
expect(parsedMultipart).to.be.an.instanceof(Multipart);
251+
expect(new TextDecoder().decode(parsedMultipart.boundary)).to.equal(boundary);
252+
expect(parsedMultipart.parts.length).to.equal(2);
253+
const part1 = parsedMultipart.parts[0];
254+
expect(part1.headers.get("x-foo")).to.equal("bar");
255+
expect(part1.body).to.deep.equal(component1.body);
256+
const part2 = parsedMultipart.parts[1];
257+
expect(part2.headers.get("content-type")).to.equal("text/plain");
258+
expect(part2.body).to.deep.equal(component2.body);
259+
});
260+
});
261+
220262
describe("formData", function () {
221263
it("should correctly create Multipart from FormData", async function () {
222264
const formData = new FormData();
@@ -259,7 +301,7 @@ describe("Multipart", function () {
259301
});
260302

261303
describe("#formData", function () {
262-
it ("should correctly return the FormData of the Multipart", async function () {
304+
it("should correctly return the FormData of the Multipart", async function () {
263305
const formData = new FormData();
264306
formData.append("foo", "bar");
265307
formData.append("bar", "baz");
@@ -278,7 +320,7 @@ describe("Multipart", function () {
278320
expect(new TextDecoder().decode(await file.arrayBuffer())).to.equal("console.log('hello world');");
279321
});
280322

281-
it("should handle empty FormData multipart", async function (){
323+
it("should handle empty FormData multipart", async function () {
282324
const multipart = await Multipart.formData(new FormData());
283325
const formData = multipart.formData();
284326
expect(formData).to.be.an.instanceof(FormData);
@@ -289,7 +331,7 @@ describe("Multipart", function () {
289331
describe("#body", function () {
290332
it("should correctly return the body of the Multipart", function () {
291333
const boundary = "test-boundary";
292-
const component = new Component({ "content-type": "text/plain" }, new TextEncoder().encode("test body"));
334+
const component = new Component({"content-type": "text/plain"}, new TextEncoder().encode("test body"));
293335
const multipart = new Multipart([component], boundary);
294336

295337
const body = multipart.body;
@@ -327,7 +369,7 @@ describe("Multipart", function () {
327369
describe("#bytes", function () {
328370
it("should correctly return the bytes of the Multipart", function () {
329371
const boundary = "test-boundary";
330-
const component = new Component({ "x-foo": "bar" }, new TextEncoder().encode("test content"));
372+
const component = new Component({"x-foo": "bar"}, new TextEncoder().encode("test content"));
331373
const multipart = new Multipart([component], boundary);
332374

333375
const bytes = multipart.bytes();
@@ -365,6 +407,19 @@ describe("Multipart", function () {
365407
});
366408
});
367409

410+
describe("#blob", async function () {
411+
it("should correctly return the blob of the Multipart", async function () {
412+
const boundary = "test-boundary";
413+
const component = new Component({"x-foo": "bar"}, new TextEncoder().encode("test content"));
414+
const multipart = new Multipart([component], boundary);
415+
416+
const blob = multipart.blob();
417+
418+
expect(blob.type).to.equal(multipart.headers.get("content-type"));
419+
expect(await blob.bytes()).to.deep.equal(multipart.bytes());
420+
});
421+
});
422+
368423
describe("#headers", function () {
369424
it("should have the Content-Type boundary parameters in quotes as per RFC 2616", function () {
370425
expect(new Multipart([], "foobar", "multipart/mixed").headers.get("content-type")).to.equal("multipart/mixed; boundary=foobar");

0 commit comments

Comments
 (0)