ASP.NET Core Blazor 파일 업로드
참고 항목
이 문서의 최신 버전은 아닙니다. 현재 릴리스는 이 문서의 .NET 8 버전을 참조 하세요.
Important
이 정보는 상업적으로 출시되기 전에 실질적으로 수정될 수 있는 시험판 제품과 관련이 있습니다. Microsoft는 여기에 제공된 정보에 대해 어떠한 명시적, 또는 묵시적인 보증을 하지 않습니다.
현재 릴리스는 이 문서의 .NET 8 버전을 참조 하세요.
이 문서에서는 InputFile 구성 요소를 사용하여 Blazor에서 파일을 업로드하는 방법을 설명합니다.
파일 업로드
Warning
사용자가 파일을 업로드할 수 있도록 허용하는 경우 항상 보안 모범 사례를 따르세요. 자세한 내용은 ASP.NET Core에서 파일 업로드를 참조하세요.
브라우저 파일 데이터를 .NET 코드로 읽어 오려면 InputFile 구성 요소를 사용합니다. InputFile 구성 요소는 단일 파일 업로드에 대한 형식 file
의 HTML <input>
요소를 렌더링합니다. 사용자가 한 번에 여러 파일을 업로드할 수 있도록 multiple
특성을 추가합니다.
InputFile 구성 요소 또는 기본 HTML <input type="file">
을 사용하는 경우 파일 선택이 누적되지 않으므로 기존 파일 선택에 파일을 추가할 수 없습니다. 이 구성 요소는 항상 사용자의 초기 파일 선택을 대체하므로 이전 선택 항목의 파일 참조를 사용할 수 없습니다.
다음 InputFile 구성 요소는 OnChange(change
) 이벤트가 발생할 때 LoadFiles
메서드를 실행합니다. InputFileChangeEventArgs는 선택한 파일 목록에 대한 액세스 권한과 각 파일에 대한 세부 정보를 제공합니다.
<InputFile OnChange="LoadFiles" multiple />
@code {
private void LoadFiles(InputFileChangeEventArgs e)
{
...
}
}
렌더링된 HTML:
<input multiple="" type="file" _bl_2="">
참고 항목
앞의 예제에서 <input>
요소의 _bl_2
특성은 Blazor의 내부 처리에 사용됩니다.
사용자가 선택한 파일에서 데이터를 읽으려면 파일에 대해 IBrowserFile.OpenReadStream을 호출하고 반환된 스트림에서 읽습니다. 자세한 내용은 파일 스트림 섹션을 참조하세요.
OpenReadStream은 Stream의 최대 크기를 바이트 단위로 적용합니다. 500KB보다 큰 파일 하나 또는 여러 개를 읽으면 예외가 발생합니다. 이 제한은 개발자가 실수로 대규모 파일을 메모리로 읽어 들이는 것을 방지하기 위한 것입니다. 필요한 경우 OpenReadStream의 maxAllowedSize
매개 변수를 사용하여 더 큰 크기를 지정할 수 있습니다.
파일의 바이트를 나타내는 Stream에 액세스해야 하는 경우 IBrowserFile.OpenReadStream을 사용합니다. 들어오는 파일 스트림을 한꺼번에 메모리로 직접 읽지 않도록 하세요. 예를 들어 파일의 바이트를 모두 MemoryStream으로 복사하거나 전체 스트림을 한꺼번에 바이트 배열로 읽지 마세요. 이러한 접근 방식은 특히 서버 쪽 구성 요소의 경우 앱 성능이 저하되고 DoS(잠재적 인 서비스 거부) 위험이 발생할 수 있습니다. 대신 다음 방법 중 하나를 사용할 수 있습니다.
- 스트림을 메모리에 읽지 않고 디스크의 파일에 직접 복사합니다. Blazor 서버에서 코드를 실행하는 앱은 클라이언트의 파일 시스템에 직접 액세스할 수 없습니다.
- 파일을 클라이언트에서 외부 서비스로 직접 업로드합니다. 자세한 내용은 외부 서비스에 파일 업로드 섹션을 참조하세요.
다음 예제에서, browserFile
은 업로드된 파일을 나타내고 IBrowserFile을 구현합니다. 작업 구현은 IBrowserFile 이 문서의 뒷부분에 있는 파일 업로드 구성 요소에 나와 있습니다.
지원됨: 파일의 Stream이 제공된 경로에 파일을 생성하는 FileStream인 소비자에게 직접 제공되므로 다음 접근 방식은 권장됩니다.
await using FileStream fs = new(path, FileMode.Create);
await browserFile.OpenReadStream().CopyToAsync(fs);
지원됨: 파일의 Stream이 UploadBlobAsync에 직접 제공되므로 다음 접근 방식은 Microsoft Azure Blob Storage에 권장됩니다.
await blobContainerClient.UploadBlobAsync(
trustedFileName, browserFile.OpenReadStream());
권장되지 않음: 파일의 Stream 콘텐츠가 메모리 내(reader
)로 읽혀지 String 므로 다음 방법은 권장되지 않습니다.
var reader =
await new StreamReader(browserFile.OpenReadStream()).ReadToEndAsync();
권장되지 않음: 다음 방법은 호출하기 전에 UploadBlobAsync파일의 Stream 콘텐츠가 메모리 내(memoryStream
)로 MemoryStream 복사되기 때문에 Microsoft Azure Blob Storage에는 권장되지 않습니다.
var memoryStream = new MemoryStream();
await browserFile.OpenReadStream().CopyToAsync(memoryStream);
await blobContainerClient.UploadBlobAsync(
trustedFileName, memoryStream));
이미지 파일을 수신하는 구성 요소는 파일에 대한 BrowserFileExtensions.RequestImageFileAsync 편의 메서드를 호출하여 이미지가 앱으로 스트리밍되기 전에 브라우저의 JavaScript 런타임 내에서 이미지 데이터의 크기를 조정할 수 있습니다. RequestImageFileAsync 호출에 대한 사용 사례는 Blazor WebAssembly 앱에 가장 적합합니다.
IoC(Autofac Inversion of Control) 컨테이너 사용자
기본 제공 ASP.NET Core 종속성 주입 컨테이너 대신 IoC(Autofac Inversion of Control) 컨테이너를 사용하는 경우 서버 쪽 회로 처리기 허브 옵션으로 설정합니다 DisableImplicitFromServicesParameters true
. 자세한 내용은 FileUpload: 할당된 시간에 데이터를 받지 못했습니다(dotnet/aspnetcore
#38842)를 참조하세요.
파일 크기 읽기 및 업로드 제한
서버 쪽 또는 클라이언트 쪽에는 구성 요소에 대한 InputFile 파일 읽기 또는 업로드 크기 제한이 없습니다. 그러나 클라이언트 쪽 Blazor 은 JavaScript에서 C#으로 데이터를 마샬링할 때 파일의 바이트를 단일 JavaScript 배열 버퍼로 읽습니다. 이 버퍼는 2GB 또는 디바이스의 사용 가능한 메모리로 제한됩니다. 구성 요소를 사용하는 InputFile 클라이언트 쪽 업로드의 경우 대용량 파일 업로드(>250MB)가 실패할 수 있습니다. 자세한 내용은 다음 토론을 참조하세요.
구성 요소에 대해 InputFile 지원되는 최대 파일 크기는 2GB입니다. 또한 클라이언트 쪽 Blazor 은 JavaScript에서 C#으로 데이터를 마샬링할 때 파일의 바이트를 단일 JavaScript 배열 버퍼로 읽습니다. 이 버퍼는 2GB 또는 디바이스의 사용 가능한 메모리로 제한됩니다. 구성 요소를 사용하는 InputFile 클라이언트 쪽 업로드의 경우 대용량 파일 업로드(>250MB)가 실패할 수 있습니다. 자세한 내용은 다음 토론을 참조하세요.
- Blazor InputFile 구성 요소는 파일이 업로드될 때 청크를 처리해야 합니다(dotnet/runtime #84685)
- http 처리기를 통한 스트리밍 업로드 요청(dotnet/runtime #36634)
구성 요소를 사용하려고 할 때 실패하는 대규모 클라이언트 쪽 파일 업로드의 InputFile 경우 구성 요소를 사용하는 InputFile 대신 여러 HTTP 범위 요청을 사용하여 사용자 지정 구성 요소로 큰 파일을 청크하는 것이 좋습니다.
클라이언트 쪽 파일 크기 업로드 제한을 해결하기 위해 현재 .NET 9(2024년 말)에 대한 작업이 예정되어 있습니다.
예제
다음 예제에서는 구성 요소에서 여러 파일 업로드를 보여 줍니다. InputFileChangeEventArgs.GetMultipleFiles는 여러 파일을 읽을 수 있습니다. 악의적인 사용자가 앱에서 예상하는 것보다 많은 수의 파일을 업로드하지 못하도록 최대 파일 수를 지정합니다. InputFileChangeEventArgs.File은 파일 업로드가 여러 파일을 지원하지 않을 경우 첫 번째 및 유일한 파일을 읽을 수 있습니다.
InputFileChangeEventArgs는 앱 _Imports.razor
파일의 네임스페이스 중 하나인 Microsoft.AspNetCore.Components.Forms 네임스페이스에 있습니다. 파일에 네임스페이스가 있으면 _Imports.razor
앱의 구성 요소에 대한 API 멤버 액세스 권한을 제공합니다.
_Imports.razor
파일의 네임스페이스는 C# 파일(.cs
)에 적용되지 않습니다. C# 파일에는 클래스 파일의 맨 위에 명시적 using
지시문이 필요합니다.
using Microsoft.AspNetCore.Components.Forms;
파일 업로드 구성 요소를 테스트하는 경우 PowerShell을 사용하여 모든 크기의 테스트 파일을 만들 수 있습니다.
$out = new-object byte[] {SIZE}; (new-object Random).NextBytes($out); [IO.File]::WriteAllBytes('{PATH}', $out)
앞의 명령에서 다음을 확인할 수 있습니다.
{SIZE}
자리 표시자는 바이트 단위의 파일 크기입니다(예: 2MB 파일의 경우2097152
).{PATH}
자리 표시자는 파일 확장명을 포함하는 경로 및 파일입니다(예:D:/test_files/testfile2MB.txt
).
서버 쪽 파일 업로드 예제
다음 코드를 사용하려면 환경에서 실행되는 앱의 루트에 폴더를 Development
만듭니 Development/unsafe_uploads
다.
이 예제에서는 파일이 저장되는 경로의 일부로 앱의 환경을 사용하므로 테스트 및 프로덕션에서 다른 환경을 사용하는 경우 추가 폴더가 필요합니다. 예를 들어 Staging
환경에 대해서는 Staging/unsafe_uploads
폴더를 만듭니다. Production
환경에 대해서는 Production/unsafe_uploads
폴더를 만듭니다.
Warning
이 예제에서는 콘텐츠를 검사하지 않고 파일을 저장하며, 이 문서의 지침은 업로드된 파일의 추가 보안 모범 사례를 고려하지 않습니다. 스테이징 및 프로덕션 시스템에서 업로드 폴더에 대한 실행 권한을 사용하지 않도록 설정하고 업로드 직후 바이러스 백신/맬웨어 방지 스캐너 API를 사용하여 파일을 검사합니다. 자세한 내용은 ASP.NET Core에서 파일 업로드를 참조하세요.
FileUpload1.razor
:
@page "/file-upload-1"
@using System
@using System.IO
@using Microsoft.AspNetCore.Hosting
@inject ILogger<FileUpload1> Logger
@inject IWebHostEnvironment Environment
<PageTitle>File Upload 1</PageTitle>
<h1>File Upload Example 1</h1>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Uploading...</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private async Task LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
var trustedFileName = Path.GetRandomFileName();
var path = Path.Combine(Environment.ContentRootPath,
Environment.EnvironmentName, "unsafe_uploads",
trustedFileName);
await using FileStream fs = new(path, FileMode.Create);
await file.OpenReadStream(maxFileSize).CopyToAsync(fs);
loadedFiles.Add(file);
Logger.LogInformation(
"Unsafe Filename: {UnsafeFilename} File saved: {Filename}",
file.Name, trustedFileName);
}
catch (Exception ex)
{
Logger.LogError("File: {Filename} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
@page "/file-upload-1"
@using System
@using System.IO
@using Microsoft.AspNetCore.Hosting
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload1> Logger
@inject IWebHostEnvironment Environment
<h3>Upload Files</h3>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Uploading...</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private async Task LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
loadedFiles.Add(file);
var trustedFileNameForFileStorage = Path.GetRandomFileName();
var path = Path.Combine(Environment.ContentRootPath,
Environment.EnvironmentName, "unsafe_uploads",
trustedFileNameForFileStorage);
await using FileStream fs = new(path, FileMode.Create);
await file.OpenReadStream(maxFileSize).CopyToAsync(fs);
}
catch (Exception ex)
{
Logger.LogError("File: {Filename} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
@page "/file-upload-1"
@using System
@using System.IO
@using Microsoft.AspNetCore.Hosting
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload1> Logger
@inject IWebHostEnvironment Environment
<h3>Upload Files</h3>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Uploading...</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private async Task LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
loadedFiles.Add(file);
var trustedFileNameForFileStorage = Path.GetRandomFileName();
var path = Path.Combine(Environment.ContentRootPath,
Environment.EnvironmentName, "unsafe_uploads",
trustedFileNameForFileStorage);
await using FileStream fs = new(path, FileMode.Create);
await file.OpenReadStream(maxFileSize).CopyToAsync(fs);
}
catch (Exception ex)
{
Logger.LogError("File: {Filename} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
@page "/file-upload-1"
@using System
@using System.IO
@using Microsoft.AspNetCore.Hosting
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload1> Logger
@inject IWebHostEnvironment Environment
<h3>Upload Files</h3>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Uploading...</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private async Task LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
loadedFiles.Add(file);
var trustedFileNameForFileStorage = Path.GetRandomFileName();
var path = Path.Combine(Environment.ContentRootPath,
Environment.EnvironmentName, "unsafe_uploads",
trustedFileNameForFileStorage);
await using FileStream fs = new(path, FileMode.Create);
await file.OpenReadStream(maxFileSize).CopyToAsync(fs);
}
catch (Exception ex)
{
Logger.LogError("File: {Filename} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
클라이언트 쪽 파일 업로드 예제
다음 예제에서는 파일 바이트를 처리하고 앱 외부의 대상으로 파일을 보내지 않습니다. 서버 또는 서비스에 파일을 보내는 Razor 구성 요소의 예는 다음 섹션을 참조하세요.
구성 요소는 Interactive WebAssembly 렌더링 모드(InteractiveWebAssembly
)가 부모 구성 요소에서 상속되거나 앱에 전역적으로 적용된다고 가정합니다.
@page "/file-upload-1"
@inject ILogger<FileUpload1> Logger
<PageTitle>File Upload 1</PageTitle>
<h1>File Upload Example 1</h1>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Uploading...</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private void LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
loadedFiles.Add(file);
}
catch (Exception ex)
{
Logger.LogError("File: {FileName} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
@page "/file-upload-1"
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload1> Logger
<h3>Upload Files</h3>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Uploading...</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private void LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
loadedFiles.Add(file);
}
catch (Exception ex)
{
Logger.LogError("File: {FileName} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
@page "/file-upload-1"
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload1> Logger
<h3>Upload Files</h3>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Uploading...</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private void LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
loadedFiles.Add(file);
}
catch (Exception ex)
{
Logger.LogError("File: {Filename} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
@page "/file-upload-1"
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload1> Logger
<h3>Upload Files</h3>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Uploading...</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private void LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
loadedFiles.Add(file);
}
catch (Exception ex)
{
Logger.LogError("File: {Filename} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
IBrowserFile은 브라우저에서 속성으로 노출하는 메타데이터를 반환합니다. 예비 유효성 검사에 이 메타데이터를 사용합니다.
이전 속성의 값, 특히 UI에 Name 표시할 속성을 신뢰하지 마세요. 사용자가 제공한 모든 데이터를 앱, 서버 및 네트워크에 대한 중요한 보안 위험으로 처리합니다. 자세한 내용은 ASP.NET Core에서 파일 업로드를 참조하세요.
서버 쪽 렌더링을 사용하여 서버에 파일 업로드
이 섹션은 s 또는 Blazor Server 앱의 대화형 서버 구성 요소에 Blazor Web App적용됩니다.
다음 예제에서는 별도의 서버에서 서버 쪽 앱에서 백 엔드 웹 API 컨트롤러로 파일을 업로드하는 방법을 보여 줍니다.
서버 쪽 앱의 Program
파일에서 앱이 인스턴스를 만들 HttpClient 수 있도록 허용하는 관련 서비스를 추가 IHttpClientFactory 합니다.
builder.Services.AddHttpClient();
자세한 내용은 ASP.NET Core에서 IHttpClientFactory를 사용하여 HTTP 요청 만들기를 참조하세요.
이 섹션의 예제에서:
- 웹 API는 URL
https://localhost:5001
에서 실행됩니다. - 서버 쪽 앱은 URL에서 실행됩니다.
https://localhost:5003
테스트를 위해 앞의 URL은 프로젝트의 Properties/launchSettings.json
파일에서 구성됩니다.
다음 UploadResult
클래스는 업로드된 파일의 결과를 유지 관리합니다. 서버에서 파일을 업로드하지 못할 경우 사용자에게 표시하기 위해 ErrorCode
로 오류 코드가 반환됩니다. 각 파일에 대해 안전한 파일 이름이 서버에서 생성되며 표시를 위해 StoredFileName
으로 클라이언트에 반환됩니다. FileName
의 안전하지 않은/신뢰할 수 없는 파일 이름을 사용하여 클라이언트와 서버 간에 파일의 키가 지정됩니다.
UploadResult.cs
:
public class UploadResult
{
public bool Uploaded { get; set; }
public string? FileName { get; set; }
public string? StoredFileName { get; set; }
public int ErrorCode { get; set; }
}
프로덕션 앱에 대한 보안 모범 사례는 앱, 서버 또는 네트워크에 대한 중요한 정보를 표시할 수 있는 오류 메시지를 클라이언트에 보내지 않도록 하는 것입니다. 자세한 오류 메시지를 제공하면 악의적인 사용자가 앱, 서버 또는 네트워크에 대한 공격을 고안하는 데 도움이 될 수 있습니다. 이 섹션의 예제 코드에서는 서버 쪽 오류가 발생할 경우 구성 요소 클라이언트 쪽에 표시할 오류 코드 번호(int
)만 반환합니다. 파일 업로드에 대한 지원이 필요한 사용자는 오류의 정확한 원인을 몰라도 지원 티켓 해결을 위해 지원 담당자에게 오류 코드를 제공합니다.
다음 LazyBrowserFileStream
클래스는 스트림의 첫 번째 바이트가 요청되기 직전에 지연 호출 OpenReadStream 하는 사용자 지정 스트림 형식을 정의합니다. 스트림은 .NET에서 스트림 읽기가 시작될 때까지 브라우저에서 서버로 전송되지 않습니다.
LazyBrowserFileStream.cs
:
using Microsoft.AspNetCore.Components.Forms;
using System.Diagnostics.CodeAnalysis;
namespace BlazorSample;
internal sealed class LazyBrowserFileStream(IBrowserFile file, int maxAllowedSize)
: Stream
{
private readonly IBrowserFile file = file;
private readonly int maxAllowedSize = maxAllowedSize;
private Stream? underlyingStream;
private bool isDisposed;
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => file.Size;
public override long Position
{
get => underlyingStream?.Position ?? 0;
set => throw new NotSupportedException();
}
public override void Flush() => underlyingStream?.Flush();
public override Task<int> ReadAsync(byte[] buffer, int offset, int count,
CancellationToken cancellationToken)
{
EnsureStreamIsOpen();
return underlyingStream.ReadAsync(buffer, offset, count, cancellationToken);
}
public override ValueTask<int> ReadAsync(Memory<byte> buffer,
CancellationToken cancellationToken = default)
{
EnsureStreamIsOpen();
return underlyingStream.ReadAsync(buffer, cancellationToken);
}
[MemberNotNull(nameof(underlyingStream))]
private void EnsureStreamIsOpen() =>
underlyingStream ??= file.OpenReadStream(maxAllowedSize);
protected override void Dispose(bool disposing)
{
if (isDisposed)
{
return;
}
underlyingStream?.Dispose();
isDisposed = true;
base.Dispose(disposing);
}
public override int Read(byte[] buffer, int offset, int count)
=> throw new NotSupportedException();
public override long Seek(long offset, SeekOrigin origin)
=> throw new NotSupportedException();
public override void SetLength(long value)
=> throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count)
=> throw new NotSupportedException();
}
using Microsoft.AspNetCore.Components.Forms;
using System.Diagnostics.CodeAnalysis;
namespace BlazorSample;
internal sealed class LazyBrowserFileStream : Stream
{
private readonly IBrowserFile file;
private readonly int maxAllowedSize;
private Stream? underlyingStream;
private bool isDisposed;
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => file.Size;
public override long Position
{
get => underlyingStream?.Position ?? 0;
set => throw new NotSupportedException();
}
public LazyBrowserFileStream(IBrowserFile file, int maxAllowedSize)
{
this.file = file;
this.maxAllowedSize = maxAllowedSize;
}
public override void Flush()
{
underlyingStream?.Flush();
}
public override Task<int> ReadAsync(byte[] buffer, int offset, int count,
CancellationToken cancellationToken)
{
EnsureStreamIsOpen();
return underlyingStream.ReadAsync(buffer, offset, count, cancellationToken);
}
public override ValueTask<int> ReadAsync(Memory<byte> buffer,
CancellationToken cancellationToken = default)
{
EnsureStreamIsOpen();
return underlyingStream.ReadAsync(buffer, cancellationToken);
}
[MemberNotNull(nameof(underlyingStream))]
private void EnsureStreamIsOpen()
{
underlyingStream ??= file.OpenReadStream(maxAllowedSize);
}
protected override void Dispose(bool disposing)
{
if (isDisposed)
{
return;
}
underlyingStream?.Dispose();
isDisposed = true;
base.Dispose(disposing);
}
public override int Read(byte[] buffer, int offset, int count)
=> throw new NotSupportedException();
public override long Seek(long offset, SeekOrigin origin)
=> throw new NotSupportedException();
public override void SetLength(long value)
=> throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count)
=> throw new NotSupportedException();
}
using Microsoft.AspNetCore.Components.Forms;
using System.Diagnostics.CodeAnalysis;
namespace BlazorSample;
internal sealed class LazyBrowserFileStream : Stream
{
private readonly IBrowserFile file;
private readonly int maxAllowedSize;
private Stream? underlyingStream;
private bool isDisposed;
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => file.Size;
public override long Position
{
get => underlyingStream?.Position ?? 0;
set => throw new NotSupportedException();
}
public LazyBrowserFileStream(IBrowserFile file, int maxAllowedSize)
{
this.file = file;
this.maxAllowedSize = maxAllowedSize;
}
public override void Flush()
{
underlyingStream?.Flush();
}
public override Task<int> ReadAsync(byte[] buffer, int offset, int count,
CancellationToken cancellationToken)
{
EnsureStreamIsOpen();
return underlyingStream.ReadAsync(buffer, offset, count, cancellationToken);
}
public override ValueTask<int> ReadAsync(Memory<byte> buffer,
CancellationToken cancellationToken = default)
{
EnsureStreamIsOpen();
return underlyingStream.ReadAsync(buffer, cancellationToken);
}
[MemberNotNull(nameof(underlyingStream))]
private void EnsureStreamIsOpen()
{
underlyingStream ??= file.OpenReadStream(maxAllowedSize);
}
protected override void Dispose(bool disposing)
{
if (isDisposed)
{
return;
}
underlyingStream?.Dispose();
isDisposed = true;
base.Dispose(disposing);
}
public override int Read(byte[] buffer, int offset, int count)
=> throw new NotSupportedException();
public override long Seek(long offset, SeekOrigin origin)
=> throw new NotSupportedException();
public override void SetLength(long value)
=> throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count)
=> throw new NotSupportedException();
}
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Forms;
namespace BlazorSample;
internal sealed class LazyBrowserFileStream : Stream
{
private readonly IBrowserFile file;
private readonly int maxAllowedSize;
private Stream? underlyingStream;
private bool isDisposed;
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => file.Size;
public override long Position
{
get => underlyingStream?.Position ?? 0;
set => throw new NotSupportedException();
}
public LazyBrowserFileStream(IBrowserFile file, int maxAllowedSize)
{
this.file = file;
this.maxAllowedSize = maxAllowedSize;
}
public override void Flush()
{
underlyingStream?.Flush();
}
public override Task<int> ReadAsync(byte[] buffer, int offset, int count,
CancellationToken cancellationToken)
{
EnsureStreamIsOpen();
return underlyingStream.ReadAsync(buffer, offset, count, cancellationToken);
}
public override ValueTask<int> ReadAsync(Memory<byte> buffer,
CancellationToken cancellationToken = default)
{
EnsureStreamIsOpen();
return underlyingStream.ReadAsync(buffer, cancellationToken);
}
[MemberNotNull(nameof(underlyingStream))]
private void EnsureStreamIsOpen()
{
underlyingStream ??= file.OpenReadStream(maxAllowedSize);
}
protected override void Dispose(bool disposing)
{
if (isDisposed)
{
return;
}
underlyingStream?.Dispose();
isDisposed = true;
base.Dispose(disposing);
}
public override int Read(byte[] buffer, int offset, int count)
=> throw new NotSupportedException();
public override long Seek(long offset, SeekOrigin origin)
=> throw new NotSupportedException();
public override void SetLength(long value)
=> throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count)
=> throw new NotSupportedException();
}
다음 FileUpload2
구성 요소는
- 사용자가 클라이언트에서 파일을 업로드하도록 허용합니다.
- 클라이언트가 제공하는 신뢰할 수 없는/안전하지 않은 파일 이름을 UI에 표시합니다. 신뢰할 수 없는/안전하지 않은 파일 이름은 UI에 안전하게 표시하기 위해 Razor에 의해 자동으로 HTML로 인코딩됩니다.
Warning
다음을 위해 클라이언트가 제공하는 파일 이름을 신뢰하지 마세요.
- 파일 시스템 또는 서비스에 파일을 저장
- 자동으로 또는 개발자 코드를 통해 파일 이름을 인코딩하지 않는 UI에 표시
서버에 파일을 업로드할 때의 보안 고려 사항에 대한 자세한 내용은 ASP.NET Core에서 파일 업로드를 참조하세요.
FileUpload2.razor
:
@page "/file-upload-2"
@using System.Net.Http.Headers
@using System.Text.Json
@inject IHttpClientFactory ClientFactory
@inject ILogger<FileUpload2> Logger
<PageTitle>File Upload 2</PageTitle>
<h1>File Upload Example 2</h1>
<p>
This example requires a backend server API to function. For more information,
see the <em>Upload files to a server</em> section
of the <em>ASP.NET Core Blazor file uploads</em> article.
</p>
<p>
<label>
Upload up to @maxAllowedFiles files:
<InputFile OnChange="OnInputFileChange" multiple />
</label>
</p>
@if (files.Any())
{
<div class="card">
<div class="card-body">
<ul>
@foreach (var file in files)
{
<li>
File: @file.Name
<br>
@if (FileUpload(uploadResults, file.Name, Logger,
out var result))
{
<span>
Stored File Name: @result.StoredFileName
</span>
}
else
{
<span>
There was an error uploading the file
(Error: @result.ErrorCode).
</span>
}
</li>
}
</ul>
</div>
</div>
}
@code {
private List<File> files = new();
private List<UploadResult> uploadResults = new();
private int maxAllowedFiles = 3;
private bool shouldRender;
protected override bool ShouldRender() => shouldRender;
private async Task OnInputFileChange(InputFileChangeEventArgs e)
{
shouldRender = false;
int maxFileSize = 1024 * 15;
var upload = false;
using var content = new MultipartFormDataContent();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
if (uploadResults.SingleOrDefault(
f => f.FileName == file.Name) is null)
{
try
{
files.Add(new() { Name = file.Name });
var stream = new LazyBrowserFileStream(file, maxFileSize);
var fileContent = new StreamContent(stream);
fileContent.Headers.ContentType =
new MediaTypeHeaderValue(file.ContentType);
content.Add(
content: fileContent,
name: "\"files\"",
fileName: file.Name);
upload = true;
}
catch (Exception ex)
{
Logger.LogInformation(
"{FileName} not uploaded (Err: 6): {Message}",
file.Name, ex.Message);
uploadResults.Add(
new()
{
FileName = file.Name,
ErrorCode = 6,
Uploaded = false
});
}
}
}
if (upload)
{
var client = ClientFactory.CreateClient();
var response =
await client.PostAsync("https://localhost:5001/Filesave",
content);
if (response.IsSuccessStatusCode)
{
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
};
using var responseStream =
await response.Content.ReadAsStreamAsync();
var newUploadResults = await JsonSerializer
.DeserializeAsync<IList<UploadResult>>(responseStream, options);
if (newUploadResults is not null)
{
uploadResults = uploadResults.Concat(newUploadResults).ToList();
}
}
}
shouldRender = true;
}
private static bool FileUpload(IList<UploadResult> uploadResults,
string? fileName, ILogger<FileUpload2> logger, out UploadResult result)
{
result = uploadResults.SingleOrDefault(f => f.FileName == fileName) ?? new();
if (!result.Uploaded)
{
logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
result.ErrorCode = 5;
}
return result.Uploaded;
}
private class File
{
public string? Name { get; set; }
}
}
@page "/file-upload-2"
@using System.Net.Http.Headers
@using System.Text.Json
@inject IHttpClientFactory ClientFactory
@inject ILogger<FileUpload2> Logger
<h1>File Upload Example 2</h1>
<p>
This example requires a backend server API to function. For more information,
see the <em>Upload files to a server</em> section
of the <em>ASP.NET Core Blazor file uploads</em> article.
</p>
<p>
<label>
Upload up to @maxAllowedFiles files:
<InputFile OnChange="OnInputFileChange" multiple />
</label>
</p>
@if (files.Count > 0)
{
<div class="card">
<div class="card-body">
<ul>
@foreach (var file in files)
{
<li>
File: @file.Name
<br>
@if (FileUpload(uploadResults, file.Name, Logger,
out var result))
{
<span>
Stored File Name: @result.StoredFileName
</span>
}
else
{
<span>
There was an error uploading the file
(Error: @result.ErrorCode).
</span>
}
</li>
}
</ul>
</div>
</div>
}
@code {
private List<File> files = new();
private List<UploadResult> uploadResults = new();
private int maxAllowedFiles = 3;
private bool shouldRender;
protected override bool ShouldRender() => shouldRender;
private async Task OnInputFileChange(InputFileChangeEventArgs e)
{
shouldRender = false;
int maxFileSize = 1024 * 15;
var upload = false;
using var content = new MultipartFormDataContent();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
if (uploadResults.SingleOrDefault(
f => f.FileName == file.Name) is null)
{
try
{
files.Add(new() { Name = file.Name });
var stream = new LazyBrowserFileStream(file, maxFileSize);
var fileContent = new StreamContent(stream);
fileContent.Headers.ContentType =
new MediaTypeHeaderValue(file.ContentType);
content.Add(
content: fileContent,
name: "\"files\"",
fileName: file.Name);
upload = true;
}
catch (Exception ex)
{
Logger.LogInformation(
"{FileName} not uploaded (Err: 6): {Message}",
file.Name, ex.Message);
uploadResults.Add(
new()
{
FileName = file.Name,
ErrorCode = 6,
Uploaded = false
});
}
}
}
if (upload)
{
var client = ClientFactory.CreateClient();
var response =
await client.PostAsync("https://localhost:5001/Filesave",
content);
if (response.IsSuccessStatusCode)
{
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
};
using var responseStream =
await response.Content.ReadAsStreamAsync();
var newUploadResults = await JsonSerializer
.DeserializeAsync<IList<UploadResult>>(responseStream, options);
if (newUploadResults is not null)
{
uploadResults = uploadResults.Concat(newUploadResults).ToList();
}
}
}
shouldRender = true;
}
private static bool FileUpload(IList<UploadResult> uploadResults,
string? fileName, ILogger<FileUpload2> logger, out UploadResult result)
{
result = uploadResults.SingleOrDefault(f => f.FileName == fileName) ?? new();
if (!result.Uploaded)
{
logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
result.ErrorCode = 5;
}
return result.Uploaded;
}
private class File
{
public string? Name { get; set; }
}
}
@page "/file-upload-2"
@using System.Net.Http.Headers
@using System.Text.Json
@inject IHttpClientFactory ClientFactory
@inject ILogger<FileUpload2> Logger
<h1>File Upload Example 2</h1>
<p>
This example requires a backend server API to function. For more information,
see the <em>Upload files to a server</em> section
of the <em>ASP.NET Core Blazor file uploads</em> article.
</p>
<p>
<label>
Upload up to @maxAllowedFiles files:
<InputFile OnChange="OnInputFileChange" multiple />
</label>
</p>
@if (files.Count > 0)
{
<div class="card">
<div class="card-body">
<ul>
@foreach (var file in files)
{
<li>
File: @file.Name
<br>
@if (FileUpload(uploadResults, file.Name, Logger,
out var result))
{
<span>
Stored File Name: @result.StoredFileName
</span>
}
else
{
<span>
There was an error uploading the file
(Error: @result.ErrorCode).
</span>
}
</li>
}
</ul>
</div>
</div>
}
@code {
private List<File> files = new();
private List<UploadResult> uploadResults = new();
private int maxAllowedFiles = 3;
private bool shouldRender;
protected override bool ShouldRender() => shouldRender;
private async Task OnInputFileChange(InputFileChangeEventArgs e)
{
shouldRender = false;
int maxFileSize = 1024 * 15;
var upload = false;
using var content = new MultipartFormDataContent();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
if (uploadResults.SingleOrDefault(
f => f.FileName == file.Name) is null)
{
try
{
files.Add(new() { Name = file.Name });
var stream = new LazyBrowserFileStream(file, maxFileSize);
var fileContent = new StreamContent(stream);
fileContent.Headers.ContentType =
new MediaTypeHeaderValue(file.ContentType);
content.Add(
content: fileContent,
name: "\"files\"",
fileName: file.Name);
upload = true;
}
catch (Exception ex)
{
Logger.LogInformation(
"{FileName} not uploaded (Err: 6): {Message}",
file.Name, ex.Message);
uploadResults.Add(
new()
{
FileName = file.Name,
ErrorCode = 6,
Uploaded = false
});
}
}
}
if (upload)
{
var client = ClientFactory.CreateClient();
var response =
await client.PostAsync("https://localhost:5001/Filesave",
content);
if (response.IsSuccessStatusCode)
{
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
};
using var responseStream =
await response.Content.ReadAsStreamAsync();
var newUploadResults = await JsonSerializer
.DeserializeAsync<IList<UploadResult>>(responseStream, options);
if (newUploadResults is not null)
{
uploadResults = uploadResults.Concat(newUploadResults).ToList();
}
}
}
shouldRender = true;
}
private static bool FileUpload(IList<UploadResult> uploadResults,
string? fileName, ILogger<FileUpload2> logger, out UploadResult result)
{
result = uploadResults.SingleOrDefault(f => f.FileName == fileName) ?? new();
if (!result.Uploaded)
{
logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
result.ErrorCode = 5;
}
return result.Uploaded;
}
private class File
{
public string? Name { get; set; }
}
}
@page "/file-upload-2"
@using System.Net.Http.Headers
@using System.Text.Json
@using Microsoft.Extensions.Logging
@inject IHttpClientFactory ClientFactory
@inject ILogger<FileUpload2> Logger
<h1>File Upload Example 2</h1>
<p>
This example requires a backend server API to function. For more information,
see the <em>Upload files to a server</em> section
of the <em>ASP.NET Core Blazor file uploads</em> article.
</p>
<p>
<label>
Upload up to @maxAllowedFiles files:
<InputFile OnChange="OnInputFileChange" multiple />
</label>
</p>
@if (files.Count > 0)
{
<div class="card">
<div class="card-body">
<ul>
@foreach (var file in files)
{
<li>
File: @file.Name
<br>
@if (FileUpload(uploadResults, file.Name, Logger,
out var result))
{
<span>
Stored File Name: @result.StoredFileName
</span>
}
else
{
<span>
There was an error uploading the file
(Error: @result.ErrorCode).
</span>
}
</li>
}
</ul>
</div>
</div>
}
@code {
private List<File> files = new();
private List<UploadResult> uploadResults = new();
private int maxAllowedFiles = 3;
private bool shouldRender;
protected override bool ShouldRender() => shouldRender;
private async Task OnInputFileChange(InputFileChangeEventArgs e)
{
shouldRender = false;
int maxFileSize = 1024 * 15;
var upload = false;
using var content = new MultipartFormDataContent();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
if (uploadResults.SingleOrDefault(
f => f.FileName == file.Name) is null)
{
try
{
files.Add(new() { Name = file.Name });
var stream = new LazyBrowserFileStream(file, maxFileSize);
var fileContent = new StreamContent(stream);
fileContent.Headers.ContentType =
new MediaTypeHeaderValue(file.ContentType);
content.Add(
content: fileContent,
name: "\"files\"",
fileName: file.Name);
upload = true;
}
catch (Exception ex)
{
Logger.LogInformation(
"{FileName} not uploaded (Err: 6): {Message}",
file.Name, ex.Message);
uploadResults.Add(
new()
{
FileName = file.Name,
ErrorCode = 6,
Uploaded = false
});
}
}
}
if (upload)
{
var client = ClientFactory.CreateClient();
var response =
await client.PostAsync("https://localhost:5001/Filesave",
content);
if (response.IsSuccessStatusCode)
{
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
};
using var responseStream =
await response.Content.ReadAsStreamAsync();
var newUploadResults = await JsonSerializer
.DeserializeAsync<IList<UploadResult>>(responseStream, options);
if (newUploadResults is not null)
{
uploadResults = uploadResults.Concat(newUploadResults).ToList();
}
}
}
shouldRender = true;
}
private static bool FileUpload(IList<UploadResult> uploadResults,
string fileName, ILogger<FileUpload2> logger, out UploadResult result)
{
result = uploadResults.SingleOrDefault(f => f.FileName == fileName) ?? new();
if (!result.Uploaded)
{
logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
result.ErrorCode = 5;
}
return result.Uploaded;
}
private class File
{
public string Name { get; set; }
}
}
구성 요소에서 한 번에 단일 파일에 대한 파일 업로드를 제한하거나 구성 요소가 대화형 클라이언트 쪽 렌더링(CSR InteractiveWebAssembly
)만 채택하는 경우 구성 요소는 해당 파일의 LazyBrowserFileStream
사용을 방지하고 사용할 Stream수 있습니다. 다음은 구성 요소의 변경 내용을 FileUpload2
보여 줍니다.
- var stream = new LazyBrowserFileStream(file, maxFileSize);
- var fileContent = new StreamContent(stream);
+ var fileContent = new StreamContent(file.OpenReadStream(maxFileSize));
LazyBrowserFileStream
사용되지 않으므로 클래스(LazyBrowserFileStream.cs
)를 제거합니다.
구성 요소에서 파일 업로드를 한 번에 단일 파일로 제한하는 경우 구성 요소는 해당 파일의 LazyBrowserFileStream
사용을 방지하고 Stream사용할 수 있습니다. 다음은 구성 요소의 변경 내용을 FileUpload2
보여 줍니다.
- var stream = new LazyBrowserFileStream(file, maxFileSize);
- var fileContent = new StreamContent(stream);
+ var fileContent = new StreamContent(file.OpenReadStream(maxFileSize));
LazyBrowserFileStream
사용되지 않으므로 클래스(LazyBrowserFileStream.cs
)를 제거합니다.
웹 API 프로젝트의 다음 컨트롤러는 클라이언트에서 업로드된 파일을 저장합니다.
Important
이 섹션의 컨트롤러는 Blazor 앱과 별도의 Web API 프로젝트에서 사용하기 위한 것입니다. 파일 업로드 사용자가 인증된 경우 웹 API는 XSRF/CSRF(교차 사이트 요청 위조) 공격을 완화해야 합니다.
참고 항목
특성으로[FromForm]
양식 값 바인딩은 .NET 6의 ASP.NET Core에서 최소 API에 기본적으로 지원되지 않습니다. 따라서 다음 Filesave
컨트롤러 예제는 최소 API를 사용하도록 변환할 수 없습니다. 최소 API를 사용하여 양식 값에서 바인딩에 대한 지원은 .NET 7 이상의 ASP.NET Core에서 사용할 수 있습니다.
다음 코드를 사용하려면 Development
환경에서 실행되는 앱에 대한 웹 API 프로젝트의 루트에 Development/unsafe_uploads
폴더를 만듭니다.
이 예제에서는 파일이 저장되는 경로의 일부로 앱의 환경을 사용하므로 테스트 및 프로덕션에서 다른 환경을 사용하는 경우 추가 폴더가 필요합니다. 예를 들어 Staging
환경에 대해서는 Staging/unsafe_uploads
폴더를 만듭니다. Production
환경에 대해서는 Production/unsafe_uploads
폴더를 만듭니다.
Warning
이 예제에서는 콘텐츠를 검사하지 않고 파일을 저장하며, 이 문서의 지침은 업로드된 파일의 추가 보안 모범 사례를 고려하지 않습니다. 스테이징 및 프로덕션 시스템에서 업로드 폴더에 대한 실행 권한을 사용하지 않도록 설정하고 업로드 직후 바이러스 백신/맬웨어 방지 스캐너 API를 사용하여 파일을 검사합니다. 자세한 내용은 ASP.NET Core에서 파일 업로드를 참조하세요.
Controllers/FilesaveController.cs
:
using System.Net;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("[controller]")]
public class FilesaveController(
IHostEnvironment env, ILogger<FilesaveController> logger)
: ControllerBase
{
[HttpPost]
public async Task<ActionResult<IList<UploadResult>>> PostFile(
[FromForm] IEnumerable<IFormFile> files)
{
var maxAllowedFiles = 3;
long maxFileSize = 1024 * 15;
var filesProcessed = 0;
var resourcePath = new Uri($"{Request.Scheme}://{Request.Host}/");
List<UploadResult> uploadResults = [];
foreach (var file in files)
{
var uploadResult = new UploadResult();
string trustedFileNameForFileStorage;
var untrustedFileName = file.FileName;
uploadResult.FileName = untrustedFileName;
var trustedFileNameForDisplay =
WebUtility.HtmlEncode(untrustedFileName);
if (filesProcessed < maxAllowedFiles)
{
if (file.Length == 0)
{
logger.LogInformation("{FileName} length is 0 (Err: 1)",
trustedFileNameForDisplay);
uploadResult.ErrorCode = 1;
}
else if (file.Length > maxFileSize)
{
logger.LogInformation("{FileName} of {Length} bytes is " +
"larger than the limit of {Limit} bytes (Err: 2)",
trustedFileNameForDisplay, file.Length, maxFileSize);
uploadResult.ErrorCode = 2;
}
else
{
try
{
trustedFileNameForFileStorage = Path.GetRandomFileName();
var path = Path.Combine(env.ContentRootPath,
env.EnvironmentName, "unsafe_uploads",
trustedFileNameForFileStorage);
await using FileStream fs = new(path, FileMode.Create);
await file.CopyToAsync(fs);
logger.LogInformation("{FileName} saved at {Path}",
trustedFileNameForDisplay, path);
uploadResult.Uploaded = true;
uploadResult.StoredFileName = trustedFileNameForFileStorage;
}
catch (IOException ex)
{
logger.LogError("{FileName} error on upload (Err: 3): {Message}",
trustedFileNameForDisplay, ex.Message);
uploadResult.ErrorCode = 3;
}
}
filesProcessed++;
}
else
{
logger.LogInformation("{FileName} not uploaded because the " +
"request exceeded the allowed {Count} of files (Err: 4)",
trustedFileNameForDisplay, maxAllowedFiles);
uploadResult.ErrorCode = 4;
}
uploadResults.Add(uploadResult);
}
return new CreatedResult(resourcePath, uploadResults);
}
}
앞의 코드 GetRandomFileName 에서 보안 파일 이름을 생성하기 위해 호출됩니다. 사이버 공격자는 기존 파일을 덮어쓰는 기존 파일 이름을 선택하거나 앱 외부에서 쓰려는 경로를 보낼 수 있으므로 브라우저에서 제공하는 파일 이름을 신뢰하지 마세요.
서버 앱은 컨트롤러 서비스를 등록하고 컨트롤러 엔드포인트를 매핑해야 합니다. 자세한 내용은 ASP.NET Core에서 컨트롤러 작업으로 라우팅을 참조 하세요.
CSR(클라이언트 쪽 렌더링)을 사용하여 서버에 파일 업로드
이 섹션은 S 또는 Blazor WebAssembly 앱의 CSR(클라이언트 쪽 렌더링) 구성 요소에 Blazor Web App적용됩니다.
다음 예제에서는 CSR 또는 앱의 구성 요소를 채택하는 구성 요소 Blazor Web App 에서 별도의 서버에서 별도의 앱의 백 엔드 웹 API 컨트롤러에 Blazor WebAssembly 파일을 업로드하는 방법을 보여 줍니다.
다음 UploadResult
클래스는 업로드된 파일의 결과를 유지 관리합니다. 서버에서 파일을 업로드하지 못할 경우 사용자에게 표시하기 위해 ErrorCode
로 오류 코드가 반환됩니다. 각 파일에 대해 안전한 파일 이름이 서버에서 생성되며 표시를 위해 StoredFileName
으로 클라이언트에 반환됩니다. FileName
의 안전하지 않은/신뢰할 수 없는 파일 이름을 사용하여 클라이언트와 서버 간에 파일의 키가 지정됩니다.
UploadResult.cs
:
public class UploadResult
{
public bool Uploaded { get; set; }
public string? FileName { get; set; }
public string? StoredFileName { get; set; }
public int ErrorCode { get; set; }
}
참고 항목
이전 UploadResult
클래스는 클라이언트 및 서버 기반 프로젝트 간에 공유할 수 있습니다. 클라이언트 및 서버 프로젝트가 클래스를 공유하는 경우 공유 프로젝트에 대한 각 프로젝트의 _Imports.razor
파일에 가져오기를 추가합니다. 예시:
@using BlazorSample.Shared
다음 FileUpload2
구성 요소는
- 사용자가 클라이언트에서 파일을 업로드하도록 허용합니다.
- 클라이언트가 제공하는 신뢰할 수 없는/안전하지 않은 파일 이름을 UI에 표시합니다. 신뢰할 수 없는/안전하지 않은 파일 이름은 UI에 안전하게 표시하기 위해 Razor에 의해 자동으로 HTML로 인코딩됩니다.
프로덕션 앱에 대한 보안 모범 사례는 앱, 서버 또는 네트워크에 대한 중요한 정보를 표시할 수 있는 오류 메시지를 클라이언트에 보내지 않도록 하는 것입니다. 자세한 오류 메시지를 제공하면 악의적인 사용자가 앱, 서버 또는 네트워크에 대한 공격을 고안하는 데 도움이 될 수 있습니다. 이 섹션의 예제 코드에서는 서버 쪽 오류가 발생할 경우 구성 요소 클라이언트 쪽에 표시할 오류 코드 번호(int
)만 반환합니다. 파일 업로드에 대한 지원이 필요한 사용자는 오류의 정확한 원인을 몰라도 지원 티켓 해결을 위해 지원 담당자에게 오류 코드를 제공합니다.
Warning
다음을 위해 클라이언트가 제공하는 파일 이름을 신뢰하지 마세요.
- 파일 시스템 또는 서비스에 파일을 저장
- 자동으로 또는 개발자 코드를 통해 파일 이름을 인코딩하지 않는 UI에 표시
서버에 파일을 업로드할 때의 보안 고려 사항에 대한 자세한 내용은 ASP.NET Core에서 파일 업로드를 참조하세요.
Blazor Web App 기본 프로젝트에서 프로젝트의 파일에 관련 서비스를 추가 IHttpClientFactory 합니다Program
.
builder.Services.AddHttpClient();
HttpClient
클라이언트 쪽 구성 요소가 서버에서 미리 렌더링되므로 서비스를 주 프로젝트에 추가해야 합니다. 다음 구성 요소에 대해 미리 렌더링을 사용하지 않도록 설정하는 경우 기본 앱에서 서비스를 제공할 HttpClient
필요가 없으며 이전 줄을 주 프로젝트에 추가할 필요가 없습니다.
ASP.NET Core 앱에 서비스를 추가하는 HttpClient
방법에 대한 자세한 내용은 ASP.NET Core에서 IHttpClientFactory를 사용하여 HTTP 요청 만들기를 참조 하세요.
또한 클라이언트 프로젝트(.Client
) Blazor Web App 는 백 엔드 웹 API 컨트롤러에 HttpClient HTTP POST 요청을 등록해야 합니다. 클라이언트 프로젝트의 Program
파일에 다음을 확인하거나 추가합니다.
builder.Services.AddScoped(sp =>
new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
앞의 예제에서는 앱의 기본 주소를 가져오고 일반적으로 호스트 페이지의 태그 href
값에서 <base>
파생되는 (IWebAssemblyHostEnvironment.BaseAddress)로 기본 주소를 builder.HostEnvironment.BaseAddress
설정합니다. 외부 웹 API를 호출하는 경우 웹 API의 기본 주소로 URI를 설정합니다.
다음 구성 요소의 맨 위에 대화형 WebAssembly 렌더링 모드 특성을 지정합니다 Blazor Web App.
@rendermode InteractiveWebAssembly
FileUpload2.razor
:
@page "/file-upload-2"
@using System.Linq
@using System.Net.Http.Headers
@inject HttpClient Http
@inject ILogger<FileUpload2> Logger
<PageTitle>File Upload 2</PageTitle>
<h1>File Upload Example 2</h1>
<p>
<label>
Upload up to @maxAllowedFiles files:
<InputFile OnChange="OnInputFileChange" multiple />
</label>
</p>
@if (files.Count > 0)
{
<div class="card">
<div class="card-body">
<ul>
@foreach (var file in files)
{
<li>
File: @file.Name
<br>
@if (FileUpload(uploadResults, file.Name, Logger,
out var result))
{
<span>
Stored File Name: @result.StoredFileName
</span>
}
else
{
<span>
There was an error uploading the file
(Error: @result.ErrorCode).
</span>
}
</li>
}
</ul>
</div>
</div>
}
@code {
private List<File> files = new();
private List<UploadResult> uploadResults = new();
private int maxAllowedFiles = 3;
private bool shouldRender;
protected override bool ShouldRender() => shouldRender;
private async Task OnInputFileChange(InputFileChangeEventArgs e)
{
shouldRender = false;
long maxFileSize = 1024 * 15;
var upload = false;
using var content = new MultipartFormDataContent();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
if (uploadResults.SingleOrDefault(
f => f.FileName == file.Name) is null)
{
try
{
files.Add(new() { Name = file.Name });
var fileContent = new StreamContent(file.OpenReadStream(maxFileSize));
fileContent.Headers.ContentType =
new MediaTypeHeaderValue(file.ContentType);
content.Add(
content: fileContent,
name: "\"files\"",
fileName: file.Name);
upload = true;
}
catch (Exception ex)
{
Logger.LogInformation(
"{FileName} not uploaded (Err: 6): {Message}",
file.Name, ex.Message);
uploadResults.Add(
new()
{
FileName = file.Name,
ErrorCode = 6,
Uploaded = false
});
}
}
}
if (upload)
{
var response = await Http.PostAsync("/Filesave", content);
var newUploadResults = await response.Content
.ReadFromJsonAsync<IList<UploadResult>>();
if (newUploadResults is not null)
{
uploadResults = uploadResults.Concat(newUploadResults).ToList();
}
}
shouldRender = true;
}
private static bool FileUpload(IList<UploadResult> uploadResults,
string? fileName, ILogger<FileUpload2> logger, out UploadResult result)
{
result = uploadResults.SingleOrDefault(f => f.FileName == fileName) ?? new();
if (!result.Uploaded)
{
logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
result.ErrorCode = 5;
}
return result.Uploaded;
}
private class File
{
public string? Name { get; set; }
}
}
@page "/file-upload-2"
@using System.Linq
@using System.Net.Http.Headers
@using Microsoft.Extensions.Logging
@inject HttpClient Http
@inject ILogger<FileUpload2> Logger
<h1>Upload Files</h1>
<p>
<label>
Upload up to @maxAllowedFiles files:
<InputFile OnChange="OnInputFileChange" multiple />
</label>
</p>
@if (files.Count > 0)
{
<div class="card">
<div class="card-body">
<ul>
@foreach (var file in files)
{
<li>
File: @file.Name
<br>
@if (FileUpload(uploadResults, file.Name, Logger,
out var result))
{
<span>
Stored File Name: @result.StoredFileName
</span>
}
else
{
<span>
There was an error uploading the file
(Error: @result.ErrorCode).
</span>
}
</li>
}
</ul>
</div>
</div>
}
@code {
private List<File> files = new();
private List<UploadResult> uploadResults = new();
private int maxAllowedFiles = 3;
private bool shouldRender;
protected override bool ShouldRender() => shouldRender;
private async Task OnInputFileChange(InputFileChangeEventArgs e)
{
shouldRender = false;
long maxFileSize = 1024 * 15;
var upload = false;
using var content = new MultipartFormDataContent();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
if (uploadResults.SingleOrDefault(
f => f.FileName == file.Name) is null)
{
try
{
files.Add(new() { Name = file.Name });
var fileContent = new StreamContent(file.OpenReadStream(maxFileSize));
fileContent.Headers.ContentType =
new MediaTypeHeaderValue(file.ContentType);
content.Add(
content: fileContent,
name: "\"files\"",
fileName: file.Name);
upload = true;
}
catch (Exception ex)
{
Logger.LogInformation(
"{FileName} not uploaded (Err: 6): {Message}",
file.Name, ex.Message);
uploadResults.Add(
new()
{
FileName = file.Name,
ErrorCode = 6,
Uploaded = false
});
}
}
}
if (upload)
{
var response = await Http.PostAsync("/Filesave", content);
var newUploadResults = await response.Content
.ReadFromJsonAsync<IList<UploadResult>>();
if (newUploadResults is not null)
{
uploadResults = uploadResults.Concat(newUploadResults).ToList();
}
}
shouldRender = true;
}
private static bool FileUpload(IList<UploadResult> uploadResults,
string? fileName, ILogger<FileUpload2> logger, out UploadResult result)
{
result = uploadResults.SingleOrDefault(f => f.FileName == fileName) ?? new();
if (!result.Uploaded)
{
logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
result.ErrorCode = 5;
}
return result.Uploaded;
}
private class File
{
public string? Name { get; set; }
}
}
@page "/file-upload-2"
@using System.Linq
@using System.Net.Http.Headers
@using Microsoft.Extensions.Logging
@inject HttpClient Http
@inject ILogger<FileUpload2> Logger
<h1>Upload Files</h1>
<p>
<label>
Upload up to @maxAllowedFiles files:
<InputFile OnChange="OnInputFileChange" multiple />
</label>
</p>
@if (files.Count > 0)
{
<div class="card">
<div class="card-body">
<ul>
@foreach (var file in files)
{
<li>
File: @file.Name
<br>
@if (FileUpload(uploadResults, file.Name, Logger,
out var result))
{
<span>
Stored File Name: @result.StoredFileName
</span>
}
else
{
<span>
There was an error uploading the file
(Error: @result.ErrorCode).
</span>
}
</li>
}
</ul>
</div>
</div>
}
@code {
private List<File> files = new();
private List<UploadResult> uploadResults = new();
private int maxAllowedFiles = 3;
private bool shouldRender;
protected override bool ShouldRender() => shouldRender;
private async Task OnInputFileChange(InputFileChangeEventArgs e)
{
shouldRender = false;
long maxFileSize = 1024 * 15;
var upload = false;
using var content = new MultipartFormDataContent();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
if (uploadResults.SingleOrDefault(
f => f.FileName == file.Name) is null)
{
try
{
files.Add(new() { Name = file.Name });
var fileContent = new StreamContent(file.OpenReadStream(maxFileSize));
fileContent.Headers.ContentType =
new MediaTypeHeaderValue(file.ContentType);
content.Add(
content: fileContent,
name: "\"files\"",
fileName: file.Name);
upload = true;
}
catch (Exception ex)
{
Logger.LogInformation(
"{FileName} not uploaded (Err: 6): {Message}",
file.Name, ex.Message);
uploadResults.Add(
new()
{
FileName = file.Name,
ErrorCode = 6,
Uploaded = false
});
}
}
}
if (upload)
{
var response = await Http.PostAsync("/Filesave", content);
var newUploadResults = await response.Content
.ReadFromJsonAsync<IList<UploadResult>>();
if (newUploadResults is not null)
{
uploadResults = uploadResults.Concat(newUploadResults).ToList();
}
}
shouldRender = true;
}
private static bool FileUpload(IList<UploadResult> uploadResults,
string? fileName, ILogger<FileUpload2> logger, out UploadResult result)
{
result = uploadResults.SingleOrDefault(f => f.FileName == fileName) ?? new();
if (!result.Uploaded)
{
logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
result.ErrorCode = 5;
}
return result.Uploaded;
}
private class File
{
public string? Name { get; set; }
}
}
@page "/file-upload-2"
@using System.Linq
@using System.Net.Http.Headers
@using Microsoft.Extensions.Logging
@inject HttpClient Http
@inject ILogger<FileUpload2> Logger
<h1>Upload Files</h1>
<p>
<label>
Upload up to @maxAllowedFiles files:
<InputFile OnChange="OnInputFileChange" multiple />
</label>
</p>
@if (files.Count > 0)
{
<div class="card">
<div class="card-body">
<ul>
@foreach (var file in files)
{
<li>
File: @file.Name
<br>
@if (FileUpload(uploadResults, file.Name, Logger,
out var result))
{
<span>
Stored File Name: @result.StoredFileName
</span>
}
else
{
<span>
There was an error uploading the file
(Error: @result.ErrorCode).
</span>
}
</li>
}
</ul>
</div>
</div>
}
@code {
private List<File> files = new();
private List<UploadResult> uploadResults = new();
private int maxAllowedFiles = 3;
private bool shouldRender;
protected override bool ShouldRender() => shouldRender;
private async Task OnInputFileChange(InputFileChangeEventArgs e)
{
shouldRender = false;
long maxFileSize = 1024 * 15;
var upload = false;
using var content = new MultipartFormDataContent();
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
if (uploadResults.SingleOrDefault(
f => f.FileName == file.Name) is null)
{
try
{
files.Add(new() { Name = file.Name });
var fileContent = new StreamContent(file.OpenReadStream(maxFileSize));
fileContent.Headers.ContentType =
new MediaTypeHeaderValue(file.ContentType);
content.Add(
content: fileContent,
name: "\"files\"",
fileName: file.Name);
upload = true;
}
catch (Exception ex)
{
Logger.LogInformation(
"{FileName} not uploaded (Err: 6): {Message}",
file.Name, ex.Message);
uploadResults.Add(
new()
{
FileName = file.Name,
ErrorCode = 6,
Uploaded = false
});
}
}
}
if (upload)
{
var response = await Http.PostAsync("/Filesave", content);
var newUploadResults = await response.Content
.ReadFromJsonAsync<IList<UploadResult>>();
uploadResults = uploadResults.Concat(newUploadResults).ToList();
}
shouldRender = true;
}
private static bool FileUpload(IList<UploadResult> uploadResults,
string fileName, ILogger<FileUpload2> logger, out UploadResult result)
{
result = uploadResults.SingleOrDefault(f => f.FileName == fileName);
if (result is null)
{
logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
result = new();
result.ErrorCode = 5;
}
return result.Uploaded;
}
private class File
{
public string Name { get; set; }
}
}
서버 쪽 프로젝트의 다음 컨트롤러는 클라이언트에서 업로드된 파일을 저장합니다.
참고 항목
특성으로[FromForm]
양식 값 바인딩은 .NET 6의 ASP.NET Core에서 최소 API에 기본적으로 지원되지 않습니다. 따라서 다음 Filesave
컨트롤러 예제는 최소 API를 사용하도록 변환할 수 없습니다. 최소 API를 사용하여 양식 값에서 바인딩에 대한 지원은 .NET 7 이상의 ASP.NET Core에서 사용할 수 있습니다.
다음 코드를 사용하려면 환경에서 실행되는 앱에 대한 서버 쪽 프로젝트의 루트에 폴더를 Development
만듭니 Development/unsafe_uploads
다.
이 예제에서는 파일이 저장되는 경로의 일부로 앱의 환경을 사용하므로 테스트 및 프로덕션에서 다른 환경을 사용하는 경우 추가 폴더가 필요합니다. 예를 들어 Staging
환경에 대해서는 Staging/unsafe_uploads
폴더를 만듭니다. Production
환경에 대해서는 Production/unsafe_uploads
폴더를 만듭니다.
Warning
이 예제에서는 콘텐츠를 검사하지 않고 파일을 저장하며, 이 문서의 지침은 업로드된 파일의 추가 보안 모범 사례를 고려하지 않습니다. 스테이징 및 프로덕션 시스템에서 업로드 폴더에 대한 실행 권한을 사용하지 않도록 설정하고 업로드 직후 바이러스 백신/맬웨어 방지 스캐너 API를 사용하여 파일을 검사합니다. 자세한 내용은 ASP.NET Core에서 파일 업로드를 참조하세요.
다음 예제에서는 공유 프로젝트가 클래스를 제공하는 경우 공유 프로젝트의 네임스페이스를 공유 프로젝트와 UploadResult
일치하도록 업데이트합니다.
Controllers/FilesaveController.cs
:
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using BlazorSample.Shared;
[ApiController]
[Route("[controller]")]
public class FilesaveController(
IHostEnvironment env, ILogger<FilesaveController> logger)
: ControllerBase
{
[HttpPost]
public async Task<ActionResult<IList<UploadResult>>> PostFile(
[FromForm] IEnumerable<IFormFile> files)
{
var maxAllowedFiles = 3;
long maxFileSize = 1024 * 15;
var filesProcessed = 0;
var resourcePath = new Uri($"{Request.Scheme}://{Request.Host}/");
List<UploadResult> uploadResults = [];
foreach (var file in files)
{
var uploadResult = new UploadResult();
string trustedFileNameForFileStorage;
var untrustedFileName = file.FileName;
uploadResult.FileName = untrustedFileName;
var trustedFileNameForDisplay =
WebUtility.HtmlEncode(untrustedFileName);
if (filesProcessed < maxAllowedFiles)
{
if (file.Length == 0)
{
logger.LogInformation("{FileName} length is 0 (Err: 1)",
trustedFileNameForDisplay);
uploadResult.ErrorCode = 1;
}
else if (file.Length > maxFileSize)
{
logger.LogInformation("{FileName} of {Length} bytes is " +
"larger than the limit of {Limit} bytes (Err: 2)",
trustedFileNameForDisplay, file.Length, maxFileSize);
uploadResult.ErrorCode = 2;
}
else
{
try
{
trustedFileNameForFileStorage = Path.GetRandomFileName();
var path = Path.Combine(env.ContentRootPath,
env.EnvironmentName, "unsafe_uploads",
trustedFileNameForFileStorage);
await using FileStream fs = new(path, FileMode.Create);
await file.CopyToAsync(fs);
logger.LogInformation("{FileName} saved at {Path}",
trustedFileNameForDisplay, path);
uploadResult.Uploaded = true;
uploadResult.StoredFileName = trustedFileNameForFileStorage;
}
catch (IOException ex)
{
logger.LogError("{FileName} error on upload (Err: 3): {Message}",
trustedFileNameForDisplay, ex.Message);
uploadResult.ErrorCode = 3;
}
}
filesProcessed++;
}
else
{
logger.LogInformation("{FileName} not uploaded because the " +
"request exceeded the allowed {Count} of files (Err: 4)",
trustedFileNameForDisplay, maxAllowedFiles);
uploadResult.ErrorCode = 4;
}
uploadResults.Add(uploadResult);
}
return new CreatedResult(resourcePath, uploadResults);
}
}
앞의 코드 GetRandomFileName 에서 보안 파일 이름을 생성하기 위해 호출됩니다. 사이버 공격자는 기존 파일을 덮어쓰는 기존 파일 이름을 선택하거나 앱 외부에서 쓰려는 경로를 보낼 수 있으므로 브라우저에서 제공하는 파일 이름을 신뢰하지 마세요.
서버 앱은 컨트롤러 서비스를 등록하고 컨트롤러 엔드포인트를 매핑해야 합니다. 자세한 내용은 ASP.NET Core에서 컨트롤러 작업으로 라우팅을 참조 하세요.
파일 업로드 취소
파일 업로드 구성 요소는 사용자가 IBrowserFile.OpenReadStream 또는 StreamReader.ReadAsync에 호출할 때 CancellationToken을 사용하여 업로드를 취소한 시기를 감지할 수 있습니다.
InputFile
구성 요소에 대한 CancellationTokenSource를 만듭니다. OnInputFileChange
메서드를 시작할 때 이전 업로드가 진행 중인지 확인합니다.
파일 업로드가 진행 중인 경우:
- 이전 업로드에서 Cancel을 호출합니다.
- 다음 업로드를 위한 새 CancellationTokenSource를 만들고 CancellationTokenSource.Token을 OpenReadStream 또는 ReadAsync에 전달합니다.
진행률이 있는 파일 서버 쪽 업로드
다음 예제에서는 사용자에게 업로드 진행률이 표시된 서버 쪽 앱에서 파일을 업로드하는 방법을 보여 줍니다.
테스트 앱에서 다음 예제를 사용하려면 다음을 수행합니다.
-
Development
환경에 대해 업로드된 파일을 저장할 폴더를 만듭니다.Development/unsafe_uploads
. - 최대 파일 크기(
maxFileSize
, 다음 예제의 경우 15KB) 및 허용되는 최대 파일 수(maxAllowedFiles
, 다음 예제의 경우 3)를 구성합니다. - 원하는 경우 버퍼를 다른 값(다음 예제에서는 10KB)으로 설정하여 진행 중인 보고를 보다 자세히 구성합니다. 성능 및 보안상의 문제로 인해 30KB보다 큰 버퍼를 사용하지 않는 것이 좋습니다.
Warning
이 예제에서는 콘텐츠를 검사하지 않고 파일을 저장하며, 이 문서의 지침은 업로드된 파일의 추가 보안 모범 사례를 고려하지 않습니다. 스테이징 및 프로덕션 시스템에서 업로드 폴더에 대한 실행 권한을 사용하지 않도록 설정하고 업로드 직후 바이러스 백신/맬웨어 방지 스캐너 API를 사용하여 파일을 검사합니다. 자세한 내용은 ASP.NET Core에서 파일 업로드를 참조하세요.
FileUpload3.razor
:
@page "/file-upload-3"
@inject ILogger<FileUpload3> Logger
@inject IWebHostEnvironment Environment
<PageTitle>File Upload 3</PageTitle>
<h1>File Upload Example 3</h1>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Progress: @string.Format("{0:P0}", progressPercent)</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private decimal progressPercent;
private async Task LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
progressPercent = 0;
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
var trustedFileName = Path.GetRandomFileName();
var path = Path.Combine(Environment.ContentRootPath,
Environment.EnvironmentName, "unsafe_uploads", trustedFileName);
await using FileStream writeStream = new(path, FileMode.Create);
using var readStream = file.OpenReadStream(maxFileSize);
var bytesRead = 0;
var totalRead = 0;
var buffer = new byte[1024 * 10];
while ((bytesRead = await readStream.ReadAsync(buffer)) != 0)
{
totalRead += bytesRead;
await writeStream.WriteAsync(buffer, 0, bytesRead);
progressPercent = Decimal.Divide(totalRead, file.Size);
StateHasChanged();
}
loadedFiles.Add(file);
Logger.LogInformation(
"Unsafe Filename: {UnsafeFilename} File saved: {Filename}",
file.Name, trustedFileName);
}
catch (Exception ex)
{
Logger.LogError("File: {FileName} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
@page "/file-upload-3"
@using System
@using System.IO
@using Microsoft.AspNetCore.Hosting
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload3> Logger
@inject IWebHostEnvironment Environment
<h3>Upload Files</h3>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Progress: @string.Format("{0:P0}", progressPercent)</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private decimal progressPercent;
private async Task LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
progressPercent = 0;
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
var trustedFileName = Path.GetRandomFileName();
var path = Path.Combine(Environment.ContentRootPath,
Environment.EnvironmentName, "unsafe_uploads", trustedFileName);
await using FileStream writeStream = new(path, FileMode.Create);
using var readStream = file.OpenReadStream(maxFileSize);
var bytesRead = 0;
var totalRead = 0;
var buffer = new byte[1024 * 10];
while ((bytesRead = await readStream.ReadAsync(buffer)) != 0)
{
totalRead += bytesRead;
await writeStream.WriteAsync(buffer, 0, bytesRead);
progressPercent = Decimal.Divide(totalRead, file.Size);
StateHasChanged();
}
loadedFiles.Add(file);
}
catch (Exception ex)
{
Logger.LogError("File: {FileName} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
@page "/file-upload-3"
@using System
@using System.IO
@using Microsoft.AspNetCore.Hosting
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload3> Logger
@inject IWebHostEnvironment Environment
<h3>Upload Files</h3>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Progress: @string.Format("{0:P0}", progressPercent)</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private decimal progressPercent;
private async Task LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
progressPercent = 0;
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
var trustedFileName = Path.GetRandomFileName();
var path = Path.Combine(Environment.ContentRootPath,
Environment.EnvironmentName, "unsafe_uploads", trustedFileName);
await using FileStream writeStream = new(path, FileMode.Create);
using var readStream = file.OpenReadStream(maxFileSize);
var bytesRead = 0;
var totalRead = 0;
var buffer = new byte[1024 * 10];
while ((bytesRead = await readStream.ReadAsync(buffer)) != 0)
{
totalRead += bytesRead;
await writeStream.WriteAsync(buffer, 0, bytesRead);
progressPercent = Decimal.Divide(totalRead, file.Size);
StateHasChanged();
}
loadedFiles.Add(file);
}
catch (Exception ex)
{
Logger.LogError("File: {Filename} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
@page "/file-upload-3"
@using System
@using System.IO
@using Microsoft.AspNetCore.Hosting
@using Microsoft.Extensions.Logging
@inject ILogger<FileUpload3> Logger
@inject IWebHostEnvironment Environment
<h3>Upload Files</h3>
<p>
<label>
Max file size:
<input type="number" @bind="maxFileSize" />
</label>
</p>
<p>
<label>
Max allowed files:
<input type="number" @bind="maxAllowedFiles" />
</label>
</p>
<p>
<label>
Upload up to @maxAllowedFiles of up to @maxFileSize bytes:
<InputFile OnChange="LoadFiles" multiple />
</label>
</p>
@if (isLoading)
{
<p>Progress: @string.Format("{0:P0}", progressPercent)</p>
}
else
{
<ul>
@foreach (var file in loadedFiles)
{
<li>
<ul>
<li>Name: @file.Name</li>
<li>Last modified: @file.LastModified.ToString()</li>
<li>Size (bytes): @file.Size</li>
<li>Content type: @file.ContentType</li>
</ul>
</li>
}
</ul>
}
@code {
private List<IBrowserFile> loadedFiles = new();
private long maxFileSize = 1024 * 15;
private int maxAllowedFiles = 3;
private bool isLoading;
private decimal progressPercent;
private async Task LoadFiles(InputFileChangeEventArgs e)
{
isLoading = true;
loadedFiles.Clear();
progressPercent = 0;
foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
{
try
{
var trustedFileName = Path.GetRandomFileName();
var path = Path.Combine(Environment.ContentRootPath,
Environment.EnvironmentName, "unsafe_uploads", trustedFileName);
await using FileStream writeStream = new(path, FileMode.Create);
using var readStream = file.OpenReadStream(maxFileSize);
var bytesRead = 0;
var totalRead = 0;
var buffer = new byte[1024 * 10];
while ((bytesRead = await readStream.ReadAsync(buffer)) != 0)
{
totalRead += bytesRead;
await writeStream.WriteAsync(buffer, 0, bytesRead);
progressPercent = Decimal.Divide(totalRead, file.Size);
StateHasChanged();
}
loadedFiles.Add(file);
}
catch (Exception ex)
{
Logger.LogError("File: {Filename} Error: {Error}",
file.Name, ex.Message);
}
}
isLoading = false;
}
}
자세한 내용은 다음 API 리소스를 참조하세요.
- FileStream: 파일에 대해 Stream을 제공하여 동기 및 비동기 읽기/쓰기 작업을 모두 지원합니다.
- FileStream.ReadAsync: 앞의
FileUpload3
구성 요소는 ReadAsync를 사용하여 스트림을 비동기적으로 읽습니다. Read를 사용하여 스트림을 동기적으로 읽는 것은 Razor 구성 요소에서 지원되지 않습니다.
파일 스트림
서버 대화형 작업을 사용하면 파일을 읽을 때 서버의 SignalR .NET 코드에 대한 연결을 통해 파일 데이터가 스트리밍됩니다.
RemoteBrowserFileStreamOptions 을 사용하면 파일 업로드 특성을 구성할 수 있습니다.
WebAssembly 렌더링 구성 요소의 경우 파일 데이터는 브라우저 내의 .NET 코드로 직접 스트리밍됩니다.
이미지 미리 보기 업로드
이미지 업로드의 이미지 미리 보기의 경우, 먼저 구성 요소 참조 및 OnChange
처리기를 사용하여 InputFile
구성 요소를 추가합니다.
<InputFile @ref="inputFile" OnChange="ShowPreview" />
이미지 미리 보기의 자리 표시자 역할을 하는 요소 참조를 사용하여 이미지 요소를 추가합니다.
<img @ref="previewImageElem" />
관련된 다음 참조를 추가합니다.
@code {
private InputFile? inputFile;
private ElementReference previewImageElem;
}
JavaScript에서 다음을 수행하며 HTML input
및 img
요소와 함께 호출되는 함수를 추가합니다.
- 선택한 파일을 추출합니다.
createObjectURL
을 사용하여 개체 URL을 만듭니다.- 이미지가 로드된 후,
revokeObjectURL
로 개체 URL을 취소하도록 이벤트 수신기를 설정하므로 메모리가 유출되지 않습니다. - 이미지를 표시할
img
요소의 소스를 설정합니다.
window.previewImage = (inputElem, imgElem) => {
const url = URL.createObjectURL(inputElem.files[0]);
imgElem.addEventListener('load', () => URL.revokeObjectURL(url), { once: true });
imgElem.src = url;
}
마지막으로, 삽입된 IJSRuntime을 사용하여 다음 JavaScript 함수를 호출하는 OnChange
처리기를 추가합니다.
@inject IJSRuntime JS
...
@code {
...
private async Task ShowPreview() => await JS.InvokeVoidAsync(
"previewImage", inputFile!.Element, previewImageElem);
}
앞의 예제는 단일 이미지를 업로드하는 것입니다. multiple
이미지를 지원하도록 접근 방식을 확장할 수 있습니다.
다음 FileUpload4
구성 요소는 전체 예제를 보여줍니다.
FileUpload4.razor
:
@page "/file-upload-4"
@inject IJSRuntime JS
<h1>File Upload Example</h1>
<InputFile @ref="inputFile" OnChange="ShowPreview" />
<img style="max-width:200px;max-height:200px" @ref="previewImageElem" />
@code {
private InputFile? inputFile;
private ElementReference previewImageElem;
private async Task ShowPreview() => await JS.InvokeVoidAsync(
"previewImage", inputFile!.Element, previewImageElem);
}
@page "/file-upload-4"
@inject IJSRuntime JS
<h1>File Upload Example</h1>
<InputFile @ref="inputFile" OnChange="ShowPreview" />
<img style="max-width:200px;max-height:200px" @ref="previewImageElem" />
@code {
private InputFile? inputFile;
private ElementReference previewImageElem;
private async Task ShowPreview() => await JS.InvokeVoidAsync(
"previewImage", inputFile!.Element, previewImageElem);
}
외부 서비스에 파일 업로드
앱이 파일 업로드 바이트를 처리하고 앱의 서버가 업로드된 파일을 수신하는 대신 클라이언트가 외부 서비스에 파일을 직접 업로드할 수 있습니다. 앱은 요청 시 외부 서비스에서 파일을 안전하게 처리할 수 있습니다. 이 방법은 악의적인 공격 및 잠재적인 성능 문제로부터 앱과 해당 서버를 강화합니다.
다음과 같은 잠재적 이점이 있는 Azure Files, Azure Blob Storage 또는 타사 서비스를 사용하는 방법을 고려하세요.
- JavaScript 클라이언트 라이브러리 또는 REST API를 사용하여 파일을 클라이언트에서 외부 서비스로 직접 업로드합니다. 예를 들어 Azure는 다음과 같은 클라이언트 라이브러리 및 API를 제공합니다.
- 각 클라이언트 파일 업로드에 대해 앱(서버 쪽)에서 생성된 SAS(사용자 위임 공유 액세스 서명) 토큰을 사용하여 사용자 업로드에 권한을 부여합니다. 예를 들어 Azure는 다음과 같은 SAS 기능을 제공합니다.
- 자동 중복 및 파일 공유 백업을 제공합니다.
- 할당량을 사용하여 업로드를 제한합니다. Azure Blob Storage 할당량은 컨테이너 수준이 아닌 계정 수준에서 설정됩니다. 그러나 Azure Files 할당량은 파일 공유 수준이며 업로드 제한을 더 잘 제어할 수 있습니다. 자세한 내용은 앞부분에서 목록에 링크된 Azure 문서를 참조하세요.
- SSE(서버 쪽 암호화)를 사용하여 파일을 보호합니다.
Azure Blob Storage 및 Azure Files에 대한 자세한 내용은 Azure Storage 설명서를 참조하세요.
서버 쪽 SignalR 메시지 크기 제한
SignalR가 최대 Blazor 메시지 크기를 초과하는 파일에 대한 데이터를 검색하는 경우 파일 업로드가 시작되기 전에 실패할 수 있습니다.
SignalR은 Blazor가 수신하는 모든 메시지에 적용되는 메시지 크기 제한을 정의하고, InputFile 구성 요소는 구성된 제한을 준수하는 메시지로 파일을 서버에 스트리밍합니다. 그러나 업로드할 파일 집합을 나타내는 첫 번째 메시지는 고유한 단일 메시지로 전송됩니다. 첫 번째 메시지의 크기가 SignalR 메시지 크기 제한을 초과할 수 있습니다. 이 문제는 파일 크기와 관련이 없으며 파일 수와 관련이 있습니다.
로그된 오류는 다음과 같습니다.
오류: '오류: 서버가 닫을 때 오류를 반환했습니다. 연결이 오류로 닫혔습니다.'라는 오류로 연결이 끊어졌습니다. e.log @ blazor.server.js:1
파일을 업로드할 때 첫 번째 메시지가 메시지 크기 제한에 도달하는 경우는 드뭅니다. 제한에 도달하는 경우 앱이 HubOptions.MaximumReceiveMessageSize를 더 큰 값으로 구성할 수 있습니다.
SignalR 구성 및 MaximumReceiveMessageSize 설정 방법에 대한 자세한 내용은 ASP.NET Core BlazorSignalR 지침을 참조하세요.
클라이언트 허브 설정당 최대 병렬 호출
Blazor 는 MaximumParallelInvocationsPerClient 기본값인 1로 설정됩니다.
값을 늘리면 작업이 throw할 CopyTo
확률이 높아 System.InvalidOperationException: 'Reading is not allowed after reader was completed.'
집니다. 자세한 내용은 MaximumParallelInvocationsPerClient > 1에서 파일 업로드를 모드에서 Blazor Server 중단(dotnet/aspnetcore
#53951)을 참조하세요.
문제 해결
호출 IBrowserFile.OpenReadStream 하는 줄은 다음을 System.TimeoutExceptionthrow합니다.
System.TimeoutException: Did not receive any data in the allotted time.
가능한 원인:
기본 제공 ASP.NET Core 종속성 주입 컨테이너 대신 IoC(Autofac Inversion of Control) 컨테이너를 사용합니다. 이 문제를 해결하려면 서버 쪽 회로 처리기 허브 옵션으로 설정합니다 DisableImplicitFromServicesParameters
true
. 자세한 내용은 FileUpload: 할당된 시간에 데이터를 받지 못했습니다(dotnet/aspnetcore
#38842)를 참조하세요.완료할 스트림을 읽지 않습니다. 프레임워크 문제는 아닙니다. 예외를 트래핑하고 로컬 환경/네트워크에서 추가로 조사합니다.
- 서버 쪽 렌더링을 사용하고 완료할 때 읽기 전에 여러 파일을 호출 OpenReadStream 합니다. 이 문제를 해결하려면 이 문서의 서버 쪽 렌더링 섹션이 있는 서버에 파일 업로드에 설명된 클래스 및 접근 방식을 사용합니다
LazyBrowserFileStream
.
추가 리소스
ASP.NET Core