unggahan file ASP.NET Core Blazor
Catatan
Ini bukan versi terbaru dari artikel ini. Untuk rilis saat ini, lihat versi .NET 8 dari artikel ini.
Peringatan
Versi ASP.NET Core ini tidak lagi didukung. Untuk informasi selengkapnya, lihat Kebijakan Dukungan .NET dan .NET Core. Untuk rilis saat ini, lihat versi .NET 8 dari artikel ini.
Penting
Informasi ini berkaitan dengan produk pra-rilis yang mungkin dimodifikasi secara substansial sebelum dirilis secara komersial. Microsoft tidak memberikan jaminan, tersirat maupun tersurat, sehubungan dengan informasi yang diberikan di sini.
Untuk rilis saat ini, lihat versi .NET 8 dari artikel ini.
Artikel ini menjelaskan cara mengunggah file dengan Blazor InputFile komponen.
Unggahan file
Peringatan
Selalu ikuti praktik terbaik keamanan saat mengizinkan pengguna mengunggah file. Untuk informasi selengkapnya, lihat Mengunggah file di ASP.NET Core.
InputFile Gunakan komponen untuk membaca data file browser ke dalam kode .NET. Komponen InputFile merender elemen HTML <input>
jenis file
untuk unggahan file tunggal. multiple
Tambahkan atribut untuk mengizinkan pengguna mengunggah beberapa file sekaligus.
Pemilihan file tidak kumulatif saat menggunakan InputFile komponen atau HTML <input type="file">
yang mendasarnya, sehingga Anda tidak dapat menambahkan file ke pilihan file yang ada. Komponen selalu menggantikan pilihan file awal pengguna, sehingga referensi file dari pilihan sebelumnya tidak tersedia.
Komponen berikut InputFile menjalankan LoadFiles
metode ketika OnChange peristiwa (change
) terjadi. Menyediakan InputFileChangeEventArgs akses ke daftar file yang dipilih dan detail tentang setiap file:
<InputFile OnChange="LoadFiles" multiple />
@code {
private void LoadFiles(InputFileChangeEventArgs e)
{
...
}
}
HTML yang dirender:
<input multiple="" type="file" _bl_2="">
Catatan
Dalam contoh sebelumnya, <input>
atribut elemen _bl_2
digunakan untuk Blazorpemrosesan internal.
Untuk membaca data dari file yang dipilih pengguna, panggil IBrowserFile.OpenReadStream pada file dan baca dari aliran yang dikembalikan. Untuk informasi selengkapnya, lihat bagian Aliran file.
OpenReadStream memberlakukan ukuran maksimum dalam byte dari Stream. Membaca satu file atau beberapa file yang lebih besar dari 500 KB menghasilkan pengecualian. Batas ini mencegah pengembang membaca file besar secara tidak sengaja ke dalam memori. Parameter maxAllowedSize
OpenReadStream dapat digunakan untuk menentukan ukuran yang lebih besar jika diperlukan.
Jika Anda memerlukan akses ke Stream yang mewakili byte file, gunakan IBrowserFile.OpenReadStream. Hindari membaca aliran file masuk langsung ke memori sekaligus. Misalnya, jangan menyalin semua byte file ke dalam MemoryStream atau membaca seluruh aliran ke dalam array byte sekaligus. Pendekatan ini dapat mengakibatkan penurunan performa aplikasi dan potensi risiko Denial of Service (DoS), terutama untuk komponen sisi server. Sebagai gantinya, pertimbangkan untuk mengadopsi salah satu pendekatan berikut:
- Salin aliran langsung ke file pada disk tanpa membacanya ke dalam memori. Perhatikan bahwa aplikasi yang Blazor menjalankan kode di server tidak dapat mengakses sistem file klien secara langsung.
- Unggah file dari klien langsung ke layanan eksternal. Untuk informasi selengkapnya, lihat bagian Mengunggah file ke layanan eksternal.
Dalam contoh berikut, browserFile
mewakili file yang diunggah dan mengimplementasikan IBrowserFile. Implementasi kerja untuk IBrowserFile ditampilkan dalam komponen unggahan file nanti di artikel ini.
Stream disediakan langsung kepada konsumen, FileStream yang membuat file di jalur yang disediakan:
Pendekatan berikut direkomendasikan karena fileawait using FileStream fs = new(path, FileMode.Create);
await browserFile.OpenReadStream().CopyToAsync(fs);
Didukung: Pendekatan berikut direkomendasikan untuk Microsoft Azure Blob Storage karena file Stream disediakan langsung ke UploadBlobAsync:
await blobContainerClient.UploadBlobAsync(
trustedFileName, browserFile.OpenReadStream());
Tidak disarankan: Pendekatan berikut TIDAK disarankan karena konten file Stream dibaca ke dalam String memori (reader
):
var reader =
await new StreamReader(browserFile.OpenReadStream()).ReadToEndAsync();
Tidak disarankan: Pendekatan berikut TIDAK disarankan untuk Microsoft Azure Blob Storage karena konten file Stream disalin ke dalam MemoryStream memori (memoryStream
) sebelum memanggil UploadBlobAsync:
var memoryStream = new MemoryStream();
await browserFile.OpenReadStream().CopyToAsync(memoryStream);
await blobContainerClient.UploadBlobAsync(
trustedFileName, memoryStream));
Komponen yang menerima file gambar dapat memanggil BrowserFileExtensions.RequestImageFileAsync metode kenyamanan pada file untuk mengubah ukuran data gambar dalam runtime JavaScript browser sebelum gambar dialirkan ke aplikasi. Kasus penggunaan untuk panggilan RequestImageFileAsync paling tepat untuk Blazor WebAssembly aplikasi.
Pengguna kontainer Inversion of Control (IoC) Autofac
Jika Anda menggunakan kontainer Autofac Inversion of Control (IoC) alih-alih kontainer injeksi dependensi ASP.NET Core bawaan, atur DisableImplicitFromServicesParameters ke true
dalam opsi hub handler sirkuit sisi server. Untuk informasi selengkapnya, lihat FileUpload: Tidak menerima data apa pun dalam waktu yang dialokasikan (dotnet/aspnetcore
#38842).
Batas baca dan unggahan ukuran file
Sisi server atau sisi klien, tidak ada batas ukuran baca atau unggahan file khusus untuk InputFile komponen. Namun, sisi Blazor klien membaca byte file ke dalam satu buffer array JavaScript saat menghidupkan data dari JavaScript ke C#, yang dibatasi hingga 2 GB atau ke memori perangkat yang tersedia. Unggahan file besar (> 250 MB) mungkin gagal untuk unggahan sisi klien menggunakan InputFile komponen . Untuk informasi selengkapnya, lihat diskusi berikut ini:
Ukuran file maksimum yang InputFile didukung untuk komponen adalah 2 GB. Selain itu, sisi Blazor klien membaca byte file ke dalam satu buffer array JavaScript saat menghidupkan data dari JavaScript ke C#, yang dibatasi hingga 2 GB atau ke memori perangkat yang tersedia. Unggahan file besar (> 250 MB) mungkin gagal untuk unggahan sisi klien menggunakan InputFile komponen . Untuk informasi selengkapnya, lihat diskusi berikut ini:
- Komponen Blazor InputFile harus menangani penggugusan saat file diunggah (dotnet/runtime #84685)
- Unggahan Streaming Permintaan melalui handler http (dotnet/runtime #36634)
Untuk unggahan file sisi klien besar yang gagal saat mencoba menggunakan InputFile komponen, kami sarankan untuk memotong file besar dengan komponen kustom menggunakan beberapa permintaan rentang HTTP alih-alih menggunakan InputFile komponen.
Pekerjaan saat ini dijadwalkan untuk .NET 9 (akhir 2024) untuk mengatasi batasan unggahan ukuran file sisi klien.
Contoh
Contoh berikut menunjukkan beberapa unggahan file dalam komponen. InputFileChangeEventArgs.GetMultipleFiles memungkinkan membaca beberapa file. Tentukan jumlah maksimum file untuk mencegah pengguna berbahaya mengunggah jumlah file yang lebih besar dari yang diharapkan aplikasi. InputFileChangeEventArgs.File memungkinkan membaca file pertama dan satu-satunya jika unggahan file tidak mendukung beberapa file.
InputFileChangeEventArgs berada di Microsoft.AspNetCore.Components.Forms namespace layanan, yang biasanya merupakan salah satu namespace dalam file aplikasi _Imports.razor
. Saat namespace ada dalam _Imports.razor
file, namespace menyediakan akses anggota API ke komponen aplikasi.
Namespace dalam _Imports.razor
file tidak diterapkan ke file C# (.cs
). File C# memerlukan arahan eksplisit using
di bagian atas file kelas:
using Microsoft.AspNetCore.Components.Forms;
Untuk menguji komponen pengunggahan file, Anda dapat membuat file pengujian dengan ukuran apa pun dengan PowerShell:
$out = new-object byte[] {SIZE}; (new-object Random).NextBytes($out); [IO.File]::WriteAllBytes('{PATH}', $out)
Pada perintah sebelumnya:
- Tempat
{SIZE}
penampung adalah ukuran file dalam byte (misalnya,2097152
untuk file 2 MB). - Tempat
{PATH}
penampung adalah jalur dan file dengan ekstensi file (misalnya,D:/test_files/testfile2MB.txt
).
Contoh unggahan file sisi server
Untuk menggunakan kode berikut, buat Development/unsafe_uploads
folder di akar aplikasi yang berjalan di Development
lingkungan.
Karena contoh menggunakan lingkungan aplikasi sebagai bagian dari jalur tempat file disimpan, folder tambahan diperlukan jika lingkungan lain digunakan dalam pengujian dan produksi. Misalnya, buat Staging/unsafe_uploads
folder untuk Staging
lingkungan. Buat Production/unsafe_uploads
folder untuk Production
lingkungan.
Peringatan
Contoh menyimpan file tanpa memindai kontennya, dan panduan dalam artikel ini tidak memperhitungkan praktik terbaik keamanan tambahan untuk file yang diunggah. Pada sistem penahapan dan produksi, nonaktifkan izin eksekusi pada folder unggah dan pindai file dengan API pemindai anti-virus/anti-malware segera setelah diunggah. Untuk informasi selengkapnya, lihat Mengunggah file di ASP.NET Core.
FileUpload1.razor
:
@page "/file-upload-1"
@using System
@using System.IO
@using Microsoft.AspNetCore.Hosting
@inject ILogger<FileUpload1> Logger
@inject IWebHostEnvironment Environment
<PageTitle>File Upload 1</PageTitle>
<h1>File Upload Example 1</h1>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Uploading...</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private async Task LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
var trustedFileName = Path.GetRandomFileName();
var path = Path.Combine(Environment.ContentRootPath,
Environment.EnvironmentName, "unsafe_uploads",
trustedFileName);
await using FileStream fs = new(path, FileMode.Create);
await file.OpenReadStream(maxFileSize).CopyToAsync(fs);
loadedFiles.Add(file);
Logger.LogInformation(
"Unsafe Filename: {UnsafeFilename} File saved: {Filename}",
file.Name, trustedFileName);
}
catch (Exception ex)
{
Logger.LogError("File: {Filename} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
@page "/file-upload-1"
@using System
@using System.IO
@using Microsoft.AspNetCore.Hosting
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload1> Logger
@inject IWebHostEnvironment Environment
<h3>Upload Files</h3>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Uploading...</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private async Task LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
loadedFiles.Add(file);
var trustedFileNameForFileStorage = Path.GetRandomFileName();
var path = Path.Combine(Environment.ContentRootPath,
Environment.EnvironmentName, "unsafe_uploads",
trustedFileNameForFileStorage);
await using FileStream fs = new(path, FileMode.Create);
await file.OpenReadStream(maxFileSize).CopyToAsync(fs);
}
catch (Exception ex)
{
Logger.LogError("File: {Filename} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
@page "/file-upload-1"
@using System
@using System.IO
@using Microsoft.AspNetCore.Hosting
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload1> Logger
@inject IWebHostEnvironment Environment
<h3>Upload Files</h3>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Uploading...</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private async Task LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
loadedFiles.Add(file);
var trustedFileNameForFileStorage = Path.GetRandomFileName();
var path = Path.Combine(Environment.ContentRootPath,
Environment.EnvironmentName, "unsafe_uploads",
trustedFileNameForFileStorage);
await using FileStream fs = new(path, FileMode.Create);
await file.OpenReadStream(maxFileSize).CopyToAsync(fs);
}
catch (Exception ex)
{
Logger.LogError("File: {Filename} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
@page "/file-upload-1"
@using System
@using System.IO
@using Microsoft.AspNetCore.Hosting
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload1> Logger
@inject IWebHostEnvironment Environment
<h3>Upload Files</h3>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Uploading...</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private async Task LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
loadedFiles.Add(file);
var trustedFileNameForFileStorage = Path.GetRandomFileName();
var path = Path.Combine(Environment.ContentRootPath,
Environment.EnvironmentName, "unsafe_uploads",
trustedFileNameForFileStorage);
await using FileStream fs = new(path, FileMode.Create);
await file.OpenReadStream(maxFileSize).CopyToAsync(fs);
}
catch (Exception ex)
{
Logger.LogError("File: {Filename} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
Contoh unggahan file sisi klien
Contoh berikut memproses byte file dan tidak mengirim file ke tujuan di luar aplikasi. Untuk contoh Razor komponen yang mengirim file ke server atau layanan, lihat bagian berikut ini:
Komponen mengasumsikan bahwa mode render Interactive WebAssembly (InteractiveWebAssembly
) diwarisi dari komponen induk atau diterapkan secara global ke aplikasi.
@page "/file-upload-1"
@inject ILogger<FileUpload1> Logger
<PageTitle>File Upload 1</PageTitle>
<h1>File Upload Example 1</h1>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Uploading...</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private void LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
loadedFiles.Add(file);
}
catch (Exception ex)
{
Logger.LogError("File: {FileName} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
@page "/file-upload-1"
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload1> Logger
<h3>Upload Files</h3>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Uploading...</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private void LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
loadedFiles.Add(file);
}
catch (Exception ex)
{
Logger.LogError("File: {FileName} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
@page "/file-upload-1"
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload1> Logger
<h3>Upload Files</h3>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Uploading...</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private void LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
loadedFiles.Add(file);
}
catch (Exception ex)
{
Logger.LogError("File: {Filename} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
@page "/file-upload-1"
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload1> Logger
<h3>Upload Files</h3>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Uploading...</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private void LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
loadedFiles.Add(file);
}
catch (Exception ex)
{
Logger.LogError("File: {Filename} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
IBrowserFile mengembalikan metadata yang diekspos oleh browser sebagai properti. Gunakan metadata ini untuk validasi awal.
Jangan pernah mempercayai nilai properti sebelumnya, terutama Name properti untuk ditampilkan di UI. Perlakukan semua data yang disediakan pengguna sebagai risiko keamanan yang signifikan terhadap aplikasi, server, dan jaringan. Untuk informasi selengkapnya, lihat Mengunggah file di ASP.NET Core.
Mengunggah file ke server dengan penyajian sisi server
Bagian ini berlaku untuk komponen Server Interaktif di Blazor Web Apps atau Blazor Server aplikasi.
Contoh berikut menunjukkan pengunggahan file dari aplikasi sisi server ke pengontrol API web backend di aplikasi terpisah, mungkin di server terpisah.
Dalam file aplikasi Program
sisi server, tambahkan IHttpClientFactory dan layanan terkait yang memungkinkan aplikasi membuat HttpClient instans:
builder.Services.AddHttpClient();
Untuk informasi lebih lanjut, lihat Membuat permintaan HTTP menggunakan IHttpClientFactory di ASP.NET Core.
Untuk contoh di bagian ini:
- API web berjalan di URL:
https://localhost:5001
- Aplikasi sisi server berjalan di URL:
https://localhost:5003
Untuk pengujian, URL sebelumnya dikonfigurasi dalam file proyek Properties/launchSettings.json
.
Kelas berikut UploadResult
mempertahankan hasil file yang diunggah. Ketika file gagal diunggah di server, kode kesalahan dikembalikan ErrorCode
untuk ditampilkan kepada pengguna. Nama file aman dihasilkan di server untuk setiap file dan dikembalikan ke klien untuk StoredFileName
ditampilkan. File di-key antara klien dan server menggunakan nama file yang tidak aman/tidak tepercaya di 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; }
}
Praktik terbaik keamanan untuk aplikasi produksi adalah menghindari pengiriman pesan kesalahan ke klien yang mungkin mengungkapkan informasi sensitif tentang aplikasi, server, atau jaringan. Menyediakan pesan kesalahan terperinci dapat membantu pengguna berbahaya dalam merancang serangan pada aplikasi, server, atau jaringan. Contoh kode di bagian ini hanya mengirim kembali nomor kode kesalahan (int
) untuk ditampilkan oleh sisi klien komponen jika terjadi kesalahan sisi server. Jika pengguna memerlukan bantuan dengan unggahan file, mereka memberikan kode kesalahan untuk mendukung personel untuk resolusi tiket dukungan tanpa mengetahui penyebab pasti kesalahan.
Kelas berikut LazyBrowserFileStream
mendefinisikan jenis aliran kustom yang dengan malas OpenReadStream memanggil tepat sebelum byte pertama aliran diminta. Aliran tidak ditransmisikan dari browser ke server hingga membaca aliran dimulai di .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();
}
Komponen berikut FileUpload2
:
- Mengizinkan pengguna mengunggah file dari klien.
- Menampilkan nama file yang tidak tepercaya/tidak aman yang disediakan oleh klien di UI. Nama file yang tidak tepercaya/tidak aman secara otomatis dikodekan HTML oleh Razor untuk tampilan aman di UI.
Peringatan
Jangan percaya nama file yang disediakan oleh klien untuk:
- Menyimpan file ke sistem atau layanan file.
- Tampilkan di UI yang tidak mengodekan nama file secara otomatis atau melalui kode pengembang.
Untuk informasi selengkapnya tentang pertimbangan keamanan saat mengunggah file ke server, lihat Mengunggah file di ASP.NET Core.
FileUpload2.razor
:
@page "/file-upload-2"
@using System.Net.Http.Headers
@using System.Text.Json
@inject IHttpClientFactory ClientFactory
@inject ILogger<FileUpload2> Logger
<PageTitle>File Upload 2</PageTitle>
<h1>File Upload Example 2</h1>
<p>
This example requires a backend server API to function. For more information,
see the <em>Upload files to a server</em> section
of the <em>ASP.NET Core Blazor file uploads</em> article.
</p>
<p>
<label>
Upload up to @maxAllowedFiles files:
<InputFile OnChange="OnInputFileChange" multiple />
</label>
</p>
@if (files.Any())
{
<div class="card">
<div class="card-body">
<ul>
@foreach (var file in files)
{
<li>
File: @file.Name
<br>
@if (FileUpload(uploadResults, file.Name, Logger,
out var result))
{
<span>
Stored File Name: @result.StoredFileName
</span>
}
else
{
<span>
There was an error uploading the file
(Error: @result.ErrorCode).
</span>
}
</li>
}
</ul>
</div>
</div>
}
@code {
private List<File> files = new();
private List<UploadResult> uploadResults = new();
private int maxAllowedFiles = 3;
private bool shouldRender;
protected override bool ShouldRender() => shouldRender;
private async Task OnInputFileChange(InputFileChangeEventArgs e)
{
shouldRender = false;
int maxFileSize = 1024 * 15;
var upload = false;
using var content = new MultipartFormDataContent();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
if (uploadResults.SingleOrDefault(
f => f.FileName == file.Name) is null)
{
try
{
files.Add(new() { Name = file.Name });
var stream = new LazyBrowserFileStream(file, maxFileSize);
var fileContent = new StreamContent(stream);
fileContent.Headers.ContentType =
new MediaTypeHeaderValue(file.ContentType);
content.Add(
content: fileContent,
name: "\"files\"",
fileName: file.Name);
upload = true;
}
catch (Exception ex)
{
Logger.LogInformation(
"{FileName} not uploaded (Err: 6): {Message}",
file.Name, ex.Message);
uploadResults.Add(
new()
{
FileName = file.Name,
ErrorCode = 6,
Uploaded = false
});
}
}
}
if (upload)
{
var client = ClientFactory.CreateClient();
var response =
await client.PostAsync("https://localhost:5001/Filesave",
content);
if (response.IsSuccessStatusCode)
{
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
};
using var responseStream =
await response.Content.ReadAsStreamAsync();
var newUploadResults = await JsonSerializer
.DeserializeAsync<IList<UploadResult>>(responseStream, options);
if (newUploadResults is not null)
{
uploadResults = uploadResults.Concat(newUploadResults).ToList();
}
}
}
shouldRender = true;
}
private static bool FileUpload(IList<UploadResult> uploadResults,
string? fileName, ILogger<FileUpload2> logger, out UploadResult result)
{
result = uploadResults.SingleOrDefault(f => f.FileName == fileName) ?? new();
if (!result.Uploaded)
{
logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
result.ErrorCode = 5;
}
return result.Uploaded;
}
private class File
{
public string? Name { get; set; }
}
}
@page "/file-upload-2"
@using System.Net.Http.Headers
@using System.Text.Json
@inject IHttpClientFactory ClientFactory
@inject ILogger<FileUpload2> Logger
<h1>File Upload Example 2</h1>
<p>
This example requires a backend server API to function. For more information,
see the <em>Upload files to a server</em> section
of the <em>ASP.NET Core Blazor file uploads</em> article.
</p>
<p>
<label>
Upload up to @maxAllowedFiles files:
<InputFile OnChange="OnInputFileChange" multiple />
</label>
</p>
@if (files.Count > 0)
{
<div class="card">
<div class="card-body">
<ul>
@foreach (var file in files)
{
<li>
File: @file.Name
<br>
@if (FileUpload(uploadResults, file.Name, Logger,
out var result))
{
<span>
Stored File Name: @result.StoredFileName
</span>
}
else
{
<span>
There was an error uploading the file
(Error: @result.ErrorCode).
</span>
}
</li>
}
</ul>
</div>
</div>
}
@code {
private List<File> files = new();
private List<UploadResult> uploadResults = new();
private int maxAllowedFiles = 3;
private bool shouldRender;
protected override bool ShouldRender() => shouldRender;
private async Task OnInputFileChange(InputFileChangeEventArgs e)
{
shouldRender = false;
int maxFileSize = 1024 * 15;
var upload = false;
using var content = new MultipartFormDataContent();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
if (uploadResults.SingleOrDefault(
f => f.FileName == file.Name) is null)
{
try
{
files.Add(new() { Name = file.Name });
var stream = new LazyBrowserFileStream(file, maxFileSize);
var fileContent = new StreamContent(stream);
fileContent.Headers.ContentType =
new MediaTypeHeaderValue(file.ContentType);
content.Add(
content: fileContent,
name: "\"files\"",
fileName: file.Name);
upload = true;
}
catch (Exception ex)
{
Logger.LogInformation(
"{FileName} not uploaded (Err: 6): {Message}",
file.Name, ex.Message);
uploadResults.Add(
new()
{
FileName = file.Name,
ErrorCode = 6,
Uploaded = false
});
}
}
}
if (upload)
{
var client = ClientFactory.CreateClient();
var response =
await client.PostAsync("https://localhost:5001/Filesave",
content);
if (response.IsSuccessStatusCode)
{
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
};
using var responseStream =
await response.Content.ReadAsStreamAsync();
var newUploadResults = await JsonSerializer
.DeserializeAsync<IList<UploadResult>>(responseStream, options);
if (newUploadResults is not null)
{
uploadResults = uploadResults.Concat(newUploadResults).ToList();
}
}
}
shouldRender = true;
}
private static bool FileUpload(IList<UploadResult> uploadResults,
string? fileName, ILogger<FileUpload2> logger, out UploadResult result)
{
result = uploadResults.SingleOrDefault(f => f.FileName == fileName) ?? new();
if (!result.Uploaded)
{
logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
result.ErrorCode = 5;
}
return result.Uploaded;
}
private class File
{
public string? Name { get; set; }
}
}
@page "/file-upload-2"
@using System.Net.Http.Headers
@using System.Text.Json
@inject IHttpClientFactory ClientFactory
@inject ILogger<FileUpload2> Logger
<h1>File Upload Example 2</h1>
<p>
This example requires a backend server API to function. For more information,
see the <em>Upload files to a server</em> section
of the <em>ASP.NET Core Blazor file uploads</em> article.
</p>
<p>
<label>
Upload up to @maxAllowedFiles files:
<InputFile OnChange="OnInputFileChange" multiple />
</label>
</p>
@if (files.Count > 0)
{
<div class="card">
<div class="card-body">
<ul>
@foreach (var file in files)
{
<li>
File: @file.Name
<br>
@if (FileUpload(uploadResults, file.Name, Logger,
out var result))
{
<span>
Stored File Name: @result.StoredFileName
</span>
}
else
{
<span>
There was an error uploading the file
(Error: @result.ErrorCode).
</span>
}
</li>
}
</ul>
</div>
</div>
}
@code {
private List<File> files = new();
private List<UploadResult> uploadResults = new();
private int maxAllowedFiles = 3;
private bool shouldRender;
protected override bool ShouldRender() => shouldRender;
private async Task OnInputFileChange(InputFileChangeEventArgs e)
{
shouldRender = false;
int maxFileSize = 1024 * 15;
var upload = false;
using var content = new MultipartFormDataContent();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
if (uploadResults.SingleOrDefault(
f => f.FileName == file.Name) is null)
{
try
{
files.Add(new() { Name = file.Name });
var stream = new LazyBrowserFileStream(file, maxFileSize);
var fileContent = new StreamContent(stream);
fileContent.Headers.ContentType =
new MediaTypeHeaderValue(file.ContentType);
content.Add(
content: fileContent,
name: "\"files\"",
fileName: file.Name);
upload = true;
}
catch (Exception ex)
{
Logger.LogInformation(
"{FileName} not uploaded (Err: 6): {Message}",
file.Name, ex.Message);
uploadResults.Add(
new()
{
FileName = file.Name,
ErrorCode = 6,
Uploaded = false
});
}
}
}
if (upload)
{
var client = ClientFactory.CreateClient();
var response =
await client.PostAsync("https://localhost:5001/Filesave",
content);
if (response.IsSuccessStatusCode)
{
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
};
using var responseStream =
await response.Content.ReadAsStreamAsync();
var newUploadResults = await JsonSerializer
.DeserializeAsync<IList<UploadResult>>(responseStream, options);
if (newUploadResults is not null)
{
uploadResults = uploadResults.Concat(newUploadResults).ToList();
}
}
}
shouldRender = true;
}
private static bool FileUpload(IList<UploadResult> uploadResults,
string? fileName, ILogger<FileUpload2> logger, out UploadResult result)
{
result = uploadResults.SingleOrDefault(f => f.FileName == fileName) ?? new();
if (!result.Uploaded)
{
logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
result.ErrorCode = 5;
}
return result.Uploaded;
}
private class File
{
public string? Name { get; set; }
}
}
@page "/file-upload-2"
@using System.Net.Http.Headers
@using System.Text.Json
@using Microsoft.Extensions.Logging
@inject IHttpClientFactory ClientFactory
@inject ILogger<FileUpload2> Logger
<h1>File Upload Example 2</h1>
<p>
This example requires a backend server API to function. For more information,
see the <em>Upload files to a server</em> section
of the <em>ASP.NET Core Blazor file uploads</em> article.
</p>
<p>
<label>
Upload up to @maxAllowedFiles files:
<InputFile OnChange="OnInputFileChange" multiple />
</label>
</p>
@if (files.Count > 0)
{
<div class="card">
<div class="card-body">
<ul>
@foreach (var file in files)
{
<li>
File: @file.Name
<br>
@if (FileUpload(uploadResults, file.Name, Logger,
out var result))
{
<span>
Stored File Name: @result.StoredFileName
</span>
}
else
{
<span>
There was an error uploading the file
(Error: @result.ErrorCode).
</span>
}
</li>
}
</ul>
</div>
</div>
}
@code {
private List<File> files = new();
private List<UploadResult> uploadResults = new();
private int maxAllowedFiles = 3;
private bool shouldRender;
protected override bool ShouldRender() => shouldRender;
private async Task OnInputFileChange(InputFileChangeEventArgs e)
{
shouldRender = false;
int maxFileSize = 1024 * 15;
var upload = false;
using var content = new MultipartFormDataContent();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
if (uploadResults.SingleOrDefault(
f => f.FileName == file.Name) is null)
{
try
{
files.Add(new() { Name = file.Name });
var stream = new LazyBrowserFileStream(file, maxFileSize);
var fileContent = new StreamContent(stream);
fileContent.Headers.ContentType =
new MediaTypeHeaderValue(file.ContentType);
content.Add(
content: fileContent,
name: "\"files\"",
fileName: file.Name);
upload = true;
}
catch (Exception ex)
{
Logger.LogInformation(
"{FileName} not uploaded (Err: 6): {Message}",
file.Name, ex.Message);
uploadResults.Add(
new()
{
FileName = file.Name,
ErrorCode = 6,
Uploaded = false
});
}
}
}
if (upload)
{
var client = ClientFactory.CreateClient();
var response =
await client.PostAsync("https://localhost:5001/Filesave",
content);
if (response.IsSuccessStatusCode)
{
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
};
using var responseStream =
await response.Content.ReadAsStreamAsync();
var newUploadResults = await JsonSerializer
.DeserializeAsync<IList<UploadResult>>(responseStream, options);
if (newUploadResults is not null)
{
uploadResults = uploadResults.Concat(newUploadResults).ToList();
}
}
}
shouldRender = true;
}
private static bool FileUpload(IList<UploadResult> uploadResults,
string fileName, ILogger<FileUpload2> logger, out UploadResult result)
{
result = uploadResults.SingleOrDefault(f => f.FileName == fileName) ?? new();
if (!result.Uploaded)
{
logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
result.ErrorCode = 5;
}
return result.Uploaded;
}
private class File
{
public string Name { get; set; }
}
}
Jika komponen membatasi pengunggahan file ke satu file pada satu waktu atau jika komponen hanya mengadopsi penyajian sisi klien interaktif (CSR, InteractiveWebAssembly
), komponen dapat menghindari penggunaan LazyBrowserFileStream
dan menggunakan Stream. Berikut ini menunjukkan perubahan untuk FileUpload2
komponen:
- var stream = new LazyBrowserFileStream(file, maxFileSize);
- var fileContent = new StreamContent(stream);
+ var fileContent = new StreamContent(file.OpenReadStream(maxFileSize));
LazyBrowserFileStream
Hapus kelas (LazyBrowserFileStream.cs
), karena tidak digunakan.
Jika komponen membatasi pengunggahan file ke satu file sekaligus, komponen dapat menghindari penggunaan LazyBrowserFileStream
dan menggunakan Stream. Berikut ini menunjukkan perubahan untuk FileUpload2
komponen:
- var stream = new LazyBrowserFileStream(file, maxFileSize);
- var fileContent = new StreamContent(stream);
+ var fileContent = new StreamContent(file.OpenReadStream(maxFileSize));
LazyBrowserFileStream
Hapus kelas (LazyBrowserFileStream.cs
), karena tidak digunakan.
Pengontrol berikut dalam proyek API web menyimpan file yang diunggah dari klien.
Penting
Pengontrol di bagian ini dimaksudkan untuk digunakan dalam proyek API web terpisah dari Blazor aplikasi. API web harus mengurangi serangan Pemalsuan Permintaan Lintas Situs (XSRF/CSRF) jika pengguna pengunggah file diautentikasi.
Catatan
Mengikat nilai formulir dengan [FromForm]
atribut tidak didukung secara asli untuk API Minimal di ASP.NET Core di .NET 6. Oleh karena itu, contoh pengontrol berikut Filesave
tidak dapat dikonversi untuk menggunakan API Minimal. Dukungan untuk mengikat dari nilai formulir dengan API Minimal tersedia di ASP.NET Core di .NET 7 atau yang lebih baru.
Untuk menggunakan kode berikut, buat Development/unsafe_uploads
folder di akar proyek API web untuk aplikasi yang berjalan di Development
lingkungan.
Karena contoh menggunakan lingkungan aplikasi sebagai bagian dari jalur tempat file disimpan, folder tambahan diperlukan jika lingkungan lain digunakan dalam pengujian dan produksi. Misalnya, buat Staging/unsafe_uploads
folder untuk Staging
lingkungan. Buat Production/unsafe_uploads
folder untuk Production
lingkungan.
Peringatan
Contoh menyimpan file tanpa memindai kontennya, dan panduan dalam artikel ini tidak memperhitungkan praktik terbaik keamanan tambahan untuk file yang diunggah. Pada sistem penahapan dan produksi, nonaktifkan izin eksekusi pada folder unggah dan pindai file dengan API pemindai anti-virus/anti-malware segera setelah diunggah. Untuk informasi selengkapnya, lihat Mengunggah file di 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);
}
}
Dalam kode sebelumnya, GetRandomFileName dipanggil untuk menghasilkan nama file yang aman. Jangan pernah mempercayai nama file yang disediakan oleh browser, karena cyberattacker dapat memilih nama file yang ada yang menimpa file yang ada atau mengirim jalur yang mencoba menulis di luar aplikasi.
Aplikasi server harus mendaftarkan layanan pengontrol dan titik akhir pengontrol peta. Untuk informasi selengkapnya, lihat Perutean ke tindakan pengontrol di ASP.NET Core.
Mengunggah file ke server dengan penyajian sisi klien (CSR)
Bagian ini berlaku untuk komponen yang dirender sisi klien (CSR) di Blazor Web Apps atau Blazor WebAssembly aplikasi.
Contoh berikut menunjukkan pengunggahan file ke pengontrol API web backend di aplikasi terpisah, mungkin di server terpisah, dari komponen dalam Blazor Web App yang mengadopsi CSR atau komponen dalam Blazor WebAssembly aplikasi.
Kelas berikut UploadResult
mempertahankan hasil file yang diunggah. Ketika file gagal diunggah di server, kode kesalahan dikembalikan ErrorCode
untuk ditampilkan kepada pengguna. Nama file aman dihasilkan di server untuk setiap file dan dikembalikan ke klien untuk StoredFileName
ditampilkan. File di-key antara klien dan server menggunakan nama file yang tidak aman/tidak tepercaya di 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; }
}
Catatan
Kelas sebelumnya dapat dibagikan UploadResult
antara proyek berbasis klien dan server. Saat klien dan proyek server berbagi kelas, tambahkan impor ke setiap file proyek _Imports.razor
untuk proyek bersama. Contohnya:
@using BlazorSample.Shared
Komponen berikut FileUpload2
:
- Mengizinkan pengguna mengunggah file dari klien.
- Menampilkan nama file yang tidak tepercaya/tidak aman yang disediakan oleh klien di UI. Nama file yang tidak tepercaya/tidak aman secara otomatis dikodekan HTML oleh Razor untuk tampilan aman di UI.
Praktik terbaik keamanan untuk aplikasi produksi adalah menghindari pengiriman pesan kesalahan ke klien yang mungkin mengungkapkan informasi sensitif tentang aplikasi, server, atau jaringan. Menyediakan pesan kesalahan terperinci dapat membantu pengguna berbahaya dalam merancang serangan pada aplikasi, server, atau jaringan. Contoh kode di bagian ini hanya mengirim kembali nomor kode kesalahan (int
) untuk ditampilkan oleh sisi klien komponen jika terjadi kesalahan sisi server. Jika pengguna memerlukan bantuan dengan unggahan file, mereka memberikan kode kesalahan untuk mendukung personel untuk resolusi tiket dukungan tanpa mengetahui penyebab pasti kesalahan.
Peringatan
Jangan percaya nama file yang disediakan oleh klien untuk:
- Menyimpan file ke sistem atau layanan file.
- Tampilkan di UI yang tidak mengodekan nama file secara otomatis atau melalui kode pengembang.
Untuk informasi selengkapnya tentang pertimbangan keamanan saat mengunggah file ke server, lihat Mengunggah file di ASP.NET Core.
Blazor Web App Dalam proyek utama, tambahkan IHttpClientFactory dan layanan terkait dalam file proyekProgram
:
builder.Services.AddHttpClient();
Layanan HttpClient
harus ditambahkan ke proyek utama karena komponen sisi klien telah dirender di server. Jika Anda menonaktifkan pra-penyajian untuk komponen berikut, Anda tidak diharuskan menyediakan HttpClient
layanan di aplikasi utama dan tidak perlu menambahkan baris sebelumnya ke proyek utama.
Untuk informasi selengkapnya tentang menambahkan HttpClient
layanan ke aplikasi ASP.NET Core, lihat Membuat permintaan HTTP menggunakan IHttpClientFactory di ASP.NET Core.
Proyek klien (.Client
) dari Blazor Web App juga harus mendaftarkan HttpClient permintaan HTTP POST ke pengontrol API web backend. Konfirmasi atau tambahkan yang berikut ini ke file proyek Program
klien:
builder.Services.AddScoped(sp =>
new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
Contoh sebelumnya mengatur alamat dasar dengan builder.HostEnvironment.BaseAddress
(IWebAssemblyHostEnvironment.BaseAddress), yang mendapatkan alamat dasar untuk aplikasi dan biasanya berasal dari <base>
nilai tag href
di halaman host. Jika Anda memanggil API web eksternal, atur URI ke alamat dasar API web.
Tentukan atribut mode render Interactive WebAssembly di bagian atas komponen berikut dalam Blazor Web App:
@rendermode InteractiveWebAssembly
FileUpload2.razor
:
@page "/file-upload-2"
@using System.Linq
@using System.Net.Http.Headers
@inject HttpClient Http
@inject ILogger<FileUpload2> Logger
<PageTitle>File Upload 2</PageTitle>
<h1>File Upload Example 2</h1>
<p>
<label>
Upload up to @maxAllowedFiles files:
<InputFile OnChange="OnInputFileChange" multiple />
</label>
</p>
@if (files.Count > 0)
{
<div class="card">
<div class="card-body">
<ul>
@foreach (var file in files)
{
<li>
File: @file.Name
<br>
@if (FileUpload(uploadResults, file.Name, Logger,
out var result))
{
<span>
Stored File Name: @result.StoredFileName
</span>
}
else
{
<span>
There was an error uploading the file
(Error: @result.ErrorCode).
</span>
}
</li>
}
</ul>
</div>
</div>
}
@code {
private List<File> files = new();
private List<UploadResult> uploadResults = new();
private int maxAllowedFiles = 3;
private bool shouldRender;
protected override bool ShouldRender() => shouldRender;
private async Task OnInputFileChange(InputFileChangeEventArgs e)
{
shouldRender = false;
long maxFileSize = 1024 * 15;
var upload = false;
using var content = new MultipartFormDataContent();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
if (uploadResults.SingleOrDefault(
f => f.FileName == file.Name) is null)
{
try
{
files.Add(new() { Name = file.Name });
var fileContent = new StreamContent(file.OpenReadStream(maxFileSize));
fileContent.Headers.ContentType =
new MediaTypeHeaderValue(file.ContentType);
content.Add(
content: fileContent,
name: "\"files\"",
fileName: file.Name);
upload = true;
}
catch (Exception ex)
{
Logger.LogInformation(
"{FileName} not uploaded (Err: 6): {Message}",
file.Name, ex.Message);
uploadResults.Add(
new()
{
FileName = file.Name,
ErrorCode = 6,
Uploaded = false
});
}
}
}
if (upload)
{
var response = await Http.PostAsync("/Filesave", content);
var newUploadResults = await response.Content
.ReadFromJsonAsync<IList<UploadResult>>();
if (newUploadResults is not null)
{
uploadResults = uploadResults.Concat(newUploadResults).ToList();
}
}
shouldRender = true;
}
private static bool FileUpload(IList<UploadResult> uploadResults,
string? fileName, ILogger<FileUpload2> logger, out UploadResult result)
{
result = uploadResults.SingleOrDefault(f => f.FileName == fileName) ?? new();
if (!result.Uploaded)
{
logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
result.ErrorCode = 5;
}
return result.Uploaded;
}
private class File
{
public string? Name { get; set; }
}
}
@page "/file-upload-2"
@using System.Linq
@using System.Net.Http.Headers
@using Microsoft.Extensions.Logging
@inject HttpClient Http
@inject ILogger<FileUpload2> Logger
<h1>Upload Files</h1>
<p>
<label>
Upload up to @maxAllowedFiles files:
<InputFile OnChange="OnInputFileChange" multiple />
</label>
</p>
@if (files.Count > 0)
{
<div class="card">
<div class="card-body">
<ul>
@foreach (var file in files)
{
<li>
File: @file.Name
<br>
@if (FileUpload(uploadResults, file.Name, Logger,
out var result))
{
<span>
Stored File Name: @result.StoredFileName
</span>
}
else
{
<span>
There was an error uploading the file
(Error: @result.ErrorCode).
</span>
}
</li>
}
</ul>
</div>
</div>
}
@code {
private List<File> files = new();
private List<UploadResult> uploadResults = new();
private int maxAllowedFiles = 3;
private bool shouldRender;
protected override bool ShouldRender() => shouldRender;
private async Task OnInputFileChange(InputFileChangeEventArgs e)
{
shouldRender = false;
long maxFileSize = 1024 * 15;
var upload = false;
using var content = new MultipartFormDataContent();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
if (uploadResults.SingleOrDefault(
f => f.FileName == file.Name) is null)
{
try
{
files.Add(new() { Name = file.Name });
var fileContent = new StreamContent(file.OpenReadStream(maxFileSize));
fileContent.Headers.ContentType =
new MediaTypeHeaderValue(file.ContentType);
content.Add(
content: fileContent,
name: "\"files\"",
fileName: file.Name);
upload = true;
}
catch (Exception ex)
{
Logger.LogInformation(
"{FileName} not uploaded (Err: 6): {Message}",
file.Name, ex.Message);
uploadResults.Add(
new()
{
FileName = file.Name,
ErrorCode = 6,
Uploaded = false
});
}
}
}
if (upload)
{
var response = await Http.PostAsync("/Filesave", content);
var newUploadResults = await response.Content
.ReadFromJsonAsync<IList<UploadResult>>();
if (newUploadResults is not null)
{
uploadResults = uploadResults.Concat(newUploadResults).ToList();
}
}
shouldRender = true;
}
private static bool FileUpload(IList<UploadResult> uploadResults,
string? fileName, ILogger<FileUpload2> logger, out UploadResult result)
{
result = uploadResults.SingleOrDefault(f => f.FileName == fileName) ?? new();
if (!result.Uploaded)
{
logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
result.ErrorCode = 5;
}
return result.Uploaded;
}
private class File
{
public string? Name { get; set; }
}
}
@page "/file-upload-2"
@using System.Linq
@using System.Net.Http.Headers
@using Microsoft.Extensions.Logging
@inject HttpClient Http
@inject ILogger<FileUpload2> Logger
<h1>Upload Files</h1>
<p>
<label>
Upload up to @maxAllowedFiles files:
<InputFile OnChange="OnInputFileChange" multiple />
</label>
</p>
@if (files.Count > 0)
{
<div class="card">
<div class="card-body">
<ul>
@foreach (var file in files)
{
<li>
File: @file.Name
<br>
@if (FileUpload(uploadResults, file.Name, Logger,
out var result))
{
<span>
Stored File Name: @result.StoredFileName
</span>
}
else
{
<span>
There was an error uploading the file
(Error: @result.ErrorCode).
</span>
}
</li>
}
</ul>
</div>
</div>
}
@code {
private List<File> files = new();
private List<UploadResult> uploadResults = new();
private int maxAllowedFiles = 3;
private bool shouldRender;
protected override bool ShouldRender() => shouldRender;
private async Task OnInputFileChange(InputFileChangeEventArgs e)
{
shouldRender = false;
long maxFileSize = 1024 * 15;
var upload = false;
using var content = new MultipartFormDataContent();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
if (uploadResults.SingleOrDefault(
f => f.FileName == file.Name) is null)
{
try
{
files.Add(new() { Name = file.Name });
var fileContent = new StreamContent(file.OpenReadStream(maxFileSize));
fileContent.Headers.ContentType =
new MediaTypeHeaderValue(file.ContentType);
content.Add(
content: fileContent,
name: "\"files\"",
fileName: file.Name);
upload = true;
}
catch (Exception ex)
{
Logger.LogInformation(
"{FileName} not uploaded (Err: 6): {Message}",
file.Name, ex.Message);
uploadResults.Add(
new()
{
FileName = file.Name,
ErrorCode = 6,
Uploaded = false
});
}
}
}
if (upload)
{
var response = await Http.PostAsync("/Filesave", content);
var newUploadResults = await response.Content
.ReadFromJsonAsync<IList<UploadResult>>();
if (newUploadResults is not null)
{
uploadResults = uploadResults.Concat(newUploadResults).ToList();
}
}
shouldRender = true;
}
private static bool FileUpload(IList<UploadResult> uploadResults,
string? fileName, ILogger<FileUpload2> logger, out UploadResult result)
{
result = uploadResults.SingleOrDefault(f => f.FileName == fileName) ?? new();
if (!result.Uploaded)
{
logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
result.ErrorCode = 5;
}
return result.Uploaded;
}
private class File
{
public string? Name { get; set; }
}
}
@page "/file-upload-2"
@using System.Linq
@using System.Net.Http.Headers
@using Microsoft.Extensions.Logging
@inject HttpClient Http
@inject ILogger<FileUpload2> Logger
<h1>Upload Files</h1>
<p>
<label>
Upload up to @maxAllowedFiles files:
<InputFile OnChange="OnInputFileChange" multiple />
</label>
</p>
@if (files.Count > 0)
{
<div class="card">
<div class="card-body">
<ul>
@foreach (var file in files)
{
<li>
File: @file.Name
<br>
@if (FileUpload(uploadResults, file.Name, Logger,
out var result))
{
<span>
Stored File Name: @result.StoredFileName
</span>
}
else
{
<span>
There was an error uploading the file
(Error: @result.ErrorCode).
</span>
}
</li>
}
</ul>
</div>
</div>
}
@code {
private List<File> files = new();
private List<UploadResult> uploadResults = new();
private int maxAllowedFiles = 3;
private bool shouldRender;
protected override bool ShouldRender() => shouldRender;
private async Task OnInputFileChange(InputFileChangeEventArgs e)
{
shouldRender = false;
long maxFileSize = 1024 * 15;
var upload = false;
using var content = new MultipartFormDataContent();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
if (uploadResults.SingleOrDefault(
f => f.FileName == file.Name) is null)
{
try
{
files.Add(new() { Name = file.Name });
var fileContent = new StreamContent(file.OpenReadStream(maxFileSize));
fileContent.Headers.ContentType =
new MediaTypeHeaderValue(file.ContentType);
content.Add(
content: fileContent,
name: "\"files\"",
fileName: file.Name);
upload = true;
}
catch (Exception ex)
{
Logger.LogInformation(
"{FileName} not uploaded (Err: 6): {Message}",
file.Name, ex.Message);
uploadResults.Add(
new()
{
FileName = file.Name,
ErrorCode = 6,
Uploaded = false
});
}
}
}
if (upload)
{
var response = await Http.PostAsync("/Filesave", content);
var newUploadResults = await response.Content
.ReadFromJsonAsync<IList<UploadResult>>();
uploadResults = uploadResults.Concat(newUploadResults).ToList();
}
shouldRender = true;
}
private static bool FileUpload(IList<UploadResult> uploadResults,
string fileName, ILogger<FileUpload2> logger, out UploadResult result)
{
result = uploadResults.SingleOrDefault(f => f.FileName == fileName);
if (result is null)
{
logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
result = new();
result.ErrorCode = 5;
}
return result.Uploaded;
}
private class File
{
public string Name { get; set; }
}
}
Pengontrol berikut dalam proyek sisi server menyimpan file yang diunggah dari klien.
Catatan
Mengikat nilai formulir dengan [FromForm]
atribut tidak didukung secara asli untuk API Minimal di ASP.NET Core di .NET 6. Oleh karena itu, contoh pengontrol berikut Filesave
tidak dapat dikonversi untuk menggunakan API Minimal. Dukungan untuk mengikat dari nilai formulir dengan API Minimal tersedia di ASP.NET Core di .NET 7 atau yang lebih baru.
Untuk menggunakan kode berikut, buat Development/unsafe_uploads
folder di akar proyek sisi server untuk aplikasi yang berjalan di Development
lingkungan.
Karena contoh menggunakan lingkungan aplikasi sebagai bagian dari jalur tempat file disimpan, folder tambahan diperlukan jika lingkungan lain digunakan dalam pengujian dan produksi. Misalnya, buat Staging/unsafe_uploads
folder untuk Staging
lingkungan. Buat Production/unsafe_uploads
folder untuk Production
lingkungan.
Peringatan
Contoh menyimpan file tanpa memindai kontennya, dan panduan dalam artikel ini tidak memperhitungkan praktik terbaik keamanan tambahan untuk file yang diunggah. Pada sistem penahapan dan produksi, nonaktifkan izin eksekusi pada folder unggah dan pindai file dengan API pemindai anti-virus/anti-malware segera setelah diunggah. Untuk informasi selengkapnya, lihat Mengunggah file di ASP.NET Core.
Dalam contoh berikut, perbarui namespace layanan proyek bersama agar sesuai dengan proyek bersama jika proyek bersama menyediakan UploadResult
kelas .
Controllers/FilesaveController.cs
:
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using BlazorSample.Shared;
[ApiController]
[Route("[controller]")]
public class FilesaveController(
IHostEnvironment env, ILogger<FilesaveController> logger)
: ControllerBase
{
[HttpPost]
public async Task<ActionResult<IList<UploadResult>>> PostFile(
[FromForm] IEnumerable<IFormFile> files)
{
var maxAllowedFiles = 3;
long maxFileSize = 1024 * 15;
var filesProcessed = 0;
var resourcePath = new Uri($"{Request.Scheme}://{Request.Host}/");
List<UploadResult> uploadResults = [];
foreach (var file in files)
{
var uploadResult = new UploadResult();
string trustedFileNameForFileStorage;
var untrustedFileName = file.FileName;
uploadResult.FileName = untrustedFileName;
var trustedFileNameForDisplay =
WebUtility.HtmlEncode(untrustedFileName);
if (filesProcessed < maxAllowedFiles)
{
if (file.Length == 0)
{
logger.LogInformation("{FileName} length is 0 (Err: 1)",
trustedFileNameForDisplay);
uploadResult.ErrorCode = 1;
}
else if (file.Length > maxFileSize)
{
logger.LogInformation("{FileName} of {Length} bytes is " +
"larger than the limit of {Limit} bytes (Err: 2)",
trustedFileNameForDisplay, file.Length, maxFileSize);
uploadResult.ErrorCode = 2;
}
else
{
try
{
trustedFileNameForFileStorage = Path.GetRandomFileName();
var path = Path.Combine(env.ContentRootPath,
env.EnvironmentName, "unsafe_uploads",
trustedFileNameForFileStorage);
await using FileStream fs = new(path, FileMode.Create);
await file.CopyToAsync(fs);
logger.LogInformation("{FileName} saved at {Path}",
trustedFileNameForDisplay, path);
uploadResult.Uploaded = true;
uploadResult.StoredFileName = trustedFileNameForFileStorage;
}
catch (IOException ex)
{
logger.LogError("{FileName} error on upload (Err: 3): {Message}",
trustedFileNameForDisplay, ex.Message);
uploadResult.ErrorCode = 3;
}
}
filesProcessed++;
}
else
{
logger.LogInformation("{FileName} not uploaded because the " +
"request exceeded the allowed {Count} of files (Err: 4)",
trustedFileNameForDisplay, maxAllowedFiles);
uploadResult.ErrorCode = 4;
}
uploadResults.Add(uploadResult);
}
return new CreatedResult(resourcePath, uploadResults);
}
}
Dalam kode sebelumnya, GetRandomFileName dipanggil untuk menghasilkan nama file yang aman. Jangan pernah mempercayai nama file yang disediakan oleh browser, karena cyberattacker dapat memilih nama file yang ada yang menimpa file yang ada atau mengirim jalur yang mencoba menulis di luar aplikasi.
Aplikasi server harus mendaftarkan layanan pengontrol dan titik akhir pengontrol peta. Untuk informasi selengkapnya, lihat Perutean ke tindakan pengontrol di ASP.NET Core.
Membatalkan unggahan file
Komponen pengunggahan file dapat mendeteksi kapan pengguna telah membatalkan unggahan dengan menggunakan CancellationToken saat memanggil ke IBrowserFile.OpenReadStream atau StreamReader.ReadAsync.
Buat CancellationTokenSource untuk InputFile
komponen. Di awal OnInputFileChange
metode, periksa apakah unggahan sebelumnya sedang berlangsung.
Jika unggahan file sedang berlangsung:
- Panggil Cancel pada unggahan sebelumnya.
- Buat baru CancellationTokenSource untuk unggahan berikutnya dan teruskan CancellationTokenSource.Token ke OpenReadStream atau ReadAsync.
Unggah file sisi server dengan kemajuan
Contoh berikut menunjukkan cara mengunggah file di aplikasi sisi server dengan kemajuan pengunggahan yang ditampilkan kepada pengguna.
Untuk menggunakan contoh berikut dalam aplikasi pengujian:
- Buat folder untuk menyimpan file yang
Development
diunggah untuk lingkungan:Development/unsafe_uploads
. - Konfigurasikan ukuran file maksimum (
maxFileSize
, 15 KB dalam contoh berikut) dan jumlah maksimum file yang diizinkan (maxAllowedFiles
, 3 dalam contoh berikut). - Atur buffer ke nilai yang berbeda (10 KB dalam contoh berikut), jika diinginkan, untuk peningkatan granularitas dalam pelaporan yang sedang berlangsung. Kami tidak merekomendasikan penggunaan buffer yang lebih besar dari 30 KB karena masalah performa dan keamanan.
Peringatan
Contoh menyimpan file tanpa memindai kontennya, dan panduan dalam artikel ini tidak memperhitungkan praktik terbaik keamanan tambahan untuk file yang diunggah. Pada sistem penahapan dan produksi, nonaktifkan izin eksekusi pada folder unggah dan pindai file dengan API pemindai anti-virus/anti-malware segera setelah diunggah. Untuk informasi selengkapnya, lihat Mengunggah file di ASP.NET Core.
FileUpload3.razor
:
@page "/file-upload-3"
@inject ILogger<FileUpload3> Logger
@inject IWebHostEnvironment Environment
<PageTitle>File Upload 3</PageTitle>
<h1>File Upload Example 3</h1>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Progress: @string.Format("{0:P0}", progressPercent)</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private decimal progressPercent;
private async Task LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
progressPercent = 0;
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
var trustedFileName = Path.GetRandomFileName();
var path = Path.Combine(Environment.ContentRootPath,
Environment.EnvironmentName, "unsafe_uploads", trustedFileName);
await using FileStream writeStream = new(path, FileMode.Create);
using var readStream = file.OpenReadStream(maxFileSize);
var bytesRead = 0;
var totalRead = 0;
var buffer = new byte[1024 * 10];
while ((bytesRead = await readStream.ReadAsync(buffer)) != 0)
{
totalRead += bytesRead;
await writeStream.WriteAsync(buffer, 0, bytesRead);
progressPercent = Decimal.Divide(totalRead, file.Size);
StateHasChanged();
}
loadedFiles.Add(file);
Logger.LogInformation(
"Unsafe Filename: {UnsafeFilename} File saved: {Filename}",
file.Name, trustedFileName);
}
catch (Exception ex)
{
Logger.LogError("File: {FileName} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
@page "/file-upload-3"
@using System
@using System.IO
@using Microsoft.AspNetCore.Hosting
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload3> Logger
@inject IWebHostEnvironment Environment
<h3>Upload Files</h3>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Progress: @string.Format("{0:P0}", progressPercent)</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private decimal progressPercent;
private async Task LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
progressPercent = 0;
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
var trustedFileName = Path.GetRandomFileName();
var path = Path.Combine(Environment.ContentRootPath,
Environment.EnvironmentName, "unsafe_uploads", trustedFileName);
await using FileStream writeStream = new(path, FileMode.Create);
using var readStream = file.OpenReadStream(maxFileSize);
var bytesRead = 0;
var totalRead = 0;
var buffer = new byte[1024 * 10];
while ((bytesRead = await readStream.ReadAsync(buffer)) != 0)
{
totalRead += bytesRead;
await writeStream.WriteAsync(buffer, 0, bytesRead);
progressPercent = Decimal.Divide(totalRead, file.Size);
StateHasChanged();
}
loadedFiles.Add(file);
}
catch (Exception ex)
{
Logger.LogError("File: {FileName} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
@page "/file-upload-3"
@using System
@using System.IO
@using Microsoft.AspNetCore.Hosting
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload3> Logger
@inject IWebHostEnvironment Environment
<h3>Upload Files</h3>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Progress: @string.Format("{0:P0}", progressPercent)</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private decimal progressPercent;
private async Task LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
progressPercent = 0;
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
var trustedFileName = Path.GetRandomFileName();
var path = Path.Combine(Environment.ContentRootPath,
Environment.EnvironmentName, "unsafe_uploads", trustedFileName);
await using FileStream writeStream = new(path, FileMode.Create);
using var readStream = file.OpenReadStream(maxFileSize);
var bytesRead = 0;
var totalRead = 0;
var buffer = new byte[1024 * 10];
while ((bytesRead = await readStream.ReadAsync(buffer)) != 0)
{
totalRead += bytesRead;
await writeStream.WriteAsync(buffer, 0, bytesRead);
progressPercent = Decimal.Divide(totalRead, file.Size);
StateHasChanged();
}
loadedFiles.Add(file);
}
catch (Exception ex)
{
Logger.LogError("File: {Filename} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
@page "/file-upload-3"
@using System
@using System.IO
@using Microsoft.AspNetCore.Hosting
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload3> Logger
@inject IWebHostEnvironment Environment
<h3>Upload Files</h3>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Progress: @string.Format("{0:P0}", progressPercent)</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private decimal progressPercent;
private async Task LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
progressPercent = 0;
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
var trustedFileName = Path.GetRandomFileName();
var path = Path.Combine(Environment.ContentRootPath,
Environment.EnvironmentName, "unsafe_uploads", trustedFileName);
await using FileStream writeStream = new(path, FileMode.Create);
using var readStream = file.OpenReadStream(maxFileSize);
var bytesRead = 0;
var totalRead = 0;
var buffer = new byte[1024 * 10];
while ((bytesRead = await readStream.ReadAsync(buffer)) != 0)
{
totalRead += bytesRead;
await writeStream.WriteAsync(buffer, 0, bytesRead);
progressPercent = Decimal.Divide(totalRead, file.Size);
StateHasChanged();
}
loadedFiles.Add(file);
}
catch (Exception ex)
{
Logger.LogError("File: {Filename} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
Untuk informasi selengkapnya, lihat sumber daya API berikut ini:
- FileStream: Menyediakan Stream untuk file, mendukung operasi baca dan tulis yang sinkron dan asinkron.
- FileStream.ReadAsync: Komponen sebelumnya
FileUpload3
membaca aliran secara asinkron dengan ReadAsync. Membaca aliran secara sinkron dengan Read tidak didukung dalam Razor komponen.
Aliran file
Dengan interaktivitas server, data file dialirkan melalui SignalR koneksi ke dalam kode .NET di server saat file dibaca.
RemoteBrowserFileStreamOptions memungkinkan konfigurasi karakteristik pengunggahan file.
Untuk komponen yang dirender WebAssembly, data file dialirkan langsung ke kode .NET dalam browser.
Unggah pratinjau gambar
Untuk pratinjau gambar mengunggah gambar, mulailah dengan menambahkan InputFile
komponen dengan referensi komponen dan handler OnChange
:
<InputFile @ref="inputFile" OnChange="ShowPreview" />
Tambahkan elemen gambar dengan referensi elemen, yang berfungsi sebagai tempat penampung untuk pratinjau gambar:
<img @ref="previewImageElem" />
Tambahkan referensi terkait:
@code {
private InputFile? inputFile;
private ElementReference previewImageElem;
}
Di JavaScript, tambahkan fungsi yang disebut dengan HTML input
dan img
elemen yang melakukan hal berikut:
- Mengekstrak file terpilih.
- Membuat URL objek dengan
createObjectURL
. - Mengatur pendengar peristiwa untuk mencabut URL objek dengan
revokeObjectURL
setelah gambar dimuat, sehingga memori tidak bocor. img
Mengatur sumber elemen untuk menampilkan gambar.
window.previewImage = (inputElem, imgElem) => {
const url = URL.createObjectURL(inputElem.files[0]);
imgElem.addEventListener('load', () => URL.revokeObjectURL(url), { once: true });
imgElem.src = url;
}
Terakhir, gunakan yang disuntikkan IJSRuntime untuk menambahkan OnChange
handler yang memanggil fungsi JavaScript:
@inject IJSRuntime JS
...
@code {
...
private async Task ShowPreview() => await JS.InvokeVoidAsync(
"previewImage", inputFile!.Element, previewImageElem);
}
Contoh sebelumnya adalah untuk mengunggah satu gambar. Pendekatan dapat diperluas untuk mendukung multiple
gambar.
Komponen berikut FileUpload4
menunjukkan contoh lengkapnya.
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);
}
Mengunggah file ke layanan eksternal
Alih-alih aplikasi yang menangani byte pengunggahan file dan server aplikasi yang menerima file yang diunggah, klien dapat langsung mengunggah file ke layanan eksternal. Aplikasi ini dapat memproses file dengan aman dari layanan eksternal sesuai permintaan. Pendekatan ini memperkuat aplikasi dan servernya terhadap serangan berbahaya dan potensi masalah performa.
Pertimbangkan pendekatan yang menggunakan Azure Files, Azure Blob Storage, atau layanan pihak ketiga dengan manfaat potensial berikut:
- Unggah file dari klien langsung ke layanan eksternal dengan pustaka klien JavaScript atau REST API. Misalnya, Azure menawarkan pustaka dan API klien berikut:
- Otorisasi unggahan pengguna dengan token tanda tangan akses bersama (SAS) yang didelegasikan pengguna yang dihasilkan oleh aplikasi (sisi server) untuk setiap unggahan file klien. Misalnya, Azure menawarkan fitur SAS berikut:
- Berikan redundansi otomatis dan pencadangan berbagi file.
- Batasi unggahan dengan kuota. Perhatikan bahwa kuota Azure Blob Storage diatur pada tingkat akun, bukan tingkat kontainer. Namun, kuota Azure Files berada di tingkat berbagi file dan mungkin memberikan kontrol yang lebih baik atas batas unggahan. Untuk informasi selengkapnya, lihat dokumen Azure yang ditautkan sebelumnya dalam daftar ini.
- File aman dengan enkripsi sisi server (SSE).
Untuk informasi selengkapnya tentang Azure Blob Storage dan Azure Files, lihat dokumentasi Azure Storage.
Batas ukuran pesan sisi SignalR server
Unggahan file mungkin gagal bahkan sebelum dimulai, saat Blazor mengambil data tentang file yang melebihi ukuran pesan maksimum SignalR .
SignalR menentukan batas ukuran pesan yang berlaku untuk setiap pesan Blazor yang diterima, dan InputFile komponen mengalirkan file ke server dalam pesan yang mematuhi batas yang dikonfigurasi. Namun, pesan pertama, yang menunjukkan kumpulan file yang akan diunggah, dikirim sebagai pesan tunggal yang unik. Ukuran pesan pertama mungkin melebihi SignalR batas ukuran pesan. Masalah ini tidak terkait dengan ukuran file, ini terkait dengan jumlah file.
Kesalahan yang dicatat mirip dengan yang berikut ini:
Kesalahan: Koneksi terputus dengan kesalahan 'Kesalahan: Server mengembalikan kesalahan saat ditutup: Koneksi ditutup dengan kesalahan.'. e.log @ blazor.server.js:1
Saat mengunggah file, mencapai batas ukuran pesan pada pesan pertama jarang terjadi. Jika batas tercapai, aplikasi dapat mengonfigurasi HubOptions.MaximumReceiveMessageSize dengan nilai yang lebih besar.
Untuk informasi selengkapnya tentang SignalR konfigurasi dan cara mengatur MaximumReceiveMessageSize, lihat panduan ASP.NET CoreBlazorSignalR.
Pemanggilan paralel maksimum per pengaturan hub klien
Blazor bergantung pada MaximumParallelInvocationsPerClient atur ke 1, yang merupakan nilai default.
Meningkatkan nilai mengarah pada probabilitas tinggi yang CopyTo
dilemparkan System.InvalidOperationException: 'Reading is not allowed after reader was completed.'
operasi . Untuk informasi selengkapnya, lihat MaximumParallelInvocationsPerClient > 1 memutus unggahan file dalam Blazor Server mode (dotnet/aspnetcore
#53951).
Pecahkan masalah
Baris yang memanggil IBrowserFile.OpenReadStream melempar :System.TimeoutException
System.TimeoutException: Did not receive any data in the allotted time.
Kemungkinan penyebabnya:
Menggunakan kontainer Autofac Inversion of Control (IoC) alih-alih kontainer injeksi dependensi ASP.NET Core bawaan. Untuk mengatasi masalah ini, atur DisableImplicitFromServicesParameters ke
true
di opsi hub handler sirkuit sisi server. Untuk informasi selengkapnya, lihat FileUpload: Tidak menerima data apa pun dalam waktu yang dialokasikan (dotnet/aspnetcore
#38842).Tidak membaca aliran ke penyelesaian. Ini bukan masalah kerangka kerja. Perangkap pengecualian dan selidiki lebih lanjut di lingkungan/jaringan lokal Anda.
- Menggunakan penyajian dan panggilan OpenReadStream sisi server pada beberapa file sebelum membacanya hingga selesai. Untuk mengatasi masalah ini, gunakan kelas dan pendekatan yang
LazyBrowserFileStream
dijelaskan dalam bagian Unggah file ke server dengan penyajian sisi server di artikel ini.
Sumber Daya Tambahan:
ASP.NET Core