KristofferStrube / Blazor.FileSystemAccess

A Blazor wrapper for the File System Access browser API.
https://kristofferstrube.github.io/Blazor.FileSystemAccess/
MIT License
327 stars 39 forks source link

Example crashed with file size > 50kb #49

Open EricNgo1972 opened 11 months ago

EricNgo1972 commented 11 months ago

I cloned your repository and ran it locally. On the /ViewZipFile page, if I select a zip file with a size above 50 KB, it crashes at:

     var buffer = await file.ArrayBufferAsync(); // <<----------crash happen at this line.
     using var stream = new MemoryStream(buffer);
     using var archive = new ZipArchive(stream, ZipArchiveMode.Read, true);

with the exception:

blazor.server.js:1 [2023-11-20T04:37:04.932Z] Error: Connection disconnected with error 'Error: Server returned an error on close: Connection closed with an error.'.

However your app at https://kristofferstrube.github.io/Blazor.FileSystemAccess/ViewZipFile works fine with any file size.

Is there any size limitation we need to set?

KristofferStrube commented 11 months ago

You may be using it in a Blazor Server App or a Blazor Web App project from the exception I see you get. Is that correct?

As I note in the project README under the Usage Section:

The package can be used in Blazor WebAssembly and Blazor Server projects. (Note that streaming of big files is not supported in Blazor Server due to bandwidth problems.)

This is still a valid issue though, as that is a very valid use case. I just haven't prioritized this previously. Actually, this is more related to my Blazor.Streams project, so I might end up creating an issue there. What I have done previously, for my own customers, has been to get a stream from the file using the StreamAsync method and then reading small chunks of the file into an intermediate buffer while it is streamed like I have done here in my ReadableStream implementation but limiting the buffer size and putting in some Task.Yields or Delays between reading each chunk so that the Blazor Server circuit isn't blocked by this work in too long consecutive periods. This likewise opens up the possibility to update the user with upload progress while it happens i.e "Uploading progress 23% ..."

A more straightforward way to get around this limitation is to increase the message size limit for the SignalR connection to however big files you want to send which could look like this when configuring Server Side SignalR Hub connection settings in a Blazor Server project:

builder.Services.AddServerSideBlazor().AddHubOptions(o =>
{
    o.MaximumReceiveMessageSize = 1024 * 1024; // This is 1 Megabyte
});
EricNgo1972 commented 11 months ago

Adding MaximumReceivemessageSize option indeed resolved the limitation. Thanks.

KristofferStrube commented 11 months ago

I'll keep this issue open for bit longer so that I that I can keep track of the use case and either add more documentation for this case or resolve it in the Blazor.Streams project.

georg-jung commented 5 months ago

I think I found a rather easy and quite flexible streaming solution. Not sure if it has any downsides I'm currently overlooking, but it seems to work well for me without changing the SignalR message size. I tested it with files of around ~25 MB, which worked on localhost with a minor delay (roughly >1s <5s on my machine). It uses the same mechanisms as Blazor's InputFile component:

internal static class BlazorFileSystemAccessExtensions
{
    public static async Task<IJSStreamReference> GetFileStream(this FileSystemFileHandle fileHandle, CancellationToken cancellationToken = default)
    {
        return await fileHandle.JSReference.InvokeAsync<IJSStreamReference>("getFile", cancellationToken);
    }

    public static async Task<byte[]> ReadAllBytes(this IJSStreamReference jsStream, long maxAllowedSize = 512000, CancellationToken cancellationToken = default)
    {
        await using var s = await jsStream.OpenReadStreamAsync(maxAllowedSize, cancellationToken);
        var buffer = new byte[s.Length];
        using var ms = new MemoryStream(buffer);
        await s.CopyToAsync(ms, cancellationToken);
        return buffer;
    }

    public static async Task<byte[]> ReadAllBytes(this FileSystemFileHandle fileHandle, long maxAllowedSize = 512000, CancellationToken cancellationToken = default)
    {
        await using var fs = await fileHandle.GetFileStream(cancellationToken);
        return await fs.ReadAllBytes(maxAllowedSize, cancellationToken);
    }
}

Note that Microsoft recommends to avoid the ReadAllBytes pattern:

Avoid reading the incoming file stream directly into memory all at once. For example, don't copy all of the file's bytes into a MemoryStream or read the entire stream into a byte array all at once. These approaches can result in performance and security problems, especially for server-side components.

It could lead to DoS attacks / high memory pressure if the maxAllowedSize is chosen too large. My approach shares this downsides though with the corresponding implementations in Blazor.FileSystemAccess. Only using GetFileStream is not against the advice.

This works because the File object returned by FileSystemHandle.getFile() is a Blob and Blazor JS Interop can return IJSStreamReference for these.