-
Notifications
You must be signed in to change notification settings - Fork 166
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #52 from shinspiegel/multipart_module
feat: created new multiparser
- Loading branch information
Showing
5 changed files
with
250 additions
and
36 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
import { compress as brotli } from 'https://deno.land/x/[email protected]/mod.ts' | ||
import { FormDataReader } from 'https://deno.land/x/[email protected]/multipart.ts' | ||
import { gzipEncode } from 'https://deno.land/x/[email protected]/mod.ts' | ||
import log from './log.ts' | ||
import { multiParser } from './multiparser.ts' | ||
import { ServerRequest } from './std.ts' | ||
import type { APIRequest, FormDataBody } from './types.ts' | ||
|
||
|
@@ -90,43 +90,22 @@ export class Request extends ServerRequest implements APIRequest { | |
async decodeBody(type: "form-data"): Promise<FormDataBody> | ||
async decodeBody(type: string): Promise<any> { | ||
if (type === "text") { | ||
try { | ||
const buff: Uint8Array = await Deno.readAll(this.body); | ||
const encoded = new TextDecoder("utf-8").decode(buff); | ||
return encoded; | ||
} catch (err) { | ||
console.error("Failed to parse the request body.", err); | ||
} | ||
const buff: Uint8Array = await Deno.readAll(this.body); | ||
const encoded = new TextDecoder("utf-8").decode(buff); | ||
return encoded; | ||
} | ||
|
||
if (type === "json") { | ||
try { | ||
const buff: Uint8Array = await Deno.readAll(this.body); | ||
const encoded = new TextDecoder("utf-8").decode(buff); | ||
const json = JSON.parse(encoded); | ||
return json; | ||
} catch (err) { | ||
console.error("Failed to parse the request body.", err); | ||
} | ||
const buff: Uint8Array = await Deno.readAll(this.body); | ||
const encoded = new TextDecoder("utf-8").decode(buff); | ||
const json = JSON.parse(encoded); | ||
return json; | ||
} | ||
|
||
if (type === "form-data") { | ||
try { | ||
const boundary = this.headers.get("content-type"); | ||
|
||
if (!boundary) throw new Error("Failed to get the content-type") | ||
|
||
const reader = new FormDataReader(boundary, this.body); | ||
const { fields, files } = await reader.read({ maxSize: 1024 * 1024 * 10 }); | ||
|
||
return { | ||
get: (key: string) => fields[key], | ||
getFile: (key: string) => files?.find(i => i.name === key) | ||
} | ||
|
||
} catch (err) { | ||
console.error("Failed to parse the request form-data", err) | ||
} | ||
const contentType = this.headers.get("content-type") as string | ||
const form = await multiParser(this.body, contentType); | ||
return form; | ||
} | ||
} | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,198 @@ | ||
import { bytes } from "./std.ts"; | ||
import { FormDataBody, FormFile } from "./types.ts"; | ||
|
||
const encoder = new TextEncoder(); | ||
const decoder = new TextDecoder(); | ||
|
||
const encode = { | ||
contentType: encoder.encode("Content-Type"), | ||
filename: encoder.encode("filename"), | ||
name: encoder.encode("name"), | ||
dashdash: encoder.encode("--"), | ||
boundaryEqual: encoder.encode("boundary="), | ||
returnNewline2: encoder.encode("\r\n\r\n"), | ||
carriageReturn: encoder.encode("\r"), | ||
}; | ||
|
||
export async function multiParser( | ||
body: Deno.Reader, | ||
contentType: string | ||
): Promise<FormDataBody> { | ||
let buf = await Deno.readAll(body); | ||
let boundaryByte = getBoundary(contentType); | ||
|
||
if (!boundaryByte) { | ||
throw new Error("No boundary data information"); | ||
} | ||
|
||
// Generate an array of Uint8Array | ||
const pieces = getFieldPieces(buf, boundaryByte!); | ||
|
||
// Set all the pieces into one single object | ||
const form = getForm(pieces); | ||
|
||
return form; | ||
} | ||
|
||
function createFormData(): FormDataBody { | ||
return { | ||
fields: {}, | ||
files: [], | ||
getFile(key: string) { | ||
return this.files.find((i) => i.name === key); | ||
}, | ||
get(key: string) { | ||
return this.fields[key]; | ||
}, | ||
}; | ||
} | ||
|
||
function getForm(pieces: Uint8Array[]) { | ||
let form: FormDataBody = createFormData(); | ||
|
||
for (let piece of pieces) { | ||
const { headerByte, contentByte } = splitPiece(piece); | ||
const headers = getHeaders(headerByte); | ||
|
||
// it's a string field | ||
if (typeof headers === "string") { | ||
// empty content, discard it | ||
if (contentByte.byteLength === 1 && contentByte[0] === 13) { | ||
continue; | ||
} | ||
|
||
// headers = "field1" | ||
else { | ||
form.fields[headers] = decoder.decode(contentByte); | ||
} | ||
} | ||
|
||
// it's a file field | ||
else { | ||
let file: FormFile = { | ||
name: headers.name, | ||
filename: headers.filename, | ||
contentType: headers.contentType, | ||
size: contentByte.byteLength, | ||
content: contentByte, | ||
}; | ||
|
||
form.files.push(file); | ||
} | ||
} | ||
return form; | ||
} | ||
|
||
function getHeaders(headerByte: Uint8Array) { | ||
let contentTypeIndex = bytes.findIndex(headerByte, encode.contentType); | ||
|
||
// no contentType, it may be a string field, return name only | ||
if (contentTypeIndex < 0) { | ||
return getNameOnly(headerByte); | ||
} | ||
|
||
// file field, return with name, filename and contentType | ||
else { | ||
return getHeaderNContentType(headerByte, contentTypeIndex); | ||
} | ||
} | ||
|
||
function getHeaderNContentType( | ||
headerByte: Uint8Array, | ||
contentTypeIndex: number, | ||
) { | ||
let headers: Record<string, string> = {}; | ||
|
||
let contentDispositionByte = headerByte.slice(0, contentTypeIndex - 2); | ||
headers = getHeaderOnly(contentDispositionByte); | ||
|
||
// jump over <Content-Type: > | ||
let contentTypeByte = headerByte.slice( | ||
contentTypeIndex + encode.contentType.byteLength + 2, | ||
); | ||
|
||
headers.contentType = decoder.decode(contentTypeByte); | ||
return headers; | ||
} | ||
|
||
function getHeaderOnly(headerLineByte: Uint8Array) { | ||
let headers: Record<string, string> = {}; | ||
|
||
let filenameIndex = bytes.findIndex(headerLineByte, encode.filename); | ||
if (filenameIndex < 0) { | ||
headers.name = getNameOnly(headerLineByte); | ||
} else { | ||
headers = getNameNFilename(headerLineByte, filenameIndex); | ||
} | ||
return headers; | ||
} | ||
|
||
function getNameNFilename(headerLineByte: Uint8Array, filenameIndex: number) { | ||
// fetch filename first | ||
let nameByte = headerLineByte.slice(0, filenameIndex - 2); | ||
let filenameByte = headerLineByte.slice( | ||
filenameIndex + encode.filename.byteLength + 2, | ||
headerLineByte.byteLength - 1, | ||
); | ||
|
||
let name = getNameOnly(nameByte); | ||
let filename = decoder.decode(filenameByte); | ||
return { name, filename }; | ||
} | ||
|
||
function getNameOnly(headerLineByte: Uint8Array) { | ||
let nameIndex = bytes.findIndex(headerLineByte, encode.name); | ||
|
||
// jump <name="> and get string inside double quote => "string" | ||
let nameByte = headerLineByte.slice( | ||
nameIndex + encode.name.byteLength + 2, | ||
headerLineByte.byteLength - 1, | ||
); | ||
|
||
return decoder.decode(nameByte); | ||
} | ||
|
||
function splitPiece(piece: Uint8Array) { | ||
const contentIndex = bytes.findIndex(piece, encode.returnNewline2); | ||
const headerByte = piece.slice(0, contentIndex); | ||
const contentByte = piece.slice(contentIndex + 4); | ||
|
||
return { headerByte, contentByte }; | ||
} | ||
|
||
function getFieldPieces( | ||
buf: Uint8Array, | ||
boundaryByte: Uint8Array, | ||
): Uint8Array[] { | ||
const startBoundaryByte = bytes.concat(encode.dashdash, boundaryByte); | ||
const endBoundaryByte = bytes.concat(startBoundaryByte, encode.dashdash); | ||
|
||
const pieces = []; | ||
|
||
while (!bytes.hasPrefix(buf, endBoundaryByte)) { | ||
// jump over boundary + '\r\n' | ||
buf = buf.slice(startBoundaryByte.byteLength + 2); | ||
let boundaryIndex = bytes.findIndex(buf, startBoundaryByte); | ||
|
||
// get field content piece | ||
pieces.push(buf.slice(0, boundaryIndex - 1)); | ||
buf = buf.slice(boundaryIndex); | ||
} | ||
|
||
return pieces; | ||
} | ||
|
||
function getBoundary(contentType: string): Uint8Array | undefined { | ||
let contentTypeByte = encoder.encode(contentType); | ||
let boundaryIndex = bytes.findIndex(contentTypeByte, encode.boundaryEqual); | ||
|
||
if (boundaryIndex >= 0) { | ||
// jump over 'boundary=' to get the real boundary | ||
let boundary = contentTypeByte.slice( | ||
boundaryIndex + encode.boundaryEqual.byteLength, | ||
); | ||
return boundary; | ||
} else { | ||
return undefined; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; | ||
import { multiParser } from "./multiparser.ts"; | ||
|
||
const encoder = new TextEncoder(); | ||
|
||
const contentType = "multipart/form-data; boundary=ALEPH-BOUNDARY"; | ||
const simpleString = '--ALEPH-BOUNDARY\rContent-Disposition: form-data; name="string_1"\r\n\r\nsimple string here\r--ALEPH-BOUNDARY--'; | ||
const complexString = 'some text to be ignored\r\r--ALEPH-BOUNDARY\rContent-Disposition: form-data; name="id"\r\n\r\n666\r--ALEPH-BOUNDARY\rContent-Disposition: form-data; name="title"\r\n\r\nHello World\r--ALEPH-BOUNDARY\rContent-Disposition: form-data; name="multiline"\r\n\r\nworld,\n hello\r--ALEPH-BOUNDARY\rContent-Disposition: form-data; name="file1"; filename="file_name.ext"\rContent-Type: video/mp2t\r\n\r\nsome random data\r--ALEPH-BOUNDARY--\rmore text to be ignored to be ignored\r'; | ||
|
||
Deno.test(`basic multiparser string`, async () => { | ||
const buff = new Deno.Buffer(encoder.encode(simpleString)); | ||
const multiForm = await multiParser(buff, contentType); | ||
|
||
assertEquals(multiForm.get("string_1"), "simple string here"); | ||
}); | ||
|
||
Deno.test(`complex multiparser string`, async () => { | ||
const buff = new Deno.Buffer(encoder.encode(complexString)); | ||
const multiFrom = await multiParser(buff, contentType); | ||
|
||
// Asseting multiple string values | ||
assertEquals(multiFrom.get("id"), "666"); | ||
assertEquals(multiFrom.get("title"), "Hello World"); | ||
assertEquals(multiFrom.get("multiline"), "world,\n hello"); | ||
|
||
// Asserting a file information | ||
const file = multiFrom.getFile("file1"); | ||
|
||
if (!file) { return } | ||
|
||
assertEquals(file.name, "file1"); | ||
assertEquals(file.contentType, "video/mp2t"); | ||
assertEquals(file.size, 16); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
export * as bytes from 'https://deno.land/[email protected]/bytes/mod.ts' | ||
export { Untar } from 'https://deno.land/[email protected]/archive/tar.ts' | ||
export * as colors from 'https://deno.land/[email protected]/fmt/colors.ts' | ||
export { ensureDir } from 'https://deno.land/[email protected]/fs/ensure_dir.ts' | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters