ASP.NET Core Blazor ファイルのアップロード
この記事では、InputFile コンポーネントを使用して Blazor 内のファイルをアップロードする方法について説明します。
警告
ファイルのアップロードをユーザーに許可する場合は、常に、セキュリティのベスト プラクティスに従ってください。 詳細については、「ASP.NET Core でファイルをアップロードする」をご覧ください。
ブラウザー ファイルのデータを .NET コードに読み込むには、InputFile コンポーネントを使用します。 InputFile コンポーネントにより、file
型の HTML <input>
要素がレンダリングされます。 既定では、ユーザーは単一ファイルを選択します。 multiple
属性を追加して、ユーザーが一度に複数のファイルをアップロードできるようにします。
InputFile コンポーネントまたはその基礎 HTML <input type="file">
を使用するとき、ファイル選択は累積されません。そのため、ファイルは既存のファイル選択に追加できません。 このコンポーネントでは常にユーザーの最初のファイル選択が置換されます。そのため、前の選択からのファイル参照は利用できません。
OnChange (change
) イベントが発生すると、次の InputFile コンポーネントによって LoadFiles
メソッドが実行されます。 InputFileChangeEventArgs により、選択されているファイルの一覧と各ファイルの詳細にアクセスできます。
<InputFile OnChange="@LoadFiles" multiple />
@code {
private void LoadFiles(InputFileChangeEventArgs e)
{
...
}
}
レンダリングされる HTML:
<input multiple="" type="file" _bl_2="">
Note
前の例では、<input>
要素の _bl_2
属性が、Blazor の内部処理に使用されます。
ユーザーが選択したファイルからデータを読み取るには、ファイルで IBrowserFile.OpenReadStream を呼び出し、返されるストリームから読み取ります。 詳細については、「ファイル ストリーム」セクションを参照してください。
OpenReadStream により、Stream の最大サイズ (バイト単位) が適用されます。 1 ファイルまたは複数ファイルの読み取りが 512,000 バイト (500 KB) を超えると、例外が発生します。 この制限により、開発者が誤って大きいファイルをメモリに読み取ることが防がれます。 OpenReadStream の maxAllowedSize
パラメーターを使用することにより、必要に応じてさらに大きいサイズを指定できます。
ファイルのバイト数を表す Stream にアクセスする必要がある場合は、IBrowserFile.OpenReadStream を使用します。 受信ファイル ストリームをメモリに一度に直接読み取ることは避けてください。 たとえば、ファイルのすべてのバイトを MemoryStream にコピーしたり、ストリーム全体を一度にバイト配列に読み取ったりしないでください。 このような方法は、特に Blazor Server アプリの場合、パフォーマンスやセキュリティの問題が発生する可能性があります。 代わりに、次のいずれかの方法を使うことを検討してください。
- ホステッド Blazor WebAssembly アプリまたは Blazor Server アプリのサーバーで、メモリに読み込むことなく、ディスク上のファイルにストリームを直接コピーします。 Blazor アプリではクライアントのファイル システムに直接アクセスできないことにご注意ください。
- クライアントから外部サービスにファイルを直接アップロードします。 詳しくは、「ファイルを外部サービスにアップロードする」セクションをご覧ください。
次の例では、browserFile
はアップロードされたファイルを表し、IBrowserFile を実装しています。
❌ 次の方法は、ファイルの Stream の内容がメモリ (reader
) 内の String に読み込まれるため、推奨されません。
var reader =
await new StreamReader(browserFile.OpenReadStream()).ReadToEndAsync();
❌ 次の方法は、UploadBlobAsync を呼び出す前に、ファイルの Stream の内容がメモリ (memoryStream
) 内の MemoryStream にコピーされるため、Microsoft Azure Blob Storage には推奨されません。
var memoryStream = new MemoryStream();
browserFile.OpenReadStream().CopyToAsync(memoryStream);
await blobContainerClient.UploadBlobAsync(
trustedFilename, memoryStream));
✔️ 次の方法は、ファイルの Stream がコンシューマー (指定されたパスにファイルを作成する FileStream) に直接提供されるため、推奨されます。
await using FileStream fs = new(path, FileMode.Create);
await browserFile.OpenReadStream().CopyToAsync(fs);
✔️ 次の方法は、ファイルの Stream が UploadBlobAsync に直接提供されるため、Microsoft Azure Blob Storage に対して推奨されます。
await blobContainerClient.UploadBlobAsync(
trustedFilename, browserFile.OpenReadStream());
イメージ ファイルを受信するコンポーネントは、ファイルの便利な BrowserFileExtensions.RequestImageFileAsync メソッドを呼び出して、イメージがアプリにストリームされる前に、ブラウザーの JavaScript ランタイム内のイメージ データのサイズを変更できます。 RequestImageFileAsync を呼び出すためのユース ケースは、Blazor WebAssembly アプリに最も適しています。
次の例は、コンポーネントでの複数のファイルのアップロードを示しています。 InputFileChangeEventArgs.GetMultipleFiles では、複数のファイルを読み取ることができます。 悪意のあるユーザーがアプリで想定されているよりも多くのファイルをアップロードするのを防ぐため、ファイルの最大数を指定します。 ファイルのアップロードで複数のファイルがサポートされていない場合、InputFileChangeEventArgs.File を使用すると、最初のファイルのみを読み取ることができます。
Note
InputFileChangeEventArgs は Microsoft.AspNetCore.Components.Forms 名前空間にあります。これは、通常、アプリの _Imports.razor
ファイル内の名前空間の 1 つです。 名前空間が _Imports.razor
ファイル内に存在する場合、それにより API メンバーはアプリのコンポーネントにアクセスできます。
using Microsoft.AspNetCore.Components.Forms
_Imports.razor
ファイル内の名前空間は、C# ファイル (.cs
) には適用されません。 C# ファイルには、明示的な using
ディレクティブが必要です。
Note
ファイル アップロード コンポーネントをテストする場合は、PowerShell を使用して任意のサイズのテスト ファイルを作成できます。
$out = new-object byte[] {SIZE}; (new-object Random).NextBytes($out); [IO.File]::WriteAllBytes('{PATH}', $out)
上記のコマンドでは次のことが行われます。
{SIZE}
プレースホルダーでは、ファイルのサイズはバイト単位で表します (たとえば、2 MB のファイルの場合は2097152
)。{PATH}
プレースホルダーは、ファイル拡張子を持つパスとファイルです (例:D:/test_files/testfile2MB.txt
)。
Pages/FileUpload1.razor
:
@page "/file-upload-1"
@using System
@using System.IO
@using Microsoft.AspNetCore.Hosting
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload1> Logger
@inject IWebHostEnvironment Environment
<h3>Upload Files</h3>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="@LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Uploading...</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private async Task LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
loadedFiles.Add(file);
var trustedFileNameForFileStorage = Path.GetRandomFileName();
var path = Path.Combine(Environment.ContentRootPath,
Environment.EnvironmentName, "unsafe_uploads",
trustedFileNameForFileStorage);
await using FileStream fs = new(path, FileMode.Create);
await file.OpenReadStream(maxFileSize).CopyToAsync(fs);
}
catch (Exception ex)
{
Logger.LogError("File: {Filename} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
注意
次の例では、ファイル バイトが処理されるだけであり、アプリの外の宛先にファイルが送信される (アップロードされる) ことはありません。 ファイルをサーバーまたはサービスに送信する Razor コンポーネントの例については、次のセクションをご覧ください。
@page "/file-upload-1"
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload1> Logger
<h3>Upload Files</h3>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="@LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Uploading...</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private void LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
loadedFiles.Add(file);
}
catch (Exception ex)
{
Logger.LogError("File: {Filename} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
IBrowserFile は、ブラウザーによって公開されるメタデータをプロパティとして返します。 このメタデータは、事前検証に使用します。
警告
次のプロパティ (具体的には、UI に表示される Name プロパティ) の値は信頼しないでください。 ユーザー指定のデータはすべて、アプリ、サーバー、ネットワークに対する重要なセキュリティ リスクとして扱います。 詳細については、「ASP.NET Core でファイルをアップロードする」をご覧ください。
サーバーへのファイルのアップロード
次の例では、Blazor Server アプリから別のアプリ (場合によっては別のサーバー) 内のバックエンド Web API コントローラーに、ファイルをアップロードする方法を示します。
Blazor Server アプリにおいて、アプリで HttpClient インスタンスを作成できるようにする IHttpClientFactory と関連サービスを追加します。
Program.cs
:
builder.Services.AddHttpClient();
詳細については、「ASP.NET Core で IHttpClientFactory を使用して HTTP 要求を行う」を参照してください。
このセクションの例では次のようになります。
- Web API は次の URL で実行されます:
https://localhost:5001
- Blazor Server アプリは次の URL で実行されます:
https://localhost:5003
テストの場合は、前記の URL をプロジェクトの Properties/launchSettings.json
ファイルで構成します。
次の例では、ホストされている Blazor WebAssembly ソリューションの Server
アプリで、Web API コントローラーにファイルをアップロードする方法を示しています。
重要
ホストされている Blazor WebAssembly アプリを実行する場合は、ソリューションの Server
プロジェクトから、そのアプリを実行します。
結果のクラスをアップロードする
次の UploadResult
クラスは、アップロードされたファイルの結果を保持するために、クライアント プロジェクトと Web API プロジェクトに配置されます。 サーバーでファイルのアップロードに失敗すると、ユーザーに表示するために ErrorCode
でエラー コードが返されます。 安全なファイル名が、ファイルごとにサーバー上で生成され、表示するために StoredFileName
でクライアントに返されます。 ファイルは、FileName
の安全でないまたは信頼されていないファイル名を使用して、クライアントとサーバーの間でキー指定されます。
UploadResult.cs
:
public class UploadResult
{
public bool Uploaded { get; set; }
public string? FileName { get; set; }
public string? StoredFileName { get; set; }
public int ErrorCode { get; set; }
}
アップロードしたファイルの結果は、 Shared
プロジェクトの次の UploadResult
クラスによって保持されます。 サーバーでファイルのアップロードに失敗すると、ユーザーに表示するために ErrorCode
でエラー コードが返されます。 安全なファイル名が、ファイルごとにサーバー上で生成され、表示するために StoredFileName
でクライアントに返されます。 ファイルは、FileName
の安全でないまたは信頼されていないファイル名を使用して、クライアントとサーバーの間でキー指定されます。 次の例では、プロジェクトの名前空間は BlazorSample.Shared
です。
ホストされている Blazor WebAssembly ソリューションの Shared
プロジェクト内にある UploadResult.cs
。
namespace BlazorSample.Shared
{
public class UploadResult
{
public bool Uploaded { get; set; }
public string? FileName { get; set; }
public string? StoredFileName { get; set; }
public int ErrorCode { get; set; }
}
}
UploadResult
クラスを Client
プロジェクトで使用できるようにするには、Shared
プロジェクト用に Client
プロジェクトの _Imports.razor
ファイルにインポートを追加します。
@using BlazorSample.Shared
注意
運用アプリのセキュリティのベスト プラクティスは、アプリ、サーバー、またはネットワークに関する機密情報を明らかにするおそれがあるエラー メッセージを、クライアントに送信しないようにすることです。 詳細なエラー メッセージを提供すると、アプリ、サーバー、またはネットワークを攻撃しようとしている悪意のあるユーザーの手助けをしてしまうことになります。 このセクションのコード例では、サーバー側でエラーが発生した場合、コンポーネントのクライアント側で表示するために、エラー コード番号 (int
) のみが返送されます。 ユーザーは、ファイルのアップロードに関するサポートが必要な場合は、エラーの正確な原因を知ることなく、サポート チケットの解決のためにエラー コードをサポート担当者に提供します。
コンポーネントをアップロードする
次の FileUpload2
コンポーネントでは、次を実行します。
- クライアントからファイルをアップロードすることをユーザーに許可します。
- クライアントから提供された信頼できない、または安全ではないファイル名を、UI に表示します。 信頼できない、または安全ではないファイル名は、UI で安全に表示するために、Razor によって自動的に HTML でエンコードされます。
警告
次の目的には、クライアントから提供されたファイル名を信頼しないでください。
- ファイルをファイル システムまたはサービスに保存する。
- ファイル名が自動的にエンコードされない UI に、または開発者コードを使用して表示する。
サーバーにファイルをアップロードする場合のセキュリティに関する考慮事項の詳細については、「ASP.NET Core でファイルをアップロードする」を参照してください。
Blazor Server アプリでの Pages/FileUpload2.razor
:
@page "/file-upload-2"
@using System.Linq
@using System.Net.Http.Headers
@using System.Text.Json
@using Microsoft.Extensions.Logging
@inject IHttpClientFactory ClientFactory
@inject ILogger<FileUpload2> Logger
<h1>Upload Files</h1>
<p>
<label>
Upload up to @maxAllowedFiles files:
<InputFile OnChange="@OnInputFileChange" multiple />
</label>
</p>
@if (files.Count > 0)
{
<div class="card">
<div class="card-body">
<ul>
@foreach (var file in files)
{
<li>
File: @file.Name
<br>
@if (FileUpload(uploadResults, file.Name, Logger,
out var result))
{
<span>
Stored File Name: @result.StoredFileName
</span>
}
else
{
<span>
There was an error uploading the file
(Error: @result.ErrorCode).
</span>
}
</li>
}
</ul>
</div>
</div>
}
@code {
private List<File> files = new();
private List<UploadResult> uploadResults = new();
private int maxAllowedFiles = 3;
private bool shouldRender;
protected override bool ShouldRender() => shouldRender;
private async Task OnInputFileChange(InputFileChangeEventArgs e)
{
shouldRender = false;
long maxFileSize = 1024 * 1024 * 15;
var upload = false;
using var content = new MultipartFormDataContent();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
if (uploadResults.SingleOrDefault(
f => f.FileName == file.Name) is null)
{
try
{
var fileContent =
new StreamContent(file.OpenReadStream(maxFileSize));
fileContent.Headers.ContentType =
new MediaTypeHeaderValue(file.ContentType);
files.Add(new() { Name = file.Name });
content.Add(
content: fileContent,
name: "\"files\"",
fileName: file.Name);
upload = true;
}
catch (Exception ex)
{
Logger.LogInformation(
"{FileName} not uploaded (Err: 6): {Message}",
file.Name, ex.Message);
uploadResults.Add(
new()
{
FileName = file.Name,
ErrorCode = 6,
Uploaded = false
});
}
}
}
if (upload)
{
var client = ClientFactory.CreateClient();
var response =
await client.PostAsync("https://localhost:5001/Filesave",
content);
if (response.IsSuccessStatusCode)
{
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
};
using var responseStream =
await response.Content.ReadAsStreamAsync();
var newUploadResults = await JsonSerializer
.DeserializeAsync<IList<UploadResult>>(responseStream, options);
if (newUploadResults is not null)
{
uploadResults = uploadResults.Concat(newUploadResults).ToList();
}
}
}
shouldRender = true;
}
private static bool FileUpload(IList<UploadResult> uploadResults,
string? fileName, ILogger<FileUpload2> logger, out UploadResult result)
{
result = uploadResults.SingleOrDefault(f => f.FileName == fileName) ?? new();
if (!result.Uploaded)
{
logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
result.ErrorCode = 5;
}
return result.Uploaded;
}
private class File
{
public string? Name { get; set; }
}
}
Client
プロジェクトの Pages/FileUpload2.razor
:
@page "/file-upload-2"
@using System.Linq
@using System.Net.Http.Headers
@using Microsoft.Extensions.Logging
@inject HttpClient Http
@inject ILogger<FileUpload2> Logger
<h1>Upload Files</h1>
<p>
<label>
Upload up to @maxAllowedFiles files:
<InputFile OnChange="@OnInputFileChange" multiple />
</label>
</p>
@if (files.Count > 0)
{
<div class="card">
<div class="card-body">
<ul>
@foreach (var file in files)
{
<li>
File: @file.Name
<br>
@if (FileUpload(uploadResults, file.Name, Logger,
out var result))
{
<span>
Stored File Name: @result.StoredFileName
</span>
}
else
{
<span>
There was an error uploading the file
(Error: @result.ErrorCode).
</span>
}
</li>
}
</ul>
</div>
</div>
}
@code {
private List<File> files = new();
private List<UploadResult> uploadResults = new();
private int maxAllowedFiles = 3;
private bool shouldRender;
protected override bool ShouldRender() => shouldRender;
private async Task OnInputFileChange(InputFileChangeEventArgs e)
{
shouldRender = false;
long maxFileSize = 1024 * 1024 * 15;
var upload = false;
using var content = new MultipartFormDataContent();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
if (uploadResults.SingleOrDefault(
f => f.FileName == file.Name) is null)
{
try
{
var fileContent =
new StreamContent(file.OpenReadStream(maxFileSize));
fileContent.Headers.ContentType =
new MediaTypeHeaderValue(file.ContentType);
files.Add(new() { Name = file.Name });
content.Add(
content: fileContent,
name: "\"files\"",
fileName: file.Name);
upload = true;
}
catch (Exception ex)
{
Logger.LogInformation(
"{FileName} not uploaded (Err: 6): {Message}",
file.Name, ex.Message);
uploadResults.Add(
new()
{
FileName = file.Name,
ErrorCode = 6,
Uploaded = false
});
}
}
}
if (upload)
{
var response = await Http.PostAsync("/Filesave", content);
var newUploadResults = await response.Content
.ReadFromJsonAsync<IList<UploadResult>>();
if (newUploadResults is not null)
{
uploadResults = uploadResults.Concat(newUploadResults).ToList();
}
}
shouldRender = true;
}
private static bool FileUpload(IList<UploadResult> uploadResults,
string? fileName, ILogger<FileUpload2> logger, out UploadResult result)
{
result = uploadResults.SingleOrDefault(f => f.FileName == fileName) ?? new();
if (!result.Uploaded)
{
logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
result.ErrorCode = 5;
}
return result.Uploaded;
}
private class File
{
public string? Name { get; set; }
}
}
コントローラーをアップロードする
Web API プロジェクトの次のコントローラーによって、クライアントからアップロードされたファイルが保存されます。
次のコードを使用するには、Development
環境で実行されているアプリの Web API プロジェクトのルートに、Development/unsafe_uploads
フォルダーを作成します。 この例では、ファイルが保存されるパスの一部としてアプリの環境を使用するため、テストと運用で他の環境を使用する場合は、追加のフォルダーが必要です。 たとえば、Staging
環境用には Staging/unsafe_uploads
フォルダーを作成します。 Production
環境用には Production/unsafe_uploads
フォルダーを作成します。
警告
この例では、内容をスキャンせずにファイルを保存します。 運用シナリオでは、アップロードされたファイルにウイルス対策またはマルウェア対策スキャナー API を使用してから、ダウンロードまたは他のシステムで使用できるようにしています。 サーバーにファイルをアップロードする場合のセキュリティに関する考慮事項の詳細については、「ASP.NET Core でファイルをアップロードする」を参照してください。
Controllers/FilesaveController.cs
:
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
[ApiController]
[Route("[controller]")]
public class FilesaveController : ControllerBase
{
private readonly IWebHostEnvironment env;
private readonly ILogger<FilesaveController> logger;
public FilesaveController(IWebHostEnvironment env,
ILogger<FilesaveController> logger)
{
this.env = env;
this.logger = logger;
}
[HttpPost]
public async Task<ActionResult<IList<UploadResult>>> PostFile(
[FromForm] IEnumerable<IFormFile> files)
{
var maxAllowedFiles = 3;
long maxFileSize = 1024 * 1024 * 15;
var filesProcessed = 0;
var resourcePath = new Uri($"{Request.Scheme}://{Request.Host}/");
List<UploadResult> uploadResults = new();
foreach (var file in files)
{
var uploadResult = new UploadResult();
string trustedFileNameForFileStorage;
var untrustedFileName = file.FileName;
uploadResult.FileName = untrustedFileName;
var trustedFileNameForDisplay =
WebUtility.HtmlEncode(untrustedFileName);
if (filesProcessed < maxAllowedFiles)
{
if (file.Length == 0)
{
logger.LogInformation("{FileName} length is 0 (Err: 1)",
trustedFileNameForDisplay);
uploadResult.ErrorCode = 1;
}
else if (file.Length > maxFileSize)
{
logger.LogInformation("{FileName} of {Length} bytes is " +
"larger than the limit of {Limit} bytes (Err: 2)",
trustedFileNameForDisplay, file.Length, maxFileSize);
uploadResult.ErrorCode = 2;
}
else
{
try
{
trustedFileNameForFileStorage = Path.GetRandomFileName();
var path = Path.Combine(env.ContentRootPath,
env.EnvironmentName, "unsafe_uploads",
trustedFileNameForFileStorage);
await using FileStream fs = new(path, FileMode.Create);
await file.CopyToAsync(fs);
logger.LogInformation("{FileName} saved at {Path}",
trustedFileNameForDisplay, path);
uploadResult.Uploaded = true;
uploadResult.StoredFileName = trustedFileNameForFileStorage;
}
catch (IOException ex)
{
logger.LogError("{FileName} error on upload (Err: 3): {Message}",
trustedFileNameForDisplay, ex.Message);
uploadResult.ErrorCode = 3;
}
}
filesProcessed++;
}
else
{
logger.LogInformation("{FileName} not uploaded because the " +
"request exceeded the allowed {Count} of files (Err: 4)",
trustedFileNameForDisplay, maxAllowedFiles);
uploadResult.ErrorCode = 4;
}
uploadResults.Add(uploadResult);
}
return new CreatedResult(resourcePath, uploadResults);
}
}
Server
プロジェクトの次のコントローラーによって、クライアントからアップロードされたファイルが保存されます。
次のコードを使用するには、Development
環境で実行されているアプリの Server
プロジェクトのルートに、Development/unsafe_uploads
フォルダーを作成します。 この例では、ファイルが保存されるパスの一部としてアプリの環境を使用するため、テストと運用で他の環境を使用する場合は、追加のフォルダーが必要です。 たとえば、Staging
環境用には Staging/unsafe_uploads
フォルダーを作成します。 Production
環境用には Production/unsafe_uploads
フォルダーを作成します。
警告
この例では、内容をスキャンせずにファイルを保存します。 運用シナリオでは、アップロードされたファイルにウイルス対策またはマルウェア対策スキャナー API を使用してから、ダウンロードまたは他のシステムで使用できるようにしています。 サーバーにファイルをアップロードする場合のセキュリティに関する考慮事項の詳細については、「ASP.NET Core でファイルをアップロードする」を参照してください。
Controllers/FilesaveController.cs
:
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using BlazorSample.Shared;
[ApiController]
[Route("[controller]")]
public class FilesaveController : ControllerBase
{
private readonly IWebHostEnvironment env;
private readonly ILogger<FilesaveController> logger;
public FilesaveController(IWebHostEnvironment env,
ILogger<FilesaveController> logger)
{
this.env = env;
this.logger = logger;
}
[HttpPost]
public async Task<ActionResult<IList<UploadResult>>> PostFile(
[FromForm] IEnumerable<IFormFile> files)
{
var maxAllowedFiles = 3;
long maxFileSize = 1024 * 1024 * 15;
var filesProcessed = 0;
var resourcePath = new Uri($"{Request.Scheme}://{Request.Host}/");
List<UploadResult> uploadResults = new();
foreach (var file in files)
{
var uploadResult = new UploadResult();
string trustedFileNameForFileStorage;
var untrustedFileName = file.FileName;
uploadResult.FileName = untrustedFileName;
var trustedFileNameForDisplay =
WebUtility.HtmlEncode(untrustedFileName);
if (filesProcessed < maxAllowedFiles)
{
if (file.Length == 0)
{
logger.LogInformation("{FileName} length is 0 (Err: 1)",
trustedFileNameForDisplay);
uploadResult.ErrorCode = 1;
}
else if (file.Length > maxFileSize)
{
logger.LogInformation("{FileName} of {Length} bytes is " +
"larger than the limit of {Limit} bytes (Err: 2)",
trustedFileNameForDisplay, file.Length, maxFileSize);
uploadResult.ErrorCode = 2;
}
else
{
try
{
trustedFileNameForFileStorage = Path.GetRandomFileName();
var path = Path.Combine(env.ContentRootPath,
env.EnvironmentName, "unsafe_uploads",
trustedFileNameForFileStorage);
await using FileStream fs = new(path, FileMode.Create);
await file.CopyToAsync(fs);
logger.LogInformation("{FileName} saved at {Path}",
trustedFileNameForDisplay, path);
uploadResult.Uploaded = true;
uploadResult.StoredFileName = trustedFileNameForFileStorage;
}
catch (IOException ex)
{
logger.LogError("{FileName} error on upload (Err: 3): {Message}",
trustedFileNameForDisplay, ex.Message);
uploadResult.ErrorCode = 3;
}
}
filesProcessed++;
}
else
{
logger.LogInformation("{FileName} not uploaded because the " +
"request exceeded the allowed {Count} of files (Err: 4)",
trustedFileNameForDisplay, maxAllowedFiles);
uploadResult.ErrorCode = 4;
}
uploadResults.Add(uploadResult);
}
return new CreatedResult(resourcePath, uploadResults);
}
}
上記のコードでは、GetRandomFileName が呼び出され、安全なファイル名が生成されます。 ブラウザーによって提供されるファイル名を信頼しないでください。攻撃者は、既存のファイルを上書きする既存のファイル名を選択したり、アプリの外部で書き込みを試みるパスを送信したりする可能性があります。
進行状況を表示してファイルをアップロードする
次の例は、Blazor Server アプリで、アップロードの進行状況をユーザーに表示しながらファイルをアップロードする方法を示しています。
テスト アプリで以下の例を使用するには、次のようにします。
- アップロードした
Development
環境用のファイルを保存するためのフォルダーを作成します:Development/unsafe_uploads
。 - 最大ファイル サイズ (
maxFileSize
、次の例では 15 MB) と、許可されるファイルの最大数 (maxAllowedFiles
、次の例では 3) を構成します。 - 必要に応じて、バッファーを別の値 (次の例では 10 KB) に設定して、進行状況を報告する頻度を増やします。 パフォーマンスおよびセキュリティ上の懸念があるため、30 KB を超えるバッファーの使用はお勧めしません。
Pages/FileUpload3.razor
:
@page "/file-upload-3"
@using System
@using System.IO
@using Microsoft.AspNetCore.Hosting
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload3> Logger
@inject IWebHostEnvironment Environment
<h3>Upload Files</h3>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="@LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Progress: @string.Format("{0:P0}", progressPercent)</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private decimal progressPercent;
private async Task LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
progressPercent = 0;
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
var trustedFileName = Path.GetRandomFileName();
var path = Path.Combine(Environment.ContentRootPath,
Environment.EnvironmentName, "unsafe_uploads", trustedFileName);
await using FileStream writeStream = new(path, FileMode.Create);
using var readStream = file.OpenReadStream(maxFileSize);
var bytesRead = 0;
var totalRead = 0;
var buffer = new byte[1024 * 10];
while ((bytesRead = await readStream.ReadAsync(buffer)) != 0)
{
totalRead += bytesRead;
await writeStream.WriteAsync(buffer, 0, bytesRead);
progressPercent = Decimal.Divide(totalRead, file.Size);
StateHasChanged();
}
loadedFiles.Add(file);
}
catch (Exception ex)
{
Logger.LogError("File: {Filename} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
詳細については、次の API リソースを参照してください。
- FileStream: 同期および非同期両方の読み取り操作と書き込み操作をサポートするファイル用の Stream を提供します。
- FileStream.ReadAsync: 上記の
FileUpload3
コンポーネントは、ReadAsync を使用して非同期にストリームを読み取ります。 Read を使用してストリームを同期的に読み取る操作は、Razor コンポーネントではサポートされていません。
ファイル ストリーム
Blazor Server では、ファイルが読み取られるときに、ファイル データがサーバー上の .NET コードに SignalR 接続を介してストリームされます。
Blazor WebAssembly では、ファイル データはブラウザー内の .NET コードに直接ストリームされます。
ファイルを外部サービスにアップロードする
アプリでファイルのアップロード バイトを処理し、アプリのサーバーでアップロードされたファイルを受信する代わりに、クライアントから外部サービスにファイルを直接アップロードできます。 アプリは、必要に応じて外部サービスからファイルを安全に処理できます。 この方法により、悪意のある攻撃や潜在的なパフォーマンスの問題に対してアプリとそのサーバーが強化されます。
Azure Files、Azure Blob Storage、または次のような利点を持つサード パーティのサービスを使用するアプローチを検討してください。
- JavaScript クライアント ライブラリまたは REST API を使用して、クライアントから外部サービスにファイルを直接アップロードします。 たとえば、Azure には次のクライアント ライブラリと API が用意されています。
- クライアントによるファイルのアップロードごとにアプリ (サーバー側) によって生成されるユーザー委任の Shared Access Signature (SAS) トークンを使って、ユーザーのアップロードを承認します。 たとえば、Azure には次の SAS 機能があります。
- 自動冗長性とファイル共有のバックアップを提供します。
- クォータでアップロードを制限します。 Azure Blob Storage のクォータは、コンテナー レベルではなくアカウント レベルで設定されることに注意してください。 一方、Azure Files のクォータはファイル共有レベルであり、アップロードの制限をより適切に制御できる場合があります。 詳しくは、この一覧で前にリンクを示した Azure のドキュメントをご覧ください。
- サーバー側暗号化 (SSE) でファイルをセキュリティ保護します。
Azure Blob Storage と Azure Files について詳しくは、Azure Storage のドキュメントをご覧ください。
その他のリソース
警告
ファイルのアップロードをユーザーに許可する場合は、常に、セキュリティのベスト プラクティスに従ってください。 詳細については、「ASP.NET Core でファイルをアップロードする」をご覧ください。
最大 2 GB のブラウザー ファイルのデータを .NET コードに読み込むには、InputFile コンポーネントを使用します。 InputFile コンポーネントにより、file
型の HTML <input>
要素がレンダリングされます。 既定では、ユーザーは単一ファイルを選択します。 multiple
属性を追加して、ユーザーが一度に複数のファイルをアップロードできるようにします。
InputFile コンポーネントまたはその基礎 HTML <input type="file">
を使用するとき、ファイル選択は累積されません。そのため、ファイルは既存のファイル選択に追加できません。 このコンポーネントでは常にユーザーの最初のファイル選択が置換されます。そのため、前の選択からのファイル参照は利用できません。
OnChange (change
) イベントが発生すると、次の InputFile コンポーネントによって LoadFiles
メソッドが実行されます。 InputFileChangeEventArgs により、選択されているファイルの一覧と各ファイルの詳細にアクセスできます。
<InputFile OnChange="@LoadFiles" multiple />
@code {
private void LoadFiles(InputFileChangeEventArgs e)
{
...
}
}
レンダリングされる HTML:
<input multiple="" type="file" _bl_2="">
Note
前の例では、<input>
要素の _bl_2
属性が、Blazor の内部処理に使用されます。
ユーザーが選択したファイルからデータを読み取るには、ファイルで IBrowserFile.OpenReadStream を呼び出し、返されるストリームから読み取ります。 詳細については、「ファイル ストリーム」セクションを参照してください。
OpenReadStream により、Stream の最大サイズ (バイト単位) が適用されます。 1 ファイルまたは複数ファイルの読み取りが 512,000 バイト (500 KB) を超えると、例外が発生します。 この制限により、開発者が誤って大きいファイルをメモリに読み取ることが防がれます。 OpenReadStream の maxAllowedSize
パラメーターを使用すると、サポートされる最大サイズである 2 GB (2,147,483,648 バイト) まで、必要に応じて、より大きなサイズを指定できます。
Note
2 GB というフレームワーク ファイル サイズの制限は、ASP.NET Core 5.0 にのみ適用されます。 ASP.NET Core 6.0 以降、フレームワークでファイルの最大サイズは制限されません。
ファイルのバイト数を表す Stream にアクセスする必要がある場合は、IBrowserFile.OpenReadStream を使用します。 受信ファイル ストリームをメモリに一度に直接読み取ることは避けてください。 たとえば、ファイルのすべてのバイトを MemoryStream にコピーしたり、ストリーム全体を一度にバイト配列に読み取ったりしないでください。 このような方法は、特に Blazor Server アプリの場合、パフォーマンスやセキュリティの問題が発生する可能性があります。 代わりに、次のいずれかの方法を使うことを検討してください。
- ストリームを、メモリに読み取るのではなく、ディスク上のファイルに直接コピーします。
- クライアントから外部サービスにファイルを直接アップロードします。 詳しくは、「ファイルを外部サービスにアップロードする」セクションをご覧ください。
次の例では、browserFile
はアップロードされたファイルを表し、IBrowserFile を実装しています。
❌ 次の方法は、ファイルの Stream の内容がメモリ (reader
) 内の String に読み込まれるため、推奨されません。
var reader =
await new StreamReader(browserFile.OpenReadStream()).ReadToEndAsync();
❌ 次の方法は、UploadBlobAsync を呼び出す前に、ファイルの Stream の内容がメモリ (memoryStream
) 内の MemoryStream にコピーされるため、Microsoft Azure Blob Storage には推奨されません。
var memoryStream = new MemoryStream();
browserFile.OpenReadStream().CopyToAsync(memoryStream);
await blobContainerClient.UploadBlobAsync(
trustedFilename, memoryStream));
✔️ 次の方法は、ファイルの Stream がコンシューマー (指定されたパスにファイルを作成する FileStream) に直接提供されるため、推奨されます。
await using FileStream fs = new(path, FileMode.Create);
await browserFile.OpenReadStream().CopyToAsync(fs);
✔️ 次の方法は、ファイルの Stream が UploadBlobAsync に直接提供されるため、Microsoft Azure Blob Storage に対して推奨されます。
await blobContainerClient.UploadBlobAsync(
trustedFilename, browserFile.OpenReadStream());
イメージ ファイルを受信するコンポーネントは、ファイルの便利な BrowserFileExtensions.RequestImageFileAsync メソッドを呼び出して、イメージがアプリにストリームされる前に、ブラウザーの JavaScript ランタイム内のイメージ データのサイズを変更できます。 RequestImageFileAsync を呼び出すためのユース ケースは、Blazor WebAssembly アプリに最も適しています。
次の例は、コンポーネントでの複数のファイルのアップロードを示しています。 InputFileChangeEventArgs.GetMultipleFiles では、複数のファイルを読み取ることができます。 悪意のあるユーザーがアプリで想定されているよりも多くのファイルをアップロードするのを防ぐため、ファイルの最大数を指定します。 ファイルのアップロードで複数のファイルがサポートされていない場合、InputFileChangeEventArgs.File を使用すると、最初のファイルのみを読み取ることができます。
Note
InputFileChangeEventArgs は Microsoft.AspNetCore.Components.Forms 名前空間にあります。これは、通常、アプリの _Imports.razor
ファイル内の名前空間の 1 つです。 名前空間が _Imports.razor
ファイル内に存在する場合、それにより API メンバーはアプリのコンポーネントにアクセスできます。
using Microsoft.AspNetCore.Components.Forms
_Imports.razor
ファイル内の名前空間は、C# ファイル (.cs
) には適用されません。 C# ファイルには、明示的な using
ディレクティブが必要です。
Note
ファイル アップロード コンポーネントをテストする場合は、PowerShell を使用して任意のサイズのテスト ファイルを作成できます。
$out = new-object byte[] {SIZE}; (new-object Random).NextBytes($out); [IO.File]::WriteAllBytes('{PATH}', $out)
上記のコマンドでは次のことが行われます。
{SIZE}
プレースホルダーでは、ファイルのサイズはバイト単位で表します (たとえば、2 MB のファイルの場合は2097152
)。{PATH}
プレースホルダーは、ファイル拡張子を持つパスとファイルです (例:D:/test_files/testfile2MB.txt
)。
Pages/FileUpload1.razor
:
@page "/file-upload-1"
@using System
@using System.IO
@using Microsoft.AspNetCore.Hosting
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload1> Logger
@inject IWebHostEnvironment Environment
<h3>Upload Files</h3>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="@LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Uploading...</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private async Task LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
loadedFiles.Add(file);
var trustedFileNameForFileStorage = Path.GetRandomFileName();
var path = Path.Combine(Environment.ContentRootPath,
Environment.EnvironmentName, "unsafe_uploads",
trustedFileNameForFileStorage);
await using FileStream fs = new(path, FileMode.Create);
await file.OpenReadStream(maxFileSize).CopyToAsync(fs);
}
catch (Exception ex)
{
Logger.LogError("File: {Filename} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
@page "/file-upload-1"
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload1> Logger
<h3>Upload Files</h3>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="@LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Uploading...</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private void LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
loadedFiles.Add(file);
}
catch (Exception ex)
{
Logger.LogError("File: {Filename} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
IBrowserFile は、ブラウザーによって公開されるメタデータをプロパティとして返します。 このメタデータは、事前検証に使用します。
警告
次のプロパティ (具体的には、UI に表示される Name プロパティ) の値は信頼しないでください。 ユーザー指定のデータはすべて、アプリ、サーバー、ネットワークに対する重要なセキュリティ リスクとして扱います。 詳細については、「ASP.NET Core でファイルをアップロードする」をご覧ください。
サーバーへのファイルのアップロード
次の例では、Blazor Server アプリから別のアプリ (場合によっては別のサーバー) 内のバックエンド Web API コントローラーに、ファイルをアップロードする方法を示します。
Blazor Server アプリにおいて、アプリで HttpClient インスタンスを作成できるようにする IHttpClientFactory と関連サービスを追加します。
Startup.cs
の Startup.ConfigureServices
で:
services.AddHttpClient();
詳細については、「ASP.NET Core で IHttpClientFactory を使用して HTTP 要求を行う」を参照してください。
このセクションの例では次のようになります。
- Web API は次の URL で実行されます:
https://localhost:5001
- Blazor Server アプリは次の URL で実行されます:
https://localhost:5003
テストの場合は、前記の URL をプロジェクトの Properties/launchSettings.json
ファイルで構成します。
次の例では、ホストされている Blazor WebAssembly ソリューションの Server
アプリで、Web API コントローラーにファイルをアップロードする方法を示しています。
重要
ホストされている Blazor WebAssembly アプリを実行する場合は、ソリューションの Server
プロジェクトから、そのアプリを実行します。
結果のクラスをアップロードする
次の UploadResult
クラスは、アップロードされたファイルの結果を保持するために、クライアント プロジェクトと Web API プロジェクトに配置されます。 サーバーでファイルのアップロードに失敗すると、ユーザーに表示するために ErrorCode
でエラー コードが返されます。 安全なファイル名が、ファイルごとにサーバー上で生成され、表示するために StoredFileName
でクライアントに返されます。 ファイルは、FileName
の安全でないまたは信頼されていないファイル名を使用して、クライアントとサーバーの間でキー指定されます。
UploadResult.cs
:
public class UploadResult
{
public bool Uploaded { get; set; }
public string FileName { get; set; }
public string StoredFileName { get; set; }
public int ErrorCode { get; set; }
}
アップロードしたファイルの結果は、 Shared
プロジェクトの次の UploadResult
クラスによって保持されます。 サーバーでファイルのアップロードに失敗すると、ユーザーに表示するために ErrorCode
でエラー コードが返されます。 安全なファイル名が、ファイルごとにサーバー上で生成され、表示するために StoredFileName
でクライアントに返されます。 ファイルは、FileName
の安全でないまたは信頼されていないファイル名を使用して、クライアントとサーバーの間でキー指定されます。 次の例では、プロジェクトの名前空間は BlazorSample.Shared
です。
ホストされている Blazor WebAssembly ソリューションの Shared
プロジェクト内にある UploadResult.cs
。
namespace BlazorSample.Shared
{
public class UploadResult
{
public bool Uploaded { get; set; }
public string FileName { get; set; }
public string StoredFileName { get; set; }
public int ErrorCode { get; set; }
}
}
UploadResult
クラスを Client
プロジェクトで使用できるようにするには、Shared
プロジェクト用に Client
プロジェクトの _Imports.razor
ファイルにインポートを追加します。
@using BlazorSample.Shared
注意
運用アプリのセキュリティのベスト プラクティスは、アプリ、サーバー、またはネットワークに関する機密情報を明らかにするおそれがあるエラー メッセージを、クライアントに送信しないようにすることです。 詳細なエラー メッセージを提供すると、アプリ、サーバー、またはネットワークを攻撃しようとしている悪意のあるユーザーの手助けをしてしまうことになります。 このセクションのコード例では、サーバー側でエラーが発生した場合、コンポーネントのクライアント側で表示するために、エラー コード番号 (int
) のみが返送されます。 ユーザーは、ファイルのアップロードに関するサポートが必要な場合は、エラーの正確な原因を知ることなく、サポート チケットの解決のためにエラー コードをサポート担当者に提供します。
コンポーネントをアップロードする
次の FileUpload2
コンポーネントでは、次を実行します。
- クライアントからファイルをアップロードすることをユーザーに許可します。
- クライアントから提供された信頼できない、または安全ではないファイル名を、UI に表示します。 信頼できない、または安全ではないファイル名は、UI で安全に表示するために、Razor によって自動的に HTML でエンコードされます。
警告
次の目的には、クライアントから提供されたファイル名を信頼しないでください。
- ファイルをファイル システムまたはサービスに保存する。
- ファイル名が自動的にエンコードされない UI に、または開発者コードを使用して表示する。
サーバーにファイルをアップロードする場合のセキュリティに関する考慮事項の詳細については、「ASP.NET Core でファイルをアップロードする」を参照してください。
Blazor Server アプリでの Pages/FileUpload2.razor
:
@page "/file-upload-2"
@using System.Linq
@using System.Net.Http.Headers
@using System.Text.Json
@using Microsoft.Extensions.Logging
@inject IHttpClientFactory ClientFactory
@inject ILogger<FileUpload2> Logger
<h1>Upload Files</h1>
<p>
<label>
Upload up to @maxAllowedFiles files:
<InputFile OnChange="@OnInputFileChange" multiple />
</label>
</p>
@if (files.Count > 0)
{
<div class="card">
<div class="card-body">
<ul>
@foreach (var file in files)
{
<li>
File: @file.Name
<br>
@if (FileUpload(uploadResults, file.Name, Logger,
out var result))
{
<span>
Stored File Name: @result.StoredFileName
</span>
}
else
{
<span>
There was an error uploading the file
(Error: @result.ErrorCode).
</span>
}
</li>
}
</ul>
</div>
</div>
}
@code {
private List<File> files = new();
private List<UploadResult> uploadResults = new();
private int maxAllowedFiles = 3;
private bool shouldRender;
protected override bool ShouldRender() => shouldRender;
private async Task OnInputFileChange(InputFileChangeEventArgs e)
{
shouldRender = false;
long maxFileSize = 1024 * 1024 * 15;
var upload = false;
using var content = new MultipartFormDataContent();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
if (uploadResults.SingleOrDefault(
f => f.FileName == file.Name) is null)
{
try
{
var fileContent =
new StreamContent(file.OpenReadStream(maxFileSize));
fileContent.Headers.ContentType =
new MediaTypeHeaderValue(file.ContentType);
files.Add(new() { Name = file.Name });
content.Add(
content: fileContent,
name: "\"files\"",
fileName: file.Name);
upload = true;
}
catch (Exception ex)
{
Logger.LogInformation(
"{FileName} not uploaded (Err: 6): {Message}",
file.Name, ex.Message);
uploadResults.Add(
new()
{
FileName = file.Name,
ErrorCode = 6,
Uploaded = false
});
}
}
}
if (upload)
{
var client = ClientFactory.CreateClient();
var response =
await client.PostAsync("https://localhost:5001/Filesave",
content);
if (response.IsSuccessStatusCode)
{
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
};
using var responseStream =
await response.Content.ReadAsStreamAsync();
var newUploadResults = await JsonSerializer
.DeserializeAsync<IList<UploadResult>>(responseStream, options);
uploadResults = uploadResults.Concat(newUploadResults).ToList();
}
}
shouldRender = true;
}
private static bool FileUpload(IList<UploadResult> uploadResults,
string fileName, ILogger<FileUpload2> logger, out UploadResult result)
{
result = uploadResults.SingleOrDefault(f => f.FileName == fileName);
if (result is null)
{
logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
result = new();
result.ErrorCode = 5;
}
return result.Uploaded;
}
private class File
{
public string Name { get; set; }
}
}
Client
プロジェクトの Pages/FileUpload2.razor
:
@page "/file-upload-2"
@using System.Linq
@using System.Net.Http.Headers
@using Microsoft.Extensions.Logging
@inject HttpClient Http
@inject ILogger<FileUpload2> Logger
<h1>Upload Files</h1>
<p>
<label>
Upload up to @maxAllowedFiles files:
<InputFile OnChange="@OnInputFileChange" multiple />
</label>
</p>
@if (files.Count > 0)
{
<div class="card">
<div class="card-body">
<ul>
@foreach (var file in files)
{
<li>
File: @file.Name
<br>
@if (FileUpload(uploadResults, file.Name, Logger,
out var result))
{
<span>
Stored File Name: @result.StoredFileName
</span>
}
else
{
<span>
There was an error uploading the file
(Error: @result.ErrorCode).
</span>
}
</li>
}
</ul>
</div>
</div>
}
@code {
private List<File> files = new();
private List<UploadResult> uploadResults = new();
private int maxAllowedFiles = 3;
private bool shouldRender;
protected override bool ShouldRender() => shouldRender;
private async Task OnInputFileChange(InputFileChangeEventArgs e)
{
shouldRender = false;
long maxFileSize = 1024 * 1024 * 15;
var upload = false;
using var content = new MultipartFormDataContent();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
if (uploadResults.SingleOrDefault(
f => f.FileName == file.Name) is null)
{
try
{
var fileContent =
new StreamContent(file.OpenReadStream(maxFileSize));
fileContent.Headers.ContentType =
new MediaTypeHeaderValue(file.ContentType);
files.Add(new() { Name = file.Name });
content.Add(
content: fileContent,
name: "\"files\"",
fileName: file.Name);
upload = true;
}
catch (Exception ex)
{
Logger.LogInformation(
"{FileName} not uploaded (Err: 6): {Message}",
file.Name, ex.Message);
uploadResults.Add(
new()
{
FileName = file.Name,
ErrorCode = 6,
Uploaded = false
});
}
}
}
if (upload)
{
var response = await Http.PostAsync("/Filesave", content);
var newUploadResults = await response.Content
.ReadFromJsonAsync<IList<UploadResult>>();
uploadResults = uploadResults.Concat(newUploadResults).ToList();
}
shouldRender = true;
}
private static bool FileUpload(IList<UploadResult> uploadResults,
string fileName, ILogger<FileUpload2> logger, out UploadResult result)
{
result = uploadResults.SingleOrDefault(f => f.FileName == fileName);
if (result is null)
{
logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
result = new();
result.ErrorCode = 5;
}
return result.Uploaded;
}
private class File
{
public string Name { get; set; }
}
}
コントローラーをアップロードする
Web API プロジェクトの次のコントローラーによって、クライアントからアップロードされたファイルが保存されます。
次のコードを使用するには、Development
環境で実行されているアプリの Web API プロジェクトのルートに、Development/unsafe_uploads
フォルダーを作成します。 この例では、ファイルが保存されるパスの一部としてアプリの環境を使用するため、テストと運用で他の環境を使用する場合は、追加のフォルダーが必要です。 たとえば、Staging
環境用には Staging/unsafe_uploads
フォルダーを作成します。 Production
環境用には Production/unsafe_uploads
フォルダーを作成します。
警告
この例では、内容をスキャンせずにファイルを保存します。 運用シナリオでは、アップロードされたファイルにウイルス対策またはマルウェア対策スキャナー API を使用してから、ダウンロードまたは他のシステムで使用できるようにしています。 サーバーにファイルをアップロードする場合のセキュリティに関する考慮事項の詳細については、「ASP.NET Core でファイルをアップロードする」を参照してください。
Controllers/FilesaveController.cs
:
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
[ApiController]
[Route("[controller]")]
public class FilesaveController : ControllerBase
{
private readonly IWebHostEnvironment env;
private readonly ILogger<FilesaveController> logger;
public FilesaveController(IWebHostEnvironment env,
ILogger<FilesaveController> logger)
{
this.env = env;
this.logger = logger;
}
[HttpPost]
public async Task<ActionResult<IList<UploadResult>>> PostFile(
[FromForm] IEnumerable<IFormFile> files)
{
var maxAllowedFiles = 3;
long maxFileSize = 1024 * 1024 * 15;
var filesProcessed = 0;
var resourcePath = new Uri($"{Request.Scheme}://{Request.Host}/");
List<UploadResult> uploadResults = new();
foreach (var file in files)
{
var uploadResult = new UploadResult();
string trustedFileNameForFileStorage;
var untrustedFileName = file.FileName;
uploadResult.FileName = untrustedFileName;
var trustedFileNameForDisplay =
WebUtility.HtmlEncode(untrustedFileName);
if (filesProcessed < maxAllowedFiles)
{
if (file.Length == 0)
{
logger.LogInformation("{FileName} length is 0 (Err: 1)",
trustedFileNameForDisplay);
uploadResult.ErrorCode = 1;
}
else if (file.Length > maxFileSize)
{
logger.LogInformation("{FileName} of {Length} bytes is " +
"larger than the limit of {Limit} bytes (Err: 2)",
trustedFileNameForDisplay, file.Length, maxFileSize);
uploadResult.ErrorCode = 2;
}
else
{
try
{
trustedFileNameForFileStorage = Path.GetRandomFileName();
var path = Path.Combine(env.ContentRootPath,
env.EnvironmentName, "unsafe_uploads",
trustedFileNameForFileStorage);
await using FileStream fs = new(path, FileMode.Create);
await file.CopyToAsync(fs);
logger.LogInformation("{FileName} saved at {Path}",
trustedFileNameForDisplay, path);
uploadResult.Uploaded = true;
uploadResult.StoredFileName = trustedFileNameForFileStorage;
}
catch (IOException ex)
{
logger.LogError("{FileName} error on upload (Err: 3): {Message}",
trustedFileNameForDisplay, ex.Message);
uploadResult.ErrorCode = 3;
}
}
filesProcessed++;
}
else
{
logger.LogInformation("{FileName} not uploaded because the " +
"request exceeded the allowed {Count} of files (Err: 4)",
trustedFileNameForDisplay, maxAllowedFiles);
uploadResult.ErrorCode = 4;
}
uploadResults.Add(uploadResult);
}
return new CreatedResult(resourcePath, uploadResults);
}
}
Server
プロジェクトの次のコントローラーによって、クライアントからアップロードされたファイルが保存されます。
次のコードを使用するには、Development
環境で実行されているアプリの Server
プロジェクトのルートに、Development/unsafe_uploads
フォルダーを作成します。 この例では、ファイルが保存されるパスの一部としてアプリの環境を使用するため、テストと運用で他の環境を使用する場合は、追加のフォルダーが必要です。 たとえば、Staging
環境用には Staging/unsafe_uploads
フォルダーを作成します。 Production
環境用には Production/unsafe_uploads
フォルダーを作成します。
警告
この例では、内容をスキャンせずにファイルを保存します。 運用シナリオでは、アップロードされたファイルにウイルス対策またはマルウェア対策スキャナー API を使用してから、ダウンロードまたは他のシステムで使用できるようにしています。 サーバーにファイルをアップロードする場合のセキュリティに関する考慮事項の詳細については、「ASP.NET Core でファイルをアップロードする」を参照してください。
Controllers/FilesaveController.cs
:
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using BlazorSample.Shared;
[ApiController]
[Route("[controller]")]
public class FilesaveController : ControllerBase
{
private readonly IWebHostEnvironment env;
private readonly ILogger<FilesaveController> logger;
public FilesaveController(IWebHostEnvironment env,
ILogger<FilesaveController> logger)
{
this.env = env;
this.logger = logger;
}
[HttpPost]
public async Task<ActionResult<IList<UploadResult>>> PostFile(
[FromForm] IEnumerable<IFormFile> files)
{
var maxAllowedFiles = 3;
long maxFileSize = 1024 * 1024 * 15;
var filesProcessed = 0;
var resourcePath = new Uri($"{Request.Scheme}://{Request.Host}/");
List<UploadResult> uploadResults = new();
foreach (var file in files)
{
var uploadResult = new UploadResult();
string trustedFileNameForFileStorage;
var untrustedFileName = file.FileName;
uploadResult.FileName = untrustedFileName;
var trustedFileNameForDisplay =
WebUtility.HtmlEncode(untrustedFileName);
if (filesProcessed < maxAllowedFiles)
{
if (file.Length == 0)
{
logger.LogInformation("{FileName} length is 0 (Err: 1)",
trustedFileNameForDisplay);
uploadResult.ErrorCode = 1;
}
else if (file.Length > maxFileSize)
{
logger.LogInformation("{FileName} of {Length} bytes is " +
"larger than the limit of {Limit} bytes (Err: 2)",
trustedFileNameForDisplay, file.Length, maxFileSize);
uploadResult.ErrorCode = 2;
}
else
{
try
{
trustedFileNameForFileStorage = Path.GetRandomFileName();
var path = Path.Combine(env.ContentRootPath,
env.EnvironmentName, "unsafe_uploads",
trustedFileNameForFileStorage);
await using FileStream fs = new(path, FileMode.Create);
await file.CopyToAsync(fs);
logger.LogInformation("{FileName} saved at {Path}",
trustedFileNameForDisplay, path);
uploadResult.Uploaded = true;
uploadResult.StoredFileName = trustedFileNameForFileStorage;
}
catch (IOException ex)
{
logger.LogError("{FileName} error on upload (Err: 3): {Message}",
trustedFileNameForDisplay, ex.Message);
uploadResult.ErrorCode = 3;
}
}
filesProcessed++;
}
else
{
logger.LogInformation("{FileName} not uploaded because the " +
"request exceeded the allowed {Count} of files (Err: 4)",
trustedFileNameForDisplay, maxAllowedFiles);
uploadResult.ErrorCode = 4;
}
uploadResults.Add(uploadResult);
}
return new CreatedResult(resourcePath, uploadResults);
}
}
進行状況を表示してファイルをアップロードする
次の例は、Blazor Server アプリで、アップロードの進行状況をユーザーに表示しながらファイルをアップロードする方法を示しています。
テスト アプリで以下の例を使用するには、次のようにします。
- アップロードした
Development
環境用のファイルを保存するためのフォルダーを作成します:Development/unsafe_uploads
。 - 最大ファイル サイズ (
maxFileSize
、次の例では 15 MB) と、許可されるファイルの最大数 (maxAllowedFiles
、次の例では 3) を構成します。 - 必要に応じて、バッファーを別の値 (次の例では 10 KB) に設定して、進行状況を報告する頻度を増やします。 パフォーマンスおよびセキュリティ上の懸念があるため、30 KB を超えるバッファーの使用はお勧めしません。
Pages/FileUpload3.razor
:
@page "/file-upload-3"
@using System
@using System.IO
@using Microsoft.AspNetCore.Hosting
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload3> Logger
@inject IWebHostEnvironment Environment
<h3>Upload Files</h3>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="@LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Progress: @string.Format("{0:P0}", progressPercent)</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private decimal progressPercent;
private async Task LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
progressPercent = 0;
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
var trustedFileName = Path.GetRandomFileName();
var path = Path.Combine(Environment.ContentRootPath,
Environment.EnvironmentName, "unsafe_uploads", trustedFileName);
await using FileStream writeStream = new(path, FileMode.Create);
using var readStream = file.OpenReadStream(maxFileSize);
var bytesRead = 0;
var totalRead = 0;
var buffer = new byte[1024 * 10];
while ((bytesRead = await readStream.ReadAsync(buffer)) != 0)
{
totalRead += bytesRead;
await writeStream.WriteAsync(buffer, 0, bytesRead);
progressPercent = Decimal.Divide(totalRead, file.Size);
StateHasChanged();
}
loadedFiles.Add(file);
}
catch (Exception ex)
{
Logger.LogError("File: {Filename} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
詳細については、次の API リソースを参照してください。
- FileStream: 同期および非同期両方の読み取り操作と書き込み操作をサポートするファイル用の Stream を提供します。
- FileStream.ReadAsync: 上記の
FileUpload3
コンポーネントは、ReadAsync を使用して非同期にストリームを読み取ります。 Read を使用してストリームを同期的に読み取る操作は、Razor コンポーネントではサポートされていません。
ファイル ストリーム
Blazor Server では、ファイルがストリームから読み取られるときに、ファイル データがサーバー上の .NET コードに SignalR 接続を介してストリームされます。 RemoteBrowserFileStreamOptions では、Blazor Server のファイル アップロード特性を構成することができます。
Blazor WebAssembly では、ファイル データはブラウザー内の .NET コードに直接ストリームされます。
ファイルを外部サービスにアップロードする
アプリでファイルのアップロード バイトを処理し、アプリのサーバーでアップロードされたファイルを受信する代わりに、クライアントから外部サービスにファイルを直接アップロードできます。 アプリは、必要に応じて外部サービスからファイルを安全に処理できます。 この方法により、悪意のある攻撃や潜在的なパフォーマンスの問題に対してアプリとそのサーバーが強化されます。
Azure Files、Azure Blob Storage、または次のような利点を持つサード パーティのサービスを使用するアプローチを検討してください。
- JavaScript クライアント ライブラリまたは REST API を使用して、クライアントから外部サービスにファイルを直接アップロードします。 たとえば、Azure には次のクライアント ライブラリと API が用意されています。
- クライアントによるファイルのアップロードごとにアプリ (サーバー側) によって生成されるユーザー委任の Shared Access Signature (SAS) トークンを使って、ユーザーのアップロードを承認します。 たとえば、Azure には次の SAS 機能があります。
- 自動冗長性とファイル共有のバックアップを提供します。
- クォータでアップロードを制限します。 Azure Blob Storage のクォータは、コンテナー レベルではなくアカウント レベルで設定されることに注意してください。 一方、Azure Files のクォータはファイル共有レベルであり、アップロードの制限をより適切に制御できる場合があります。 詳しくは、この一覧で前にリンクを示した Azure のドキュメントをご覧ください。
- サーバー側暗号化 (SSE) でファイルをセキュリティ保護します。
Azure Blob Storage と Azure Files について詳しくは、Azure Storage のドキュメントをご覧ください。