Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
d477b92
Started on general InputFile design.
MackinnonBuck Aug 5, 2020
95cf907
Started work on data streaming.
MackinnonBuck Aug 5, 2020
58760ba
WebAssembly file upload is functional.
MackinnonBuck Aug 6, 2020
6ae119c
Blazor Server file upload is functional.
MackinnonBuck Aug 7, 2020
d744e58
Image file support + documentation.
MackinnonBuck Aug 8, 2020
8e81b4e
Addressed CR feedback.
MackinnonBuck Aug 10, 2020
b6fdcf7
CR feedback
MackinnonBuck Aug 11, 2020
f273793
Update PrerenderedStartup.cs
MackinnonBuck Aug 11, 2020
29a6b9b
Merge branch 'master' of https://github.com/dotnet/aspnetcore into t-…
MackinnonBuck Aug 12, 2020
bf81ddf
Added E2E tests
MackinnonBuck Aug 12, 2020
d079804
Added Memory<byte> ReadAsync overload in BrowserFileStream
MackinnonBuck Aug 13, 2020
9eacd9a
Update PrerenderedStartup.cs
MackinnonBuck Aug 13, 2020
6e944be
Update AspNetCore.sln
MackinnonBuck Aug 13, 2020
eb7bbe1
CR feedback + integrated System.IO.Pipelines
MackinnonBuck Aug 14, 2020
6aced8a
Missing files from last commit.
MackinnonBuck Aug 14, 2020
d5bcd9d
Delete EncodedFileChunk.cs
MackinnonBuck Aug 14, 2020
624e62c
Removed unnecessary chunk buffer.
MackinnonBuck Aug 14, 2020
e7ece33
Addressed some CR feedback
MackinnonBuck Aug 19, 2020
751a0ae
More CR feedback
MackinnonBuck Aug 19, 2020
cc41b25
CR feedback
MackinnonBuck Aug 20, 2020
313a70e
Update RemoteBrowserFileStreamOptions.cs
MackinnonBuck Aug 20, 2020
3f4dfbf
Merge branch 'release/5.0' into t-mabuc/input-file
MackinnonBuck Aug 20, 2020
35a3042
Update blazor.webassembly.js
MackinnonBuck Aug 20, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions src/Components/Web.Extensions/src/InputFile/BrowserFile.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
54 changes: 54 additions & 0 deletions src/Components/Web.Extensions/src/InputFile/IBrowserFile.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
96 changes: 96 additions & 0 deletions src/Components/Web.Extensions/src/InputFile/InputFile.cs
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
20 changes: 20 additions & 0 deletions src/Components/Web.Extensions/src/InputFile/InputFileInterop.cs
Original file line number Diff line number Diff line change
@@ -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";
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
29 changes: 29 additions & 0 deletions src/Components/Web.Extensions/src/InputFile/ReadRequest.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading