1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2025-01-03 04:48:52 -05:00

Web APIs: File and FormData (#1056)

This commit is contained in:
Kyra 2018-11-04 19:05:02 +01:00 committed by Ryan Dahl
parent 1241b8e9ba
commit e93d686e9d
10 changed files with 360 additions and 16 deletions

View file

@ -87,10 +87,12 @@ ts_sources = [
"js/dom_types.ts",
"js/errors.ts",
"js/fetch.ts",
"js/file.ts",
"js/headers.ts",
"js/file_info.ts",
"js/files.ts",
"js/flatbuffers.ts",
"js/form_data.ts",
"js/global_eval.ts",
"js/globals.ts",
"js/io.ts",

View file

@ -97,6 +97,12 @@ function toUint8Arrays(
ret.push(element[bytesSymbol]);
} else if (element instanceof Uint8Array) {
ret.push(element);
} else if (element instanceof Uint16Array) {
const uint8 = new Uint8Array(element.buffer);
ret.push(uint8);
} else if (element instanceof Uint32Array) {
const uint8 = new Uint8Array(element.buffer);
ret.push(uint8);
} else if (ArrayBuffer.isView(element)) {
// Convert view to Uint8Array.
const uint8 = new Uint8Array(element.buffer);
@ -105,6 +111,8 @@ function toUint8Arrays(
// Create a new Uint8Array view for the given ArrayBuffer.
const uint8 = new Uint8Array(element);
ret.push(uint8);
} else {
ret.push(enc.encode(String(element)));
}
}
return ret;

View file

@ -34,7 +34,7 @@ type ReferrerPolicy =
| "origin-when-cross-origin"
| "unsafe-url";
export type BlobPart = BufferSource | Blob | string;
type FormDataEntryValue = File | string;
export type FormDataEntryValue = File | string;
export type EventListenerOrEventListenerObject =
| EventListener
| EventListenerObject;
@ -173,7 +173,7 @@ interface Event {
readonly NONE: number;
}
interface File extends Blob {
export interface File extends Blob {
readonly lastModified: number;
readonly name: string;
}
@ -242,22 +242,18 @@ interface ReadableStreamReader {
releaseLock(): void;
}
export interface FormData {
export interface FormData extends DomIterable<string, FormDataEntryValue> {
append(name: string, value: string | Blob, fileName?: string): void;
delete(name: string): void;
get(name: string): FormDataEntryValue | null;
getAll(name: string): FormDataEntryValue[];
has(name: string): boolean;
set(name: string, value: string | Blob, fileName?: string): void;
forEach(
callbackfn: (
value: FormDataEntryValue,
key: string,
parent: FormData
) => void,
// tslint:disable-next-line:no-any
thisArg?: any
): void;
}
export interface FormDataConstructor {
new (): FormData;
prototype: FormData;
}
/** A blob object represents a file-like object of immutable, raw data. */

24
js/file.ts Normal file
View file

@ -0,0 +1,24 @@
// Copyright 2018 the Deno authors. All rights reserved. MIT license.
import * as domTypes from "./dom_types";
import * as blob from "./blob";
export class DenoFile extends blob.DenoBlob implements domTypes.File {
lastModified: number;
name: string;
constructor(
fileBits: domTypes.BlobPart[],
fileName: string,
options?: domTypes.FilePropertyBag
) {
options = options || {};
super(fileBits, options);
// 4.1.2.1 Replace any "/" character (U+002F SOLIDUS)
// with a ":" (U + 003A COLON)
this.name = String(fileName).replace(/\u002F/g, "\u003A");
// 4.1.3.3 If lastModified is not provided, set lastModified to the current
// date and time represented in number of milliseconds since the Unix Epoch.
this.lastModified = options.lastModified || Date.now();
}
}

104
js/file_test.ts Normal file
View file

@ -0,0 +1,104 @@
// Copyright 2018 the Deno authors. All rights reserved. MIT license.
import { test, assert, assertEqual } from "./test_util.ts";
function testFirstArgument(arg1, expectedSize) {
const file = new File(arg1, "name");
assert(file instanceof File);
assertEqual(file.name, "name");
assertEqual(file.size, expectedSize);
assertEqual(file.type, "");
}
test(function fileEmptyFileBits() {
testFirstArgument([], 0);
});
test(function fileStringFileBits() {
testFirstArgument(["bits"], 4);
});
test(function fileUnicodeStringFileBits() {
testFirstArgument(["𝓽𝓮𝔁𝓽"], 16);
});
test(function fileStringObjectFileBits() {
// tslint:disable-next-line no-construct
testFirstArgument([new String("string object")], 13);
});
test(function fileEmptyBlobFileBits() {
testFirstArgument([new Blob()], 0);
});
test(function fileBlobFileBits() {
testFirstArgument([new Blob(["bits"])], 4);
});
test(function fileEmptyFileFileBits() {
testFirstArgument([new File([], "world.txt")], 0);
});
test(function fileFileFileBits() {
testFirstArgument([new File(["bits"], "world.txt")], 4);
});
test(function fileArrayBufferFileBits() {
testFirstArgument([new ArrayBuffer(8)], 8);
});
test(function fileTypedArrayFileBits() {
testFirstArgument([new Uint8Array([0x50, 0x41, 0x53, 0x53])], 4);
});
test(function fileVariousFileBits() {
testFirstArgument(
[
"bits",
new Blob(["bits"]),
new Blob(),
new Uint8Array([0x50, 0x41]),
new Uint16Array([0x5353]),
new Uint32Array([0x53534150])
],
16
);
});
test(function fileNumberInFileBits() {
testFirstArgument([12], 2);
});
test(function fileArrayInFileBits() {
testFirstArgument([[1, 2, 3]], 5);
});
test(function fileObjectInFileBits() {
// "[object Object]"
testFirstArgument([{}], 15);
});
function testSecondArgument(arg2, expectedFileName) {
const file = new File(["bits"], arg2);
assert(file instanceof File);
assertEqual(file.name, expectedFileName);
}
test(function fileUsingFileName() {
testSecondArgument("dummy", "dummy");
});
test(function fileUsingSpecialCharacterInFileName() {
testSecondArgument("dummy/foo", "dummy:foo");
});
test(function fileUsingNullFileName() {
testSecondArgument(null, "null");
});
test(function fileUsingNumberFileName() {
testSecondArgument(1, "1");
});
test(function fileUsingEmptyStringFileName() {
testSecondArgument("", "");
});

107
js/form_data.ts Normal file
View file

@ -0,0 +1,107 @@
// Copyright 2018 the Deno authors. All rights reserved. MIT license.
import * as domTypes from "./dom_types";
import * as blob from "./blob";
import * as file from "./file";
import { DomIterableMixin } from "./mixins/dom_iterable";
const dataSymbol = Symbol("data");
class FormDataBase {
private [dataSymbol]: Array<[string, domTypes.FormDataEntryValue]> = [];
/** Appends a new value onto an existing key inside a `FormData`
* object, or adds the key if it does not already exist.
*
* formData.append('name', 'first');
* formData.append('name', 'second');
*/
append(name: string, value: string): void;
append(name: string, value: blob.DenoBlob, filename?: string): void;
append(name: string, value: string | blob.DenoBlob, filename?: string): void {
if (value instanceof blob.DenoBlob) {
const dfile = new file.DenoFile([value], filename || name);
this[dataSymbol].push([name, dfile]);
} else {
this[dataSymbol].push([name, String(value)]);
}
}
/** Deletes a key/value pair from a `FormData` object.
*
* formData.delete('name');
*/
delete(name: string): void {
let i = 0;
while (i < this[dataSymbol].length) {
if (this[dataSymbol][i][0] === name) {
this[dataSymbol].splice(i, 1);
} else {
i++;
}
}
}
/** Returns an array of all the values associated with a given key
* from within a `FormData`.
*
* formData.getAll('name');
*/
getAll(name: string): domTypes.FormDataEntryValue[] {
const values = [];
for (const entry of this[dataSymbol]) {
if (entry[0] === name) {
values.push(entry[1]);
}
}
return values;
}
/** Returns the first value associated with a given key from within a
* `FormData` object.
*
* formData.get('name');
*/
get(name: string): domTypes.FormDataEntryValue | null {
for (const entry of this[dataSymbol]) {
if (entry[0] === name) {
return entry[1];
}
}
return null;
}
/** Returns a boolean stating whether a `FormData` object contains a
* certain key/value pair.
*
* formData.has('name');
*/
has(name: string): boolean {
return this[dataSymbol].some(entry => entry[0] === name);
}
/** Sets a new value for an existing key inside a `FormData` object, or
* adds the key/value if it does not already exist.
*
* formData.set('name', 'value');
*/
set(name: string, value: string): void;
set(name: string, value: blob.DenoBlob, filename?: string): void;
set(name: string, value: string | blob.DenoBlob, filename?: string): void {
this.delete(name);
if (value instanceof blob.DenoBlob) {
const dfile = new file.DenoFile([value], filename || name);
this[dataSymbol].push([name, dfile]);
} else {
this[dataSymbol].push([name, String(value)]);
}
}
}
// tslint:disable-next-line:variable-name
export const FormData = DomIterableMixin<
string,
domTypes.FormDataEntryValue,
typeof FormDataBase
>(FormDataBase, dataSymbol);

92
js/form_data_test.ts Normal file
View file

@ -0,0 +1,92 @@
// Copyright 2018 the Deno authors. All rights reserved. MIT license.
import { test, assert, assertEqual } from "./test_util.ts";
test(function formDataParamsAppendSuccess() {
const formData = new FormData();
formData.append("a", "true");
assertEqual(formData.get("a"), "true");
});
test(function formDataParamsDeleteSuccess() {
const formData = new FormData();
formData.append("a", "true");
formData.append("b", "false");
assertEqual(formData.get("b"), "false");
formData.delete("b");
assertEqual(formData.get("a"), "true");
assertEqual(formData.get("b"), null);
});
test(function formDataParamsGetAllSuccess() {
const formData = new FormData();
formData.append("a", "true");
formData.append("b", "false");
formData.append("a", "null");
assertEqual(formData.getAll("a"), ["true", "null"]);
assertEqual(formData.getAll("b"), ["false"]);
assertEqual(formData.getAll("c"), []);
});
test(function formDataParamsGetSuccess() {
const formData = new FormData();
formData.append("a", "true");
formData.append("b", "false");
formData.append("a", "null");
formData.append("d", undefined);
formData.append("e", null);
assertEqual(formData.get("a"), "true");
assertEqual(formData.get("b"), "false");
assertEqual(formData.get("c"), null);
assertEqual(formData.get("d"), "undefined");
assertEqual(formData.get("e"), "null");
});
test(function formDataParamsHasSuccess() {
const formData = new FormData();
formData.append("a", "true");
formData.append("b", "false");
assert(formData.has("a"));
assert(formData.has("b"));
assert(!formData.has("c"));
});
test(function formDataParamsSetSuccess() {
const formData = new FormData();
formData.append("a", "true");
formData.append("b", "false");
formData.append("a", "null");
assertEqual(formData.getAll("a"), ["true", "null"]);
assertEqual(formData.getAll("b"), ["false"]);
formData.set("a", "false");
assertEqual(formData.getAll("a"), ["false"]);
formData.set("d", undefined);
assertEqual(formData.get("d"), "undefined");
formData.set("e", null);
assertEqual(formData.get("e"), "null");
});
test(function formDataSetEmptyBlobSuccess() {
const formData = new FormData();
formData.set("a", new Blob([]), "blank.txt");
const file = formData.get("a");
assert(file instanceof File);
if (typeof file !== "string") {
assertEqual(file.name, "blank.txt");
}
});
test(function formDataParamsForEachSuccess() {
const init = [["a", "54"], ["b", "true"]];
const formData = new FormData();
for (const [name, value] of init) {
formData.append(name, value);
}
let callNum = 0;
formData.forEach((value, key, parent) => {
assertEqual(formData, parent);
assertEqual(value, init[callNum][1]);
assertEqual(key, init[callNum][0]);
callNum++;
});
assertEqual(callNum, init.length);
});

View file

@ -1,5 +1,7 @@
// Copyright 2018 the Deno authors. All rights reserved. MIT license.
import * as blob from "./blob";
import * as file from "./file";
import * as formdata from "./form_data";
import * as console_ from "./console";
import * as fetch_ from "./fetch";
import { Headers } from "./headers";
@ -44,3 +46,5 @@ window.fetch = fetch_.fetch;
// runtime library
window.Headers = Headers as domTypes.HeadersConstructor;
window.Blob = blob.DenoBlob;
window.File = file.DenoFile;
window.FormData = formdata.FormData as domTypes.FormDataConstructor;

View file

@ -21,22 +21,27 @@ export function DomIterableMixin<K, V, TBase extends Constructor>(
// Base class in a way where the Symbol `dataSymbol` is defined. So the
// runtime code works, but we do lose a little bit of type safety.
// Additionally, we have to not use .keys() nor .values() since the internal
// slot differs in type - some have a Map, which yields [K, V] in
// Symbol.iterator, and some have an Array, which yields V, in this case
// [K, V] too as they are arrays of tuples.
// tslint:disable-next-line:variable-name
const DomIterable = class extends Base {
*entries(): IterableIterator<[K, V]> {
for (const entry of (this as any)[dataSymbol].entries()) {
for (const entry of (this as any)[dataSymbol]) {
yield entry;
}
}
*keys(): IterableIterator<K> {
for (const key of (this as any)[dataSymbol].keys()) {
for (const [key] of (this as any)[dataSymbol]) {
yield key;
}
}
*values(): IterableIterator<V> {
for (const value of (this as any)[dataSymbol].values()) {
for (const [, value] of (this as any)[dataSymbol]) {
yield value;
}
}
@ -47,7 +52,7 @@ export function DomIterableMixin<K, V, TBase extends Constructor>(
thisArg?: any
): void {
callbackfn = callbackfn.bind(thisArg == null ? window : Object(thisArg));
for (const [key, value] of (this as any)[dataSymbol].entries()) {
for (const [key, value] of (this as any)[dataSymbol]) {
callbackfn(value, key, this);
}
}

View file

@ -10,7 +10,9 @@ import "./console_test.ts";
import "./copy_file_test.ts";
import "./dir_test";
import "./fetch_test.ts";
import "./file_test.ts";
import "./files_test.ts";
import "./form_data_test.ts";
import "./headers_test.ts";
import "./make_temp_dir_test.ts";
import "./metrics_test.ts";