Skip to content

Commit a2bca86

Browse files
authored
Blazor Server Large File Upload Support (#33900)
* Prototype Large File Upload Support Blazor * Blob.slice Implementation * Allow WASM to keep working * Update CircuitStreamingInterop.ts * PR Feedback * Cleanup * PR Feedback
1 parent 10beca5 commit a2bca86

File tree

6 files changed

+62
-38
lines changed

6 files changed

+62
-38
lines changed

src/Components/Web.JS/dist/Release/blazor.server.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Components/Web.JS/dist/Release/blazor.webview.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Components/Web.JS/src/InputFile.ts

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
export const InputFile = {
32
init,
43
toImageFile,
@@ -14,6 +13,7 @@ interface BrowserFile {
1413
contentType: string;
1514
readPromise: Promise<ArrayBuffer> | undefined;
1615
arrayBuffer: ArrayBuffer | undefined;
16+
blob: Blob;
1717
}
1818

1919
export interface InputElement extends HTMLInputElement {
@@ -33,7 +33,7 @@ function init(callbackWrapper: any, elem: InputElement): void {
3333
// Reduce to purely serializable data, plus an index by ID.
3434
elem._blazorFilesById = {};
3535

36-
const fileList = Array.prototype.map.call(elem.files, function(file): BrowserFile {
36+
const fileList = Array.prototype.map.call(elem.files, function(file: File): BrowserFile {
3737
const result = {
3838
id: ++elem._blazorInputFileNextFileId,
3939
lastModified: new Date(file.lastModified).toISOString(),
@@ -42,13 +42,11 @@ function init(callbackWrapper: any, elem: InputElement): void {
4242
contentType: file.type,
4343
readPromise: undefined,
4444
arrayBuffer: undefined,
45+
blob: file,
4546
};
4647

4748
elem._blazorFilesById[result.id] = result;
4849

49-
// Attach the blob data itself as a non-enumerable property so it doesn't appear in the JSON.
50-
Object.defineProperty(result, 'blob', { value: file });
51-
5250
return result;
5351
});
5452

@@ -78,6 +76,7 @@ async function toImageFile(elem: InputElement, fileId: number, format: string, m
7876
canvas.getContext('2d')?.drawImage(loadedImage, 0, 0, canvas.width, canvas.height);
7977
canvas.toBlob(resolve, format);
8078
});
79+
8180
const result: BrowserFile = {
8281
id: ++elem._blazorInputFileNextFileId,
8382
lastModified: originalFile.lastModified,
@@ -86,13 +85,11 @@ async function toImageFile(elem: InputElement, fileId: number, format: string, m
8685
contentType: format,
8786
readPromise: undefined,
8887
arrayBuffer: undefined,
88+
blob: resizedImageBlob ? resizedImageBlob : originalFile.blob,
8989
};
9090

9191
elem._blazorFilesById[result.id] = result;
9292

93-
// Attach the blob data itself as a non-enumerable property so it doesn't appear in the JSON.
94-
Object.defineProperty(result, 'blob', { value: resizedImageBlob });
95-
9693
return result;
9794
}
9895

@@ -101,9 +98,9 @@ async function ensureArrayBufferReadyForSharedMemoryInterop(elem: InputElement,
10198
getFileById(elem, fileId).arrayBuffer = arrayBuffer;
10299
}
103100

104-
async function readFileData(elem: InputElement, fileId: number): Promise<Uint8Array> {
105-
const arrayBuffer = await getArrayBufferFromFileAsync(elem, fileId);
106-
return new Uint8Array(arrayBuffer);
101+
async function readFileData(elem: InputElement, fileId: number): Promise<Blob> {
102+
const file = getFileById(elem, fileId);
103+
return file.blob;
107104
}
108105

109106
export function getFileById(elem: InputElement, fileId: number): BrowserFile {
@@ -129,7 +126,7 @@ function getArrayBufferFromFileAsync(elem: InputElement, fileId: number): Promis
129126
reader.onerror = function(err): void {
130127
reject(err);
131128
};
132-
reader.readAsArrayBuffer(file['blob']);
129+
reader.readAsArrayBuffer(file.blob);
133130
});
134131
}
135132

src/Components/Web.JS/src/Platform/Circuits/CircuitStreamingInterop.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { HubConnection } from '@microsoft/signalr';
22

3-
export function sendJSDataStream(connection: HubConnection, data: ArrayBufferView, streamId: string, chunkSize: number) {
3+
export function sendJSDataStream(connection: HubConnection, data: ArrayBufferView | Blob, streamId: string, chunkSize: number) {
44
// Run the rest in the background, without delaying the completion of the call to sendJSDataStream
55
// otherwise we'll deadlock (.NET can't begin reading until this completes, but it won't complete
66
// because nobody's reading the pipe)
@@ -9,12 +9,13 @@ export function sendJSDataStream(connection: HubConnection, data: ArrayBufferVie
99
let numChunksUntilNextAck = 5;
1010
let lastAckTime = new Date().valueOf();
1111
try {
12+
const byteLength = data instanceof Blob ? data.size : data.byteLength;
1213
let position = 0;
1314
let chunkId = 0;
1415

15-
while (position < data.byteLength) {
16-
const nextChunkSize = Math.min(chunkSize, data.byteLength - position);
17-
const nextChunkData = new Uint8Array(data.buffer, data.byteOffset + position, nextChunkSize);
16+
while (position < byteLength) {
17+
const nextChunkSize = Math.min(chunkSize, byteLength - position);
18+
const nextChunkData = await getNextChunk(data, position, nextChunkSize);
1819

1920
numChunksUntilNextAck--;
2021
if (numChunksUntilNextAck > 1) {
@@ -49,3 +50,23 @@ export function sendJSDataStream(connection: HubConnection, data: ArrayBufferVie
4950
}
5051
}, 0);
5152
};
53+
54+
async function getNextChunk(data: ArrayBufferView | Blob, position: number, nextChunkSize: number): Promise<Uint8Array> {
55+
if (data instanceof Blob) {
56+
return await getChunkFromBlob(data, position, nextChunkSize);
57+
} else {
58+
return getChunkFromArrayBufferView(data, position, nextChunkSize);
59+
}
60+
}
61+
62+
async function getChunkFromBlob(data: Blob, position: number, nextChunkSize: number): Promise<Uint8Array> {
63+
const chunkBlob = data.slice(position, position + nextChunkSize);
64+
const arrayBuffer = await chunkBlob.arrayBuffer();
65+
const nextChunkData = new Uint8Array(arrayBuffer);
66+
return nextChunkData;
67+
}
68+
69+
function getChunkFromArrayBufferView(data: ArrayBufferView, position: number, nextChunkSize: number) {
70+
const nextChunkData = new Uint8Array(data.buffer, data.byteOffset + position, nextChunkSize);
71+
return nextChunkData;
72+
}

src/Components/Web/src/Forms/InputFile/BrowserFileStream.cs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,8 @@ public override Task<int> ReadAsync(byte[] buffer, int offset, int count, Cancel
5353

5454
public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
5555
{
56-
int maxBytesToRead = (int)(Length - Position);
57-
58-
if (maxBytesToRead > buffer.Length)
59-
{
60-
maxBytesToRead = buffer.Length;
61-
}
62-
56+
var bytesAvailableToRead = Length - Position;
57+
var maxBytesToRead = (int)Math.Min(bytesAvailableToRead, buffer.Length);
6358
if (maxBytesToRead <= 0)
6459
{
6560
return 0;

src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ export module DotNet {
1010
class JSObject {
1111
_cachedFunctions: Map<string, Function>;
1212

13-
constructor(private _jsObject: any)
14-
{
13+
constructor(private _jsObject: any) {
1514
this._cachedFunctions = new Map<string, Function>();
1615
}
1716

@@ -142,28 +141,40 @@ export module DotNet {
142141
/**
143142
* Creates a JavaScript data reference that can be passed to .NET via interop calls.
144143
*
145-
* @param arrayBufferView The ArrayBufferView used to create the JavaScript stream reference.
144+
* @param streamReference The ArrayBufferView or Blob used to create the JavaScript stream reference.
146145
* @returns The JavaScript data reference (this will be the same instance as the given object).
147146
* @throws Error if the given value is not an Object or doesn't have a valid byteLength.
148147
*/
149-
export function createJSStreamReference(arrayBufferView: ArrayBufferView | any): any {
150-
// Check if this is an ArrayBufferView, and if it has a valid byteLength for transfer
151-
// using a JSStreamReference.
152-
if (!(arrayBufferView.buffer instanceof ArrayBuffer)) {
153-
throw new Error(`Cannot create a JSStreamReference from the value '${arrayBufferView}' as it is not have a 'buffer' property of type 'ArrayBuffer'.`);
154-
} else if (arrayBufferView.byteLength === undefined) {
155-
throw new Error(`Cannot create a JSStreamReference from the value '${arrayBufferView}' as it doesn't have a byteLength.`);
148+
export function createJSStreamReference(streamReference: ArrayBuffer | ArrayBufferView | Blob | any): any {
149+
let length = -1;
150+
151+
// If we're given a raw Array Buffer, we interpret it as a `Uint8Array` as
152+
// ArrayBuffers' aren't directly readable.
153+
if (streamReference instanceof ArrayBuffer) {
154+
streamReference = new Uint8Array(streamReference);
155+
}
156+
157+
if (streamReference instanceof Blob) {
158+
length = streamReference.size;
159+
} else if (streamReference.buffer instanceof ArrayBuffer) {
160+
if (streamReference.byteLength === undefined) {
161+
throw new Error(`Cannot create a JSStreamReference from the value '${streamReference}' as it doesn't have a byteLength.`);
162+
}
163+
164+
length = streamReference.byteLength;
165+
} else {
166+
throw new Error('Supplied value is not a typed array or blob.');
156167
}
157168

158169
const result: any = {
159-
[jsStreamReferenceLengthKey]: arrayBufferView.byteLength,
170+
[jsStreamReferenceLengthKey]: length,
160171
}
161172

162173
try {
163-
const jsObjectReference = createJSObjectReference(arrayBufferView);
174+
const jsObjectReference = createJSObjectReference(streamReference);
164175
result[jsObjectIdKey] = jsObjectReference[jsObjectIdKey];
165176
} catch {
166-
throw new Error(`Cannot create a JSStreamReference from the value '${arrayBufferView}'.`);
177+
throw new Error(`Cannot create a JSStreamReference from the value '${streamReference}'.`);
167178
}
168179

169180
return result;

0 commit comments

Comments
 (0)