InputFile Component (#24640)
This commit is contained in:
parent
a700662dec
commit
0b1042c54e
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.Web.Extensions
|
||||||
|
{
|
||||||
|
internal class BrowserFile : IBrowserFile
|
||||||
|
{
|
||||||
|
internal InputFile Owner { get; set; } = default!;
|
||||||
|
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public DateTime LastModified { get; set; }
|
||||||
|
|
||||||
|
public long Size { get; set; }
|
||||||
|
|
||||||
|
public string Type { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string? RelativePath { get; set; }
|
||||||
|
|
||||||
|
public Stream OpenReadStream(CancellationToken cancellationToken = default)
|
||||||
|
=> Owner.OpenReadStream(this, cancellationToken);
|
||||||
|
|
||||||
|
public Task<IBrowserFile> ToImageFileAsync(string format, int maxWidth, int maxHeight)
|
||||||
|
=> Owner.ConvertToImageFileAsync(this, format, maxWidth, maxHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.Web.Extensions
|
||||||
|
{
|
||||||
|
internal abstract class BrowserFileStream : Stream
|
||||||
|
{
|
||||||
|
private long _position;
|
||||||
|
|
||||||
|
protected BrowserFile File { get; }
|
||||||
|
|
||||||
|
protected BrowserFileStream(BrowserFile file)
|
||||||
|
{
|
||||||
|
File = file;
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
int maxBytesToRead = (int)(Length - Position);
|
||||||
|
|
||||||
|
if (maxBytesToRead > buffer.Length)
|
||||||
|
{
|
||||||
|
maxBytesToRead = buffer.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxBytesToRead <= 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var bytesRead = await CopyFileDataIntoBuffer(_position, buffer.Slice(0, maxBytesToRead), cancellationToken);
|
||||||
|
|
||||||
|
_position += bytesRead;
|
||||||
|
|
||||||
|
return bytesRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract ValueTask<int> CopyFileDataIntoBuffer(long sourceOffset, Memory<byte> destination, CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.Web.Extensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the data of a file selected from an <see cref="InputFile"/> component.
|
||||||
|
/// </summary>
|
||||||
|
public interface IBrowserFile
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the name of the file.
|
||||||
|
/// </summary>
|
||||||
|
string Name { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the last modified date.
|
||||||
|
/// </summary>
|
||||||
|
DateTime LastModified { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the size of the file in bytes.
|
||||||
|
/// </summary>
|
||||||
|
long Size { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the MIME type of the file.
|
||||||
|
/// </summary>
|
||||||
|
string Type { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Opens the stream for reading the uploaded file.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">A cancellation token to signal the cancellation of streaming file data.</param>
|
||||||
|
Stream OpenReadStream(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts the current image file to a new one of the specified file type and maximum file dimensions.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The image will be scaled to fit the specified dimensions while preserving the original aspect ratio.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="format">The new image format.</param>
|
||||||
|
/// <param name="maxWith">The maximum image width.</param>
|
||||||
|
/// <param name="maxHeight">The maximum image height</param>
|
||||||
|
/// <returns>A <see cref="Task"/> representing the completion of the operation.</returns>
|
||||||
|
Task<IBrowserFile> ToImageFileAsync(string format, int maxWith, int maxHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.Web.Extensions
|
||||||
|
{
|
||||||
|
internal interface IInputFileJsCallbacks
|
||||||
|
{
|
||||||
|
Task NotifyChange(BrowserFile[] files);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Components.Rendering;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Microsoft.JSInterop;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.Web.Extensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A component that wraps the HTML file input element and exposes a <see cref="Stream"/> for each file's contents.
|
||||||
|
/// </summary>
|
||||||
|
public class InputFile : ComponentBase, IInputFileJsCallbacks, IDisposable
|
||||||
|
{
|
||||||
|
private ElementReference _inputFileElement;
|
||||||
|
|
||||||
|
private IJSUnmarshalledRuntime? _jsUnmarshalledRuntime;
|
||||||
|
|
||||||
|
private InputFileJsCallbacksRelay? _jsCallbacksRelay;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private IJSRuntime JSRuntime { get; set; } = default!;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
private IOptions<RemoteBrowserFileStreamOptions> Options { get; set; } = default!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the event callback that will be invoked when the collection of selected files changes.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback<InputFileChangeEventArgs> OnChange { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a collection of additional attributes that will be applied to the input element.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter(CaptureUnmatchedValues = true)]
|
||||||
|
public IDictionary<string, object>? AdditionalAttributes { get; set; }
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
_jsUnmarshalledRuntime = JSRuntime as IJSUnmarshalledRuntime;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (firstRender)
|
||||||
|
{
|
||||||
|
_jsCallbacksRelay = new InputFileJsCallbacksRelay(this);
|
||||||
|
await JSRuntime.InvokeVoidAsync(InputFileInterop.Init, _jsCallbacksRelay.DotNetReference, _inputFileElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
||||||
|
{
|
||||||
|
builder.OpenElement(0, "input");
|
||||||
|
builder.AddMultipleAttributes(1, AdditionalAttributes);
|
||||||
|
builder.AddAttribute(2, "type", "file");
|
||||||
|
builder.AddElementReferenceCapture(3, elementReference => _inputFileElement = elementReference);
|
||||||
|
builder.CloseElement();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal Stream OpenReadStream(BrowserFile file, CancellationToken cancellationToken)
|
||||||
|
=> _jsUnmarshalledRuntime != null ?
|
||||||
|
(Stream)new SharedBrowserFileStream(JSRuntime, _jsUnmarshalledRuntime, _inputFileElement, file) :
|
||||||
|
new RemoteBrowserFileStream(JSRuntime, _inputFileElement, file, Options.Value, cancellationToken);
|
||||||
|
|
||||||
|
internal async Task<IBrowserFile> ConvertToImageFileAsync(BrowserFile file, string format, int maxWidth, int maxHeight)
|
||||||
|
{
|
||||||
|
var imageFile = await JSRuntime.InvokeAsync<BrowserFile>(InputFileInterop.ToImageFile, _inputFileElement, file.Id, format, maxWidth, maxHeight);
|
||||||
|
|
||||||
|
imageFile.Owner = this;
|
||||||
|
|
||||||
|
return imageFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
Task IInputFileJsCallbacks.NotifyChange(BrowserFile[] files)
|
||||||
|
{
|
||||||
|
foreach (var file in files)
|
||||||
|
{
|
||||||
|
file.Owner = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
return OnChange.InvokeAsync(new InputFileChangeEventArgs(files));
|
||||||
|
}
|
||||||
|
|
||||||
|
void IDisposable.Dispose()
|
||||||
|
{
|
||||||
|
_jsCallbacksRelay?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.Web.Extensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Supplies information about an <see cref="InputFile.OnChange"/> event being raised.
|
||||||
|
/// </summary>
|
||||||
|
public class InputFileChangeEventArgs : EventArgs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The updated file entries list.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<IBrowserFile> Files { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructs a new <see cref="InputFileChangeEventArgs"/> instance.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="files">The updated file entries list.</param>
|
||||||
|
public InputFileChangeEventArgs(IReadOnlyList<IBrowserFile> files)
|
||||||
|
{
|
||||||
|
Files = files;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.Web.Extensions
|
||||||
|
{
|
||||||
|
internal static class InputFileInterop
|
||||||
|
{
|
||||||
|
private const string JsFunctionsPrefix = "_blazorInputFile.";
|
||||||
|
|
||||||
|
public const string Init = JsFunctionsPrefix + "init";
|
||||||
|
|
||||||
|
public const string EnsureArrayBufferReadyForSharedMemoryInterop = JsFunctionsPrefix + "ensureArrayBufferReadyForSharedMemoryInterop";
|
||||||
|
|
||||||
|
public const string ReadFileData = JsFunctionsPrefix + "readFileData";
|
||||||
|
|
||||||
|
public const string ReadFileDataSharedMemory = JsFunctionsPrefix + "readFileDataSharedMemory";
|
||||||
|
|
||||||
|
public const string ToImageFile = JsFunctionsPrefix + "toImageFile";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.JSInterop;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.Web.Extensions
|
||||||
|
{
|
||||||
|
internal class InputFileJsCallbacksRelay : IDisposable
|
||||||
|
{
|
||||||
|
private readonly IInputFileJsCallbacks _callbacks;
|
||||||
|
|
||||||
|
public IDisposable DotNetReference { get; }
|
||||||
|
|
||||||
|
public InputFileJsCallbacksRelay(IInputFileJsCallbacks callbacks)
|
||||||
|
{
|
||||||
|
_callbacks = callbacks;
|
||||||
|
|
||||||
|
DotNetReference = DotNetObjectReference.Create(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
[JSInvokable]
|
||||||
|
public Task NotifyChange(BrowserFile[] files)
|
||||||
|
=> _callbacks.NotifyChange(files);
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
DotNetReference.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.Web.Extensions
|
||||||
|
{
|
||||||
|
[StructLayout(LayoutKind.Explicit)]
|
||||||
|
internal struct ReadRequest
|
||||||
|
{
|
||||||
|
[FieldOffset(0)]
|
||||||
|
public string InputFileElementReferenceId;
|
||||||
|
|
||||||
|
[FieldOffset(4)]
|
||||||
|
public int FileId;
|
||||||
|
|
||||||
|
[FieldOffset(8)]
|
||||||
|
public long SourceOffset;
|
||||||
|
|
||||||
|
[FieldOffset(16)]
|
||||||
|
public byte[] Destination;
|
||||||
|
|
||||||
|
[FieldOffset(20)]
|
||||||
|
public int DestinationOffset;
|
||||||
|
|
||||||
|
[FieldOffset(24)]
|
||||||
|
public int MaxBytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,145 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Buffers;
|
||||||
|
using System.IO.Pipelines;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.JSInterop;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.Web.Extensions
|
||||||
|
{
|
||||||
|
internal class RemoteBrowserFileStream : BrowserFileStream
|
||||||
|
{
|
||||||
|
private readonly IJSRuntime _jsRuntime;
|
||||||
|
private readonly ElementReference _inputFileElement;
|
||||||
|
private readonly int _maxSegmentSize;
|
||||||
|
private readonly PipeReader _pipeReader;
|
||||||
|
private readonly CancellationTokenSource _fillBufferCts;
|
||||||
|
private readonly TimeSpan _segmentFetchTimeout;
|
||||||
|
|
||||||
|
private bool _isReadingCompleted;
|
||||||
|
private bool _isDisposed;
|
||||||
|
|
||||||
|
public RemoteBrowserFileStream(
|
||||||
|
IJSRuntime jsRuntime,
|
||||||
|
ElementReference inputFileElement,
|
||||||
|
BrowserFile file,
|
||||||
|
RemoteBrowserFileStreamOptions options,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
: base(file)
|
||||||
|
{
|
||||||
|
_jsRuntime = jsRuntime;
|
||||||
|
_inputFileElement = inputFileElement;
|
||||||
|
_maxSegmentSize = options.SegmentSize;
|
||||||
|
_segmentFetchTimeout = options.SegmentFetchTimeout;
|
||||||
|
|
||||||
|
var pipe = new Pipe(new PipeOptions(pauseWriterThreshold: options.MaxBufferSize, resumeWriterThreshold: options.MaxBufferSize));
|
||||||
|
_pipeReader = pipe.Reader;
|
||||||
|
_fillBufferCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||||
|
|
||||||
|
_ = FillBuffer(pipe.Writer, _fillBufferCts.Token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task FillBuffer(PipeWriter writer, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
long offset = 0;
|
||||||
|
|
||||||
|
while (offset < File.Size)
|
||||||
|
{
|
||||||
|
var pipeBuffer = writer.GetMemory(_maxSegmentSize);
|
||||||
|
var segmentSize = (int)Math.Min(_maxSegmentSize, File.Size - offset);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var readSegmentCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||||
|
readSegmentCts.CancelAfter(_segmentFetchTimeout);
|
||||||
|
|
||||||
|
var bytes = await _jsRuntime.InvokeAsync<byte[]>(
|
||||||
|
InputFileInterop.ReadFileData,
|
||||||
|
readSegmentCts.Token,
|
||||||
|
_inputFileElement,
|
||||||
|
File.Id,
|
||||||
|
offset,
|
||||||
|
segmentSize);
|
||||||
|
|
||||||
|
if (bytes.Length != segmentSize)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"A segment with size {bytes.Length} bytes was received, but {segmentSize} bytes were expected.");
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes.CopyTo(pipeBuffer);
|
||||||
|
writer.Advance(segmentSize);
|
||||||
|
offset += segmentSize;
|
||||||
|
|
||||||
|
var result = await writer.FlushAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (result.IsCompleted)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
await writer.CompleteAsync(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await writer.CompleteAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async ValueTask<int> CopyFileDataIntoBuffer(long sourceOffset, Memory<byte> destination, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (_isReadingCompleted)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int totalBytesCopied = 0;
|
||||||
|
|
||||||
|
while (destination.Length > 0)
|
||||||
|
{
|
||||||
|
var result = await _pipeReader.ReadAsync(cancellationToken);
|
||||||
|
var bytesToCopy = (int)Math.Min(result.Buffer.Length, destination.Length);
|
||||||
|
|
||||||
|
if (bytesToCopy == 0)
|
||||||
|
{
|
||||||
|
if (result.IsCompleted)
|
||||||
|
{
|
||||||
|
_isReadingCompleted = true;
|
||||||
|
await _pipeReader.CompleteAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var slice = result.Buffer.Slice(0, bytesToCopy);
|
||||||
|
slice.CopyTo(destination.Span);
|
||||||
|
|
||||||
|
_pipeReader.AdvanceTo(slice.End);
|
||||||
|
|
||||||
|
totalBytesCopied += bytesToCopy;
|
||||||
|
destination = destination.Slice(bytesToCopy);
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalBytesCopied;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (_isDisposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_fillBufferCts.Cancel();
|
||||||
|
|
||||||
|
_isDisposed = true;
|
||||||
|
|
||||||
|
base.Dispose(disposing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Runtime.Versioning;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.Web.Extensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Repesents configurable options for <see cref="RemoteBrowserFileStream"/>.
|
||||||
|
/// </summary>
|
||||||
|
[UnsupportedOSPlatform("browser")]
|
||||||
|
public class RemoteBrowserFileStreamOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the maximum segment size for file data sent over a SignalR circuit.
|
||||||
|
/// The default value is 20K.
|
||||||
|
/// <para>
|
||||||
|
/// This only has an effect when using Blazor Server.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
public int SegmentSize { get; set; } = 20 * 1024; // SignalR limit is 32K.
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the maximum internal buffer size for unread data sent over a SignalR circuit.
|
||||||
|
/// <para>
|
||||||
|
/// This only has an effect when using Blazor Server.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
public int MaxBufferSize { get; set; } = 1024 * 1024;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the time limit for fetching a segment of file data.
|
||||||
|
/// <para>
|
||||||
|
/// This only has an effect when using Blazor Server.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan SegmentFetchTimeout { get; set; } = TimeSpan.FromSeconds(3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.JSInterop;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.Web.Extensions
|
||||||
|
{
|
||||||
|
internal class SharedBrowserFileStream : BrowserFileStream
|
||||||
|
{
|
||||||
|
private readonly IJSRuntime _jsRuntime;
|
||||||
|
|
||||||
|
private readonly IJSUnmarshalledRuntime _jsUnmarshalledRuntime;
|
||||||
|
|
||||||
|
private readonly ElementReference _inputFileElement;
|
||||||
|
|
||||||
|
public SharedBrowserFileStream(IJSRuntime jsRuntime, IJSUnmarshalledRuntime jsUnmarshalledRuntime, ElementReference inputFileElement, BrowserFile file)
|
||||||
|
: base(file)
|
||||||
|
{
|
||||||
|
_jsRuntime = jsRuntime;
|
||||||
|
_jsUnmarshalledRuntime = jsUnmarshalledRuntime;
|
||||||
|
_inputFileElement = inputFileElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async ValueTask<int> CopyFileDataIntoBuffer(long sourceOffset, Memory<byte> destination, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await _jsRuntime.InvokeVoidAsync(InputFileInterop.EnsureArrayBufferReadyForSharedMemoryInterop, cancellationToken, _inputFileElement, File.Id);
|
||||||
|
|
||||||
|
var readRequest = new ReadRequest
|
||||||
|
{
|
||||||
|
InputFileElementReferenceId = _inputFileElement.Id,
|
||||||
|
FileId = File.Id,
|
||||||
|
SourceOffset = sourceOffset
|
||||||
|
};
|
||||||
|
|
||||||
|
if (MemoryMarshal.TryGetArray(destination, out ArraySegment<byte> destinationArraySegment))
|
||||||
|
{
|
||||||
|
readRequest.Destination = destinationArraySegment.Array!;
|
||||||
|
readRequest.DestinationOffset = destinationArraySegment.Offset;
|
||||||
|
readRequest.MaxBytes = destinationArraySegment.Count;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Worst case, we need to copy to a temporary array.
|
||||||
|
readRequest.Destination = new byte[destination.Length];
|
||||||
|
readRequest.DestinationOffset = 0;
|
||||||
|
readRequest.MaxBytes = destination.Length;
|
||||||
|
|
||||||
|
destination.CopyTo(new Memory<byte>(readRequest.Destination));
|
||||||
|
}
|
||||||
|
|
||||||
|
return _jsUnmarshalledRuntime.InvokeUnmarshalled<ReadRequest, int>(InputFileInterop.ReadFileDataSharedMemory, readRequest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,142 @@
|
||||||
|
(function () {
|
||||||
|
|
||||||
|
// Exported functions
|
||||||
|
|
||||||
|
function init(callbackWrapper, elem) {
|
||||||
|
elem._blazorInputFileNextFileId = 0;
|
||||||
|
|
||||||
|
elem.addEventListener('click', function () {
|
||||||
|
// Permits replacing an existing file with a new one of the same file name.
|
||||||
|
elem.value = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
elem.addEventListener('change', function () {
|
||||||
|
// Reduce to purely serializable data, plus an index by ID.
|
||||||
|
elem._blazorFilesById = {};
|
||||||
|
|
||||||
|
const fileList = Array.prototype.map.call(elem.files, function (file) {
|
||||||
|
const result = {
|
||||||
|
id: ++elem._blazorInputFileNextFileId,
|
||||||
|
lastModified: new Date(file.lastModified).toISOString(),
|
||||||
|
name: file.name,
|
||||||
|
size: file.size,
|
||||||
|
type: file.type,
|
||||||
|
};
|
||||||
|
|
||||||
|
elem._blazorFilesById[result.id] = result;
|
||||||
|
|
||||||
|
// Attach the blob data itself as a non-enumerable property so it doesn't appear in the JSON.
|
||||||
|
Object.defineProperty(result, 'blob', { value: file });
|
||||||
|
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
callbackWrapper.invokeMethodAsync('NotifyChange', fileList);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function toImageFile(elem, fileId, format, maxWidth, maxHeight) {
|
||||||
|
var originalFile = getFileById(elem, fileId);
|
||||||
|
|
||||||
|
return new Promise(function (resolve) {
|
||||||
|
var originalFileImage = new Image();
|
||||||
|
originalFileImage.onload = function () { resolve(originalFileImage); };
|
||||||
|
originalFileImage.src = URL.createObjectURL(originalFile.blob);
|
||||||
|
}).then(function (loadedImage) {
|
||||||
|
return new Promise(function (resolve) {
|
||||||
|
var desiredWidthRatio = Math.min(1, maxWidth / loadedImage.width);
|
||||||
|
var desiredHeightRatio = Math.min(1, maxHeight / loadedImage.height);
|
||||||
|
var chosenSizeRatio = Math.min(desiredWidthRatio, desiredHeightRatio);
|
||||||
|
|
||||||
|
var canvas = document.createElement('canvas');
|
||||||
|
canvas.width = Math.round(loadedImage.width * chosenSizeRatio);
|
||||||
|
canvas.height = Math.round(loadedImage.height * chosenSizeRatio);
|
||||||
|
canvas.getContext('2d').drawImage(loadedImage, 0, 0, canvas.width, canvas.height);
|
||||||
|
canvas.toBlob(resolve, format);
|
||||||
|
});
|
||||||
|
}).then(function (resizedImageBlob) {
|
||||||
|
var result = {
|
||||||
|
id: ++elem._blazorInputFileNextFileId,
|
||||||
|
lastModified: originalFile.lastModified,
|
||||||
|
name: originalFile.name, // Note: we're not changing the file extension.
|
||||||
|
size: resizedImageBlob.size,
|
||||||
|
type: format,
|
||||||
|
relativePath: originalFile.relativePath
|
||||||
|
};
|
||||||
|
|
||||||
|
elem._blazorFilesById[result.id] = result;
|
||||||
|
|
||||||
|
// Attach the blob data itself as a non-enumerable property so it doesn't appear in the JSON.
|
||||||
|
Object.defineProperty(result, 'blob', { value: resizedImageBlob });
|
||||||
|
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureArrayBufferReadyForSharedMemoryInterop(elem, fileId) {
|
||||||
|
return getArrayBufferFromFileAsync(elem, fileId).then(function (arrayBuffer) {
|
||||||
|
getFileById(elem, fileId).arrayBuffer = arrayBuffer;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function readFileData(elem, fileId, startOffset, count) {
|
||||||
|
return getArrayBufferFromFileAsync(elem, fileId).then(function (arrayBuffer) {
|
||||||
|
return btoa(String.fromCharCode.apply(null, new Uint8Array(arrayBuffer, startOffset, count)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function readFileDataSharedMemory(readRequest) {
|
||||||
|
const inputFileElementReferenceId = Blazor.platform.readStringField(readRequest, 0);
|
||||||
|
const inputFileElement = document.querySelector(`[_bl_${inputFileElementReferenceId}]`);
|
||||||
|
const fileId = Blazor.platform.readInt32Field(readRequest, 4);
|
||||||
|
const sourceOffset = Blazor.platform.readUint64Field(readRequest, 8);
|
||||||
|
const destination = Blazor.platform.readInt32Field(readRequest, 16);
|
||||||
|
const destinationOffset = Blazor.platform.readInt32Field(readRequest, 20);
|
||||||
|
const maxBytes = Blazor.platform.readInt32Field(readRequest, 24);
|
||||||
|
|
||||||
|
const sourceArrayBuffer = getFileById(inputFileElement, fileId).arrayBuffer;
|
||||||
|
const bytesToRead = Math.min(maxBytes, sourceArrayBuffer.byteLength - sourceOffset);
|
||||||
|
const sourceUint8Array = new Uint8Array(sourceArrayBuffer, sourceOffset, bytesToRead);
|
||||||
|
|
||||||
|
const destinationUint8Array = Blazor.platform.toUint8Array(destination);
|
||||||
|
destinationUint8Array.set(sourceUint8Array, destinationOffset);
|
||||||
|
|
||||||
|
return bytesToRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Local helpers
|
||||||
|
|
||||||
|
function getFileById(elem, fileId) {
|
||||||
|
const file = elem._blazorFilesById[fileId];
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
throw new Error(`There is no file with ID ${fileId}. The file list may have changed.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getArrayBufferFromFileAsync(elem, fileId) {
|
||||||
|
const file = getFileById(elem, fileId);
|
||||||
|
|
||||||
|
// On the first read, convert the FileReader into a Promise<ArrayBuffer>.
|
||||||
|
if (!file.readPromise) {
|
||||||
|
file.readPromise = new Promise(function (resolve, reject) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = function () { resolve(reader.result); };
|
||||||
|
reader.onerror = function (err) { reject(err); };
|
||||||
|
reader.readAsArrayBuffer(file.blob);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return file.readPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
window._blazorInputFile = {
|
||||||
|
init,
|
||||||
|
toImageFile,
|
||||||
|
ensureArrayBufferReadyForSharedMemoryInterop,
|
||||||
|
readFileData,
|
||||||
|
readFileDataSharedMemory,
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
@ -11,7 +11,7 @@ namespace Microsoft.JSInterop.WebAssembly
|
||||||
/// Provides methods for invoking JavaScript functions for applications running
|
/// Provides methods for invoking JavaScript functions for applications running
|
||||||
/// on the Mono WebAssembly runtime.
|
/// on the Mono WebAssembly runtime.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class WebAssemblyJSRuntime : JSInProcessRuntime
|
public abstract class WebAssemblyJSRuntime : JSInProcessRuntime, IJSUnmarshalledRuntime
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override string InvokeJS(string identifier, string argsJson)
|
protected override string InvokeJS(string identifier, string argsJson)
|
||||||
|
|
@ -42,52 +42,20 @@ namespace Microsoft.JSInterop.WebAssembly
|
||||||
BeginInvokeJS(0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", args);
|
BeginInvokeJS(0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", args);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc />
|
||||||
/// Invokes the JavaScript function registered with the specified identifier.
|
TResult IJSUnmarshalledRuntime.InvokeUnmarshalled<TResult>(string identifier)
|
||||||
/// </summary>
|
=> ((IJSUnmarshalledRuntime)this).InvokeUnmarshalled<object, object, object, TResult>(identifier, null, null, null);
|
||||||
/// <typeparam name="TResult">The .NET type corresponding to the function's return value type.</typeparam>
|
|
||||||
/// <param name="identifier">The identifier used when registering the target function.</param>
|
|
||||||
/// <returns>The result of the function invocation.</returns>
|
|
||||||
public TResult InvokeUnmarshalled<TResult>(string identifier)
|
|
||||||
=> InvokeUnmarshalled<object, object, object, TResult>(identifier, null, null, null);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc />
|
||||||
/// Invokes the JavaScript function registered with the specified identifier.
|
TResult IJSUnmarshalledRuntime.InvokeUnmarshalled<T0, TResult>(string identifier, T0 arg0)
|
||||||
/// </summary>
|
=> ((IJSUnmarshalledRuntime)this).InvokeUnmarshalled<T0, object, object, TResult>(identifier, arg0, null, null);
|
||||||
/// <typeparam name="T0">The type of the first argument.</typeparam>
|
|
||||||
/// <typeparam name="TResult">The .NET type corresponding to the function's return value type.</typeparam>
|
|
||||||
/// <param name="identifier">The identifier used when registering the target function.</param>
|
|
||||||
/// <param name="arg0">The first argument.</param>
|
|
||||||
/// <returns>The result of the function invocation.</returns>
|
|
||||||
public TResult InvokeUnmarshalled<T0, TResult>(string identifier, T0 arg0)
|
|
||||||
=> InvokeUnmarshalled<T0, object, object, TResult>(identifier, arg0, null, null);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc />
|
||||||
/// Invokes the JavaScript function registered with the specified identifier.
|
TResult IJSUnmarshalledRuntime.InvokeUnmarshalled<T0, T1, TResult>(string identifier, T0 arg0, T1 arg1)
|
||||||
/// </summary>
|
=> ((IJSUnmarshalledRuntime)this).InvokeUnmarshalled<T0, T1, object, TResult>(identifier, arg0, arg1, null);
|
||||||
/// <typeparam name="T0">The type of the first argument.</typeparam>
|
|
||||||
/// <typeparam name="T1">The type of the second argument.</typeparam>
|
|
||||||
/// <typeparam name="TResult">The .NET type corresponding to the function's return value type.</typeparam>
|
|
||||||
/// <param name="identifier">The identifier used when registering the target function.</param>
|
|
||||||
/// <param name="arg0">The first argument.</param>
|
|
||||||
/// <param name="arg1">The second argument.</param>
|
|
||||||
/// <returns>The result of the function invocation.</returns>
|
|
||||||
public TResult InvokeUnmarshalled<T0, T1, TResult>(string identifier, T0 arg0, T1 arg1)
|
|
||||||
=> InvokeUnmarshalled<T0, T1, object, TResult>(identifier, arg0, arg1, null);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc />
|
||||||
/// Invokes the JavaScript function registered with the specified identifier.
|
TResult IJSUnmarshalledRuntime.InvokeUnmarshalled<T0, T1, T2, TResult>(string identifier, T0 arg0, T1 arg1, T2 arg2)
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T0">The type of the first argument.</typeparam>
|
|
||||||
/// <typeparam name="T1">The type of the second argument.</typeparam>
|
|
||||||
/// <typeparam name="T2">The type of the third argument.</typeparam>
|
|
||||||
/// <typeparam name="TResult">The .NET type corresponding to the function's return value type.</typeparam>
|
|
||||||
/// <param name="identifier">The identifier used when registering the target function.</param>
|
|
||||||
/// <param name="arg0">The first argument.</param>
|
|
||||||
/// <param name="arg1">The second argument.</param>
|
|
||||||
/// <param name="arg2">The third argument.</param>
|
|
||||||
/// <returns>The result of the function invocation.</returns>
|
|
||||||
public TResult InvokeUnmarshalled<T0, T1, T2, TResult>(string identifier, T0 arg0, T1 arg1, T2 arg2)
|
|
||||||
{
|
{
|
||||||
var result = InternalCalls.InvokeJSUnmarshalled<T0, T1, T2, TResult>(out var exception, identifier, arg0, arg1, arg2);
|
var result = InternalCalls.InvokeJSUnmarshalled<T0, T1, T2, TResult>(out var exception, identifier, arg0, arg1, arg2);
|
||||||
return exception != null
|
return exception != null
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Components.RenderTree;
|
using Microsoft.AspNetCore.Components.RenderTree;
|
||||||
using Microsoft.AspNetCore.Components.WebAssembly.Services;
|
using Microsoft.AspNetCore.Components.WebAssembly.Services;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.JSInterop;
|
||||||
using Microsoft.JSInterop.WebAssembly;
|
using Microsoft.JSInterop.WebAssembly;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Components.WebAssembly.Rendering
|
namespace Microsoft.AspNetCore.Components.WebAssembly.Rendering
|
||||||
|
|
@ -95,7 +96,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Rendering
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override Task UpdateDisplayAsync(in RenderBatch batch)
|
protected override Task UpdateDisplayAsync(in RenderBatch batch)
|
||||||
{
|
{
|
||||||
DefaultWebAssemblyJSRuntime.Instance.InvokeUnmarshalled<int, RenderBatch, object>(
|
((IJSUnmarshalledRuntime)DefaultWebAssemblyJSRuntime.Instance).InvokeUnmarshalled<int, RenderBatch, object>(
|
||||||
"Blazor._internal.renderBatch",
|
"Blazor._internal.renderBatch",
|
||||||
_webAssemblyRendererId,
|
_webAssemblyRendererId,
|
||||||
batch);
|
batch);
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Services
|
||||||
var newAssembliesToLoad = assembliesToLoad.Where(assembly => !_loadedAssemblyCache.Contains(assembly));
|
var newAssembliesToLoad = assembliesToLoad.Where(assembly => !_loadedAssemblyCache.Contains(assembly));
|
||||||
var loadedAssemblies = new List<Assembly>();
|
var loadedAssemblies = new List<Assembly>();
|
||||||
|
|
||||||
var count = (int)await ((WebAssemblyJSRuntime)_jsRuntime).InvokeUnmarshalled<string[], object, object, Task<object>>(
|
var count = (int)await ((IJSUnmarshalledRuntime)_jsRuntime).InvokeUnmarshalled<string[], object, object, Task<object>>(
|
||||||
GetDynamicAssemblies,
|
GetDynamicAssemblies,
|
||||||
newAssembliesToLoad.ToArray(),
|
newAssembliesToLoad.ToArray(),
|
||||||
null,
|
null,
|
||||||
|
|
@ -91,7 +91,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Services
|
||||||
return loadedAssemblies;
|
return loadedAssemblies;
|
||||||
}
|
}
|
||||||
|
|
||||||
var assemblies = ((WebAssemblyJSRuntime)_jsRuntime).InvokeUnmarshalled<object, object, object, object[]>(
|
var assemblies = ((IJSUnmarshalledRuntime)_jsRuntime).InvokeUnmarshalled<object, object, object, object[]>(
|
||||||
ReadDynamicAssemblies,
|
ReadDynamicAssemblies,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Services
|
||||||
_jsRuntime.InvokeVoid("console.error", formattedMessage);
|
_jsRuntime.InvokeVoid("console.error", formattedMessage);
|
||||||
break;
|
break;
|
||||||
case LogLevel.Critical:
|
case LogLevel.Critical:
|
||||||
_jsRuntime.InvokeUnmarshalled<string, object>("Blazor._internal.dotNetCriticalError", formattedMessage);
|
((IJSUnmarshalledRuntime)_jsRuntime).InvokeUnmarshalled<string, object>("Blazor._internal.dotNetCriticalError", formattedMessage);
|
||||||
break;
|
break;
|
||||||
default: // LogLevel.None or invalid enum values
|
default: // LogLevel.None or invalid enum values
|
||||||
Console.WriteLine(formattedMessage);
|
Console.WriteLine(formattedMessage);
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using Microsoft.JSInterop.WebAssembly;
|
using Microsoft.JSInterop;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Components.WebAssembly.Services
|
namespace Microsoft.AspNetCore.Components.WebAssembly.Services
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This class exists to enable unit testing for code that needs to call
|
/// This class exists to enable unit testing for code that needs to call
|
||||||
/// <see cref="WebAssemblyJSRuntime.InvokeUnmarshalled{T0, T1, T2, TResult}(string, T0, T1, T2)"/>.
|
/// <see cref="IJSUnmarshalledRuntime.InvokeUnmarshalled{T0, T1, T2, TResult}(string, T0, T1, T2)"/>.
|
||||||
///
|
///
|
||||||
/// We should only use this in non-perf-critical code paths (for example, during hosting startup,
|
/// We should only use this in non-perf-critical code paths (for example, during hosting startup,
|
||||||
/// where we only call this a fixed number of times, and not during rendering where it might be
|
/// where we only call this a fixed number of times, and not during rendering where it might be
|
||||||
|
|
@ -22,6 +22,6 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Services
|
||||||
public static WebAssemblyJSRuntimeInvoker Instance = new WebAssemblyJSRuntimeInvoker();
|
public static WebAssemblyJSRuntimeInvoker Instance = new WebAssemblyJSRuntimeInvoker();
|
||||||
|
|
||||||
public virtual TResult InvokeUnmarshalled<T0, T1, T2, TResult>(string identifier, T0 arg0, T1 arg1, T2 arg2)
|
public virtual TResult InvokeUnmarshalled<T0, T1, T2, TResult>(string identifier, T0 arg0, T1 arg1, T2 arg2)
|
||||||
=> DefaultWebAssemblyJSRuntime.Instance.InvokeUnmarshalled<T0, T1, T2, TResult>(identifier, arg0, arg1, arg2);
|
=> ((IJSUnmarshalledRuntime)DefaultWebAssemblyJSRuntime.Instance).InvokeUnmarshalled<T0, T1, T2, TResult>(identifier, arg0, arg1, arg2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,175 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using BasicTestApp;
|
||||||
|
using Microsoft.AspNetCore.Components.E2ETest;
|
||||||
|
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
|
||||||
|
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
|
||||||
|
using Microsoft.AspNetCore.E2ETesting;
|
||||||
|
using OpenQA.Selenium;
|
||||||
|
using OpenQA.Selenium.Support.Extensions;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.E2ETests.Tests
|
||||||
|
{
|
||||||
|
public class InputFileTest : ServerTestBase<ToggleExecutionModeServerFixture<Program>>, IDisposable
|
||||||
|
{
|
||||||
|
private string _tempDirectory;
|
||||||
|
|
||||||
|
public InputFileTest(
|
||||||
|
BrowserFixture browserFixture,
|
||||||
|
ToggleExecutionModeServerFixture<Program> serverFixture,
|
||||||
|
ITestOutputHelper output)
|
||||||
|
: base(browserFixture, serverFixture, output)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void InitializeAsyncCore()
|
||||||
|
{
|
||||||
|
_tempDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
|
||||||
|
Directory.CreateDirectory(_tempDirectory);
|
||||||
|
|
||||||
|
Navigate(ServerPathBase, noReload: _serverFixture.ExecutionMode == ExecutionMode.Client);
|
||||||
|
Browser.MountTestComponent<InputFileComponent>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanUploadSingleSmallFile()
|
||||||
|
{
|
||||||
|
// Create a temporary text file
|
||||||
|
var file = TempFile.Create(_tempDirectory, "txt", "This file was uploaded to the browser and read from .NET.");
|
||||||
|
|
||||||
|
// Upload the file
|
||||||
|
var inputFile = Browser.FindElement(By.Id("input-file"));
|
||||||
|
inputFile.SendKeys(file.Path);
|
||||||
|
|
||||||
|
var fileContainer = Browser.FindElement(By.Id($"file-{file.Name}"));
|
||||||
|
var fileSizeElement = fileContainer.FindElement(By.Id("file-size"));
|
||||||
|
var fileContentElement = fileContainer.FindElement(By.Id("file-content"));
|
||||||
|
|
||||||
|
// Validate that the file was uploaded correctly
|
||||||
|
Browser.Equal(file.Contents.Length.ToString(), () => fileSizeElement.Text);
|
||||||
|
Browser.Equal(file.Text, () => fileContentElement.Text);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanUploadSingleLargeFile()
|
||||||
|
{
|
||||||
|
// Create a large text file
|
||||||
|
var fileContentSizeInBytes = 1024 * 1024;
|
||||||
|
var contentBuilder = new StringBuilder();
|
||||||
|
|
||||||
|
for (int i = 0; i < fileContentSizeInBytes; i++)
|
||||||
|
{
|
||||||
|
contentBuilder.Append((i % 10).ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
var file = TempFile.Create(_tempDirectory, "txt", contentBuilder.ToString());
|
||||||
|
|
||||||
|
// Upload the file
|
||||||
|
var inputFile = Browser.FindElement(By.Id("input-file"));
|
||||||
|
inputFile.SendKeys(file.Path);
|
||||||
|
|
||||||
|
var fileContainer = Browser.FindElement(By.Id($"file-{file.Name}"));
|
||||||
|
var fileSizeElement = fileContainer.FindElement(By.Id("file-size"));
|
||||||
|
var fileContentElement = fileContainer.FindElement(By.Id("file-content"));
|
||||||
|
|
||||||
|
// Validate that the file was uploaded correctly
|
||||||
|
Browser.Equal(file.Contents.Length.ToString(), () => fileSizeElement.Text);
|
||||||
|
Browser.Equal(file.Text, () => fileContentElement.Text);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanUploadMultipleFiles()
|
||||||
|
{
|
||||||
|
// Create multiple small text files
|
||||||
|
var files = Enumerable.Range(1, 3)
|
||||||
|
.Select(i => TempFile.Create(_tempDirectory, "txt", $"Contents of file {i}."))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// Upload each file
|
||||||
|
var inputFile = Browser.FindElement(By.Id("input-file"));
|
||||||
|
inputFile.SendKeys(string.Join("\n", files.Select(f => f.Path)));
|
||||||
|
|
||||||
|
// VAlidate that each file was uploaded correctly
|
||||||
|
Assert.All(files, file =>
|
||||||
|
{
|
||||||
|
var fileContainer = Browser.FindElement(By.Id($"file-{file.Name}"));
|
||||||
|
var fileSizeElement = fileContainer.FindElement(By.Id("file-size"));
|
||||||
|
var fileContentElement = fileContainer.FindElement(By.Id("file-content"));
|
||||||
|
|
||||||
|
Browser.Equal(file.Contents.Length.ToString(), () => fileSizeElement.Text);
|
||||||
|
Browser.Equal(file.Text, () => fileContentElement.Text);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanUploadAndConvertImageFile()
|
||||||
|
{
|
||||||
|
var sourceImageId = "image-source";
|
||||||
|
|
||||||
|
// Get the source image base64
|
||||||
|
var base64 = Browser.ExecuteJavaScript<string>($@"
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const context = canvas.getContext('2d');
|
||||||
|
const image = document.getElementById('{sourceImageId}');
|
||||||
|
|
||||||
|
canvas.width = image.naturalWidth;
|
||||||
|
canvas.height = image.naturalHeight;
|
||||||
|
context.drawImage(image, 0, 0, image.naturalWidth, image.naturalHeight);
|
||||||
|
|
||||||
|
return canvas.toDataURL().split(',').pop();");
|
||||||
|
|
||||||
|
// Save the image file locally
|
||||||
|
var file = TempFile.Create(_tempDirectory, "png", Convert.FromBase64String(base64));
|
||||||
|
|
||||||
|
// Re-upload the image file (it will be converted to a JPEG and scaled to fix 640x480)
|
||||||
|
var inputFile = Browser.FindElement(By.Id("input-image"));
|
||||||
|
inputFile.SendKeys(file.Path);
|
||||||
|
|
||||||
|
// Validate that the image was converted without error and is the correct size
|
||||||
|
var uploadedImage = Browser.FindElement(By.Id("image-uploaded"));
|
||||||
|
|
||||||
|
Browser.Equal(480, () => uploadedImage.Size.Width);
|
||||||
|
Browser.Equal(480, () => uploadedImage.Size.Height);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Directory.Delete(_tempDirectory, recursive: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct TempFile
|
||||||
|
{
|
||||||
|
public string Name { get; }
|
||||||
|
public string Path { get; }
|
||||||
|
public byte[] Contents { get; }
|
||||||
|
|
||||||
|
public string Text => Encoding.ASCII.GetString(Contents);
|
||||||
|
|
||||||
|
private TempFile(string tempDirectory, string extension, byte[] contents)
|
||||||
|
{
|
||||||
|
Name = $"{Guid.NewGuid():N}.{extension}";
|
||||||
|
Path = $"{tempDirectory}\\{Name}";
|
||||||
|
Contents = contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TempFile Create(string tempDirectory, string extension, byte[] contents)
|
||||||
|
{
|
||||||
|
var file = new TempFile(tempDirectory, extension, contents);
|
||||||
|
|
||||||
|
File.WriteAllBytes(file.Path, contents);
|
||||||
|
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TempFile Create(string tempDirectory, string extension, string text)
|
||||||
|
=> Create(tempDirectory, extension, Encoding.ASCII.GetBytes(text));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -46,6 +46,7 @@
|
||||||
<option value="BasicTestApp.HttpClientTest.CookieCounterComponent">HttpClient cookies</option>
|
<option value="BasicTestApp.HttpClientTest.CookieCounterComponent">HttpClient cookies</option>
|
||||||
<option value="BasicTestApp.HttpClientTest.HttpRequestsComponent">HttpClient tester</option>
|
<option value="BasicTestApp.HttpClientTest.HttpRequestsComponent">HttpClient tester</option>
|
||||||
<option value="BasicTestApp.InputEventComponent">Input events</option>
|
<option value="BasicTestApp.InputEventComponent">Input events</option>
|
||||||
|
<option value="BasicTestApp.InputFileComponent">Input file</option>
|
||||||
<option value="BasicTestApp.InteropComponent">Interop component</option>
|
<option value="BasicTestApp.InteropComponent">Interop component</option>
|
||||||
<option value="BasicTestApp.InteropOnInitializationComponent">Interop on initialization</option>
|
<option value="BasicTestApp.InteropOnInitializationComponent">Interop on initialization</option>
|
||||||
<option value="BasicTestApp.JsonSerializationCases">JSON serialization</option>
|
<option value="BasicTestApp.JsonSerializationCases">JSON serialization</option>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
@using System.IO;
|
||||||
|
@using Microsoft.AspNetCore.Components.Web.Extensions
|
||||||
|
|
||||||
|
<h1>File preview</h1>
|
||||||
|
|
||||||
|
<InputFile OnChange="LoadFiles" id="input-file" multiple /><br />
|
||||||
|
|
||||||
|
@if (isLoading)
|
||||||
|
{
|
||||||
|
<p>Loading...</p><br />
|
||||||
|
}
|
||||||
|
|
||||||
|
@foreach (var (file, content) in loadedFiles)
|
||||||
|
{
|
||||||
|
<p id="file-@(file.Name)">
|
||||||
|
<strong>File name:</strong> @(file.Name)<br />
|
||||||
|
<strong>File size (bytes):</strong> <span id="file-size">@(file.Size)</span><br />
|
||||||
|
<strong>File content:</strong> <span id="file-content">@content</span><br />
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
<h1>Image upload</h1>
|
||||||
|
|
||||||
|
<InputFile OnChange="LoadImage" id="input-image" /><br />
|
||||||
|
|
||||||
|
@if (imageDataUri != null)
|
||||||
|
{
|
||||||
|
<p>
|
||||||
|
Uploaded image:<br />
|
||||||
|
<img id="image-uploaded" src="@imageDataUri" />
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Source image:<br />
|
||||||
|
<img id="image-source" src="images/blazor_logo_1000x.png" />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
Dictionary<IBrowserFile, string> loadedFiles = new Dictionary<IBrowserFile, string>();
|
||||||
|
|
||||||
|
bool isLoading;
|
||||||
|
|
||||||
|
string imageDataUri;
|
||||||
|
|
||||||
|
async Task LoadFiles(InputFileChangeEventArgs e)
|
||||||
|
{
|
||||||
|
isLoading = true;
|
||||||
|
loadedFiles.Clear();
|
||||||
|
|
||||||
|
foreach (var file in e.Files)
|
||||||
|
{
|
||||||
|
StateHasChanged();
|
||||||
|
|
||||||
|
using var reader = new StreamReader(file.OpenReadStream());
|
||||||
|
|
||||||
|
loadedFiles.Add(file, await reader.ReadToEndAsync());
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task LoadImage(InputFileChangeEventArgs e)
|
||||||
|
{
|
||||||
|
var file = e.Files.SingleOrDefault();
|
||||||
|
|
||||||
|
if (file != null)
|
||||||
|
{
|
||||||
|
var format = "image/jpeg";
|
||||||
|
var imageFile = await file.ToImageFileAsync(format, 640, 480);
|
||||||
|
|
||||||
|
using var fileStream = imageFile.OpenReadStream();
|
||||||
|
using var memoryStream = new MemoryStream();
|
||||||
|
await fileStream.CopyToAsync(memoryStream);
|
||||||
|
|
||||||
|
imageDataUri = $"data:{format};base64,{Convert.ToBase64String(memoryStream.ToArray())}";
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
|
|
@ -45,6 +45,8 @@
|
||||||
|
|
||||||
<script src="_content/Microsoft.AspNetCore.Components.Web.Extensions/headManager.js"></script>
|
<script src="_content/Microsoft.AspNetCore.Components.Web.Extensions/headManager.js"></script>
|
||||||
|
|
||||||
|
<script src="_content/Microsoft.AspNetCore.Components.Web.Extensions/inputFile.js"></script>
|
||||||
|
|
||||||
<!-- Used by ExternalContentPackage -->
|
<!-- Used by ExternalContentPackage -->
|
||||||
<script src="_content/TestContentPackage/prompt.js"></script>
|
<script src="_content/TestContentPackage/prompt.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,8 @@
|
||||||
|
|
||||||
<script src="_content/Microsoft.AspNetCore.Components.Web.Extensions/headManager.js"></script>
|
<script src="_content/Microsoft.AspNetCore.Components.Web.Extensions/headManager.js"></script>
|
||||||
|
|
||||||
|
<script src="_content/Microsoft.AspNetCore.Components.Web.Extensions/inputFile.js"></script>
|
||||||
|
|
||||||
<!-- Used by ExternalContentPackage -->
|
<!-- Used by ExternalContentPackage -->
|
||||||
<script src="_content/TestContentPackage/prompt.js"></script>
|
<script src="_content/TestContentPackage/prompt.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
namespace Microsoft.JSInterop
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents an instance of a JavaScript runtime to which calls may be dispatched without JSON marshalling.
|
||||||
|
/// Not all JavaScript runtimes support this capability. Currently it is only supported on WebAssembly and for
|
||||||
|
/// security reasons, will never be supported for .NET code that runs on the server.
|
||||||
|
/// This is an advanced mechanism that should only be used in performance-critical scenarios.
|
||||||
|
/// </summary>
|
||||||
|
public interface IJSUnmarshalledRuntime
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Invokes the JavaScript function registered with the specified identifier.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TResult">The .NET type corresponding to the function's return value type.</typeparam>
|
||||||
|
/// <param name="identifier">The identifier used when registering the target function.</param>
|
||||||
|
/// <returns>The result of the function invocation.</returns>
|
||||||
|
TResult InvokeUnmarshalled<TResult>(string identifier);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invokes the JavaScript function registered with the specified identifier.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T0">The type of the first argument.</typeparam>
|
||||||
|
/// <typeparam name="TResult">The .NET type corresponding to the function's return value type.</typeparam>
|
||||||
|
/// <param name="identifier">The identifier used when registering the target function.</param>
|
||||||
|
/// <param name="arg0">The first argument.</param>
|
||||||
|
/// <returns>The result of the function invocation.</returns>
|
||||||
|
TResult InvokeUnmarshalled<T0, TResult>(string identifier, T0 arg0);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invokes the JavaScript function registered with the specified identifier.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T0">The type of the first argument.</typeparam>
|
||||||
|
/// <typeparam name="T1">The type of the second argument.</typeparam>
|
||||||
|
/// <typeparam name="TResult">The .NET type corresponding to the function's return value type.</typeparam>
|
||||||
|
/// <param name="identifier">The identifier used when registering the target function.</param>
|
||||||
|
/// <param name="arg0">The first argument.</param>
|
||||||
|
/// <param name="arg1">The second argument.</param>
|
||||||
|
/// <returns>The result of the function invocation.</returns>
|
||||||
|
TResult InvokeUnmarshalled<T0, T1, TResult>(string identifier, T0 arg0, T1 arg1);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invokes the JavaScript function registered with the specified identifier.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T0">The type of the first argument.</typeparam>
|
||||||
|
/// <typeparam name="T1">The type of the second argument.</typeparam>
|
||||||
|
/// <typeparam name="T2">The type of the third argument.</typeparam>
|
||||||
|
/// <typeparam name="TResult">The .NET type corresponding to the function's return value type.</typeparam>
|
||||||
|
/// <param name="identifier">The identifier used when registering the target function.</param>
|
||||||
|
/// <param name="arg0">The first argument.</param>
|
||||||
|
/// <param name="arg1">The second argument.</param>
|
||||||
|
/// <param name="arg2">The third argument.</param>
|
||||||
|
/// <returns>The result of the function invocation.</returns>
|
||||||
|
TResult InvokeUnmarshalled<T0, T1, T2, TResult>(string identifier, T0 arg0, T1 arg1, T2 arg2);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue