Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

changed: [std/multipart] make readForm() return value more type safe #4710

Merged
merged 2 commits into from
Apr 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 58 additions & 8 deletions std/mime/multipart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,17 @@ function skipLWSPChar(u: Uint8Array): Uint8Array {
return ret.slice(0, j);
}

export interface MultipartFormData {
file(key: string): FormFile | undefined;
value(key: string): string | undefined;
entries(): IterableIterator<[string, string | FormFile | undefined]>;
[Symbol.iterator](): IterableIterator<
[string, string | FormFile | undefined]
>;
/** Remove all tempfiles */
removeAll(): Promise<void>;
}

/** Reader for parsing multipart/form-data */
export class MultipartReader {
readonly newLine = encoder.encode("\r\n");
Expand All @@ -268,11 +279,11 @@ export class MultipartReader {
* overflowed file data will be written to temporal files.
* String field values are never written to files.
* null value means parsing or writing to file was failed in some reason.
* @param maxMemory maximum memory size to store file in memory. bytes. @default 1048576 (1MB)
* */
async readForm(
maxMemory: number
): Promise<{ [key: string]: null | string | FormFile }> {
const result = Object.create(null);
async readForm(maxMemory = 10 << 20): Promise<MultipartFormData> {
const fileMap = new Map<string, FormFile>();
const valueMap = new Map<string, string>();
let maxValueBytes = maxMemory + (10 << 20);
const buf = new Buffer(new Uint8Array(maxValueBytes));
for (;;) {
Expand All @@ -292,11 +303,11 @@ export class MultipartReader {
throw new RangeError("message too large");
}
const value = buf.toString();
result[p.formName] = value;
valueMap.set(p.formName, value);
continue;
}
// file
let formFile: FormFile | null = null;
let formFile: FormFile | undefined;
const n = await copy(buf, p);
const contentType = p.headers.get("content-type");
assert(contentType != null, "content-type must be set");
Expand Down Expand Up @@ -333,9 +344,11 @@ export class MultipartReader {
maxMemory -= n;
maxValueBytes -= n;
}
result[p.formName] = formFile;
if (formFile) {
fileMap.set(p.formName, formFile);
}
}
return result;
return multipatFormData(fileMap, valueMap);
}

private currentPart: PartReader | undefined;
Expand Down Expand Up @@ -399,6 +412,43 @@ export class MultipartReader {
}
}

function multipatFormData(
fileMap: Map<string, FormFile>,
valueMap: Map<string, string>
): MultipartFormData {
function file(key: string): FormFile | undefined {
return fileMap.get(key);
}
function value(key: string): string | undefined {
return valueMap.get(key);
}
function* entries(): IterableIterator<
[string, string | FormFile | undefined]
> {
yield* fileMap;
yield* valueMap;
}
async function removeAll(): Promise<void> {
const promises: Array<Promise<void>> = [];
for (const val of fileMap.values()) {
if (!val.tempfile) continue;
promises.push(Deno.remove(val.tempfile));
}
await Promise.all(promises);
}
return {
file,
value,
entries,
removeAll,
[Symbol.iterator](): IterableIterator<
[string, string | FormFile | undefined]
> {
return entries();
},
};
}

class PartWriter implements Writer {
closed = false;
private readonly partHeader: string;
Expand Down
127 changes: 86 additions & 41 deletions std/mime/multipart_test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.

const { Buffer, copy, open, remove } = Deno;
const { Buffer, copy, open, test } = Deno;
import {
assert,
assertEquals,
assertThrows,
assertThrowsAsync,
} from "../testing/asserts.ts";
const { test } = Deno;
import * as path from "../path/mod.ts";
import {
FormFile,
MultipartReader,
MultipartWriter,
isFormFile,
Expand Down Expand Up @@ -173,46 +171,93 @@ test(async function multipartMultipartWriter3(): Promise<void> {
);
});

test(async function multipartMultipartReader(): Promise<void> {
// FIXME: path resolution
const o = await open(path.resolve("./mime/testdata/sample.txt"));
const mr = new MultipartReader(
o,
"--------------------------434049563556637648550474"
);
const form = await mr.readForm(10 << 20);
assertEquals(form["foo"], "foo");
assertEquals(form["bar"], "bar");
const file = form["file"] as FormFile;
assertEquals(isFormFile(file), true);
assert(file.content !== void 0);
o.close();
test({
name: "[mime/multipart] readForm() basic",
async fn() {
const o = await open(path.resolve("./mime/testdata/sample.txt"));
const mr = new MultipartReader(
o,
"--------------------------434049563556637648550474"
);
const form = await mr.readForm();
assertEquals(form.value("foo"), "foo");
assertEquals(form.value("bar"), "bar");
const file = form.file("file");
assert(isFormFile(file));
assert(file.content !== void 0);
o.close();
},
});

test(async function multipartMultipartReader2(): Promise<void> {
const o = await open(path.resolve("./mime/testdata/sample.txt"));
const mr = new MultipartReader(
o,
"--------------------------434049563556637648550474"
);
const form = await mr.readForm(20); //
try {
assertEquals(form["foo"], "foo");
assertEquals(form["bar"], "bar");
const file = form["file"] as FormFile;
assertEquals(file.type, "application/octet-stream");
assert(file.tempfile != null);
const f = await open(file.tempfile);
const w = new StringWriter();
await copy(w, f);
const json = JSON.parse(w.toString());
assertEquals(json["compilerOptions"]["target"], "es2018");
f.close();
} finally {
const file = form["file"] as FormFile;
if (file.tempfile) {
await remove(file.tempfile);
test({
name: "[mime/multipart] readForm() should store big file in temp file",
async fn() {
const o = await open(path.resolve("./mime/testdata/sample.txt"));
const mr = new MultipartReader(
o,
"--------------------------434049563556637648550474"
);
// use low-memory to write "file" into temp file.
const form = await mr.readForm(20);
try {
assertEquals(form.value("foo"), "foo");
assertEquals(form.value("bar"), "bar");
const file = form.file("file");
assert(file != null);
assertEquals(file.type, "application/octet-stream");
assert(file.tempfile != null);
const f = await open(file.tempfile);
const w = new StringWriter();
await copy(w, f);
const json = JSON.parse(w.toString());
assertEquals(json["compilerOptions"]["target"], "es2018");
f.close();
} finally {
await form.removeAll();
o.close();
}
},
});

test({
name: "[mime/multipart] removeAll() should remove all tempfiles",
async fn() {
const o = await open(path.resolve("./mime/testdata/sample.txt"));
const mr = new MultipartReader(
o,
"--------------------------434049563556637648550474"
);
const form = await mr.readForm(20);
const file = form.file("file");
assert(file != null);
const { tempfile, content } = file;
assert(tempfile != null);
assert(content == null);
const stat = await Deno.stat(tempfile);
assertEquals(stat.size, file.size);
await form.removeAll();
await assertThrowsAsync(async () => {
await Deno.stat(tempfile);
}, Deno.errors.NotFound);
o.close();
},
});

test({
name: "[mime/multipart] entries()",
async fn() {
const o = await open(path.resolve("./mime/testdata/sample.txt"));
const mr = new MultipartReader(
o,
"--------------------------434049563556637648550474"
);
const form = await mr.readForm();
const map = new Map(form.entries());
assertEquals(map.get("foo"), "foo");
assertEquals(map.get("bar"), "bar");
const file = map.get("file");
assert(isFormFile(file));
assertEquals(file.filename, "tsconfig.json");
o.close();
}
},
});