Отправка файлов в ASP.NET Core Blazor
Примечание.
Это не последняя версия этой статьи. В текущем выпуске см . версию .NET 9 этой статьи.
Предупреждение
Эта версия ASP.NET Core больше не поддерживается. Дополнительные сведения см. в политике поддержки .NET и .NET Core. В текущем выпуске см . версию .NET 9 этой статьи.
Внимание
Эта информация относится к предварительному выпуску продукта, который может быть существенно изменен до его коммерческого выпуска. Майкрософт не предоставляет никаких гарантий, явных или подразумеваемых, относительно приведенных здесь сведений.
В текущем выпуске см . версию .NET 9 этой статьи.
В этой статье объясняется, как отправлять файлы в Blazor с помощью компонента InputFile.
Отправка файлов
Предупреждение
Разрешая пользователям отправлять файлы, всегда следуйте рекомендациям по обеспечению безопасности. Подробные сведения см. в статье Отправка файлов в ASP.NET Core.
Используйте компонент InputFile для считывания файловых данных из браузера в код .NET. Компонент InputFile отрисовывает html-элемент <input>
типа file
для отправки одного файла. Добавьте атрибут multiple
, чтобы разрешить пользователю отправлять несколько файлов одновременно.
Выбор файла не является накопительным при использовании компонента InputFile или его базового HTML-тега <input type="file">
, поэтому к выбранному файлу нельзя добавить другие. Компонент всегда заменяет первоначально выбранный файл, поэтому нельзя ссылаться на ранее выбранные файлы.
Следующий компонент InputFile выполняет метод LoadFiles
, когда происходит событие OnChange (change
). Объект InputFileChangeEventArgs предоставляет доступ к списку выбранных файлов и сведениям о каждом из них.
<InputFile OnChange="LoadFiles" multiple />
@code {
private void LoadFiles(InputFileChangeEventArgs e)
{
...
}
}
Полученный код HTML:
<input multiple="" type="file" _bl_2="">
Примечание.
В примере выше атрибут _bl_2
элемента <input>
используется Blazor для внутренней обработки.
Чтобы считать данные из выбранного пользователем файла, вызовите IBrowserFile.OpenReadStream для файла и считайте данные из возвращенного потока. Дополнительные сведения см. в разделе Файловые потоки.
OpenReadStream принудительно задает максимальный размер в байтах для своего потока Stream. Чтение одного файла или нескольких файлов, превышающих 500 КБ, приводит к исключению. Это ограничение не позволяет разработчикам случайно считывать в память файлы большого размера. При необходимости можно указать больший размер с помощью параметра maxAllowedSize
для OpenReadStream.
Если необходим доступ к классу Stream, представляющему байты файла, используйте IBrowserFile.OpenReadStream. Избегайте считывания сразу всего входящего файлового потока непосредственно в память. Например, не копируйте все байты файлов в MemoryStream и не считывайте сразу весь поток в массив байтов. Эти подходы могут привести к снижению производительности приложения и потенциальному риску типа "отказ в обслуживании" (DoS), особенно для компонентов на стороне сервера. Вместо этого попробуйте применить один из следующих подходов:
- Копируйте поток непосредственно в файл на диске, не считывая его в память. Обратите внимание, что Blazor приложения, выполняющие код на сервере, не могут напрямую получить доступ к файловой системе клиента.
- Отправляйте файлы из клиента непосредственно во внешнюю службу. Дополнительные сведения см. в разделе Отправка файлов во внешнюю службу.
В следующих примерах browserFile
представляет загруженный файл и реализует IBrowserFile. Рабочие реализации показаны в компонентах IBrowserFile отправки файлов далее в этой статье.
Поддерживается: рекомендуется использовать следующий подход, так как файл предоставляется непосредственно потребителю, что FileStream создает файл Stream по указанному пути:
await using FileStream fs = new(path, FileMode.Create);
await browserFile.OpenReadStream().CopyToAsync(fs);
await blobContainerClient.UploadBlobAsync(
trustedFileName, browserFile.OpenReadStream());
Не рекомендуется: не рекомендуется использовать следующий подход, так как содержимое файла Stream считывается в String памяти (reader
):
var reader =
await new StreamReader(browserFile.OpenReadStream()).ReadToEndAsync();
Не рекомендуется: для Microsoft Хранилище BLOB-объектов Azure не рекомендуется использовать следующий подход, так как содержимое файла Stream копируется в MemoryStream память (memoryStream
) перед вызовом:UploadBlobAsync
var memoryStream = new MemoryStream();
await browserFile.OpenReadStream().CopyToAsync(memoryStream);
await blobContainerClient.UploadBlobAsync(
trustedFileName, memoryStream));
Компонент, получающий файл изображения, может вызвать для файла удобный метод BrowserFileExtensions.RequestImageFileAsync, чтобы изменить размер данных изображения в среде выполнения JavaScript браузера до передачи изображения в приложение. Варианты использования вызова RequestImageFileAsync лучше всего подходят для приложений Blazor WebAssembly.
Пользователи контейнеров Autofac Inversion of Control (IoC)
Если вы используете контейнер autofac Inversion of Control (IoC) вместо встроенного контейнера внедрения зависимостей core ASP.NET, установите значение DisableImplicitFromServicesParameters true
в параметрах концентратора обработчика каналов на стороне сервера. Дополнительные сведения см. в разделе FileUpload: не было получено никаких данных в выделенное время (dotnet/aspnetcore
#38842).
Ограничения на чтение и отправку файлов
На стороне сервера или на стороне клиента нет ограничения размера файла или чтения или отправки специально для InputFile компонента. Однако клиентская сторона Blazor считывает байты файла в один буфер массива JavaScript при маршализации данных из JavaScript в C#, что ограничено 2 ГБ или доступной памятью устройства. Отправка больших файлов (> 250 МБ) может завершиться ошибкой InputFile для отправки на стороне клиента с помощью компонента. Дополнительные сведения см. в следующих обсуждениях:
Максимальный поддерживаемый размер файла компонента InputFile составляет 2 ГБ. Кроме того, клиент Blazor считывает байты файла в один буфер массива JavaScript при маршализации данных из JavaScript в C#, что ограничено 2 ГБ или доступной памятью устройства. Отправка больших файлов (> 250 МБ) может завершиться ошибкой InputFile для отправки на стороне клиента с помощью компонента. Дополнительные сведения см. в следующих обсуждениях:
- Компонент Blazor InputFile должен обрабатывать фрагментирование при отправке файла (dotnet/runtime #84685)
- Отправка потоковой передачи запросов через обработчик http (dotnet/runtime #36634)
Для отправки больших клиентских файлов, которые завершаются сбоем при попытке использования InputFile компонента, рекомендуется фрагментировать большие файлы с пользовательским компонентом с помощью нескольких запросов диапазона HTTP вместо использования InputFile компонента.
В настоящее время работа запланирована на .NET 9 (конец 2024 г.) для решения ограничения на отправку файлов на стороне клиента.
Примеры
В следующих примерах показано несколько отправки файлов в компоненте. InputFileChangeEventArgs.GetMultipleFiles позволяет считывать несколько файлов. Укажите максимальное число файлов, чтобы злоумышленник не мог отправить их больше, чем ожидается приложением. InputFileChangeEventArgs.File позволяет считывать только первый файл, если отправка нескольких файлов не поддерживается.
InputFileChangeEventArgs находится в пространстве имен Microsoft.AspNetCore.Components.Forms, которое обычно является одним из пространств имен в файле _Imports.razor
приложения. Когда пространство имен присутствует в _Imports.razor
файле, оно предоставляет членам API доступ к компонентам приложения.
Пространства имен в файле _Imports.razor
не применяются к файлам C# (.cs
). Для файлов C# требуется явная using
директива в верхней части файла класса:
using Microsoft.AspNetCore.Components.Forms;
Для тестирования компонентов отправки файлов можно создать тестовые файлы любого размера с помощью PowerShell:
$out = new-object byte[] {SIZE}; (new-object Random).NextBytes($out); [IO.File]::WriteAllBytes('{PATH}', $out)
В предыдущей команде используются следующие параметры:
- Заполнитель
{SIZE}
— это размер файла в байтах (например,2097152
для файла размером 2 МБ). - Заполнитель
{PATH}
— это путь и файл с расширением файла (например,D:/test_files/testfile2MB.txt
).
Пример отправки файлов на стороне сервера
Чтобы использовать следующий код, создайте Development/unsafe_uploads
папку в корне приложения, работающего в Development
среде.
Пример использует среду приложения в составе пути, по которому сохраняются файлы, поэтому, если для тестирования и работы используются другие среды, требуются дополнительные папки. Например, создайте папку Staging/unsafe_uploads
для среды Staging
. Создайте папку Production/unsafe_uploads
для среды Production
.
Предупреждение
В этом примере файлы сохраняются без проверки их содержимого, а указания, приведенные в этой статье, не учитывают дополнительные рекомендации по обеспечению безопасности для отправляемых файлов. В промежуточных и рабочих системах отключите разрешение на выполнение для папки отправки и проверьте файлы с помощью API сканера антивирусной или антивредоносной программы сразу после отправки. Подробные сведения см. в статье Отправка файлов в ASP.NET Core.
FileUpload1.razor
:
@page "/file-upload-1"
@using System
@using System.IO
@using Microsoft.AspNetCore.Hosting
@inject ILogger<FileUpload1> Logger
@inject IWebHostEnvironment Environment
<PageTitle>File Upload 1</PageTitle>
<h1>File Upload Example 1</h1>
<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
{
var trustedFileName = Path.GetRandomFileName();
var path = Path.Combine(Environment.ContentRootPath,
Environment.EnvironmentName, "unsafe_uploads",
trustedFileName);
await using FileStream fs = new(path, FileMode.Create);
await file.OpenReadStream(maxFileSize).CopyToAsync(fs);
loadedFiles.Add(file);
Logger.LogInformation(
"Unsafe Filename: {UnsafeFilename} File saved: {Filename}",
file.Name, trustedFileName);
}
catch (Exception ex)
{
Logger.LogError("File: {Filename} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
@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 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 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, который отправляет файл на сервер или в службу, см. в следующих разделах:
- Отправка файлов на сервер с помощью отрисовки на стороне клиента (CSR)
- Отправка файлов во внешнюю службу
Компонент предполагает, что режим интерактивной отрисовки WebAssembly (InteractiveWebAssembly
) наследуется от родительского компонента или применяется глобально к приложению.
@page "/file-upload-1"
@inject ILogger<FileUpload1> Logger
<PageTitle>File Upload 1</PageTitle>
<h1>File Upload Example 1</h1>
<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;
}
}
@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;
}
}
@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;
}
}
@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 возвращает метаданные, предоставленные браузером, как свойства. Используйте эти метаданные для предварительной проверки.
Никогда не доверяйте значениям предыдущих свойств, особенно Name свойству для отображения в пользовательском интерфейсе. Обрабатывать все предоставленные пользователем данные как значительный риск безопасности для приложения, сервера и сети. Подробные сведения см. в статье Отправка файлов в ASP.NET Core.
Отправка файлов на сервер с отрисовкой на стороне сервера
Этот раздел относится к компонентам интерактивного сервера в Blazor Web Appприложениях или Blazor Server приложениях.
В следующем примере показано, как отправлять файлы из серверного приложения в контроллер веб-API серверной части в отдельном приложении, возможно, на отдельном сервере.
В файле серверного приложения Program
добавьте IHttpClientFactory и связанные службы, позволяющие приложению создавать HttpClient экземпляры:
builder.Services.AddHttpClient();
Дополнительные сведения см. в статье Выполнение HTTP-запросов с помощью IHttpClientFactory в ASP.NET Core.
В примерах в этом разделе:
- веб-API выполняется по URL-адресу
https://localhost:5001
; - Серверное приложение выполняется по URL-адресу:
https://localhost:5003
при тестировании вышеуказанные URL-адреса настраиваются в файлах Properties/launchSettings.json
проекта.
UploadResult
Следующий класс поддерживает результат отправленного файла. Если файл не удается отправить на сервер, пользователю отображается код ошибки, возвращенный в 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; }
}
В целях безопасности в приложениях для рабочей среды рекомендуем не отправлять клиентам сообщения об ошибках, которые могут раскрывать конфиденциальные сведения о приложении, сервере или сети. Предоставление подробных сообщений об ошибках может помочь злоумышленникам в атаках на приложение, сервер или сеть. Пример кода в этом разделе отправляет обратно только код ошибки (int
) для отображения на клиентской стороне компонента, если возникает ошибка на стороне сервера. Если пользователю нужна помощь с передачей файла, он предоставляет код ошибки специалистам в рамках запроса в службу поддержки. При этом точная причина ошибки остается для него неизвестной.
LazyBrowserFileStream
Следующий класс определяет пользовательский тип потока, который неявно вызывается OpenReadStream непосредственно перед запросом первых байтов потока. Поток не передается из браузера на сервер, пока не начнется чтение потока в .NET.
LazyBrowserFileStream.cs
:
using Microsoft.AspNetCore.Components.Forms;
using System.Diagnostics.CodeAnalysis;
namespace BlazorSample;
internal sealed class LazyBrowserFileStream(IBrowserFile file, int maxAllowedSize)
: Stream
{
private readonly IBrowserFile file = file;
private readonly int maxAllowedSize = maxAllowedSize;
private Stream? underlyingStream;
private bool isDisposed;
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => file.Size;
public override long Position
{
get => underlyingStream?.Position ?? 0;
set => throw new NotSupportedException();
}
public override void Flush() => underlyingStream?.Flush();
public override Task<int> ReadAsync(byte[] buffer, int offset, int count,
CancellationToken cancellationToken)
{
EnsureStreamIsOpen();
return underlyingStream.ReadAsync(buffer, offset, count, cancellationToken);
}
public override ValueTask<int> ReadAsync(Memory<byte> buffer,
CancellationToken cancellationToken = default)
{
EnsureStreamIsOpen();
return underlyingStream.ReadAsync(buffer, cancellationToken);
}
[MemberNotNull(nameof(underlyingStream))]
private void EnsureStreamIsOpen() =>
underlyingStream ??= file.OpenReadStream(maxAllowedSize);
protected override void Dispose(bool disposing)
{
if (isDisposed)
{
return;
}
underlyingStream?.Dispose();
isDisposed = true;
base.Dispose(disposing);
}
public override int Read(byte[] buffer, int offset, int count)
=> throw new NotSupportedException();
public override long Seek(long offset, SeekOrigin origin)
=> throw new NotSupportedException();
public override void SetLength(long value)
=> throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count)
=> throw new NotSupportedException();
}
using Microsoft.AspNetCore.Components.Forms;
using System.Diagnostics.CodeAnalysis;
namespace BlazorSample;
internal sealed class LazyBrowserFileStream : Stream
{
private readonly IBrowserFile file;
private readonly int maxAllowedSize;
private Stream? underlyingStream;
private bool isDisposed;
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => file.Size;
public override long Position
{
get => underlyingStream?.Position ?? 0;
set => throw new NotSupportedException();
}
public LazyBrowserFileStream(IBrowserFile file, int maxAllowedSize)
{
this.file = file;
this.maxAllowedSize = maxAllowedSize;
}
public override void Flush()
{
underlyingStream?.Flush();
}
public override Task<int> ReadAsync(byte[] buffer, int offset, int count,
CancellationToken cancellationToken)
{
EnsureStreamIsOpen();
return underlyingStream.ReadAsync(buffer, offset, count, cancellationToken);
}
public override ValueTask<int> ReadAsync(Memory<byte> buffer,
CancellationToken cancellationToken = default)
{
EnsureStreamIsOpen();
return underlyingStream.ReadAsync(buffer, cancellationToken);
}
[MemberNotNull(nameof(underlyingStream))]
private void EnsureStreamIsOpen()
{
underlyingStream ??= file.OpenReadStream(maxAllowedSize);
}
protected override void Dispose(bool disposing)
{
if (isDisposed)
{
return;
}
underlyingStream?.Dispose();
isDisposed = true;
base.Dispose(disposing);
}
public override int Read(byte[] buffer, int offset, int count)
=> throw new NotSupportedException();
public override long Seek(long offset, SeekOrigin origin)
=> throw new NotSupportedException();
public override void SetLength(long value)
=> throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count)
=> throw new NotSupportedException();
}
using Microsoft.AspNetCore.Components.Forms;
using System.Diagnostics.CodeAnalysis;
namespace BlazorSample;
internal sealed class LazyBrowserFileStream : Stream
{
private readonly IBrowserFile file;
private readonly int maxAllowedSize;
private Stream? underlyingStream;
private bool isDisposed;
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => file.Size;
public override long Position
{
get => underlyingStream?.Position ?? 0;
set => throw new NotSupportedException();
}
public LazyBrowserFileStream(IBrowserFile file, int maxAllowedSize)
{
this.file = file;
this.maxAllowedSize = maxAllowedSize;
}
public override void Flush()
{
underlyingStream?.Flush();
}
public override Task<int> ReadAsync(byte[] buffer, int offset, int count,
CancellationToken cancellationToken)
{
EnsureStreamIsOpen();
return underlyingStream.ReadAsync(buffer, offset, count, cancellationToken);
}
public override ValueTask<int> ReadAsync(Memory<byte> buffer,
CancellationToken cancellationToken = default)
{
EnsureStreamIsOpen();
return underlyingStream.ReadAsync(buffer, cancellationToken);
}
[MemberNotNull(nameof(underlyingStream))]
private void EnsureStreamIsOpen()
{
underlyingStream ??= file.OpenReadStream(maxAllowedSize);
}
protected override void Dispose(bool disposing)
{
if (isDisposed)
{
return;
}
underlyingStream?.Dispose();
isDisposed = true;
base.Dispose(disposing);
}
public override int Read(byte[] buffer, int offset, int count)
=> throw new NotSupportedException();
public override long Seek(long offset, SeekOrigin origin)
=> throw new NotSupportedException();
public override void SetLength(long value)
=> throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count)
=> throw new NotSupportedException();
}
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Forms;
namespace BlazorSample;
internal sealed class LazyBrowserFileStream : Stream
{
private readonly IBrowserFile file;
private readonly int maxAllowedSize;
private Stream? underlyingStream;
private bool isDisposed;
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => file.Size;
public override long Position
{
get => underlyingStream?.Position ?? 0;
set => throw new NotSupportedException();
}
public LazyBrowserFileStream(IBrowserFile file, int maxAllowedSize)
{
this.file = file;
this.maxAllowedSize = maxAllowedSize;
}
public override void Flush()
{
underlyingStream?.Flush();
}
public override Task<int> ReadAsync(byte[] buffer, int offset, int count,
CancellationToken cancellationToken)
{
EnsureStreamIsOpen();
return underlyingStream.ReadAsync(buffer, offset, count, cancellationToken);
}
public override ValueTask<int> ReadAsync(Memory<byte> buffer,
CancellationToken cancellationToken = default)
{
EnsureStreamIsOpen();
return underlyingStream.ReadAsync(buffer, cancellationToken);
}
[MemberNotNull(nameof(underlyingStream))]
private void EnsureStreamIsOpen()
{
underlyingStream ??= file.OpenReadStream(maxAllowedSize);
}
protected override void Dispose(bool disposing)
{
if (isDisposed)
{
return;
}
underlyingStream?.Dispose();
isDisposed = true;
base.Dispose(disposing);
}
public override int Read(byte[] buffer, int offset, int count)
=> throw new NotSupportedException();
public override long Seek(long offset, SeekOrigin origin)
=> throw new NotSupportedException();
public override void SetLength(long value)
=> throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count)
=> throw new NotSupportedException();
}
Приведенный ниже компонент FileUpload2
делает следующее.
- Разрешает пользователям отправлять файлы из клиента.
- Отображает ненадежное или небезопасное имя файла, предоставленное клиентом в пользовательском интерфейсе. Razor автоматически кодирует в HTML ненадежное или небезопасное имя файла для безопасного вывода в пользовательском интерфейсе.
Предупреждение
Не доверяйте именам файлов, указываемым клиентами, в следующих случаях:
- при сохранении файла в файловой системе или службе;
- при отображении в пользовательских интерфейсах, которые не кодируют имена файлов автоматически или в соответствии с кодом разработчика.
Дополнительные сведения о вопросах безопасности при отправке файлов на сервер см. в разделе Отправка файлов в ASP.NET Core.
FileUpload2.razor
:
@page "/file-upload-2"
@using System.Net.Http.Headers
@using System.Text.Json
@inject IHttpClientFactory ClientFactory
@inject ILogger<FileUpload2> Logger
<PageTitle>File Upload 2</PageTitle>
<h1>File Upload Example 2</h1>
<p>
This example requires a backend server API to function. For more information,
see the <em>Upload files to a server</em> section
of the <em>ASP.NET Core Blazor file uploads</em> article.
</p>
<p>
<label>
Upload up to @maxAllowedFiles files:
<InputFile OnChange="OnInputFileChange" multiple />
</label>
</p>
@if (files.Any())
{
<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;
int maxFileSize = 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
{
files.Add(new() { Name = file.Name });
var stream = new LazyBrowserFileStream(file, maxFileSize);
var fileContent = new StreamContent(stream);
fileContent.Headers.ContentType =
new MediaTypeHeaderValue(file.ContentType);
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; }
}
}
@page "/file-upload-2"
@using System.Net.Http.Headers
@using System.Text.Json
@inject IHttpClientFactory ClientFactory
@inject ILogger<FileUpload2> Logger
<h1>File Upload Example 2</h1>
<p>
This example requires a backend server API to function. For more information,
see the <em>Upload files to a server</em> section
of the <em>ASP.NET Core Blazor file uploads</em> article.
</p>
<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;
int maxFileSize = 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
{
files.Add(new() { Name = file.Name });
var stream = new LazyBrowserFileStream(file, maxFileSize);
var fileContent = new StreamContent(stream);
fileContent.Headers.ContentType =
new MediaTypeHeaderValue(file.ContentType);
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; }
}
}
@page "/file-upload-2"
@using System.Net.Http.Headers
@using System.Text.Json
@inject IHttpClientFactory ClientFactory
@inject ILogger<FileUpload2> Logger
<h1>File Upload Example 2</h1>
<p>
This example requires a backend server API to function. For more information,
see the <em>Upload files to a server</em> section
of the <em>ASP.NET Core Blazor file uploads</em> article.
</p>
<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;
int maxFileSize = 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
{
files.Add(new() { Name = file.Name });
var stream = new LazyBrowserFileStream(file, maxFileSize);
var fileContent = new StreamContent(stream);
fileContent.Headers.ContentType =
new MediaTypeHeaderValue(file.ContentType);
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; }
}
}
@page "/file-upload-2"
@using System.Net.Http.Headers
@using System.Text.Json
@using Microsoft.Extensions.Logging
@inject IHttpClientFactory ClientFactory
@inject ILogger<FileUpload2> Logger
<h1>File Upload Example 2</h1>
<p>
This example requires a backend server API to function. For more information,
see the <em>Upload files to a server</em> section
of the <em>ASP.NET Core Blazor file uploads</em> article.
</p>
<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;
int maxFileSize = 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
{
files.Add(new() { Name = file.Name });
var stream = new LazyBrowserFileStream(file, maxFileSize);
var fileContent = new StreamContent(stream);
fileContent.Headers.ContentType =
new MediaTypeHeaderValue(file.ContentType);
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; }
}
}
Если компонент ограничивает отправку файлов в один файл одновременно или если компонент принимает только интерактивную отрисовку на стороне клиента (CSR), InteractiveWebAssembly
компонент может избежать использования и использования LazyBrowserFileStream
.Stream Ниже показаны изменения компонента FileUpload2
:
- var stream = new LazyBrowserFileStream(file, maxFileSize);
- var fileContent = new StreamContent(stream);
+ var fileContent = new StreamContent(file.OpenReadStream(maxFileSize));
LazyBrowserFileStream
Удалите класс (LazyBrowserFileStream.cs
), так как он не используется.
Если компонент ограничивает отправку файла в один файл за раз, компонент может избежать использования LazyBrowserFileStream
и использования Streamфайла. Ниже показаны изменения компонента FileUpload2
:
- var stream = new LazyBrowserFileStream(file, maxFileSize);
- var fileContent = new StreamContent(stream);
+ var fileContent = new StreamContent(file.OpenReadStream(maxFileSize));
LazyBrowserFileStream
Удалите класс (LazyBrowserFileStream.cs
), так как он не используется.
Следующий контроллер в проекте веб-API сохраняет файлы, отправленные из клиента.
Внимание
Контроллер в этом разделе предназначен для использования в проекте веб-API, отдельном от приложения Blazor. Веб-API должен устранять атаки межсайтовых запросов forgery (XSRF/CSRF) при проверке подлинности пользователей отправки файлов.
Примечание.
Значения формы привязки с [FromForm]
атрибутом изначально не поддерживаются для минимальных API в ASP.NET Core в .NET 6. Следовательно, следующий пример контроллера Filesave
нельзя преобразовать для использования минимальных API. Поддержка привязки из значений формы с минимальными API доступна в ASP.NET Core в .NET 7 или более поздней версии.
Чтобы использовать следующий код, создайте папку Development/unsafe_uploads
в корне проекта веб-API для приложения, работающего в среде Development
.
Пример использует среду приложения в составе пути, по которому сохраняются файлы, поэтому, если для тестирования и работы используются другие среды, требуются дополнительные папки. Например, создайте папку Staging/unsafe_uploads
для среды Staging
. Создайте папку Production/unsafe_uploads
для среды Production
.
Предупреждение
В этом примере файлы сохраняются без проверки их содержимого, а указания, приведенные в этой статье, не учитывают дополнительные рекомендации по обеспечению безопасности для отправляемых файлов. В промежуточных и рабочих системах отключите разрешение на выполнение для папки отправки и проверьте файлы с помощью API сканера антивирусной или антивредоносной программы сразу после отправки. Подробные сведения см. в статье Отправка файлов в ASP.NET Core.
Controllers/FilesaveController.cs
:
using System.Net;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("[controller]")]
public class FilesaveController(
IHostEnvironment env, ILogger<FilesaveController> logger)
: ControllerBase
{
[HttpPost]
public async Task<ActionResult<IList<UploadResult>>> PostFile(
[FromForm] IEnumerable<IFormFile> files)
{
var maxAllowedFiles = 3;
long maxFileSize = 1024 * 15;
var filesProcessed = 0;
var resourcePath = new Uri($"{Request.Scheme}://{Request.Host}/");
List<UploadResult> uploadResults = [];
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 вызывается для создания защищенного имени файла. Никогда не доверяйте имени файла, предоставленному браузером, так как кибератака может выбрать существующее имя файла, которое перезаписывает существующий файл или отправляет путь, который пытается записать за пределами приложения.
Серверные приложения должны регистрировать службы контроллеров и конечные точки контроллера карты. Дополнительные сведения см. в статье "Маршрутизация действий контроллера" в ASP.NET Core.
Отправка файлов на сервер с помощью отрисовки на стороне клиента (CSR)
Этот раздел относится к компонентам клиентской отрисовки (CSR) в Blazor Web Appприложениях или Blazor WebAssembly приложениях.
В следующем примере показано, как отправлять файлы в контроллер веб-API серверной части в отдельном приложении, возможно, на отдельном сервере, от компонента в компоненте, Blazor Web App который принимает CSR или компонент в Blazor WebAssembly приложении.
UploadResult
Следующий класс поддерживает результат отправленного файла. Если файл не удается отправить на сервер, пользователю отображается код ошибки, возвращенный в 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; }
}
Примечание.
UploadResult
Предыдущий класс можно совместно использовать между клиентскими и серверными проектами. Когда клиентские и серверные проекты совместно используют класс, добавьте импорт в файл каждого проекта _Imports.razor
для общего проекта. Например:
@using BlazorSample.Shared
Приведенный ниже компонент FileUpload2
делает следующее.
- Разрешает пользователям отправлять файлы из клиента.
- Отображает ненадежное или небезопасное имя файла, предоставленное клиентом в пользовательском интерфейсе. Razor автоматически кодирует в HTML ненадежное или небезопасное имя файла для безопасного вывода в пользовательском интерфейсе.
В целях безопасности в приложениях для рабочей среды рекомендуем не отправлять клиентам сообщения об ошибках, которые могут раскрывать конфиденциальные сведения о приложении, сервере или сети. Предоставление подробных сообщений об ошибках может помочь злоумышленникам в атаках на приложение, сервер или сеть. Пример кода в этом разделе отправляет обратно только код ошибки (int
) для отображения на клиентской стороне компонента, если возникает ошибка на стороне сервера. Если пользователю нужна помощь с передачей файла, он предоставляет код ошибки специалистам в рамках запроса в службу поддержки. При этом точная причина ошибки остается для него неизвестной.
Предупреждение
Не доверяйте именам файлов, указываемым клиентами, в следующих случаях:
- при сохранении файла в файловой системе или службе;
- при отображении в пользовательских интерфейсах, которые не кодируют имена файлов автоматически или в соответствии с кодом разработчика.
Дополнительные сведения о вопросах безопасности при отправке файлов на сервер см. в разделе Отправка файлов в ASP.NET Core.
Blazor Web App В основном проекте добавьте IHttpClientFactory и связанные службы в файл проектаProgram
:
builder.Services.AddHttpClient();
Службы HttpClient
необходимо добавить в основной проект, так как клиентский компонент предварительно создается на сервере. Если вы отключите предварительное отображение для следующего компонента, вам не нужно HttpClient
предоставлять службы в основном приложении и не нужно добавлять предыдущую строку в основной проект.
Дополнительные сведения о добавлении HttpClient
служб в приложение ASP.NET Core см. в статье "Создание HTTP-запросов с помощью IHttpClientFactory" в ASP.NET Core.
Клиентский проект (.Client
) Blazor Web App должен также зарегистрировать HttpClient запросы HTTP POST на контроллер веб-API серверной части. Подтвердите или добавьте следующее в файл клиентского проекта Program
:
builder.Services.AddScoped(sp =>
new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
В предыдущем примере задается базовый адрес (IWebAssemblyHostEnvironment.BaseAddress), который получает базовый адрес builder.HostEnvironment.BaseAddress
для приложения и обычно является производным от <base>
значения тега href
на хост-странице. Если вы вызываете внешний веб-API, задайте универсальный код ресурса (URI) базовым адресом веб-API.
Укажите атрибут режима интерактивной отрисовки WebAssembly в верхней части следующего компонента:Blazor Web App
@rendermode InteractiveWebAssembly
FileUpload2.razor
:
@page "/file-upload-2"
@using System.Linq
@using System.Net.Http.Headers
@inject HttpClient Http
@inject ILogger<FileUpload2> Logger
<PageTitle>File Upload 2</PageTitle>
<h1>File Upload Example 2</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 * 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
{
files.Add(new() { Name = file.Name });
var fileContent = new StreamContent(file.OpenReadStream(maxFileSize));
fileContent.Headers.ContentType =
new MediaTypeHeaderValue(file.ContentType);
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; }
}
}
@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 * 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
{
files.Add(new() { Name = file.Name });
var fileContent = new StreamContent(file.OpenReadStream(maxFileSize));
fileContent.Headers.ContentType =
new MediaTypeHeaderValue(file.ContentType);
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; }
}
}
@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 * 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
{
files.Add(new() { Name = file.Name });
var fileContent = new StreamContent(file.OpenReadStream(maxFileSize));
fileContent.Headers.ContentType =
new MediaTypeHeaderValue(file.ContentType);
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; }
}
}
@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 * 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
{
files.Add(new() { Name = file.Name });
var fileContent = new StreamContent(file.OpenReadStream(maxFileSize));
fileContent.Headers.ContentType =
new MediaTypeHeaderValue(file.ContentType);
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; }
}
}
Следующий контроллер в серверном проекте сохраняет загруженные файлы из клиента.
Примечание.
Значения формы привязки с [FromForm]
атрибутом изначально не поддерживаются для минимальных API в ASP.NET Core в .NET 6. Следовательно, следующий пример контроллера Filesave
нельзя преобразовать для использования минимальных API. Поддержка привязки из значений формы с минимальными API доступна в ASP.NET Core в .NET 7 или более поздней версии.
Чтобы использовать следующий код, создайте Development/unsafe_uploads
папку в корне проекта на стороне сервера для приложения, работающего Development
в среде.
Пример использует среду приложения в составе пути, по которому сохраняются файлы, поэтому, если для тестирования и работы используются другие среды, требуются дополнительные папки. Например, создайте папку Staging/unsafe_uploads
для среды Staging
. Создайте папку Production/unsafe_uploads
для среды Production
.
Предупреждение
В этом примере файлы сохраняются без проверки их содержимого, а указания, приведенные в этой статье, не учитывают дополнительные рекомендации по обеспечению безопасности для отправляемых файлов. В промежуточных и рабочих системах отключите разрешение на выполнение для папки отправки и проверьте файлы с помощью API сканера антивирусной или антивредоносной программы сразу после отправки. Подробные сведения см. в статье Отправка файлов в ASP.NET Core.
В следующем примере обновите пространство имен общего проекта, чтобы оно соответствовало общему проекту, если общий проект предоставляет UploadResult
класс.
Controllers/FilesaveController.cs
:
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using BlazorSample.Shared;
[ApiController]
[Route("[controller]")]
public class FilesaveController(
IHostEnvironment env, ILogger<FilesaveController> logger)
: ControllerBase
{
[HttpPost]
public async Task<ActionResult<IList<UploadResult>>> PostFile(
[FromForm] IEnumerable<IFormFile> files)
{
var maxAllowedFiles = 3;
long maxFileSize = 1024 * 15;
var filesProcessed = 0;
var resourcePath = new Uri($"{Request.Scheme}://{Request.Host}/");
List<UploadResult> uploadResults = [];
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 вызывается для создания защищенного имени файла. Никогда не доверяйте имени файла, предоставленному браузером, так как кибератака может выбрать существующее имя файла, которое перезаписывает существующий файл или отправляет путь, который пытается записать за пределами приложения.
Серверные приложения должны регистрировать службы контроллеров и конечные точки контроллера карты. Дополнительные сведения см. в статье "Маршрутизация действий контроллера" в ASP.NET Core.
Отмена отправки файла
Компонент отправки файла может определить, когда пользователь отменил отправку с помощью CancellationToken при вызове в IBrowserFile.OpenReadStream или StreamReader.ReadAsync.
Создайте CancellationTokenSource для компонента InputFile
. В начале метода OnInputFileChange
проверьте, выполняется ли предыдущая отправка.
Если выполняется отправка файла:
- Вызовите Cancelдля предыдущей отправки.
- Создайте новый объект CancellationTokenSource для следующей отправки и передайте CancellationTokenSource.Token в OpenReadStream или ReadAsync.
Отправка файлов на стороне сервера с прогрессом
В следующем примере показано, как отправлять файлы в серверное приложение с выполнением отправки, отображаемым пользователю.
Чтобы использовать следующий пример в тестовом приложении:
- Создайте папку для сохранения отправленных файлов для среды
Development
:Development/unsafe_uploads
. - Настройте максимальный размер файла (
maxFileSize
, 15 КБ в следующем примере) и максимальное количество допустимых файлов (maxAllowedFiles
, три в следующем примере). - Задайте для буфера другое значение (10 КБ в следующем примере), если это необходимо, для повышения детализации отчета о ходе выполнения. Мы не рекомендуем использовать буфер размером более 30 КБ из-за проблем с производительностью и безопасностью.
Предупреждение
В этом примере файлы сохраняются без проверки их содержимого, а указания, приведенные в этой статье, не учитывают дополнительные рекомендации по обеспечению безопасности для отправляемых файлов. В промежуточных и рабочих системах отключите разрешение на выполнение для папки отправки и проверьте файлы с помощью API сканера антивирусной или антивредоносной программы сразу после отправки. Подробные сведения см. в статье Отправка файлов в ASP.NET Core.
FileUpload3.razor
:
@page "/file-upload-3"
@inject ILogger<FileUpload3> Logger
@inject IWebHostEnvironment Environment
<PageTitle>File Upload 3</PageTitle>
<h1>File Upload Example 3</h1>
<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 * 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);
Logger.LogInformation(
"Unsafe Filename: {UnsafeFilename} File saved: {Filename}",
file.Name, trustedFileName);
}
catch (Exception ex)
{
Logger.LogError("File: {FileName} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
@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 * 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;
}
}
@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 * 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;
}
}
@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 * 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.
Файловые потоки
При интерактивности сервера данные файлов передаются через SignalR подключение к коду .NET на сервере по мере чтения файла.
RemoteBrowserFileStreamOptions позволяет настраивать характеристики отправки файлов.
Для компонента WebAssembly данные файлов передаются непосредственно в код .NET в браузере.
Отправка предварительного просмотра образа
Для предварительного просмотра изображений для отправки изображений начните с добавления InputFile
компонента со ссылкой на компонент и обработчиком OnChange
:
<InputFile @ref="inputFile" OnChange="ShowPreview" />
Добавьте элемент изображения со ссылкой на элемент, который служит заполнителем для предварительного просмотра образа:
<img @ref="previewImageElem" />
Добавьте связанные ссылки:
@code {
private InputFile? inputFile;
private ElementReference previewImageElem;
}
В JavaScript добавьте функцию, вызванную HTML input
и img
элементом, выполняющим следующие действия:
- Извлекает выбранный файл.
- Создает URL-адрес объекта с
createObjectURL
помощью . - Задает прослушиватель событий для отзыва URL-адреса объекта после
revokeObjectURL
загрузки образа, поэтому память не утечка. img
Задает источник элемента для отображения изображения.
window.previewImage = (inputElem, imgElem) => {
const url = URL.createObjectURL(inputElem.files[0]);
imgElem.addEventListener('load', () => URL.revokeObjectURL(url), { once: true });
imgElem.src = url;
}
Наконец, используйте внедренный IJSRuntime обработчик для добавления OnChange
обработчика, который вызывает функцию JavaScript:
@inject IJSRuntime JS
...
@code {
...
private async Task ShowPreview() => await JS.InvokeVoidAsync(
"previewImage", inputFile!.Element, previewImageElem);
}
Приведенный выше пример предназначен для отправки одного изображения. Этот подход можно расширить для поддержки multiple
изображений.
В следующем FileUpload4
компоненте показан полный пример.
FileUpload4.razor
:
@page "/file-upload-4"
@inject IJSRuntime JS
<h1>File Upload Example</h1>
<InputFile @ref="inputFile" OnChange="ShowPreview" />
<img style="max-width:200px;max-height:200px" @ref="previewImageElem" />
@code {
private InputFile? inputFile;
private ElementReference previewImageElem;
private async Task ShowPreview() => await JS.InvokeVoidAsync(
"previewImage", inputFile!.Element, previewImageElem);
}
@page "/file-upload-4"
@inject IJSRuntime JS
<h1>File Upload Example</h1>
<InputFile @ref="inputFile" OnChange="ShowPreview" />
<img style="max-width:200px;max-height:200px" @ref="previewImageElem" />
@code {
private InputFile? inputFile;
private ElementReference previewImageElem;
private async Task ShowPreview() => await JS.InvokeVoidAsync(
"previewImage", inputFile!.Element, previewImageElem);
}
Отправка файлов во внешнюю службу
Чтобы не использовать подход, при котором приложение обрабатывает байты передаваемых файлов и сервер принимает отправленные файлы, клиенты могут напрямую отправлять файлы во внешнюю службу. Приложение может безопасно обрабатывать файлы внешней службы по требованию. При таком подходе приложение и его сервер лучше защищены от вредоносных атак и потенциальных проблем с производительностью.
Рассмотрите подход с использованием служб Файлы Azure и Хранилища BLOB-объектов Azure или сторонней службы со следующими потенциальными преимуществами:
- Отправка файлов из клиента непосредственно во внешнюю службу с использованием клиентской библиотекой JavaScript или REST API. Например, Azure предлагает следующие клиентские библиотеки и API:
- Авторизация пользовательских операций отправки с помощью делегированного пользователем маркера подписанного URL-адреса (SAS). Этот маркер создается приложением (на стороне сервера) для каждой операции отправки клиентского файла. Например, Azure предлагает следующие функции SAS:
- Обеспечение автоматической избыточности и резервного копирования общей папки.
- Ограничение количества отправок с помощью квот. Обратите внимание, что квоты Хранилища BLOB-объектов Azure устанавливаются на уровне учетной записи, а не на уровне контейнера. Однако квоты на Файлы Azure устанавливаются на уровне общей папки и могут обеспечить лучшее управление ограничениями на отправку. Дополнительные сведения см. в документах Azure, ссылки на которые приведены выше в этом списке.
- Защита файлов с помощью шифрования на стороне сервера (SSE).
Дополнительные сведения о Хранилище BLOB-объектов Azure и Файлах Azure см. в документации по службе хранилища Azure.
Ограничение размера сообщения на стороне SignalR сервера
Отправка файлов может завершиться сбоем даже до начала операции, если Blazor получает данные о файлах, превышающие максимальный размер сообщения SignalR.
SignalR определяет ограничение размера сообщения, которое применяется ко всем сообщениям, получаемым Blazor, а компонент InputFile выполняет потоковую передачу файлов на сервер в сообщениях с учетом настроенного ограничения. Но первое сообщение, в котором указан набор файлов для отправки, отправляется как отдельное сообщение. Его размер может превышать предельный размер сообщения SignalR. Проблема не связана с размером файлов, она связана с их количеством.
Зарегистрированная ошибка имеет примерно такой вид:
Ошибка: подключение отключено с ошибкой "Ошибка: сервер вернул ошибку при закрытии: соединение закрыто с ошибкой." e.log @ blazor.server.js:1
При отправке файлов первое сообщение редко достигает предельного размера. Если предел достигнут, приложение может настроить для HubOptions.MaximumReceiveMessageSize большее значение.
Дополнительные сведения о настройке SignalR и указании MaximumReceiveMessageSize см. в здесь: Руководство по ASP.NET Core BlazorSignalR.
Максимальное число параллельных вызовов для каждого параметра клиентского концентратора
BlazorMaximumParallelInvocationsPerClient использует значение 1, которое является значением по умолчанию.
Увеличение значения приводит к высокой вероятности возникновения CopyTo
операций System.InvalidOperationException: 'Reading is not allowed after reader was completed.'
. Дополнительные сведения см. в разделе MaximumParallelInvocationsPerClient > 1 разорвает отправку файла в Blazor Server режиме (dotnet/aspnetcore
#53951).
Устранение неполадок
Строка, вызывающая IBrowserFile.OpenReadStream исключение System.TimeoutException:
System.TimeoutException: Did not receive any data in the allotted time.
Возможные причины:
Использование контейнера autofac Inversion of Control (IoC) вместо встроенного контейнера внедрения зависимостей core ASP.NET. Чтобы устранить проблему, задайте DisableImplicitFromServicesParameters значение
true
в параметрах концентратора обработчика канала на стороне сервера. Дополнительные сведения см. в разделе FileUpload: не было получено никаких данных в выделенное время (dotnet/aspnetcore
#38842).Не считывание потока до завершения. Это не проблема с платформой. Захватить исключение и изучить его дальше в локальной среде или сети.
- Использование отрисовки на стороне сервера и вызова OpenReadStream нескольких файлов перед их завершением. Чтобы устранить проблему, используйте
LazyBrowserFileStream
класс и подход, описанный в разделе "Отправка файлов на сервер" с разделом отрисовки на стороне сервера этой статьи.
Дополнительные ресурсы
ASP.NET Core