InputFile follow-up (#25248)

* Changes from API review

* Feedback from security review

* Update IBrowserFile.cs

* Updated documentation.

* Moved InputFile to M.A.Components.Forms

* More changes

* Move ProtectedBrowserStorage to it's own package
* Mark Components.Web.Extensions as non-shipping until we can move it over
* Allow InputFile.OpenReadStreamAsync to specify an expected file size.

* Fix E2E tests

* CR: Throw if user supplies too many files.

* Another build fix

* CR: Zero files is not a scenario

* Update E2E tests

* Update JS binaries after rebase

* Update test

Co-authored-by: Pranav K <prkrishn@hotmail.com>
Co-authored-by: Steve Sanderson <SteveSandersonMS@users.noreply.github.com>
This commit is contained in:
Mackinnon Buck 2020-08-31 09:07:36 -07:00 committed by GitHub
parent d3ebaf0782
commit c2f97933fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 724 additions and 390 deletions

View File

@ -1395,8 +1395,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Web.Extensions", "Web.Exten
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Components.Web.Extensions", "src\Components\Web.Extensions\src\Microsoft.AspNetCore.Components.Web.Extensions.csproj", "{8294A74F-7DAA-4B69-BC56-7634D93C9693}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Components.Web.Extensions.Tests", "src\Components\Web.Extensions\test\Microsoft.AspNetCore.Components.Web.Extensions.Tests.csproj", "{157605CB-5170-4C1A-980F-4BAE42DB60DE}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Sdk", "Sdk", "{FED4267E-E5E4-49C5-98DB-8B3F203596EE}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.NET.Sdk.BlazorWebAssembly", "src\Components\WebAssembly\Sdk\src\Microsoft.NET.Sdk.BlazorWebAssembly.csproj", "{6B2734BF-C61D-4889-ABBF-456A4075D59B}"
@ -1489,6 +1487,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Diagno
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.JSInterop.Tests", "src\JSInterop\Microsoft.JSInterop\test\Microsoft.JSInterop.Tests.csproj", "{DAAB6B35-CBD2-4573-B633-CDD42F583A0E}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ProtectedBrowserStorage", "ProtectedBrowserStorage", "{1B06FD32-3A1D-46A4-B2AF-99159FAD8127}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Components.ProtectedBrowserStorage", "src\Components\ProtectedBrowserStorage\src\Microsoft.AspNetCore.Components.ProtectedBrowserStorage.csproj", "{9059AC97-7547-4CC1-A076-680CBCCC1F33}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Components.ProtectedBrowserStorage.Tests", "src\Components\ProtectedBrowserStorage\test\Microsoft.AspNetCore.Components.ProtectedBrowserStorage.Tests.csproj", "{943FD3EC-D330-4277-B3F3-3DFABB57D3B5}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -6671,18 +6675,6 @@ Global
{8294A74F-7DAA-4B69-BC56-7634D93C9693}.Release|x64.Build.0 = Release|Any CPU
{8294A74F-7DAA-4B69-BC56-7634D93C9693}.Release|x86.ActiveCfg = Release|Any CPU
{8294A74F-7DAA-4B69-BC56-7634D93C9693}.Release|x86.Build.0 = Release|Any CPU
{157605CB-5170-4C1A-980F-4BAE42DB60DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{157605CB-5170-4C1A-980F-4BAE42DB60DE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{157605CB-5170-4C1A-980F-4BAE42DB60DE}.Debug|x64.ActiveCfg = Debug|Any CPU
{157605CB-5170-4C1A-980F-4BAE42DB60DE}.Debug|x64.Build.0 = Debug|Any CPU
{157605CB-5170-4C1A-980F-4BAE42DB60DE}.Debug|x86.ActiveCfg = Debug|Any CPU
{157605CB-5170-4C1A-980F-4BAE42DB60DE}.Debug|x86.Build.0 = Debug|Any CPU
{157605CB-5170-4C1A-980F-4BAE42DB60DE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{157605CB-5170-4C1A-980F-4BAE42DB60DE}.Release|Any CPU.Build.0 = Release|Any CPU
{157605CB-5170-4C1A-980F-4BAE42DB60DE}.Release|x64.ActiveCfg = Release|Any CPU
{157605CB-5170-4C1A-980F-4BAE42DB60DE}.Release|x64.Build.0 = Release|Any CPU
{157605CB-5170-4C1A-980F-4BAE42DB60DE}.Release|x86.ActiveCfg = Release|Any CPU
{157605CB-5170-4C1A-980F-4BAE42DB60DE}.Release|x86.Build.0 = Release|Any CPU
{6B2734BF-C61D-4889-ABBF-456A4075D59B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6B2734BF-C61D-4889-ABBF-456A4075D59B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6B2734BF-C61D-4889-ABBF-456A4075D59B}.Debug|x64.ActiveCfg = Debug|Any CPU
@ -7113,6 +7105,30 @@ Global
{DAAB6B35-CBD2-4573-B633-CDD42F583A0E}.Release|x64.Build.0 = Release|Any CPU
{DAAB6B35-CBD2-4573-B633-CDD42F583A0E}.Release|x86.ActiveCfg = Release|Any CPU
{DAAB6B35-CBD2-4573-B633-CDD42F583A0E}.Release|x86.Build.0 = Release|Any CPU
{9059AC97-7547-4CC1-A076-680CBCCC1F33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9059AC97-7547-4CC1-A076-680CBCCC1F33}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9059AC97-7547-4CC1-A076-680CBCCC1F33}.Debug|x64.ActiveCfg = Debug|Any CPU
{9059AC97-7547-4CC1-A076-680CBCCC1F33}.Debug|x64.Build.0 = Debug|Any CPU
{9059AC97-7547-4CC1-A076-680CBCCC1F33}.Debug|x86.ActiveCfg = Debug|Any CPU
{9059AC97-7547-4CC1-A076-680CBCCC1F33}.Debug|x86.Build.0 = Debug|Any CPU
{9059AC97-7547-4CC1-A076-680CBCCC1F33}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9059AC97-7547-4CC1-A076-680CBCCC1F33}.Release|Any CPU.Build.0 = Release|Any CPU
{9059AC97-7547-4CC1-A076-680CBCCC1F33}.Release|x64.ActiveCfg = Release|Any CPU
{9059AC97-7547-4CC1-A076-680CBCCC1F33}.Release|x64.Build.0 = Release|Any CPU
{9059AC97-7547-4CC1-A076-680CBCCC1F33}.Release|x86.ActiveCfg = Release|Any CPU
{9059AC97-7547-4CC1-A076-680CBCCC1F33}.Release|x86.Build.0 = Release|Any CPU
{943FD3EC-D330-4277-B3F3-3DFABB57D3B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{943FD3EC-D330-4277-B3F3-3DFABB57D3B5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{943FD3EC-D330-4277-B3F3-3DFABB57D3B5}.Debug|x64.ActiveCfg = Debug|Any CPU
{943FD3EC-D330-4277-B3F3-3DFABB57D3B5}.Debug|x64.Build.0 = Debug|Any CPU
{943FD3EC-D330-4277-B3F3-3DFABB57D3B5}.Debug|x86.ActiveCfg = Debug|Any CPU
{943FD3EC-D330-4277-B3F3-3DFABB57D3B5}.Debug|x86.Build.0 = Debug|Any CPU
{943FD3EC-D330-4277-B3F3-3DFABB57D3B5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{943FD3EC-D330-4277-B3F3-3DFABB57D3B5}.Release|Any CPU.Build.0 = Release|Any CPU
{943FD3EC-D330-4277-B3F3-3DFABB57D3B5}.Release|x64.ActiveCfg = Release|Any CPU
{943FD3EC-D330-4277-B3F3-3DFABB57D3B5}.Release|x64.Build.0 = Release|Any CPU
{943FD3EC-D330-4277-B3F3-3DFABB57D3B5}.Release|x86.ActiveCfg = Release|Any CPU
{943FD3EC-D330-4277-B3F3-3DFABB57D3B5}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -7814,7 +7830,6 @@ Global
{1542DC58-1836-4191-A9C5-51D1716D2543} = {05A169C7-4F20-4516-B10A-B13C5649D346}
{F71FE795-9923-461B-9809-BB1821A276D0} = {60D51C98-2CC0-40DF-B338-44154EFEE2FF}
{8294A74F-7DAA-4B69-BC56-7634D93C9693} = {F71FE795-9923-461B-9809-BB1821A276D0}
{157605CB-5170-4C1A-980F-4BAE42DB60DE} = {F71FE795-9923-461B-9809-BB1821A276D0}
{FED4267E-E5E4-49C5-98DB-8B3F203596EE} = {562D5067-8CD8-4F19-BCBB-873204932C61}
{6B2734BF-C61D-4889-ABBF-456A4075D59B} = {FED4267E-E5E4-49C5-98DB-8B3F203596EE}
{83371889-9A3E-4D16-AE77-EB4F83BC6374} = {FED4267E-E5E4-49C5-98DB-8B3F203596EE}
@ -7861,6 +7876,9 @@ Global
{55CACC1F-FE96-47C8-8073-91F4CAA55C75} = {2A91479A-4ABE-4BB7-9A5E-CA3B9CCFC69E}
{7509AA1E-3093-4BEE-984F-E11579E98A11} = {7CB09412-C9B0-47E8-A8C3-311AA4CFDE04}
{DAAB6B35-CBD2-4573-B633-CDD42F583A0E} = {16898702-3E33-41C1-B8D8-4CE3F1D46BD9}
{1B06FD32-3A1D-46A4-B2AF-99159FAD8127} = {60D51C98-2CC0-40DF-B338-44154EFEE2FF}
{9059AC97-7547-4CC1-A076-680CBCCC1F33} = {1B06FD32-3A1D-46A4-B2AF-99159FAD8127}
{943FD3EC-D330-4277-B3F3-3DFABB57D3B5} = {1B06FD32-3A1D-46A4-B2AF-99159FAD8127}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F}

View File

@ -140,6 +140,7 @@
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Components" ProjectPath="$(RepoRoot)src\Components\Components\src\Microsoft.AspNetCore.Components.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Components.Forms" ProjectPath="$(RepoRoot)src\Components\Forms\src\Microsoft.AspNetCore.Components.Forms.csproj" />
<ProjectReferenceProvider Include="Ignitor" ProjectPath="$(RepoRoot)src\Components\Ignitor\src\Ignitor.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Components.ProtectedBrowserStorage" ProjectPath="$(RepoRoot)src\Components\ProtectedBrowserStorage\src\Microsoft.AspNetCore.Components.ProtectedBrowserStorage.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Components.Server" ProjectPath="$(RepoRoot)src\Components\Server\src\Microsoft.AspNetCore.Components.Server.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Components.Web.Extensions" ProjectPath="$(RepoRoot)src\Components\Web.Extensions\src\Microsoft.AspNetCore.Components.Web.Extensions.csproj" />
<ProjectReferenceProvider Include="Microsoft.Authentication.WebAssembly.Msal" ProjectPath="$(RepoRoot)src\Components\WebAssembly\Authentication.Msal\src\Microsoft.Authentication.WebAssembly.Msal.csproj" />

View File

@ -101,7 +101,6 @@
"src\\Components\\benchmarkapps\\Wasm.Performance\\Driver\\Wasm.Performance.Driver.csproj",
"src\\Components\\benchmarkapps\\Wasm.Performance\\TestApp\\Wasm.Performance.TestApp.csproj",
"src\\Components\\Web.Extensions\\src\\Microsoft.AspNetCore.Components.Web.Extensions.csproj",
"src\\Components\\Web.Extensions\\test\\Microsoft.AspNetCore.Components.Web.Extensions.Tests.csproj",
"src\\Components\\WebAssembly\\Server\\test\\Microsoft.AspNetCore.Components.WebAssembly.Server.Tests.csproj",
"src\\Components\\WebAssembly\\Authentication.Msal\\src\\Microsoft.Authentication.WebAssembly.Msal.csproj",
"src\\Components\\WebAssembly\\JSInterop\\src\\Microsoft.JSInterop.WebAssembly.csproj",
@ -112,7 +111,9 @@
"src\\Components\\WebAssembly\\Sdk\\src\\Microsoft.NET.Sdk.BlazorWebAssembly.csproj",
"src\\Components\\WebAssembly\\Sdk\\test\\Microsoft.NET.Sdk.BlazorWebAssembly.Tests.csproj",
"src\\Components\\WebAssembly\\Sdk\\integrationtests\\Microsoft.NET.Sdk.BlazorWebAssembly.IntegrationTests.csproj",
"src\\JSInterop\\Microsoft.JSInterop\\src\\Microsoft.JSInterop.csproj"
"src\\JSInterop\\Microsoft.JSInterop\\src\\Microsoft.JSInterop.csproj",
"src\\Components\\ProtectedBrowserStorage\\src\\Microsoft.AspNetCore.Components.ProtectedBrowserStorage.csproj",
"src\\Components\\ProtectedBrowserStorage\\test\\Microsoft.AspNetCore.Components.ProtectedBrowserStorage.Tests.csproj"
]
}
}

View File

@ -7,6 +7,6 @@ using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Components.Server.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Components.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Components.Web.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Components.Web.Extensions.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Components.ProtectedBrowserStorage.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]

View File

@ -17,7 +17,6 @@
"src\\Components\\Server\\src\\Microsoft.AspNetCore.Components.Server.csproj",
"src\\Components\\Server\\test\\Microsoft.AspNetCore.Components.Server.Tests.csproj",
"src\\Components\\Web.Extensions\\src\\Microsoft.AspNetCore.Components.Web.Extensions.csproj",
"src\\Components\\Web.Extensions\\test\\Microsoft.AspNetCore.Components.Web.Extensions.Tests.csproj",
"src\\Components\\WebAssembly\\Authentication.Msal\\src\\Microsoft.Authentication.WebAssembly.Msal.csproj",
"src\\Components\\WebAssembly\\DevServer\\src\\Microsoft.AspNetCore.Components.WebAssembly.DevServer.csproj",
"src\\Components\\WebAssembly\\JSInterop\\src\\Microsoft.JSInterop.WebAssembly.csproj",
@ -46,7 +45,9 @@
"src\\Components\\test\\testassets\\BasicTestApp\\BasicTestApp.csproj",
"src\\Components\\test\\testassets\\ComponentsApp.Server\\ComponentsApp.Server.csproj",
"src\\Components\\test\\testassets\\TestContentPackage\\TestContentPackage.csproj",
"src\\Components\\test\\testassets\\TestServer\\Components.TestServer.csproj"
"src\\Components\\test\\testassets\\TestServer\\Components.TestServer.csproj",
"src\\Components\\ProtectedBrowserStorage\\src\\Microsoft.AspNetCore.Components.ProtectedBrowserStorage.csproj",
"src\\Components\\ProtectedBrowserStorage\\test\\Microsoft.AspNetCore.Components.ProtectedBrowserStorage.Tests.csproj"
]
}
}

View File

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<Description>Provides functionality for storing protected data using the browser's localStorage and sessionStorage APIs.</Description>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Components" />
<Reference Include="Microsoft.AspNetCore.DataProtection" />
<Reference Include="Microsoft.JSInterop" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\..\Shared\src\JsonSerializerOptionsProvider.cs" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,4 @@
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Components.ProtectedBrowserStorage.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]

View File

@ -10,7 +10,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.JSInterop;
namespace Microsoft.AspNetCore.Components.Web.Extensions
namespace Microsoft.AspNetCore.Components.ProtectedBrowserStorage
{
/// <summary>

View File

@ -1,6 +1,9 @@
// 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.Diagnostics.CodeAnalysis;
namespace Microsoft.AspNetCore.Components.Web.Extensions
namespace Microsoft.AspNetCore.Components.ProtectedBrowserStorage
{
/// <summary>
/// Contains the result of a protected browser storage operation.

View File

@ -1,7 +1,7 @@
// 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 Microsoft.AspNetCore.Components.Web.Extensions;
using Microsoft.AspNetCore.Components.ProtectedBrowserStorage;
namespace Microsoft.Extensions.DependencyInjection
{

View File

@ -5,7 +5,7 @@ using System.Runtime.Versioning;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.JSInterop;
namespace Microsoft.AspNetCore.Components.Web.Extensions
namespace Microsoft.AspNetCore.Components.ProtectedBrowserStorage
{
/// <summary>
/// Provides mechanisms for storing and retrieving data in the browser's

View File

@ -5,7 +5,7 @@ using System.Runtime.Versioning;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.JSInterop;
namespace Microsoft.AspNetCore.Components.Web.Extensions
namespace Microsoft.AspNetCore.Components.ProtectedBrowserStorage
{
/// <summary>
/// Provides mechanisms for storing and retrieving data in the browser's

View File

@ -2,12 +2,11 @@
<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<RootNamespace>Microsoft.AspNetCore.Components</RootNamespace>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Components" />
<Reference Include="Microsoft.AspNetCore.Components.Web.Extensions" />
<Reference Include="Microsoft.AspNetCore.Components.ProtectedBrowserStorage" />
<Reference Include="Microsoft.AspNetCore.WebUtilities" />
<Reference Include="Microsoft.Extensions.DependencyInjection" />
</ItemGroup>

View File

@ -14,7 +14,7 @@ using Microsoft.AspNetCore.WebUtilities;
using Microsoft.JSInterop;
using Xunit;
namespace Microsoft.AspNetCore.Components.Web.Extensions
namespace Microsoft.AspNetCore.Components.ProtectedBrowserStorage
{
public class ProtectedBrowserStorageTest
{

View File

@ -1,33 +0,0 @@
// 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);
}
}

View File

@ -1,54 +0,0 @@
// 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);
}
}

View File

@ -1,28 +0,0 @@
// 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;
}
}
}

View File

@ -6,11 +6,11 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<RootNamespace>Microsoft.AspNetCore.Components</RootNamespace>
<Nullable>enable</Nullable>
<IsShipping>false</IsShipping>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Components" />
<Reference Include="Microsoft.AspNetCore.DataProtection" />
<Reference Include="Microsoft.JSInterop" />
</ItemGroup>

View File

@ -1,142 +0,0 @@
(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,
};
})();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -2,6 +2,7 @@ import { navigateTo, internalFunctions as navigationManagerInternalFunctions } f
import { attachRootComponentToElement } from './Rendering/Renderer';
import { domFunctions } from './DomWrapper';
import { Virtualize } from './Virtualize';
import { InputFile } from './InputFile';
// Make the following APIs available in global scope for invocation from JS
window['Blazor'] = {
@ -11,5 +12,6 @@ window['Blazor'] = {
navigationManager: navigationManagerInternalFunctions,
domWrapper: domFunctions,
Virtualize,
InputFile,
},
};

View File

@ -0,0 +1,159 @@
import { monoPlatform } from './Platform/Mono/MonoPlatform';
import { System_Array } from './Platform/Platform';
export const InputFile = {
init,
toImageFile,
ensureArrayBufferReadyForSharedMemoryInterop,
readFileData,
readFileDataSharedMemory,
};
interface BrowserFile {
id: number;
lastModified: string;
name: string;
size: number;
type: string;
readPromise: Promise<ArrayBuffer> | undefined;
arrayBuffer: ArrayBuffer | undefined;
}
interface InputElement extends HTMLInputElement {
_blazorInputFileNextFileId: number;
_blazorFilesById: { [id: number]: BrowserFile };
}
function init(callbackWrapper: any, elem: InputElement): void {
elem._blazorInputFileNextFileId = 0;
elem.addEventListener('click', function(): void {
// Permits replacing an existing file with a new one of the same file name.
elem.value = '';
});
elem.addEventListener('change', function(): void {
// Reduce to purely serializable data, plus an index by ID.
elem._blazorFilesById = {};
const fileList = Array.prototype.map.call(elem.files, function(file): BrowserFile {
const result = {
id: ++elem._blazorInputFileNextFileId,
lastModified: new Date(file.lastModified).toISOString(),
name: file.name,
size: file.size,
type: file.type,
readPromise: undefined,
arrayBuffer: undefined,
};
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);
});
}
async function toImageFile(elem: InputElement, fileId: number, format: string, maxWidth: number, maxHeight: number): Promise<BrowserFile> {
const originalFile = getFileById(elem, fileId);
const loadedImage = await new Promise(function(resolve: (loadedImage: HTMLImageElement) => void): void {
const originalFileImage = new Image();
originalFileImage.onload = function(): void {
resolve(originalFileImage);
};
originalFileImage.src = URL.createObjectURL(originalFile['blob']);
});
const resizedImageBlob = await new Promise(function(resolve: BlobCallback): void {
const desiredWidthRatio = Math.min(1, maxWidth / loadedImage.width);
const desiredHeightRatio = Math.min(1, maxHeight / loadedImage.height);
const chosenSizeRatio = Math.min(desiredWidthRatio, desiredHeightRatio);
const 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);
});
const result: BrowserFile = {
id: ++elem._blazorInputFileNextFileId,
lastModified: originalFile.lastModified,
name: originalFile.name,
size: resizedImageBlob?.size || 0,
type: format,
readPromise: undefined,
arrayBuffer: undefined,
};
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;
}
async function ensureArrayBufferReadyForSharedMemoryInterop(elem: InputElement, fileId: number): Promise<void> {
const arrayBuffer = await getArrayBufferFromFileAsync(elem, fileId);
getFileById(elem, fileId).arrayBuffer = arrayBuffer;
}
async function readFileData(elem: InputElement, fileId: number, startOffset: number, count: number): Promise<string> {
const arrayBuffer = await getArrayBufferFromFileAsync(elem, fileId);
return btoa(String.fromCharCode.apply(null, new Uint8Array(arrayBuffer, startOffset, count) as unknown as number[]));
}
function readFileDataSharedMemory(readRequest: any): number {
const inputFileElementReferenceId = monoPlatform.readStringField(readRequest, 0);
const inputFileElement = document.querySelector(`[_bl_${inputFileElementReferenceId}]`);
const fileId = monoPlatform.readInt32Field(readRequest, 4);
const sourceOffset = monoPlatform.readUint64Field(readRequest, 8);
const destination = monoPlatform.readInt32Field(readRequest, 16) as unknown as System_Array<number>;
const destinationOffset = monoPlatform.readInt32Field(readRequest, 20);
const maxBytes = monoPlatform.readInt32Field(readRequest, 24);
const sourceArrayBuffer = getFileById(inputFileElement as InputElement, fileId).arrayBuffer as ArrayBuffer;
const bytesToRead = Math.min(maxBytes, sourceArrayBuffer.byteLength - sourceOffset);
const sourceUint8Array = new Uint8Array(sourceArrayBuffer, sourceOffset, bytesToRead);
const destinationUint8Array = monoPlatform.toUint8Array(destination);
destinationUint8Array.set(sourceUint8Array, destinationOffset);
return bytesToRead;
}
function getFileById(elem: InputElement, fileId: number): BrowserFile {
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: InputElement, fileId: number): Promise<ArrayBuffer> {
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: (buffer: ArrayBuffer) => void, reject): void {
const reader = new FileReader();
reader.onload = function(): void {
resolve(reader.result as ArrayBuffer);
};
reader.onerror = function(err): void {
reject(err);
};
reader.readAsArrayBuffer(file['blob']);
});
}
return file.readPromise;
}

View File

@ -10,10 +10,10 @@ using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.Extensions.Options;
using Microsoft.JSInterop;
namespace Microsoft.AspNetCore.Components.Web.Extensions
namespace Microsoft.AspNetCore.Components.Forms
{
/// <summary>
/// A component that wraps the HTML file input element and exposes a <see cref="Stream"/> for each file's contents.
/// A component that wraps the HTML file input element and supplies a <see cref="Stream"/> for each file's contents.
/// </summary>
public class InputFile : ComponentBase, IInputFileJsCallbacks, IDisposable
{
@ -69,7 +69,7 @@ namespace Microsoft.AspNetCore.Components.Web.Extensions
(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)
internal async ValueTask<IBrowserFile> ConvertToImageFileAsync(BrowserFile file, string format, int maxWidth, int maxHeight)
{
var imageFile = await JSRuntime.InvokeAsync<BrowserFile>(InputFileInterop.ToImageFile, _inputFileElement, file.Id, format, maxWidth, maxHeight);

View File

@ -0,0 +1,50 @@
// 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;
namespace Microsoft.AspNetCore.Components.Forms
{
internal sealed class BrowserFile : IBrowserFile
{
private long _size;
internal InputFile Owner { get; set; } = default!;
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public DateTimeOffset LastModified { get; set; }
public long Size
{
get => _size;
set
{
if (value < 0)
{
throw new ArgumentOutOfRangeException(nameof(Size), $"Size must be a non-negative value. Value provided: {value}.");
}
_size = value;
}
}
public string ContentType { get; set; } = string.Empty;
public string? RelativePath { get; set; }
public Stream OpenReadStream(long maxAllowedSize = 512000, CancellationToken cancellationToken = default)
{
if (Size > maxAllowedSize)
{
throw new IOException($"Supplied file with size {Size} bytes exceeds the maximum of {maxAllowedSize} bytes.");
}
return Owner.OpenReadStream(this, cancellationToken);
}
}
}

View File

@ -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.Threading.Tasks;
namespace Microsoft.AspNetCore.Components.Forms
{
/// <summary>
/// Contains helper methods for <see cref="IBrowserFile"/>.
/// </summary>
public static class BrowserFileExtensions
{
/// <summary>
/// Attempts to convert the current image file to a new one of the specified file type and maximum file dimensions.
/// <para>
/// Caution: there is no guarantee that the file will be converted, or will even be a valid image file at all, either
/// before or after conversion. The conversion is requested within the browser before it is transferred to .NET
/// code, so the resulting data should be treated as untrusted.
/// </para>
/// </summary>
/// <remarks>
/// The image will be scaled to fit the specified dimensions while preserving the original aspect ratio.
/// </remarks>
/// <param name="browserFile">The <see cref="IBrowserFile"/> to convert to a new image file.</param>
/// <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="ValueTask"/> representing the completion of the operation.</returns>
public static ValueTask<IBrowserFile> RequestImageFileAsync(this IBrowserFile browserFile, string format, int maxWith, int maxHeight)
{
if (browserFile is BrowserFile browserFileInternal)
{
return browserFileInternal.Owner.ConvertToImageFileAsync(browserFileInternal, format, maxWith, maxHeight);
}
throw new InvalidOperationException($"Cannot perform this operation on custom {typeof(IBrowserFile)} implementations.");
}
}
}

View File

@ -6,7 +6,7 @@ using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Components.Web.Extensions
namespace Microsoft.AspNetCore.Components.Forms
{
internal abstract class BrowserFileStream : Stream
{

View File

@ -0,0 +1,62 @@
// 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;
namespace Microsoft.AspNetCore.Components.Forms
{
/// <summary>
/// Represents the data of a file selected from an <see cref="InputFile"/> component.
/// <para>
/// Note: Metadata is provided by the client and is untrusted.
/// </para>
/// </summary>
public interface IBrowserFile
{
/// <summary>
/// Gets the name of the file as specified by the browser.
/// </summary>
string Name { get; }
/// <summary>
/// Gets the last modified date as specified by the browser.
/// </summary>
DateTimeOffset LastModified { get; }
/// <summary>
/// Gets the size of the file in bytes as specified by the browser.
/// </summary>
long Size { get; }
/// <summary>
/// Gets the MIME type of the file as specified by the browser.
/// </summary>
string ContentType { get; }
/// <summary>
/// Opens the stream for reading the uploaded file.
/// </summary>
/// <param name="maxAllowedSize">
/// The maximum number of bytes that can be supplied by the Stream. Defaults to 500 KB.
/// <para>
/// Calling <see cref="OpenReadStream(long, CancellationToken)"/>
/// will throw if the file's size, as specified by <see cref="Size"/> is larger than
/// <paramref name="maxAllowedSize"/>. By default, if the user supplies a file larger than 500 KB, this method will throw an exception.
/// </para>
/// <para>
/// It is valuable to choose a size limit that corresponds to your use case. If you allow excessively large files, this
/// may result in excessive consumption of memory or disk/database space, depending on what your code does
/// with the supplied <see cref="Stream"/>.
/// </para>
/// <para>
/// For Blazor Server in particular, beware of reading the entire stream into a memory buffer unless you have
/// passed a suitably low size limit, since you will be consuming that memory on the server.
/// </para>
/// </param>
/// <param name="cancellationToken">A cancellation token to signal the cancellation of streaming file data.</param>
/// <exception cref="IOException">Thrown if the file's length exceeds the <paramref name="maxAllowedSize"/> value.</exception>
Stream OpenReadStream(long maxAllowedSize = 500 * 1024, CancellationToken cancellationToken = default);
}
}

View File

@ -3,7 +3,7 @@
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Components.Web.Extensions
namespace Microsoft.AspNetCore.Components.Forms
{
internal interface IInputFileJsCallbacks
{

View File

@ -0,0 +1,57 @@
// 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.Forms
{
/// <summary>
/// Supplies information about an <see cref="InputFile.OnChange"/> event being raised.
/// </summary>
public sealed class InputFileChangeEventArgs : EventArgs
{
private readonly IReadOnlyList<IBrowserFile> _files;
/// <summary>
/// Constructs a new <see cref="InputFileChangeEventArgs"/> instance.
/// </summary>
/// <param name="files">The list of <see cref="IBrowserFile"/>.</param>
public InputFileChangeEventArgs(IReadOnlyList<IBrowserFile> files)
{
_files = files ?? throw new ArgumentNullException(nameof(files));
}
/// <summary>
/// Gets the number of supplied files.
/// </summary>
public int FileCount => _files.Count;
/// <summary>
/// Gets the supplied file. Note that if the input accepts multiple files, then instead of
/// reading this property, you should call <see cref="GetMultipleFiles(int)"/>.
/// </summary>
public IBrowserFile File => _files.Count switch
{
0 => throw new InvalidOperationException("No file was supplied."),
1 => _files[0],
_ => throw new InvalidOperationException($"More than one file was supplied. Call {nameof(GetMultipleFiles)} to receive multiple files."),
};
/// <summary>
/// Gets the file entries list. This method should be used for inputs that accept multiple
/// files. If the input accepts only a single file, then use the <see cref="File"/> property
/// instead.
/// </summary>
/// <param name="maximumFileCount">The maximum number of files to accept. If the number of files exceeds this value, this method will throw an exception.</param>
public IReadOnlyList<IBrowserFile> GetMultipleFiles(int maximumFileCount = 10)
{
if (_files.Count > maximumFileCount)
{
throw new InvalidOperationException($"The maximum number of files accepted is {maximumFileCount}, but {_files.Count} were supplied.");
}
return _files;
}
}
}

View File

@ -1,11 +1,11 @@
// 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
namespace Microsoft.AspNetCore.Components.Forms
{
internal static class InputFileInterop
{
private const string JsFunctionsPrefix = "_blazorInputFile.";
private const string JsFunctionsPrefix = "Blazor._internal.InputFile.";
public const string Init = JsFunctionsPrefix + "init";

View File

@ -5,7 +5,7 @@ using System;
using System.Threading.Tasks;
using Microsoft.JSInterop;
namespace Microsoft.AspNetCore.Components.Web.Extensions
namespace Microsoft.AspNetCore.Components.Forms
{
internal class InputFileJsCallbacksRelay : IDisposable
{

View File

@ -3,7 +3,7 @@
using System.Runtime.InteropServices;
namespace Microsoft.AspNetCore.Components.Web.Extensions
namespace Microsoft.AspNetCore.Components.Forms
{
[StructLayout(LayoutKind.Explicit)]
internal struct ReadRequest

View File

@ -8,7 +8,7 @@ using System.Threading;
using System.Threading.Tasks;
using Microsoft.JSInterop;
namespace Microsoft.AspNetCore.Components.Web.Extensions
namespace Microsoft.AspNetCore.Components.Forms
{
internal class RemoteBrowserFileStream : BrowserFileStream
{
@ -32,7 +32,7 @@ namespace Microsoft.AspNetCore.Components.Web.Extensions
{
_jsRuntime = jsRuntime;
_inputFileElement = inputFileElement;
_maxSegmentSize = options.SegmentSize;
_maxSegmentSize = options.MaxSegmentSize;
_segmentFetchTimeout = options.SegmentFetchTimeout;
var pipe = new Pipe(new PipeOptions(pauseWriterThreshold: options.MaxBufferSize, resumeWriterThreshold: options.MaxBufferSize));

View File

@ -4,7 +4,7 @@
using System;
using System.Runtime.Versioning;
namespace Microsoft.AspNetCore.Components.Web.Extensions
namespace Microsoft.AspNetCore.Components.Forms
{
/// <summary>
/// Repesents configurable options for <see cref="RemoteBrowserFileStream"/>.
@ -19,7 +19,7 @@ namespace Microsoft.AspNetCore.Components.Web.Extensions
/// This only has an effect when using Blazor Server.
/// </para>
/// </summary>
public int SegmentSize { get; set; } = 20 * 1024; // SignalR limit is 32K.
public int MaxSegmentSize { 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.
@ -35,6 +35,6 @@ namespace Microsoft.AspNetCore.Components.Web.Extensions
/// This only has an effect when using Blazor Server.
/// </para>
/// </summary>
public TimeSpan SegmentFetchTimeout { get; set; } = TimeSpan.FromSeconds(3);
public TimeSpan SegmentFetchTimeout { get; set; } = TimeSpan.FromMinutes(1);
}
}

View File

@ -7,7 +7,7 @@ using System.Threading;
using System.Threading.Tasks;
using Microsoft.JSInterop;
namespace Microsoft.AspNetCore.Components.Web.Extensions
namespace Microsoft.AspNetCore.Components.Forms
{
internal class SharedBrowserFileStream : BrowserFileStream
{

View File

@ -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 Xunit;
namespace Microsoft.AspNetCore.Components.Forms
{
public class BrowserFileTest
{
[Fact]
public void SetSize_ThrowsIfSizeIsNegative()
{
// Arrange
var file = new BrowserFile();
// Act & Assert
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => file.Size = -7);
}
[Fact]
public void OpenReadStream_ThrowsIfFileSizeIsLargerThanAllowedSize()
{
// Arrange
var file = new BrowserFile { Size = 100 };
// Act & Assert
var ex = Assert.Throws<IOException>(() => file.OpenReadStream(80));
Assert.Equal("Supplied file with size 100 bytes exceeds the maximum of 80 bytes.", ex.Message);
}
}
}

View File

@ -0,0 +1,69 @@
// 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 Xunit;
namespace Microsoft.AspNetCore.Components.Forms
{
public class InputFileChangeEventArgsTest
{
[Fact]
public void SuppliesNumberOfFiles()
{
var emptySet = new InputFileChangeEventArgs(Array.Empty<IBrowserFile>());
Assert.Equal(0, emptySet.FileCount);
var twoItemSet = new InputFileChangeEventArgs(new[] { new BrowserFile(), new BrowserFile() });
Assert.Equal(2, twoItemSet.FileCount);
}
[Fact]
public void File_CanSupplySingle()
{
var file = new BrowserFile();
var instance = new InputFileChangeEventArgs(new[] { file });
Assert.Same(file, instance.File);
}
[Fact]
public void File_ThrowsIfZeroFiles()
{
var instance = new InputFileChangeEventArgs(Array.Empty<IBrowserFile>());
var ex = Assert.Throws<InvalidOperationException>(() => instance.File);
Assert.StartsWith("No file was supplied", ex.Message);
}
[Fact]
public void File_ThrowsIfMultipleFiles()
{
var instance = new InputFileChangeEventArgs(new[] { new BrowserFile(), new BrowserFile() });
var ex = Assert.Throws<InvalidOperationException>(() => instance.File);
Assert.StartsWith("More than one file was supplied", ex.Message);
}
[Fact]
public void GetMultipleFiles_CanSupplyEmpty()
{
var instance = new InputFileChangeEventArgs(Array.Empty<IBrowserFile>());
Assert.Empty(instance.GetMultipleFiles());
}
[Fact]
public void GetMultipleFiles_CanSupplyFiles()
{
var files = new[] { new BrowserFile(), new BrowserFile() };
var instance = new InputFileChangeEventArgs(files);
Assert.Same(files, instance.GetMultipleFiles());
}
[Fact]
public void GetMultipleFiles_ThrowsIfTooManyFiles()
{
var files = new[] { new BrowserFile(), new BrowserFile() };
var instance = new InputFileChangeEventArgs(files);
var ex = Assert.Throws<InvalidOperationException>(() => instance.GetMultipleFiles(1));
Assert.Equal($"The maximum number of files accepted is 1, but 2 were supplied.", ex.Message);
}
}
}

View File

@ -75,4 +75,12 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
{
}
}
public class ServerInputFileTest : InputFileTest
{
public ServerInputFileTest(BrowserFixture browserFixture, ToggleExecutionModeServerFixture<Program> serverFixture, ITestOutputHelper output)
: base(browserFixture, serverFixture.WithServerExecution(), output)
{
}
}
}

View File

@ -6,6 +6,7 @@ using System.IO;
using System.Linq;
using System.Text;
using BasicTestApp;
using BasicTestApp.FormsTest;
using Microsoft.AspNetCore.Components.E2ETest;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
@ -15,7 +16,7 @@ using OpenQA.Selenium.Support.Extensions;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Components.E2ETests.Tests
namespace Microsoft.AspNetCore.Components.E2ETest.Tests
{
public class InputFileTest : ServerTestBase<ToggleExecutionModeServerFixture<Program>>, IDisposable
{
@ -139,6 +140,45 @@ namespace Microsoft.AspNetCore.Components.E2ETests.Tests
Browser.Equal(480, () => uploadedImage.Size.Height);
}
[Fact]
public void ThrowsWhenTooManyFilesAreSelected()
{
var maxAllowedFilesElement = Browser.FindElement(By.Id("max-allowed-files"));
maxAllowedFilesElement.Clear();
maxAllowedFilesElement.SendKeys("1\n");
// Save two files locally
var file1 = TempFile.Create(_tempDirectory, "txt", "This is file 1.");
var file2 = TempFile.Create(_tempDirectory, "txt", "This is file 2.");
// Select both files
var inputFile = Browser.FindElement(By.Id("input-file"));
inputFile.SendKeys($"{file1.Path}\n{file2.Path}");
// Validate that the proper exception is thrown
var exceptionMessage = Browser.FindElement(By.Id("exception-message"));
Browser.Equal("The maximum number of files accepted is 1, but 2 were supplied.", () => exceptionMessage.Text);
}
[Fact]
public void ThrowsWhenOversizedFileIsSelected()
{
var maxFileSizeElement = Browser.FindElement(By.Id("max-file-size"));
maxFileSizeElement.Clear();
maxFileSizeElement.SendKeys("10\n");
// Save a file that exceeds the specified file size limit
var file = TempFile.Create(_tempDirectory, "txt", "This file is over 10 bytes long.");
// Select the file
var inputFile = Browser.FindElement(By.Id("input-file"));
inputFile.SendKeys(file.Path);
// Validate that the proper exception is thrown
var exceptionMessage = Browser.FindElement(By.Id("exception-message"));
Browser.Equal("Supplied file with size 32 bytes exceeds the maximum of 10 bytes.", () => exceptionMessage.Text);
}
public void Dispose()
{
Directory.Delete(_tempDirectory, recursive: true);

View File

@ -15,6 +15,7 @@
<Reference Include="System.Net.Http.Json" />
<Reference Include="Microsoft.AspNetCore.Components.WebAssembly" />
<Reference Include="Microsoft.AspNetCore.Components.Authorization" />
<Reference Include="Microsoft.AspNetCore.Components.ProtectedBrowserStorage" />
<Reference Include="Microsoft.AspNetCore.Components.Web.Extensions" />
<Reference Include="Microsoft.Extensions.Logging.Configuration" />
<Reference Include="Newtonsoft.Json" />

View File

@ -0,0 +1,102 @@
@using System.IO;
@using Microsoft.AspNetCore.Components.Forms
<h1>File preview</h1>
Max file size:
<br />
<input type="number" id="max-file-size" @bind-value="@maxFileSize" />
<br />
Max allowed files:
<br />
<input type="number" id="max-allowed-files" @bind-value="@maxAllowedFiles" />
<br />
<InputFile OnChange="LoadFiles" id="input-file" multiple />
<br />
<span id="exception-message">@exceptionMessage</span>
@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>();
long maxFileSize = 1024 * 1024 * 15;
int maxAllowedFiles = 3;
bool isLoading;
string imageDataUri;
string exceptionMessage;
async Task LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
exceptionMessage = string.Empty;
try
{
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
StateHasChanged();
using var reader = new StreamReader(file.OpenReadStream(maxFileSize));
loadedFiles.Add(file, await reader.ReadToEndAsync());
}
}
catch (Exception ex)
{
exceptionMessage = ex.Message;
}
isLoading = false;
}
async Task LoadImage(InputFileChangeEventArgs e)
{
var format = "image/jpeg";
var imageFile = await e.File.RequestImageFileAsync(format, 640, 480);
using var fileStream = imageFile.OpenReadStream(maxFileSize);
using var memoryStream = new MemoryStream();
await fileStream.CopyToAsync(memoryStream);
imageDataUri = $"data:{format};base64,{Convert.ToBase64String(memoryStream.ToArray())}";
}
}

View File

@ -36,6 +36,7 @@
<option value="BasicTestApp.FormsTest.SimpleValidationComponentUsingExperimentalValidator">Simple validation using experimental validator</option>
<option value="BasicTestApp.FormsTest.TypicalValidationComponent">Typical validation</option>
<option value="BasicTestApp.FormsTest.TypicalValidationComponentUsingExperimentalValidator">Typical validation using experimental validator</option>
<option value="BasicTestApp.FormsTest.InputFileComponent">Input file</option>
<option value="BasicTestApp.NavigateOnSubmit">Navigate to submit</option>
<option value="BasicTestApp.GlobalizationBindCases">Globalization Bind Cases</option>
<option value="BasicTestApp.HierarchicalImportsTest.Subdir.ComponentUsingImports">Imports statement</option>
@ -46,7 +47,6 @@
<option value="BasicTestApp.HttpClientTest.CookieCounterComponent">HttpClient cookies</option>
<option value="BasicTestApp.HttpClientTest.HttpRequestsComponent">HttpClient tester</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.InteropOnInitializationComponent">Interop on initialization</option>
<option value="BasicTestApp.JsonSerializationCases">JSON serialization</option>

View File

@ -1,80 +0,0 @@
@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();
}
}
}

View File

@ -19,6 +19,7 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Configuration;
using Microsoft.JSInterop;
using Microsoft.AspNetCore.Components.ProtectedBrowserStorage;
namespace BasicTestApp
{

View File

@ -1,5 +1,6 @@
@using Microsoft.Extensions.DependencyInjection
@using Microsoft.AspNetCore.Components.Web.Extensions
@using Microsoft.AspNetCore.Components.ProtectedBrowserStorage
@inject IServiceProvider ServiceProvider
<button id="inject-local" @onclick="Inject<ProtectedLocalStorage>">Inject @(nameof(ProtectedLocalStorage))</button>

View File

@ -1,4 +1,5 @@
@using Microsoft.AspNetCore.Components.Web.Extensions
@using Microsoft.AspNetCore.Components.ProtectedBrowserStorage
@inject ProtectedLocalStorage ProtectedLocalStore
@inject ProtectedSessionStorage ProtectedSessionStore

View File

@ -45,8 +45,6 @@
<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 -->
<script src="_content/TestContentPackage/prompt.js"></script>
</body>