Uwaga
Dostęp do tej strony wymaga autoryzacji. Może spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
Uwaga
Nie jest to najnowsza wersja tego artykułu. Aby zapoznać się z bieżącą wersją, zobacz wersję tego artykułu platformy .NET 9.
Ważne
Te informacje odnoszą się do produktu w wersji wstępnej, który może zostać znacząco zmodyfikowany, zanim zostanie wydany komercyjnie. Firma Microsoft nie udziela żadnych gwarancji, jawnych lub domniemanych, w odniesieniu do informacji podanych w tym miejscu.
Aby zapoznać się z bieżącą wersją, zobacz wersję tego artykułu platformy .NET 9.
W tym artykule wyjaśniono, jak przekazywać pliki za Blazor pomocą InputFile składnika.
Operacje przekazywania plików
Ostrzeżenie
Zawsze przestrzegaj najlepszych rozwiązań w zakresie zabezpieczeń, gdy zezwalają użytkownikom na przekazywanie plików. Aby uzyskać więcej informacji, zobacz Przekazywanie plików w programie ASP.NET Core.
InputFile Użyj składnika, aby odczytać dane pliku przeglądarki do kodu platformy .NET. Składnik InputFile renderuje element HTML <input>
typu file
dla przekazywania pojedynczych plików.
multiple
Dodaj atrybut, aby zezwolić użytkownikowi na przekazywanie wielu plików jednocześnie.
Wybór pliku nie jest skumulowany w przypadku używania składnika lub jego bazowego InputFilekodu HTML <input type="file">
, więc nie można dodawać plików do istniejącego zaznaczenia pliku. Składnik zawsze zastępuje początkowy wybór pliku użytkownika, więc odwołania do plików z poprzednich wyborów nie są dostępne.
Poniższy InputFile składnik wykonuje metodę LoadFiles
, gdy OnChange wystąpi zdarzenie (change
). Element InputFileChangeEventArgs zapewnia dostęp do wybranej listy plików i szczegółów dotyczących każdego pliku:
<InputFile OnChange="LoadFiles" multiple />
@code {
private void LoadFiles(InputFileChangeEventArgs e)
{
...
}
}
Renderowany kod HTML:
<input multiple="" type="file" _bl_2="">
Uwaga
W poprzednim przykładzie <input>
atrybut elementu _bl_2
jest używany do Blazorwewnętrznego przetwarzania.
Aby odczytać dane z wybranego przez użytkownika pliku z wartością Stream reprezentującą bajty pliku, wywołaj IBrowserFile.OpenReadStream plik i odczytaj z zwróconego strumienia. Aby uzyskać więcej informacji, zobacz sekcję Strumienie plików.
OpenReadStream wymusza maksymalny rozmiar w bajtach jego Streamwartości . Odczytywanie jednego pliku lub wielu plików większych niż 500 KB powoduje wyjątek. Ten limit uniemożliwia deweloperom przypadkowe odczytywanie dużych plików do pamięci. Parametr maxAllowedSize
parametru można użyć do określenia większego rozmiaru OpenReadStream , jeśli jest to wymagane.
Poza przetwarzaniem małego pliku unikaj odczytywania przychodzącego strumienia plików bezpośrednio do pamięci jednocześnie. Na przykład nie kopiuj wszystkich bajtów pliku do MemoryStream ani nie odczytaj całego strumienia do tablicy bajtów jednocześnie. Te podejścia mogą spowodować obniżenie wydajności aplikacji i potencjalne ryzyko odmowy usługi (DoS ), szczególnie w przypadku składników po stronie serwera. Zamiast tego należy rozważyć przyjęcie jednego z następujących podejść:
- Skopiuj strumień bezpośrednio do pliku na dysku bez odczytywania go do pamięci. Należy pamiętać, że Blazor aplikacje wykonujące kod na serwerze nie mogą bezpośrednio uzyskać dostępu do systemu plików klienta.
- Przekazywanie plików z klienta bezpośrednio do usługi zewnętrznej. Aby uzyskać więcej informacji, zobacz sekcję Przekazywanie plików do usługi zewnętrznej.
W poniższych przykładach browserFile
implementuje IBrowserFile do reprezentowania przekazanego pliku. Działające implementacje programu IBrowserFile są wyświetlane w składnikach przekazywania plików w dalszej części tego artykułu.
Podczas wywoływania OpenReadStream zalecamy przekazanie maksymalnego dozwolonego rozmiaru pliku w parametrze maxAllowedSize
, zgodnie z limitem rozmiarów plików, które oczekujesz otrzymać. Wartość domyślna to 500 KB. W przykładach tego artykułu użyto zmiennej lub stałej o nazwie maxFileSize
do określenia maksymalnego dozwolonego rozmiaru pliku, ale zwykle nie pokazują one ustawienia konkretnej wartości.
jest następujące podejście, ponieważ plik Stream jest udostępniany bezpośrednio użytkownikowi, czyli plik, który FileStream tworzy plik w podanej ścieżce:
Obsługiwane: zalecaneawait using FileStream fs = new(path, FileMode.Create);
await browserFile.OpenReadStream(maxFileSize).CopyToAsync(fs);
Obsługiwane: w przypadku usługi Microsoft Azure Blob Storage zalecane jest następujące podejście, ponieważ plik Stream jest dostarczany bezpośrednio do UploadBlobAsyncusługi :
await blobContainerClient.UploadBlobAsync(
trustedFileName, browserFile.OpenReadStream(maxFileSize));
Zalecane tylko w przypadku małych plików: następujące podejście jest zalecane tylko w przypadku małych plików , ponieważ zawartość pliku Stream jest odczytywana w MemoryStream pamięci (memoryStream
), co wiąże się z karą za wydajność i ryzykiem systemu DoS . Przykład, który demonstruje tę technikę zapisywania obrazu miniatury w bazie danych z użyciem IBrowserFile, przy pomocy narzędzia Entity Framework Core (EF Core), znajdziesz w sekcji Zapisywanie małych plików bezpośrednio do bazy danych z EF Core dalej w artykule.
using var memoryStream = new MemoryStream();
await browserFile.OpenReadStream(maxFileSize).CopyToAsync(memoryStream);
var smallFileByteArray = memoryStream.ToArray();
Niezalecane: Następujące podejście nie jest zalecane , ponieważ zawartość pliku Stream jest odczytywana w String pamięci (reader
):
var reader =
await new StreamReader(browserFile.OpenReadStream(maxFileSize)).ReadToEndAsync();
Niezalecane: następujące podejście nie jest zalecane w przypadku usługi Microsoft Azure Blob Storage , ponieważ zawartość pliku Stream jest kopiowana do MemoryStream pamięci (memoryStream
) przed wywołaniem metody UploadBlobAsync:
var memoryStream = new MemoryStream();
await browserFile.OpenReadStream(maxFileSize).CopyToAsync(memoryStream);
await blobContainerClient.UploadBlobAsync(
trustedFileName, memoryStream));
Składnik odbierający plik obrazu może wywołać BrowserFileExtensions.RequestImageFileAsync metodę wygody w pliku, aby zmienić rozmiar danych obrazu w środowisku uruchomieniowym JavaScript przeglądarki przed przesyłaniem strumieniowym obrazu do aplikacji. Przypadki użycia wywołania RequestImageFileAsync są najbardziej odpowiednie dla Blazor WebAssembly aplikacji.
Autofac Inversion of Control (IoC) container users (Autofac Inversion of Control) users (Autofac Inversion of Control ( IoC) container users (Autofac In
Jeśli używasz kontenera Autofac Inversion of Control (IoC) zamiast wbudowanego kontenera iniekcji zależności ASP.NET Core, ustaw wartość DisableImplicitFromServicesParameters na true
w opcjach centrum obsługi obwodu po stronie serwera. Aby uzyskać więcej informacji, zobacz FileUpload: Nie odebrano żadnych danych w czasie przydzielonym (dotnet/aspnetcore
#38842).
Limity odczytu i przekazywania rozmiaru pliku
W przypadku przeglądarek opartych na chromium (na przykład Google Chrome i Microsoft Edge) przy użyciu protokołu HTTP/2, protokołu HTTPS i mechanizmu CORS po stronie Blazor klienta obsługiwane jest używanie interfejsu API strumieni w celu zezwolenia na przekazywanie dużych plików przy użyciu przesyłania strumieniowego żądań.
Bez przeglądarki Chromium, protokołu HTTP/2 lub HTTPS, Blazor po stronie klienta odczytuje bajty pliku do pojedynczego bufora tablicy JavaScript podczas przekształcania danych z JavaScript do C#, co jest ograniczone do 2 GB lub dostępnej pamięci urządzenia. Przesyłanie dużych plików może zakończyć się niepowodzeniem w przypadku przesyłania po stronie klienta przy użyciu InputFile składnika.
Aplikacja po stronie Blazor klienta odczytuje bajty pliku do pojedynczego bufora tablicy JavaScript, gdy przenosi dane z języka JavaScript do języka C#. Proces ten jest ograniczony do 2 GB lub do ilości dostępnej pamięci urządzenia. Przesyłanie dużych plików może zakończyć się niepowodzeniem w przypadku przesyłania po stronie klienta przy użyciu InputFile składnika. Zalecamy wdrożenie przesyłania strumieniowego żądań przy użyciu platformy .NET 9 lub nowszej.
Zagadnienia dotyczące zabezpieczeń
Unikaj IBrowserFile.Size
limitów rozmiaru pliku
Unikaj używania IBrowserFile.Size polecenia , aby narzucić limit rozmiaru pliku. Zamiast używać niebezpiecznego rozmiaru pliku dostarczonego przez klienta, jawnie określ maksymalny rozmiar pliku. W poniższym przykładzie użyto maksymalnego rozmiaru pliku przypisanego do maxFileSize
:
- var fileContent = new StreamContent(file.OpenReadStream(file.Size));
+ var fileContent = new StreamContent(file.OpenReadStream(maxFileSize));
Zabezpieczenia nazw plików
Nigdy nie używaj nazwy pliku dostarczonego przez klienta do zapisywania pliku w magazynie fizycznym. Utwórz bezpieczną nazwę pliku przy użyciu Path.GetRandomFileName() lub Path.GetTempFileName() aby utworzyć pełną ścieżkę (w tym nazwę pliku) do tymczasowego przechowywania.
Razor automatycznie koduje wartości właściwości html do wyświetlania. Poniższy kod jest bezpieczny do użycia:
@foreach (var file in Model.DatabaseFiles) {
<tr>
<td>
@file.UntrustedName
</td>
</tr>
}
Poza Razor, zawsze używaj funkcji HtmlEncode, aby bezpiecznie kodować nazwy plików z żądania użytkownika.
Wiele implementacji musi zawierać sprawdzenie, czy plik istnieje; w przeciwnym razie plik jest zastępowany przez plik o tej samej nazwie. Podaj dodatkową logikę, aby spełnić specyfikacje aplikacji.
Przykłady
W poniższych przykładach pokazano wiele przekazywania plików w składniku. InputFileChangeEventArgs.GetMultipleFiles umożliwia odczytywanie wielu plików. Określ maksymalną liczbę plików, aby uniemożliwić złośliwemu użytkownikowi przekazywanie większej liczby plików niż oczekiwana przez aplikację. InputFileChangeEventArgs.File umożliwia odczytywanie pierwszego pliku i tylko wtedy, gdy przekazywanie pliku nie obsługuje wielu plików.
InputFileChangeEventArgs znajduje się w Microsoft.AspNetCore.Components.Forms przestrzeni nazw, która jest zazwyczaj jedną z przestrzeni nazw w pliku aplikacji _Imports.razor
. Gdy przestrzeń nazw znajduje się w _Imports.razor
pliku, zapewnia dostęp do składowych interfejsu API do składników aplikacji.
Przestrzenie nazw w _Imports.razor
pliku nie są stosowane do plików C# (.cs
). Pliki języka C# wymagają jawnej using
dyrektywy w górnej części pliku klasy:
using Microsoft.AspNetCore.Components.Forms;
W przypadku testowania składników przekazywania plików można utworzyć pliki testowe o dowolnym rozmiarze za pomocą programu PowerShell:
$out = new-object byte[] {SIZE}; (new-object Random).NextBytes($out); [IO.File]::WriteAllBytes('{PATH}', $out)
W powyższym poleceniu:
- Symbol
{SIZE}
zastępczy to rozmiar pliku w bajtach (na przykład2097152
dla pliku o rozmiarze 2 MB). - Symbol
{PATH}
zastępczy to ścieżka i plik z rozszerzeniem pliku (na przykładD:/test_files/testfile2MB.txt
).
Przykład przekazywania pliku po stronie serwera
Aby użyć następującego kodu, utwórz Development/unsafe_uploads
folder w katalogu głównym aplikacji działającej Development
w środowisku.
Ponieważ w przykładzie używane jest środowisko aplikacji jako część ścieżki, w której są zapisywane pliki, wymagane są dodatkowe foldery, jeśli inne środowiska są używane w testowaniu i środowisku produkcyjnym. Na przykład utwórz Staging/unsafe_uploads
folder dla Staging
środowiska.
Production/unsafe_uploads
Utwórz folder dla Production
środowiska.
Ostrzeżenie
Przykład zapisuje pliki bez skanowania ich zawartości, a wskazówki zawarte w tym artykule nie uwzględniają dodatkowych najlepszych rozwiązań w zakresie zabezpieczeń przekazanych plików. W systemach przejściowych i produkcyjnych wyłącz uprawnienie do wykonywania w folderze przekazywania i skanuj pliki za pomocą interfejsu API skanera antywirusowego/chroniącego przed złośliwym oprogramowaniem natychmiast po przekazaniu. Aby uzyskać więcej informacji, zobacz Przekazywanie plików w programie 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 = [];
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;
}
}
Przykład przekazywania pliku po stronie klienta
Poniższy przykład przetwarza bajty plików i nie wysyła plików do miejsca docelowego poza aplikacją. Razor Przykładowy składnik, który wysyła plik do serwera lub usługi, zobacz następujące sekcje:
- Przekazywanie plików na serwer za pomocą renderowania po stronie klienta (CSR)
- Przekazywanie plików do usługi zewnętrznej
Składnik zakłada, że tryb renderowania interactive WebAssembly (InteractiveWebAssembly
) jest dziedziczony z składnika nadrzędnego lub stosowany globalnie do aplikacji.
@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 = [];
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 zwraca metadane uwidocznione przez przeglądarkę jako właściwości. Użyj tych metadanych do wstępnej weryfikacji.
Nigdy nie ufaj wartościom poprzednich właściwości, zwłaszcza Name właściwości do wyświetlania w interfejsie użytkownika. Traktuj wszystkie dane dostarczone przez użytkownika jako istotne zagrożenie bezpieczeństwa aplikacji, serwera i sieci. Aby uzyskać więcej informacji, zobacz Przekazywanie plików w programie ASP.NET Core.
Przekazywanie plików na serwer za pomocą renderowania po stronie serwera
Ta sekcja dotyczy składników interactive server w Blazor Web Appsystemach lub Blazor Server aplikacjach.
W poniższym przykładzie pokazano przekazywanie plików z aplikacji po stronie serwera do kontrolera internetowego interfejsu API zaplecza w oddzielnej aplikacji, prawdopodobnie na osobnym serwerze.
W pliku aplikacji Program
po stronie serwera dodaj IHttpClientFactory i powiązane usługi, które umożliwiają aplikacji tworzenie HttpClient wystąpień:
builder.Services.AddHttpClient();
Aby uzyskać więcej informacji, zobacz Tworzenie żądań HTTP za pomocą interfejsu IHttpClientFactory na platformie ASP.NET Core.
Przykłady w tej sekcji:
- Internetowy interfejs API jest uruchamiany pod adresem URL:
https://localhost:5001
- Aplikacja po stronie serwera działa pod adresem URL:
https://localhost:5003
Na potrzeby testowania poprzednie adresy URL są konfigurowane w plikach projektów Properties/launchSettings.json
.
Poniższa UploadResult
klasa zachowuje wynik przekazanego pliku. Gdy przekazywanie pliku na serwerze nie powiedzie się, zostanie zwrócony ErrorCode
kod błędu w celu wyświetlenia użytkownikowi. Bezpieczna nazwa pliku jest generowana na serwerze dla każdego pliku i zwracana do klienta w StoredFileName
programie w celu wyświetlenia. Pliki są kluczami między klientem a serwerem przy użyciu niebezpiecznej/niezaufanej nazwy pliku w programie 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; }
}
Najlepszym rozwiązaniem w zakresie zabezpieczeń dla aplikacji produkcyjnych jest unikanie wysyłania komunikatów o błędach do klientów, którzy mogą ujawniać poufne informacje o aplikacji, serwerze lub sieci. Udostępnianie szczegółowych komunikatów o błędach może pomóc złośliwego użytkownika w opracowywaniu ataków na aplikację, serwer lub sieć. Przykładowy kod w tej sekcji wysyła tylko numer kodu błędu (int
) do wyświetlenia po stronie klienta składnika, jeśli wystąpi błąd po stronie serwera. Jeśli użytkownik wymaga pomocy przy przekazaniu pliku, podaj kod błędu pomocy technicznej dla personelu pomocy technicznej w celu rozwiązania biletu pomocy technicznej bez znajomości dokładnej przyczyny błędu.
Poniższa LazyBrowserFileStream
klasa definiuje niestandardowy typ strumienia, który leniwie wywołuje OpenReadStream tuż przed żądaniem pierwszych bajtów strumienia. Strumień nie jest przesyłany z przeglądarki do serwera do momentu rozpoczęcia odczytu strumienia na platformie .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
Następujący składnik:
- Zezwala użytkownikom na przekazywanie plików z klienta.
- Wyświetla niezaufaną/niebezpieczną nazwę pliku podaną przez klienta w interfejsie użytkownika. Niezaufana/niebezpieczna nazwa pliku jest automatycznie zakodowana w Razor formacie HTML w celu bezpiecznego wyświetlania w interfejsie użytkownika.
Ostrzeżenie
Nie ufaj nazwam plików dostarczonym przez klientów dla:
- Zapisywanie pliku w systemie plików lub usłudze.
- Wyświetlaj w interfejsach użytkownika, które nie kodują automatycznie nazw plików ani za pośrednictwem kodu dewelopera.
Aby uzyskać więcej informacji na temat zagadnień dotyczących zabezpieczeń podczas przekazywania plików na serwer, zobacz Przekazywanie plików w programie 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 = [];
private List<UploadResult> uploadResults = [];
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();
using 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();
using 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();
using 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();
using 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; }
}
}
Jeśli składnik ogranicza przesyłanie plików do pojedynczego pliku w danym momencie lub jeśli przyjmuje renderowanie po stronie klienta (CSR, InteractiveWebAssembly
), może uniknąć użycia LazyBrowserFileStream
i użyć Stream. Poniżej przedstawiono zmiany składnika FileUpload2
:
- var stream = new LazyBrowserFileStream(file, maxFileSize);
- var fileContent = new StreamContent(stream);
+ var fileContent = new StreamContent(file.OpenReadStream(maxFileSize));
Usuń klasę LazyBrowserFileStream
(LazyBrowserFileStream.cs
), ponieważ nie jest używana.
Jeśli składnik ogranicza przekazywanie plików do pojedynczego pliku w danym momencie, składnik może uniknąć użycia LazyBrowserFileStream
elementu i użyć elementu Stream. Poniżej przedstawiono zmiany składnika FileUpload2
:
- var stream = new LazyBrowserFileStream(file, maxFileSize);
- var fileContent = new StreamContent(stream);
+ var fileContent = new StreamContent(file.OpenReadStream(maxFileSize));
Usuń klasę LazyBrowserFileStream
(LazyBrowserFileStream.cs
), ponieważ nie jest używana.
Następujący kontroler w projekcie internetowego interfejsu API zapisuje przekazane pliki z klienta.
Ważne
Kontroler w tej sekcji jest przeznaczony do użycia w osobnym projekcie internetowego interfejsu Blazor API z aplikacji. Internetowy interfejs API powinien ograniczyć ryzyko fałszowania żądań między witrynami (XSRF/CSRF) w przypadku uwierzytelnienia użytkowników przekazywania plików.
Uwaga
Powiązanie wartości formularza z atrybutem[FromForm]
nie jest natywnie obsługiwane dla minimalnych interfejsów API w programie ASP.NET Core na platformie .NET 6. W związku z tym nie można przekonwertować następującego Filesave
przykładu kontrolera w celu użycia interfejsów API minimalnych. Obsługa powiązań z wartości formularza z minimalnymi interfejsami API jest dostępna w programie ASP.NET Core na platformie .NET 7 lub nowszym.
Aby użyć następującego kodu, utwórz Development/unsafe_uploads
folder w katalogu głównym projektu internetowego interfejsu API dla aplikacji działającej Development
w środowisku.
Ponieważ w przykładzie używane jest środowisko aplikacji jako część ścieżki, w której są zapisywane pliki, wymagane są dodatkowe foldery, jeśli inne środowiska są używane w testowaniu i środowisku produkcyjnym. Na przykład utwórz Staging/unsafe_uploads
folder dla Staging
środowiska.
Production/unsafe_uploads
Utwórz folder dla Production
środowiska.
Ostrzeżenie
Przykład zapisuje pliki bez skanowania ich zawartości, a wskazówki zawarte w tym artykule nie uwzględniają dodatkowych najlepszych rozwiązań w zakresie zabezpieczeń przekazanych plików. W systemach przejściowych i produkcyjnych wyłącz uprawnienie do wykonywania w folderze przekazywania i skanuj pliki za pomocą interfejsu API skanera antywirusowego/chroniącego przed złośliwym oprogramowaniem natychmiast po przekazaniu. Aby uzyskać więcej informacji, zobacz Przekazywanie plików w programie 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);
}
}
W poprzednim kodzie GetRandomFileName jest wywoływana w celu wygenerowania bezpiecznej nazwy pliku. Nigdy nie ufaj nazwie pliku podanej przez przeglądarkę, ponieważ cyberattacker może wybrać istniejącą nazwę pliku, która zastępuje istniejący plik lub wysyła ścieżkę, która próbuje zapisać poza aplikacją.
Aplikacja serwera musi rejestrować usługi kontrolera i punkty końcowe kontrolera mapy. Aby uzyskać więcej informacji, zobacz Routing do akcji kontrolera w ASP.NET Core.
Przekazywanie plików na serwer za pomocą renderowania po stronie klienta (CSR)
Ta sekcja dotyczy składników renderowanych po stronie klienta (CSR) w Blazor Web Appaplikacjach lub s.Blazor WebAssembly
W poniższym przykładzie pokazano przekazywanie plików do internetowego kontrolera interfejsu API zaplecza w oddzielnej aplikacji, na przykład na osobnym serwerze, od składnika Blazor Web App , który przyjmuje csr lub składnik w Blazor WebAssembly aplikacji.
Przykład przyjmuje przesyłanie strumieniowe żądań dla przeglądarki opartej na chromium (na przykład Google Chrome lub Microsoft Edge) przy użyciu protokołu HTTP/2 i PROTOKOŁU HTTPS. Jeśli nie można używać przesyłania strumieniowego żądań, Blazor przełącza się płynnie na Fetch API bez przesyłania strumieniowego żądań. Aby uzyskać więcej informacji, zobacz sekcję Limity odczytu i przekazywania rozmiaru pliku .
Poniższa UploadResult
klasa zachowuje wynik przekazanego pliku. Gdy przekazywanie pliku na serwerze nie powiedzie się, zostanie zwrócony ErrorCode
kod błędu w celu wyświetlenia użytkownikowi. Bezpieczna nazwa pliku jest generowana na serwerze dla każdego pliku i zwracana do klienta w StoredFileName
programie w celu wyświetlenia. Pliki są kluczami między klientem a serwerem przy użyciu niebezpiecznej/niezaufanej nazwy pliku w programie 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; }
}
Uwaga
Poprzednia UploadResult
klasa może być współdzielona między projektami klienckimi i serwerowymi. Gdy projekty klienta i serwera współużytkuje klasę, dodaj import do pliku każdego projektu _Imports.razor
dla udostępnionego projektu. Na przykład:
@using BlazorSample.Shared
FileUpload2
Następujący składnik:
- Zezwala użytkownikom na przekazywanie plików z klienta.
- Wyświetla niezaufaną/niebezpieczną nazwę pliku podaną przez klienta w interfejsie użytkownika. Niezaufana/niebezpieczna nazwa pliku jest automatycznie zakodowana w Razor formacie HTML w celu bezpiecznego wyświetlania w interfejsie użytkownika.
Najlepszym rozwiązaniem w zakresie zabezpieczeń dla aplikacji produkcyjnych jest unikanie wysyłania komunikatów o błędach do klientów, którzy mogą ujawniać poufne informacje o aplikacji, serwerze lub sieci. Udostępnianie szczegółowych komunikatów o błędach może pomóc złośliwego użytkownika w opracowywaniu ataków na aplikację, serwer lub sieć. Przykładowy kod w tej sekcji wysyła tylko numer kodu błędu (int
) do wyświetlenia po stronie klienta składnika, jeśli wystąpi błąd po stronie serwera. Jeśli użytkownik wymaga pomocy przy przekazaniu pliku, podaj kod błędu pomocy technicznej dla personelu pomocy technicznej w celu rozwiązania biletu pomocy technicznej bez znajomości dokładnej przyczyny błędu.
Ostrzeżenie
Nie ufaj nazwam plików dostarczonym przez klientów dla:
- Zapisywanie pliku w systemie plików lub usłudze.
- Wyświetlaj w interfejsach użytkownika, które nie kodują automatycznie nazw plików ani za pośrednictwem kodu dewelopera.
Aby uzyskać więcej informacji na temat zagadnień dotyczących zabezpieczeń podczas przekazywania plików na serwer, zobacz Przekazywanie plików w programie ASP.NET Core.
W projekcie serwera Blazor Web App, dodaj IHttpClientFactory oraz powiązane usługi w pliku projektu Program
.
builder.Services.AddHttpClient();
Usługi HttpClient należy dodać do projektu serwera, ponieważ składnik po stronie klienta jest wstępnie zainstalowany na serwerze. Jeśli wyłączysz prerendering dla następującego składnika, nie musisz zapewniać HttpClient usług w projekcie serwera i nie musisz dodawać poprzedniego wiersza do projektu serwera.
Aby uzyskać więcej informacji na temat dodawania HttpClient usług do aplikacji ASP.NET Core, zobacz Make HTTP requests using IHttpClientFactory in ASP.NET Core (Tworzenie żądań HTTP przy użyciu elementu IHttpClientFactory w programie ASP.NET Core).
Projekt klienta (.Client
) Blazor Web App obiektu musi również zarejestrować żądania HttpClient HTTP POST na kontrolerze internetowego interfejsu API zaplecza. Potwierdź lub dodaj następujący kod do pliku projektu Program
klienta:
builder.Services.AddScoped(sp =>
new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
W poprzednim przykładzie ustawiono adres podstawowy (builder.HostEnvironment.BaseAddress
), który pobiera adres IWebAssemblyHostEnvironment.BaseAddress podstawowy dla aplikacji i jest zwykle pobierany z <base>
wartości tagu href
na stronie hosta. Jeśli wywołujesz zewnętrzny internetowy interfejs API, ustaw identyfikator URI na adres podstawowy internetowego interfejsu API.
Autonomiczna Blazor WebAssembly aplikacja, która przekazuje pliki do oddzielnego internetowego interfejsu API serwera, używa nazwy HttpClient
lub ustawia domyślną HttpClient rejestrację usługi w celu wskazania punktu końcowego internetowego interfejsu API. W poniższym przykładzie, w którym internetowy interfejs API jest hostowany lokalnie na porcie 5001, adres podstawowy to https://localhost:5001
:
builder.Services.AddScoped(sp =>
new HttpClient { BaseAddress = new Uri("https://localhost:5001") });
W pliku Blazor Web App dodaj przestrzeń nazw Microsoft.AspNetCore.Components.WebAssembly.Http do dyrektyw składnika.
@using Microsoft.AspNetCore.Components.WebAssembly.Http
FileUpload2.razor
:
@page "/file-upload-2"
@using System.Linq
@using System.Net.Http.Headers
@using System.Net
@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)
{
using var request = new HttpRequestMessage(HttpMethod.Post, "/Filesave");
request.SetBrowserRequestStreamingEnabled(true);
request.Content = content;
using var response = await Http.SendAsync(request);
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
@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 = [];
private List<UploadResult> uploadResults = [];
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)
{
using 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)
{
using 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)
{
using 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)
{
using 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; }
}
}
Następujący kontroler w projekcie po stronie serwera zapisuje przekazane pliki z klienta.
Uwaga
Powiązanie wartości formularza z atrybutem[FromForm]
nie jest natywnie obsługiwane dla minimalnych interfejsów API w programie ASP.NET Core na platformie .NET 6. W związku z tym nie można przekonwertować następującego Filesave
przykładu kontrolera w celu użycia interfejsów API minimalnych. Obsługa powiązań z wartości formularza z minimalnymi interfejsami API jest dostępna w programie ASP.NET Core na platformie .NET 7 lub nowszym.
Aby użyć następującego kodu, utwórz Development/unsafe_uploads
folder w katalogu głównym projektu po stronie serwera dla aplikacji działającej Development
w środowisku.
Ponieważ w przykładzie używane jest środowisko aplikacji jako część ścieżki, w której są zapisywane pliki, wymagane są dodatkowe foldery, jeśli inne środowiska są używane w testowaniu i środowisku produkcyjnym. Na przykład utwórz Staging/unsafe_uploads
folder dla Staging
środowiska.
Production/unsafe_uploads
Utwórz folder dla Production
środowiska.
Ostrzeżenie
Przykład zapisuje pliki bez skanowania ich zawartości, a wskazówki zawarte w tym artykule nie uwzględniają dodatkowych najlepszych rozwiązań w zakresie zabezpieczeń przekazanych plików. W systemach przejściowych i produkcyjnych wyłącz uprawnienie do wykonywania w folderze przekazywania i skanuj pliki za pomocą interfejsu API skanera antywirusowego/chroniącego przed złośliwym oprogramowaniem natychmiast po przekazaniu. Aby uzyskać więcej informacji, zobacz Przekazywanie plików w programie ASP.NET Core.
W poniższym przykładzie dla hostowanej Blazor WebAssembly aplikacji lub gdy współużytkowany projekt jest używany do dostarczania klasy UploadResult
, dodaj przestrzeń nazw współdzielonego projektu:
using BlazorSample.Shared;
Zalecamy użycie przestrzeni nazw dla następującego kontrolera (na przykład: namespace BlazorSample.Controllers
).
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;
[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);
}
}
W poprzednim kodzie GetRandomFileName jest wywoływana w celu wygenerowania bezpiecznej nazwy pliku. Nigdy nie ufaj nazwie pliku podanej przez przeglądarkę, ponieważ cyberattacker może wybrać istniejącą nazwę pliku, która zastępuje istniejący plik lub wysyła ścieżkę, która próbuje zapisać poza aplikacją.
Aplikacja serwera musi rejestrować usługi kontrolera i punkty końcowe kontrolera mapy. Aby uzyskać więcej informacji, zobacz Routing do akcji kontrolera w ASP.NET Core. Zalecamy dodanie usług kontrolera z użyciem AddControllersWithViews w celu automatycznego ograniczenia ryzyka fałszowania żądań między witrynami (XSRF/CSRF) dla uwierzytelnionych użytkowników. Jeśli używasz tylko AddControllers, antyfałszerstwo nie jest włączone automatycznie. Aby uzyskać więcej informacji, zobacz Routing do akcji kontrolera w ASP.NET Core.
Konfiguracja żądań międzydomenowych (CORS) na serwerze jest wymagana do przesyłania strumieniowego żądań, gdy serwer jest hostowany w innej domenie, a klient zawsze wysyła żądanie wstępne. W konfiguracji pliku serwera Program
(projektu serwera internetowego Blazor Web App lub internetowego interfejsu API serwera zaplecza aplikacji Blazor WebAssembly) następująca domyślna polityka CORS jest odpowiednia do testowania z przykładami zawartymi w tym artykule. Klient wysyła żądanie lokalne z portu 5003. Zmień numer portu, aby był zgodny z używanym portem aplikacji klienckiej:
Skonfiguruj żądania między źródłami (CORS) na serwerze. W konfiguracji pliku serwera Program
(projektu serwera internetowego Blazor Web App lub internetowego interfejsu API serwera zaplecza aplikacji Blazor WebAssembly) następująca domyślna polityka CORS jest odpowiednia do testowania z przykładami zawartymi w tym artykule. Klient wysyła żądanie lokalne z portu 5003. Zmień numer portu, aby był zgodny z używanym portem aplikacji klienckiej:
Skonfiguruj żądania między źródłami (CORS) na serwerze. W konfiguracji usługi pliku API internetowego serwera zaplecza Program
, następująca domyślna polityka CORS jest odpowiednia do testowania przy użyciu przykładów w tym artykule. Klient wysyła żądanie lokalne z portu 5003. Zmień numer portu, aby był zgodny z używanym portem aplikacji klienckiej:
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(
policy =>
{
policy.WithOrigins("https://localhost:5003")
.AllowAnyMethod()
.AllowAnyHeader();
});
});
Po wywołaniu UseHttpsRedirection w pliku Program
, wywołaj UseCors, aby dodać middleware CORS.
app.UseCors();
Aby uzyskać więcej informacji, zobacz Włączanie żądań między źródłami (CORS) w ASP.NET Core.
Skonfiguruj maksymalny rozmiar treści żądania serwera i limity długości treści w wielu częściach, jeśli limity ograniczają rozmiar przekazywania.
Dla serwera Kestrel ustaw MaxRequestBodySize (domyślna wartość: 30 000 000 bajtów) i FormOptions.MultipartBodyLengthLimit (domyślna wartość: 134 217 728 bajtów). Ustaw zmienną maxFileSize
w składniku i kontroler na tę samą wartość.
W następującej Program
konfiguracji pliku Kestrel (serwera projektu Blazor Web App lub internetowego interfejsu API aplikacji serwera zaplecza Blazor WebAssembly) symbol zastępczy {LIMIT}
oznacza limit w bajtach:
using Microsoft.AspNetCore.Http.Features;
...
builder.WebHost.ConfigureKestrel(serverOptions =>
{
serverOptions.Limits.MaxRequestBodySize = {LIMIT};
});
builder.Services.Configure<FormOptions>(options =>
{
options.MultipartBodyLengthLimit = {LIMIT};
});
Anulowanie przekazywania pliku
Składnik przekazywania plików może wykryć, kiedy użytkownik anulował przekazywanie przy użyciu CancellationToken elementu podczas wywoływania elementu IBrowserFile.OpenReadStream lub StreamReader.ReadAsync.
Utwórz element CancellationTokenSource dla InputFile
składnika. Na początku OnInputFileChange
metody sprawdź, czy poprzednie przekazywanie jest w toku.
Jeśli przekazywanie pliku jest w toku:
- Wywołaj Cancel poprzednią metodę przekazywania.
- Utwórz nowy CancellationTokenSource element dla następnego przekazywania i przekaż element CancellationTokenSource.Token do lub OpenReadStreamReadAsync .
Przekazywanie plików po stronie serwera z postępem
W poniższym przykładzie pokazano, jak przekazywać pliki w aplikacji po stronie serwera z postępem przekazywania wyświetlanym użytkownikowi.
Aby użyć następującego przykładu w aplikacji testowej:
-
Utwórz folder do zapisania przekazanych plików dla
Development
środowiska:Development/unsafe_uploads
. - Skonfiguruj maksymalny rozmiar pliku (
maxFileSize
15 KB w poniższym przykładzie) i maksymalną liczbę dozwolonych plików (maxAllowedFiles
3 w poniższym przykładzie). - Ustaw bufor na inną wartość (10 KB w poniższym przykładzie), jeśli jest to konieczne, aby zwiększyć stopień szczegółowości w toku raportowania. Nie zalecamy używania buforu większego niż 30 KB z powodu problemów z wydajnością i zabezpieczeniami.
Ostrzeżenie
Przykład zapisuje pliki bez skanowania ich zawartości, a wskazówki zawarte w tym artykule nie uwzględniają dodatkowych najlepszych rozwiązań w zakresie zabezpieczeń przekazanych plików. W systemach przejściowych i produkcyjnych wyłącz uprawnienie do wykonywania w folderze przekazywania i skanuj pliki za pomocą interfejsu API skanera antywirusowego/chroniącego przed złośliwym oprogramowaniem natychmiast po przekazaniu. Aby uzyskać więcej informacji, zobacz Przekazywanie plików w programie 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 = [];
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;
}
}
Aby uzyskać więcej informacji, zobacz następujące zasoby interfejsu API:
- FileStream: udostępnia Stream element dla pliku, który obsługuje zarówno operacje synchroniczne, jak i asynchroniczne operacji odczytu i zapisu.
-
FileStream.ReadAsync: Poprzedni
FileUpload3
składnik odczytuje strumień asynchronicznie za pomocą polecenia ReadAsync. Odczytywanie strumienia synchronicznie z elementem Read nie jest obsługiwane w Razor składnikach.
Strumienie plików
W przypadku interakcyjności serwera dane plików są przesyłane strumieniowo za pośrednictwem SignalR połączenia z kodem platformy .NET na serwerze, ponieważ plik jest odczytywany.
RemoteBrowserFileStreamOptions umożliwia konfigurowanie właściwości przekazywania plików.
W przypadku składnika renderowanego za pomocą zestawu WebAssembly dane plików są przesyłane strumieniowo bezpośrednio do kodu platformy .NET w przeglądarce.
Przekazywanie podglądu obrazu
Aby uzyskać podgląd obrazu przekazywania obrazów, zacznij od dodania InputFile
składnika z odwołaniem do składnika i procedurą OnChange
obsługi:
<InputFile @ref="inputFile" OnChange="ShowPreview" />
Dodaj element obrazu z odwołaniem do elementu, który służy jako symbol zastępczy podglądu obrazu:
<img @ref="previewImageElem" />
Dodaj skojarzone odwołania:
@code {
private InputFile? inputFile;
private ElementReference previewImageElem;
}
W języku JavaScript dodaj funkcję o nazwie z kodem HTML input
i img
elementem, który wykonuje następujące czynności:
- Wyodrębnia wybrany plik.
- Tworzy adres URL obiektu za pomocą polecenia
createObjectURL
. - Ustawia odbiornik zdarzeń, aby odwołać adres URL obiektu za pomocą
revokeObjectURL
polecenia po załadowaniu obrazu, więc pamięć nie zostanie ujawniona. -
img
Ustawia źródło elementu, aby wyświetlić obraz.
window.previewImage = (inputElem, imgElem) => {
const url = URL.createObjectURL(inputElem.files[0]);
imgElem.addEventListener('load', () => URL.revokeObjectURL(url), { once: true });
imgElem.src = url;
}
Na koniec użyj wstrzykniętego IJSRuntime elementu , aby dodać procedurę OnChange
obsługi, która wywołuje funkcję JavaScript:
@inject IJSRuntime JS
...
@code {
...
private async Task ShowPreview() => await JS.InvokeVoidAsync(
"previewImage", inputFile!.Element, previewImageElem);
}
Powyższy przykład dotyczy przekazywania pojedynczego obrazu. Podejście można rozszerzyć w celu obsługi multiple
obrazów.
FileUpload4
Poniższy składnik przedstawia kompletny przykład.
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);
}
Zapisywanie małych plików bezpośrednio w bazie danych za pomocą polecenia EF Core
Wiele aplikacji ASP.NET Core używa programu Entity Framework Core (EF Core) do zarządzania operacjami bazy danych. Zapisywanie miniatur i awatarów bezpośrednio w bazie danych jest typowym wymaganiem. W tej sekcji przedstawiono ogólne podejście, które można dodatkowo ulepszyć w przypadku aplikacji produkcyjnych.
Następujący wzorzec:
- Jest oparta na aplikacji do nauki korzystania z bazy danych filmów.
- Można go ulepszyć przy użyciu dodatkowego kodu w celu uzyskania informacji zwrotnych dotyczących rozmiaru pliku i weryfikacji typu zawartości.
- Wiąże się z pogorszeniem wydajności i ryzykiem DoS. Ostrożnie należy rozważyć ryzyko podczas odczytywania dowolnego pliku do pamięci i rozważyć alternatywne podejścia, szczególnie w przypadku większych plików. Alternatywne podejścia obejmują zapisywanie plików bezpośrednio na dysku lub w usłudze innej firmy na potrzeby kontroli oprogramowania antywirusowego/ochrony przed złośliwym kodem, dalszego przetwarzania i obsługi klientów.
Aby poniższy przykład Blazor Web App działał poprawnie w (.NET 8 lub nowszym), składnik musi przyjąć interaktywny tryb renderowania (na przykład @rendermode InteractiveServer
) w celu wywołania HandleSelectedThumbnail
przy zmianie pliku składnika InputFile
(OnChange
parametr/zdarzenie).
Blazor Server składniki aplikacji są zawsze interaktywne i nie wymagają trybu renderowania.
W poniższym przykładzie mała miniatura (<= 100 KB) w obiekcie IBrowserFile jest zapisywana w bazie danych za pomocą polecenia EF Core. Jeśli plik nie zostanie wybrany przez użytkownika dla InputFile
składnika, domyślna miniatura zostanie zapisana w bazie danych.
Domyślna miniatura (default-thumbnail.jpg
) znajduje się w katalogu głównym projektu z ustawieniem Kopiuj do katalogu wyjściowegoKopiuj, jeśli nowsze:
Model Movie
() ma właściwość (Movie.cs
Thumbnail
) do przechowywania danych obrazu miniatury:
[Column(TypeName = "varbinary(MAX)")]
public byte[]? Thumbnail { get; set; }
Dane obrazu są przechowywane jako bajty w bazie danych jako varbinary(MAX)
. Aplikacja base-64 koduje bajty do wyświetlenia, ponieważ zakodowane w formacie base-64 dane są mniej więcej jedną trzecią większą niż nieprzetworzone bajty obrazu, dlatego dane obrazu base-64 wymagają dodatkowego magazynu bazy danych i zmniejszają wydajność operacji odczytu/zapisu bazy danych.
Składniki, które wyświetlają dane obrazu miniatury, przekazują dane obrazu do atrybutu tagu img
src
jako dane zakodowane w formacie JPEG, base-64:
<img src="data:image/jpeg;base64,@Convert.ToBase64String(movie.Thumbnail)"
alt="User thumbnail" />
W poniższym Create
składniku następuje przetwarzanie przesyłania obrazu. Możesz jeszcze bardziej ulepszyć przykład dzięki niestandardowej weryfikacji typu i rozmiaru pliku przy użyciu metod weryfikacji formularzy ASP.NET CoreBlazor. Aby zobaczyć pełny składnik Create
bez kodu przesyłania miniatur w poniższym przykładzie, zobacz aplikację przykładową BlazorWebAppMovies
w repozytorium GitHub z przykładami Blazor.
Components/Pages/MoviePages/Create.razor
:
@page "/movies/create"
@rendermode InteractiveServer
@using Microsoft.EntityFrameworkCore
@using BlazorWebAppMovies.Models
@inject IDbContextFactory<BlazorWebAppMovies.Data.BlazorWebAppMoviesContext> DbFactory
@inject NavigationManager NavigationManager
...
<div class="row">
<div class="col-md-4">
<EditForm method="post" Model="Movie" OnValidSubmit="AddMovie"
FormName="create" Enhance>
<DataAnnotationsValidator />
<ValidationSummary class="text-danger" role="alert"/>
...
<div class="mb-3">
<label for="thumbnail" class="form-label">Thumbnail:</label>
<InputFile id="thumbnail" OnChange="HandleSelectedThumbnail"
class="form-control" />
</div>
<button type="submit" class="btn btn-primary">Create</button>
</EditForm>
</div>
</div>
...
@code {
private const long maxFileSize = 102400;
private IBrowserFile? browserFile;
[SupplyParameterFromForm]
private Movie Movie { get; set; } = new();
private void HandleSelectedThumbnail(InputFileChangeEventArgs e)
{
browserFile = e.File;
}
private async Task AddMovie()
{
using var context = DbFactory.CreateDbContext();
if (browserFile?.Size > 0 && browserFile?.Size <= maxFileSize)
{
using var memoryStream = new MemoryStream();
await browserFile.OpenReadStream(maxFileSize).CopyToAsync(memoryStream);
Movie.Thumbnail = memoryStream.ToArray();
}
else
{
Movie.Thumbnail = File.ReadAllBytes(
$"{AppDomain.CurrentDomain.BaseDirectory}default_thumbnail.jpg");
}
context.Movie.Add(Movie);
await context.SaveChangesAsync();
NavigationManager.NavigateTo("/movies");
}
}
Takie samo podejście byłoby przyjęte w składniku Edit
z trybem renderowania interakcyjnego, jeśli użytkownicy mogli edytować obraz miniatury filmu.
Przekazywanie plików do usługi zewnętrznej
Zamiast aplikacji obsługującej bajty przekazywania plików i serwera aplikacji odbierającego przekazane pliki, klienci mogą bezpośrednio przekazywać pliki do usługi zewnętrznej. Aplikacja może bezpiecznie przetwarzać pliki z usługi zewnętrznej na żądanie. Takie podejście wzmacnia zabezpieczenia aplikacji i jej serwera przed złośliwymi atakami i potencjalnymi problemami z wydajnością.
Rozważmy podejście korzystające z usługi Azure Files, Azure Blob Storage lub usługi innej firmy z następującymi potencjalnymi korzyściami:
- Przekazywanie plików z klienta bezpośrednio do usługi zewnętrznej przy użyciu biblioteki klienta języka JavaScript lub REST interfejsu API. Na przykład platforma Azure oferuje następujące biblioteki klienta i interfejsy API:
- Autoryzowanie przesyłania plików przez użytkownika za pomocą tokenu sygnatury dostępu współdzielonego (SAS) delegowanego przez użytkownika, wygenerowanego przez aplikację (po stronie serwera) dla każdego przesyłania pliku klienta. Na przykład platforma Azure oferuje następujące funkcje sygnatury dostępu współdzielonego:
- Zapewnij automatyczną nadmiarowość i kopię zapasową udziału plików.
- Ogranicz przekazywanie przy użyciu limitów przydziału. Należy pamiętać, że limity przydziału usługi Azure Blob Storage są ustawiane na poziomie konta, a nie na poziomie kontenera. Jednak limity przydziału usługi Azure Files są na poziomie udziału plików i mogą zapewnić lepszą kontrolę nad limitami przekazywania. Aby uzyskać więcej informacji, zobacz dokumenty platformy Azure połączone wcześniej na tej liście.
- Zabezpieczanie plików przy użyciu szyfrowania po stronie serwera (SSE).
Aby uzyskać więcej informacji na temat usług Azure Blob Storage i Azure Files, zobacz dokumentację usługi Azure Storage.
Limit rozmiaru komunikatów po stronie SignalR serwera
Przekazywanie plików może zakończyć się niepowodzeniem nawet przed rozpoczęciem, gdy Blazor pobiera dane dotyczące plików, które przekraczają maksymalny SignalR rozmiar komunikatu.
SignalR Definiuje limit rozmiaru komunikatów, który ma zastosowanie do każdego odbieranego komunikatu Blazor , a InputFile składnik przesyła strumieniowo pliki do serwera w komunikatach, które przestrzegają skonfigurowanego limitu. Jednak pierwszy komunikat, który wskazuje zestaw plików do przekazania, jest wysyłany jako unikatowy pojedynczy komunikat. Rozmiar pierwszego komunikatu może przekroczyć limit rozmiaru komunikatu SignalR . Problem nie jest związany z rozmiarem plików, jest związany z liczbą plików.
Zarejestrowany błąd jest podobny do następującego:
Błąd: Połączenie zostało rozłączone z powodu błędu "Błąd: Serwer zwrócił błąd podczas zamykania: połączenie zostało zamknięte z powodu błędu". e.log @ blazor.server.js:1
Podczas przekazywania plików osiągnięcie limitu rozmiaru komunikatu dla pierwszego komunikatu jest rzadkie. Jeśli limit zostanie osiągnięty, aplikacja może skonfigurować HubOptions.MaximumReceiveMessageSize z większą wartością.
Aby uzyskać więcej informacji na SignalR temat konfiguracji i sposobu ustawiania MaximumReceiveMessageSizeprogramu , zobacz Blazor dotyczące platformy ASP.NET CoreSignalR.
Maksymalna liczba wywołań równoległych na ustawienie centrum klienta
Blazor wartość jest ustawiona na MaximumParallelInvocationsPerClient wartość 1, która jest wartością domyślną.
Zwiększenie wartości prowadzi do wysokiego prawdopodobieństwa, że CopyTo
operacje zgłaszają wartość System.InvalidOperationException: 'Reading is not allowed after reader was completed.'
. Aby uzyskać więcej informacji, zobacz MaximumParallelInvocationsPerClient > 1 przerywa przekazywanie plików w Blazor Server trybie (dotnet/aspnetcore
#53951).
Rozwiązywanie problemów
Wiersz, który wywołuje IBrowserFile.OpenReadStream element , zgłasza element System.TimeoutException:
System.TimeoutException: Did not receive any data in the allotted time.
Możliwe przyczyny:
Używanie kontenera Autofac Inversion of Control (IoC) zamiast wbudowanego kontenera iniekcji zależności ASP.NET Core na platformie .NET 8 lub starszej wersji. Aby rozwiązać ten problem, ustaw wartość DisableImplicitFromServicesParameters na
true
w opcjach centrum obsługi obwodu po stronie serwera. Aby uzyskać więcej informacji, zobacz FileUpload: Nie odebrano żadnych danych w czasie przydzielonym (dotnet/aspnetcore
#38842).Nie odczytuje strumienia do ukończenia. Nie jest to problem ze strukturą. Załapuj wyjątek i zbadaj go dalej w środowisku lokalnym/sieci.
- Używanie renderowania po stronie serwera i wywoływania OpenReadStream wielu plików przed odczytaniem ich do ukończenia. Aby rozwiązać ten problem, użyj
LazyBrowserFileStream
klasy i podejścia opisanego w sekcji Przekazywanie plików do serwera z renderowaniem po stronie serwera w tym artykule.
Dodatkowe zasoby
- pobieranie plików ASP.NET Core Blazor
- Przekazywanie plików w ASP.NET Core
- Omówienie formularzy ASP.NET Core Blazor
-
Blazor (
dotnet/blazor-samples
jak pobrać)
- Przekazywanie plików w ASP.NET Core
- Omówienie formularzy ASP.NET Core Blazor
-
Blazor (
dotnet/blazor-samples
jak pobrać)