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을 호출하고 반환된 스트림에서 읽습니다. 자세한 내용은 파일 스트림 섹션을 참조하세요.

OpenReadStreamStream의 최대 크기를 바이트 단위로 적용합니다. 500KB보다 큰 파일 하나 또는 여러 개를 읽으면 예외가 발생합니다. 이 제한은 개발자가 실수로 대규모 파일을 메모리로 읽어 들이는 것을 방지하기 위한 것입니다. 필요한 경우 OpenReadStreammaxAllowedSize 매개 변수를 사용하여 더 큰 크기를 지정할 수 있습니다.

파일의 바이트를 나타내는 Stream에 액세스해야 하는 경우 IBrowserFile.OpenReadStream을 사용합니다. 들어오는 파일 스트림을 한꺼번에 메모리로 직접 읽지 않도록 하세요. 예를 들어 파일의 바이트를 모두 MemoryStream으로 복사하거나 전체 스트림을 한꺼번에 바이트 배열로 읽지 마세요. 이러한 접근 방식은 특히 서버 쪽 구성 요소의 경우 성능 및 보안 문제를 초래할 수 있습니다. 대신 다음 방법 중 하나를 사용할 수 있습니다.

  • 스트림을 메모리에 읽지 않고 디스크의 파일에 직접 복사합니다. Blazor 서버에서 코드를 실행하는 앱은 클라이언트의 파일 시스템에 직접 액세스할 수 없습니다.
  • 파일을 클라이언트에서 외부 서비스로 직접 업로드합니다. 자세한 내용은 외부 서비스에 파일 업로드 섹션을 참조하세요.

다음 예제에서, browserFile은 업로드된 파일을 나타내고 IBrowserFile을 구현합니다. 작업 구현은 IBrowserFile 이 문서의 뒷부분에 있는 파일 업로드 구성 요소에 나와 있습니다.

지원되지 않음: 파일의 Stream 콘텐츠를 메모리의 String(reader)으로 읽어 들이므로 다음 접근 방식은 권장되지 않습니다.

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));

지원됨: 파일의 Stream이 제공된 경로에 파일을 생성하는 FileStream인 소비자에게 직접 제공되므로 다음 접근 방식은 권장됩니다.

await using FileStream fs = new(path, FileMode.Create);
await browserFile.OpenReadStream().CopyToAsync(fs);

지원됨: 파일의 StreamUploadBlobAsync에 직접 제공되므로 다음 접근 방식은 Microsoft Azure Blob Storage권장됩니다.

await blobContainerClient.UploadBlobAsync(
    trustedFileName, browserFile.OpenReadStream());

이미지 파일을 수신하는 구성 요소는 파일에 대한 BrowserFileExtensions.RequestImageFileAsync 편의 메서드를 호출하여 이미지가 앱으로 스트리밍되기 전에 브라우저의 JavaScript 런타임 내에서 이미지 데이터의 크기를 조정할 수 있습니다. RequestImageFileAsync 호출에 대한 사용 사례는 Blazor WebAssembly 앱에 가장 적합합니다.

파일 크기 읽기 및 업로드 제한

서버 쪽 또는 클라이언트 쪽에는 구성 요소에 대한 InputFile 파일 읽기 또는 업로드 크기 제한이 없습니다. 그러나 클라이언트 쪽 Blazor 은 JavaScript에서 C#으로 데이터를 마샬링할 때 파일의 바이트를 단일 JavaScript 배열 버퍼로 읽습니다. 이 버퍼는 2GB 또는 디바이스의 사용 가능한 메모리로 제한됩니다. 구성 요소를 사용하는 InputFile 클라이언트 쪽 업로드의 경우 대용량 파일 업로드(>250MB)가 실패할 수 있습니다. 자세한 내용은 다음 토론을 참조하세요.

구성 요소에 대해 InputFile 지원되는 최대 파일 크기는 2GB입니다. 또한 클라이언트 쪽 Blazor 은 JavaScript에서 C#으로 데이터를 마샬링할 때 파일의 바이트를 단일 JavaScript 배열 버퍼로 읽습니다. 이 버퍼는 2GB 또는 디바이스의 사용 가능한 메모리로 제한됩니다. 구성 요소를 사용하는 InputFile 클라이언트 쪽 업로드의 경우 대용량 파일 업로드(>250MB)가 실패할 수 있습니다. 자세한 내용은 다음 토론을 참조하세요.

구성 요소를 사용하려고 할 때 실패하는 대규모 클라이언트 쪽 파일 업로드의 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에서 파일 업로드를 참조하세요.

서버 쪽 렌더링을 사용하여 서버에 파일 업로드

이 섹션은 Web Apps의 대화형 서버 구성 요소에 Blazor 적용됩니다.

이 섹션은 Blazor Server 앱에 적용됩니다.

다음 예제에서는 별도의 서버에서 서버 쪽 앱에서 백 엔드 웹 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)만 반환합니다. 파일 업로드에 대한 지원이 필요한 사용자는 오류의 정확한 원인을 몰라도 지원 티켓 해결을 위해 지원 담당자에게 오류 코드를 제공합니다.

다음 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.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 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.Linq
@using System.Net.Http.Headers
@using System.Text.Json
@using Microsoft.Extensions.Logging
@inject IHttpClientFactory ClientFactory
@inject ILogger<FileUpload2> Logger

<h1>Upload Files</h1>

<p>
    <label>
        Upload up to @maxAllowedFiles files:
        <InputFile OnChange="OnInputFileChange" multiple />
    </label>
</p>

@if (files.Count > 0)
{
    <div class="card">
        <div class="card-body">
            <ul>
                @foreach (var file in files)
                {
                    <li>
                        File: @file.Name
                        <br>
                        @if (FileUpload(uploadResults, file.Name, Logger,
                           out var result))
                        {
                            <span>
                                Stored File Name: @result.StoredFileName
                            </span>
                        }
                        else
                        {
                            <span>
                                There was an error uploading the file
                                (Error: @result.ErrorCode).
                            </span>
                        }
                    </li>
                }
            </ul>
        </div>
    </div>
}

@code {
    private List<File> files = new();
    private List<UploadResult> uploadResults = new();
    private int maxAllowedFiles = 3;
    private bool shouldRender;

    protected override bool ShouldRender() => shouldRender;

    private async Task OnInputFileChange(InputFileChangeEventArgs e)
    {
        shouldRender = false;
        long maxFileSize = 1024 * 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 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.Linq
@using System.Net.Http.Headers
@using System.Text.Json
@using Microsoft.Extensions.Logging
@inject IHttpClientFactory ClientFactory
@inject ILogger<FileUpload2> Logger

<h1>Upload Files</h1>

<p>
    <label>
        Upload up to @maxAllowedFiles files:
        <InputFile OnChange="OnInputFileChange" multiple />
    </label>
</p>

@if (files.Count > 0)
{
    <div class="card">
        <div class="card-body">
            <ul>
                @foreach (var file in files)
                {
                    <li>
                        File: @file.Name
                        <br>
                        @if (FileUpload(uploadResults, file.Name, Logger,
                           out var result))
                        {
                            <span>
                                Stored File Name: @result.StoredFileName
                            </span>
                        }
                        else
                        {
                            <span>
                                There was an error uploading the file
                                (Error: @result.ErrorCode).
                            </span>
                        }
                    </li>
                }
            </ul>
        </div>
    </div>
}

@code {
    private List<File> files = new();
    private List<UploadResult> uploadResults = new();
    private int maxAllowedFiles = 3;
    private bool shouldRender;

    protected override bool ShouldRender() => shouldRender;

    private async Task OnInputFileChange(InputFileChangeEventArgs e)
    {
        shouldRender = false;
        long maxFileSize = 1024 * 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 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.Linq
@using System.Net.Http.Headers
@using System.Text.Json
@using Microsoft.Extensions.Logging
@inject IHttpClientFactory ClientFactory
@inject ILogger<FileUpload2> Logger

<h1>Upload Files</h1>

<p>
    <label>
        Upload up to @maxAllowedFiles files:
        <InputFile OnChange="OnInputFileChange" multiple />
    </label>
</p>

@if (files.Count > 0)
{
    <div class="card">
        <div class="card-body">
            <ul>
                @foreach (var file in files)
                {
                    <li>
                        File: @file.Name
                        <br>
                        @if (FileUpload(uploadResults, file.Name, Logger,
                           out var result))
                        {
                            <span>
                                Stored File Name: @result.StoredFileName
                            </span>
                        }
                        else
                        {
                            <span>
                                There was an error uploading the file
                                (Error: @result.ErrorCode).
                            </span>
                        }
                    </li>
                }
            </ul>
        </div>
    </div>
}

@code {
    private List<File> files = new();
    private List<UploadResult> uploadResults = new();
    private int maxAllowedFiles = 3;
    private bool shouldRender;

    protected override bool ShouldRender() => shouldRender;

    private async Task OnInputFileChange(InputFileChangeEventArgs e)
    {
        shouldRender = false;
        long maxFileSize = 1024 * 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 client = ClientFactory.CreateClient();

            var response = 
                await client.PostAsync("https://localhost:5001/Filesave", 
                content);

            if (response.IsSuccessStatusCode)
            {
                var options = new JsonSerializerOptions
                {
                    PropertyNameCaseInsensitive = true,
                };

                using var responseStream =
                    await response.Content.ReadAsStreamAsync();

                var newUploadResults = await JsonSerializer
                    .DeserializeAsync<IList<UploadResult>>(responseStream, options);

                uploadResults = uploadResults.Concat(newUploadResults).ToList();
            }
        }

        shouldRender = true;
    }

    private static bool FileUpload(IList<UploadResult> uploadResults,
        string fileName, ILogger<FileUpload2> logger, out UploadResult result)
    {
        result = uploadResults.SingleOrDefault(f => f.FileName == fileName);

        if (result is null)
        {
            logger.LogInformation("{FileName} not uploaded (Err: 5)", fileName);
            result = new();
            result.ErrorCode = 5;
        }

        return result.Uploaded;
    }

    private class File
    {
        public string Name { get; set; }
    }
}

웹 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 : ControllerBase
{
    private readonly IHostEnvironment env;
    private readonly ILogger<FilesaveController> logger;

    public FilesaveController(IHostEnvironment env, 
        ILogger<FilesaveController> logger)
    {
        this.env = env;
        this.logger = logger;
    }

    [HttpPost]
    public async Task<ActionResult<IList<UploadResult>>> PostFile(
        [FromForm] IEnumerable<IFormFile> files)
    {
        var maxAllowedFiles = 3;
        long maxFileSize = 1024 * 15;
        var filesProcessed = 0;
        var resourcePath = new Uri($"{Request.Scheme}://{Request.Host}/");
        List<UploadResult> uploadResults = new();

        foreach (var file in files)
        {
            var uploadResult = new UploadResult();
            string trustedFileNameForFileStorage;
            var untrustedFileName = file.FileName;
            uploadResult.FileName = untrustedFileName;
            var trustedFileNameForDisplay =
                WebUtility.HtmlEncode(untrustedFileName);

            if (filesProcessed < maxAllowedFiles)
            {
                if (file.Length == 0)
                {
                    logger.LogInformation("{FileName} length is 0 (Err: 1)",
                        trustedFileNameForDisplay);
                    uploadResult.ErrorCode = 1;
                }
                else if (file.Length > maxFileSize)
                {
                    logger.LogInformation("{FileName} of {Length} bytes is " +
                        "larger than the limit of {Limit} bytes (Err: 2)",
                        trustedFileNameForDisplay, file.Length, maxFileSize);
                    uploadResult.ErrorCode = 2;
                }
                else
                {
                    try
                    {
                        trustedFileNameForFileStorage = Path.GetRandomFileName();
                        var path = Path.Combine(env.ContentRootPath,
                            env.EnvironmentName, "unsafe_uploads",
                            trustedFileNameForFileStorage);

                        await using FileStream fs = new(path, FileMode.Create);
                        await file.CopyToAsync(fs);

                        logger.LogInformation("{FileName} saved at {Path}",
                            trustedFileNameForDisplay, path);
                        uploadResult.Uploaded = true;
                        uploadResult.StoredFileName = trustedFileNameForFileStorage;
                    }
                    catch (IOException ex)
                    {
                        logger.LogError("{FileName} error on upload (Err: 3): {Message}",
                            trustedFileNameForDisplay, ex.Message);
                        uploadResult.ErrorCode = 3;
                    }
                }

                filesProcessed++;
            }
            else
            {
                logger.LogInformation("{FileName} not uploaded because the " +
                    "request exceeded the allowed {Count} of files (Err: 4)",
                    trustedFileNameForDisplay, maxAllowedFiles);
                uploadResult.ErrorCode = 4;
            }

            uploadResults.Add(uploadResult);
        }

        return new CreatedResult(resourcePath, uploadResults);
    }
}

앞의 코드 GetRandomFileName 에서 보안 파일 이름을 생성하기 위해 호출됩니다. 공격자가 기존 파일을 덮어쓰는 기존 파일 이름을 선택하거나 앱 외부에서 쓰려는 경로를 보낼 수 있으므로 브라우저에서 제공하는 파일 이름을 신뢰하지 마세요.

서버 앱은 컨트롤러 서비스를 등록하고 컨트롤러 엔드포인트를 매핑해야 합니다. 자세한 내용은 ASP.NET Core에서 컨트롤러 작업으로 라우팅을 참조 하세요.

서버에 파일 업로드

다음 예제에서는 웹 API 컨트롤러에 파일을 업로드하는 방법을 보여 줍니다.

다음 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 요청 만들기를 참조 하세요.

웹앱의 Blazor 클라이언트 프로젝트(.Client)도 백 엔드 웹 API 컨트롤러에 HTTP POST 요청을 등록 HttpClient 해야 합니다. 클라이언트 프로젝트의 Program 파일에 다음을 확인하거나 추가합니다.

builder.Services.AddScoped(sp => 
    new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

앞의 예제에서는 앱의 기본 주소를 가져오고 일반적으로 호스트 페이지의 태그 href 값에서 <base> 파생되는 (IWebAssemblyHostEnvironment.BaseAddress)로 기본 주소를 builder.HostEnvironment.BaseAddress 설정합니다. 외부 웹 API를 호출하는 경우 웹 API의 기본 주소로 URI를 설정합니다.

웹앱에서 다음 구성 요소 Blazor 의 맨 위에 대화형 WebAssembly 렌더링 모드 특성을 지정합니다.

@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 : ControllerBase
{
    private readonly IHostEnvironment env;
    private readonly ILogger<FilesaveController> logger;

    public FilesaveController(IHostEnvironment env,
        ILogger<FilesaveController> logger)
    {
        this.env = env;
        this.logger = logger;
    }

    [HttpPost]
    public async Task<ActionResult<IList<UploadResult>>> PostFile(
        [FromForm] IEnumerable<IFormFile> files)
    {
        var maxAllowedFiles = 3;
        long maxFileSize = 1024 * 15;
        var filesProcessed = 0;
        var resourcePath = new Uri($"{Request.Scheme}://{Request.Host}/");
        List<UploadResult> uploadResults = new();

        foreach (var file in files)
        {
            var uploadResult = new UploadResult();
            string trustedFileNameForFileStorage;
            var untrustedFileName = file.FileName;
            uploadResult.FileName = untrustedFileName;
            var trustedFileNameForDisplay =
                WebUtility.HtmlEncode(untrustedFileName);

            if (filesProcessed < maxAllowedFiles)
            {
                if (file.Length == 0)
                {
                    logger.LogInformation("{FileName} length is 0 (Err: 1)",
                        trustedFileNameForDisplay);
                    uploadResult.ErrorCode = 1;
                }
                else if (file.Length > maxFileSize)
                {
                    logger.LogInformation("{FileName} of {Length} bytes is " +
                        "larger than the limit of {Limit} bytes (Err: 2)",
                        trustedFileNameForDisplay, file.Length, maxFileSize);
                    uploadResult.ErrorCode = 2;
                }
                else
                {
                    try
                    {
                        trustedFileNameForFileStorage = Path.GetRandomFileName();
                        var path = Path.Combine(env.ContentRootPath,
                            env.EnvironmentName, "unsafe_uploads",
                            trustedFileNameForFileStorage);

                        await using FileStream fs = new(path, FileMode.Create);
                        await file.CopyToAsync(fs);

                        logger.LogInformation("{FileName} saved at {Path}",
                            trustedFileNameForDisplay, path);
                        uploadResult.Uploaded = true;
                        uploadResult.StoredFileName = trustedFileNameForFileStorage;
                    }
                    catch (IOException ex)
                    {
                        logger.LogError("{FileName} error on upload (Err: 3): {Message}",
                            trustedFileNameForDisplay, ex.Message);
                        uploadResult.ErrorCode = 3;
                    }
                }

                filesProcessed++;
            }
            else
            {
                logger.LogInformation("{FileName} not uploaded because the " +
                    "request exceeded the allowed {Count} of files (Err: 4)",
                    trustedFileNameForDisplay, maxAllowedFiles);
                uploadResult.ErrorCode = 4;
            }

            uploadResults.Add(uploadResult);
        }

        return new CreatedResult(resourcePath, uploadResults);
    }
}

앞의 코드 GetRandomFileName 에서 보안 파일 이름을 생성하기 위해 호출됩니다. 공격자가 기존 파일을 덮어쓰는 기존 파일 이름을 선택하거나 앱 외부에서 쓰려는 경로를 보낼 수 있으므로 브라우저에서 제공하는 파일 이름을 신뢰하지 마세요.

서버 앱은 컨트롤러 서비스를 등록하고 컨트롤러 엔드포인트를 매핑해야 합니다. 자세한 내용은 ASP.NET Core에서 컨트롤러 작업으로 라우팅을 참조 하세요.

파일 업로드 취소

파일 업로드 구성 요소는 사용자가 IBrowserFile.OpenReadStream 또는 StreamReader.ReadAsync에 호출할 때 CancellationToken을 사용하여 업로드를 취소한 시기를 감지할 수 있습니다.

InputFile 구성 요소에 대한 CancellationTokenSource를 만듭니다. OnInputFileChange 메서드를 시작할 때 이전 업로드가 진행 중인지 확인합니다.

파일 업로드가 진행 중인 경우:

진행률이 있는 파일 서버 쪽 업로드

다음 예제에서는 사용자에게 업로드 진행률이 표시된 서버 쪽 앱에서 파일을 업로드하는 방법을 보여 줍니다.

테스트 앱에서 다음 예제를 사용하려면 다음을 수행합니다.

  • 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 inputimg 요소와 함께 호출되는 함수를 추가합니다.

  • 선택한 파일을 추출합니다.
  • 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 또는 타사 서비스를 사용하는 방법을 고려하세요.

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 지침을 참조하세요.

추가 리소스