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:
parent
1241b8e9ba
commit
e93d686e9d
10 changed files with 360 additions and 16 deletions
2
BUILD.gn
2
BUILD.gn
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
24
js/file.ts
Normal 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
104
js/file_test.ts
Normal 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
107
js/form_data.ts
Normal 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
92
js/form_data_test.ts
Normal 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);
|
||||
});
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
Loading…
Reference in a new issue