File: Forms\InputFile\BrowserFileStream.cs
Web Access
Project: src\src\Components\Web\src\Microsoft.AspNetCore.Components.Web.csproj (Microsoft.AspNetCore.Components.Web)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Microsoft.JSInterop;
 
namespace Microsoft.AspNetCore.Components.Forms;
 
internal sealed class BrowserFileStream : Stream
{
    private long _position;
    private readonly IJSRuntime _jsRuntime;
    private readonly ElementReference _inputFileElement;
    private readonly BrowserFile _file;
    private readonly long _maxAllowedSize;
    private readonly CancellationTokenSource _openReadStreamCts;
    private readonly Task<Stream> OpenReadStreamTask;
    private IJSStreamReference? _jsStreamReference;
 
    private bool _isDisposed;
    private CancellationTokenSource? _copyFileDataCts;
 
    public BrowserFileStream(
        IJSRuntime jsRuntime,
        ElementReference inputFileElement,
        BrowserFile file,
        long maxAllowedSize,
        CancellationToken cancellationToken)
    {
        _jsRuntime = jsRuntime;
        _inputFileElement = inputFileElement;
        _file = file;
        _maxAllowedSize = maxAllowedSize;
        _openReadStreamCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
 
        OpenReadStreamTask = OpenReadStreamAsync(_openReadStreamCts.Token);
    }
 
    public override bool CanRead => true;
 
    public override bool CanSeek => false;
 
    public override bool CanWrite => false;
 
    public override long Length => _file.Size;
 
    public override long Position
    {
        get => _position;
        set => throw new NotSupportedException();
    }
 
    public override void Flush()
        => throw new NotSupportedException();
 
    public override int Read(byte[] buffer, int offset, int count)
        => throw new NotSupportedException("Synchronous reads are not supported.");
 
    public override long Seek(long offset, SeekOrigin origin)
        => throw new NotSupportedException();
 
    public override void SetLength(long value)
        => throw new NotSupportedException();
 
    public override void Write(byte[] buffer, int offset, int count)
        => throw new NotSupportedException();
 
    public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
        => ReadAsync(new Memory<byte>(buffer, offset, count), cancellationToken).AsTask();
 
    public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
    {
        var bytesAvailableToRead = Length - Position;
        var maxBytesToRead = (int)Math.Min(bytesAvailableToRead, buffer.Length);
        if (maxBytesToRead <= 0)
        {
            return 0;
        }
 
        var bytesRead = await CopyFileDataIntoBuffer(buffer.Slice(0, maxBytesToRead), cancellationToken);
 
        _position += bytesRead;
 
        return bytesRead;
    }
 
    private async Task<Stream> OpenReadStreamAsync(CancellationToken cancellationToken)
    {
        // This method only gets called once, from the constructor, so we're never overwriting an
        // existing _jsStreamReference value
        _jsStreamReference = await _jsRuntime.InvokeAsync<IJSStreamReference>(
            InputFileInterop.ReadFileData,
            cancellationToken,
            _inputFileElement,
            _file.Id);
 
        return await _jsStreamReference.OpenReadStreamAsync(
            _maxAllowedSize,
            cancellationToken: cancellationToken);
    }
 
    private async ValueTask<int> CopyFileDataIntoBuffer(Memory<byte> destination, CancellationToken cancellationToken)
    {
        var stream = await OpenReadStreamTask;
        _copyFileDataCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
        return await stream.ReadAsync(destination, _copyFileDataCts.Token);
    }
 
    protected override void Dispose(bool disposing)
    {
        if (_isDisposed)
        {
            return;
        }
 
        _openReadStreamCts.Cancel();
        _copyFileDataCts?.Cancel();
 
        // If the browser connection is still live, notify the JS side that it's free to release the Blob
        // and reclaim the memory. If the browser connection is already gone, there's no way for the
        // notification to get through, but we don't want to fail the .NET-side disposal process for this.
        try
        {
            _ = _jsStreamReference?.DisposeAsync().Preserve();
        }
        catch
        {
        }
 
        _isDisposed = true;
 
        base.Dispose(disposing);
    }
}