Skip to content

Commit e07c71e

Browse files
authored
feat(ext/file): File ext (#94)
* feat(ext/file): init
1 parent b165129 commit e07c71e

File tree

11 files changed

+1186
-4
lines changed

11 files changed

+1186
-4
lines changed

Cargo.lock

Lines changed: 8 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ tokio-test = "0.4.4"
6060
tower-lsp = "0.20.0"
6161
ureq = { version = "3.0.12", features = ["json"] }
6262
url = { version = "2.5.4", features = ["serde", "expose_internals"] }
63+
uuid = { version = "1.17.0", features = ["v4"] }
6364
wgpu = { version = "26.0.1", features = ["wgsl", "webgpu"] }
6465

6566
[profile.release]

examples/blob.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
const blob = new Blob(["Hello, ", "World!"], { type: "text/plain" });
2+
console.log(" Blob size:", blob.size);
3+
console.log(" Blob type:", blob.type);
4+
const sliced = blob.slice(0, 5);
5+
console.log(" Sliced size:", sliced.size);
6+
7+
const file = new File(["File content here"], "document.txt", {
8+
type: "text/plain",
9+
lastModified: Date.now() - 10000,
10+
});
11+
console.log(" File name:", file.name);
12+
console.log(" File size:", file.size);
13+
console.log(" File type:", file.type);
14+
console.log(" File lastModified:", file.lastModified);
15+
16+
const fileSliced = file.slice(0, 4);
17+
console.log(" File sliced size:", fileSliced.size);
18+
19+
const formData = new FormData();
20+
formData.append("text", "hello world");
21+
formData.append("file", file);
22+
formData.append("blob", blob, "data.txt");
23+
24+
console.log(" FormData has text:", formData.has("text"));
25+
console.log(" FormData get text:", formData.get("text"));
26+
console.log(" FormData has file:", formData.has("file"));
27+
console.log(" FormData has blob:", formData.has("blob"));
28+
29+
console.log(" FormData keys:");
30+
for (const key of formData.keys()) {
31+
console.log(" -", key);
32+
}

runtime/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,5 @@ rusqlite = { workspace = true, optional = true, features = ["bundled"] }
3030
signal-hook.workspace = true
3131
lazy_static.workspace = true
3232
thiserror.workspace = true
33+
uuid.workspace = true
3334
wgpu = { workspace = true, optional = true }

runtime/src/ext/file/blob.ts

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+
/**
6+
* Implementation of the File API Blob interface
7+
* Based on: https://w3c.github.io/FileAPI/#blob-section
8+
* WinterTC Compliance: https://min-common-api.proposal.wintertc.org/
9+
*/
10+
11+
type BlobPart = BufferSource | string | Blob;
12+
13+
interface BlobPropertyBag {
14+
type?: string;
15+
endings?: "transparent" | "native";
16+
}
17+
18+
/**
19+
* Utility function to convert various input types to byte array
20+
*/
21+
function convertBlobPartsToBytes(blobParts: BlobPart[]): number[] {
22+
const bytes: number[] = [];
23+
24+
for (const part of blobParts) {
25+
if (typeof part === "string") {
26+
// Convert string to UTF-8 bytes
27+
const encoder = new TextEncoder();
28+
const stringBytes = encoder.encode(part);
29+
for (let i = 0; i < stringBytes.length; i++) {
30+
bytes.push(stringBytes[i]);
31+
}
32+
} else if (part instanceof Blob) {
33+
// Get bytes from another blob
34+
const partBlob = part as Blob & { _blobId: string };
35+
const blobBytes = internal_blob_get_data(partBlob._blobId);
36+
if (blobBytes) {
37+
const blobByteArray = blobBytes.split(",").map((b) =>
38+
parseInt(b, 10)
39+
).filter((b) => !isNaN(b));
40+
bytes.push(...blobByteArray);
41+
}
42+
} else if (part instanceof ArrayBuffer) {
43+
// Convert ArrayBuffer to bytes
44+
const view = new Uint8Array(part);
45+
for (let i = 0; i < view.length; i++) {
46+
bytes.push(view[i]);
47+
}
48+
} else if (ArrayBuffer.isView(part)) {
49+
// Handle TypedArray views
50+
const view = new Uint8Array(
51+
part.buffer,
52+
part.byteOffset,
53+
part.byteLength,
54+
);
55+
for (let i = 0; i < view.length; i++) {
56+
bytes.push(view[i]);
57+
}
58+
}
59+
}
60+
61+
return bytes;
62+
}
63+
64+
/**
65+
* Blob represents a file-like object of immutable, raw data
66+
*/
67+
class Blob {
68+
#blobId: string;
69+
70+
constructor(
71+
blobParts: BlobPart[] = [],
72+
options: BlobPropertyBag = {},
73+
existingBlobId?: string,
74+
) {
75+
if (existingBlobId) {
76+
// Use existing blob ID (for internal operations like slice)
77+
this.#blobId = existingBlobId;
78+
} else {
79+
// Normal blob creation
80+
const type = options.type || "";
81+
82+
// Validate and normalize type
83+
let normalizedType = "";
84+
if (type) {
85+
// Basic MIME type validation - should be lowercase and ASCII printable
86+
if (
87+
/^[a-zA-Z0-9][a-zA-Z0-9!#$&\-\^_]*\/[a-zA-Z0-9][a-zA-Z0-9!#$&\-\^_.]*$/
88+
.test(type)
89+
) {
90+
normalizedType = type.toLowerCase();
91+
}
92+
}
93+
94+
// Convert blob parts to bytes
95+
const bytes = convertBlobPartsToBytes(blobParts);
96+
const bytesString = bytes.join(",");
97+
98+
// Create blob through native implementation
99+
this.#blobId = internal_blob_create(bytesString, normalizedType);
100+
}
101+
}
102+
103+
/**
104+
* The size of the blob in bytes
105+
*/
106+
get size(): number {
107+
return internal_blob_get_size(this.#blobId);
108+
}
109+
110+
/**
111+
* The MIME type of the blob
112+
*/
113+
get type(): string {
114+
return internal_blob_get_type(this.#blobId);
115+
}
116+
117+
/**
118+
* Returns a new Blob containing the data in the specified range
119+
*/
120+
slice(start?: number, end?: number, contentType?: string): Blob {
121+
const actualStart = start ?? 0;
122+
const actualEnd = end ?? this.size;
123+
const actualContentType = contentType ?? "";
124+
125+
const newBlobId = internal_blob_slice(
126+
this.#blobId,
127+
actualStart,
128+
actualEnd,
129+
actualContentType,
130+
);
131+
132+
// Create a new Blob instance with the sliced blob ID
133+
return new Blob([], {}, newBlobId);
134+
}
135+
136+
/**
137+
* Returns a ReadableStream of the blob data
138+
*/
139+
stream(): ReadableStream<Uint8Array> {
140+
// TODO: return a proper ReadableStream
141+
const data = internal_blob_stream(this.#blobId);
142+
const bytes = data
143+
? data.split(",").map((b) => parseInt(b, 10)).filter((b) =>
144+
!isNaN(b)
145+
)
146+
: [];
147+
const uint8Array = new Uint8Array(bytes);
148+
149+
return new ReadableStream({
150+
start(controller) {
151+
controller.enqueue(uint8Array);
152+
controller.close();
153+
},
154+
});
155+
}
156+
157+
/**
158+
* Returns a Promise that resolves with the blob data as an ArrayBuffer
159+
*/
160+
arrayBuffer(): Promise<ArrayBuffer> {
161+
return new Promise((resolve) => {
162+
const data = internal_blob_array_buffer(this.#blobId);
163+
const bytes = data
164+
? data.split(",").map((b) => parseInt(b, 10)).filter((b) =>
165+
!isNaN(b)
166+
)
167+
: [];
168+
169+
const buffer = new ArrayBuffer(bytes.length);
170+
const view = new Uint8Array(buffer);
171+
for (let i = 0; i < bytes.length; i++) {
172+
view[i] = bytes[i];
173+
}
174+
175+
resolve(buffer);
176+
});
177+
}
178+
179+
/**
180+
* Returns a Promise that resolves with the blob data as a string
181+
*/
182+
text(): Promise<string> {
183+
return new Promise((resolve) => {
184+
resolve(internal_blob_text(this.#blobId));
185+
});
186+
}
187+
188+
/**
189+
* Returns the blob ID (internal method for File implementation)
190+
*/
191+
get [Symbol.toStringTag]() {
192+
return "Blob";
193+
}
194+
195+
// Internal accessor for other implementations
196+
get _blobId(): string {
197+
return this.#blobId;
198+
}
199+
}

runtime/src/ext/file/file.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// deno-lint-ignore-file no-unused-vars
2+
// This Source Code Form is subject to the terms of the Mozilla Public
3+
// License, v. 2.0. If a copy of the MPL was not distributed with this
4+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
5+
6+
/**
7+
* Implementation of the File API File interface
8+
* Based on: https://w3c.github.io/FileAPI/#file-section
9+
* WinterTC Compliance: https://min-common-api.proposal.wintertc.org/
10+
*/
11+
12+
interface FilePropertyBag extends BlobPropertyBag {
13+
lastModified?: number;
14+
}
15+
16+
/**
17+
* File represents a file-like object of immutable, raw data that implements the Blob interface
18+
*/
19+
class File {
20+
#blob: Blob;
21+
#name: string;
22+
#lastModified: number;
23+
24+
constructor(fileBits: BlobPart[], fileName: string, options: FilePropertyBag = {}) {
25+
// Create the underlying blob
26+
this.#blob = new Blob(fileBits, { type: options.type || "" });
27+
28+
// Set file-specific properties
29+
this.#name = fileName;
30+
this.#lastModified = options.lastModified ?? Date.now();
31+
}
32+
33+
/**
34+
* The size of the file in bytes
35+
*/
36+
get size(): number {
37+
return this.#blob.size;
38+
}
39+
40+
/**
41+
* The MIME type of the file
42+
*/
43+
get type(): string {
44+
return this.#blob.type;
45+
}
46+
47+
/**
48+
* The name of the file
49+
*/
50+
get name(): string {
51+
return this.#name;
52+
}
53+
54+
/**
55+
* The last modified timestamp of the file in milliseconds since Unix epoch
56+
*/
57+
get lastModified(): number {
58+
return this.#lastModified;
59+
}
60+
61+
/**
62+
* The last modified date as a Date object
63+
*/
64+
get lastModifiedDate(): Date {
65+
return new Date(this.#lastModified);
66+
}
67+
68+
/**
69+
* Returns a new Blob containing the data in the specified range
70+
*/
71+
slice(start?: number, end?: number, contentType?: string): Blob {
72+
// Delegate to the underlying blob's slice method
73+
return this.#blob.slice(start, end, contentType);
74+
}
75+
76+
/**
77+
* Returns a Promise that resolves with the contents of the blob as text
78+
*/
79+
async text(): Promise<string> {
80+
return await this.#blob.text();
81+
}
82+
83+
/**
84+
* Returns a Promise that resolves with the contents of the blob as an ArrayBuffer
85+
*/
86+
async arrayBuffer(): Promise<ArrayBuffer> {
87+
return await this.#blob.arrayBuffer();
88+
}
89+
90+
/**
91+
* Returns a ReadableStream of the blob's data
92+
*/
93+
stream(): ReadableStream<Uint8Array> {
94+
return this.#blob.stream();
95+
}
96+
97+
/**
98+
* Returns the string tag for Object.prototype.toString
99+
*/
100+
get [Symbol.toStringTag]() {
101+
return "File";
102+
}
103+
}

0 commit comments

Comments
 (0)