첨부 실행 코드는 나눔고딕코딩 폰트를 사용합니다.
본 블로그는 광고를 포함하고 있습니다.
광고 클릭에서 발생하는 수익금은 모두 블로그 콘텐츠 향상을 위해 쓰여집니다.

728x90
반응형
728x170

TestProject.zip
다운로드

▶ Utilities/FileHelper.cs

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Net.Http.Headers;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Threading.Tasks;

namespace TestProject.Utilities
{
    /// <summary>
    /// 파일 헬퍼
    /// </summary>
    public static class FileHelper
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Field
        ////////////////////////////////////////////////////////////////////////////////////////// Static
        //////////////////////////////////////////////////////////////////////////////// Private

        #region Field

        /// <summary>
        /// 허용 문자 바이트 배열
        /// </summary>
        /// <remarks>
        /// IsValidFileExtensionAndSignature 메서드의 특정 문자를 확인해야하는 경우
        /// _allowedCharacterByteArray 필드에 문자를 제공합니다.
        /// </remarks>
        private static readonly byte[] _allowCharacterByteArray = { };

        /// <summary>
        /// 파일 시그니처 딕셔너리
        /// </summary>
        /// <remarks>
        /// 더 많은 파일 서명은 파일 서명 데이터베이스 (https://www.filesignatures.net/) 및 추가하려는 파일 형식에 대한 공식 사양을 참조한다.
        /// </remarks>
        private static readonly Dictionary<string, List<byte[]>> _fileSignatureDictionary = new Dictionary<string, List<byte[]>>
        {
            {
                ".gif",
                new List<byte[]>
                {
                    new byte[] { 0x47, 0x49, 0x46, 0x38 }
                }
            },
            {
                ".png",
                new List<byte[]>
                {
                    new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }
                }
            },
            {
                ".jpeg",
                new List<byte[]>
                {
                    new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 },
                    new byte[] { 0xFF, 0xD8, 0xFF, 0xE2 },
                    new byte[] { 0xFF, 0xD8, 0xFF, 0xE3 }
                }
            },
            {
                ".jpg",
                new List<byte[]>
                {
                    new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 },
                    new byte[] { 0xFF, 0xD8, 0xFF, 0xE1 },
                    new byte[] { 0xFF, 0xD8, 0xFF, 0xE8 }
                }
            },
            {
                ".zip",
                new List<byte[]> 
                {
                    new byte[] { 0x50, 0x4B, 0x03, 0x04             }, 
                    new byte[] { 0x50, 0x4B, 0x4C, 0x49, 0x54, 0x45 },
                    new byte[] { 0x50, 0x4B, 0x53, 0x70, 0x58       },
                    new byte[] { 0x50, 0x4B, 0x05, 0x06             },
                    new byte[] { 0x50, 0x4B, 0x07, 0x08             },
                    new byte[] { 0x57, 0x69, 0x6E, 0x5A, 0x69, 0x70 }
                }
            }
        };

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Static
        //////////////////////////////////////////////////////////////////////////////// Public

        // 경고!
        // 다음 파일 처리 방법에서는 파일의 내용이 검사되지 않는다.
        // 대부분의 프로덕션 시나리오에서 바이러스 백신/멀웨어 방지 스캐너 API는
        // 사용자나 다른 시스템에서 파일을 사용할 수 있도록 하기 전에 파일에 사용된다.
        // 자세한 정보는 이 샘플 앱과 함께 제공되는 주제를 참조한다.
        #region 파일에서 처리하기 - ProcessFormFile<T>(formFile, modelStateDictionary, sourceFileExtensionArray, fileSizeLimit)

        /// <summary>
        /// 파일에서 처리하기
        /// </summary>
        /// <typeparam name="T">타입</typeparam>
        /// <param name="formFile">폼 파일</param>
        /// <param name="modelStateDictionary">모델 상태 딕셔너리</param>
        /// <param name="sourceFileExtensionArray">소스 파일 확장자 배열</param>
        /// <param name="fileSizeLimit">파일 크기 제한</param>
        /// <returns>바이트 배열 태스크</returns>
        public static async Task<byte[]> ProcessFormFile<T>
        (
            IFormFile            formFile,
            ModelStateDictionary modelStateDictionary,
            string[]             sourceFileExtensionArray,
            long                 fileSizeLimit
        )
        {
            string fieldDisplayName = string.Empty;

            // 리플렉션을 사용하여 이 IFormFile과 연결된 모델 속성의 표시 이름을 가져온다.
            // 표시 이름을 찾을 수 없는 경우 오류 메시지에 표시 이름이 표시되지 않는다.
            MemberInfo propertyMemberInfo = typeof(T).GetProperty
            (
                formFile.Name.Substring
                (
                    formFile.Name.IndexOf(".", StringComparison.Ordinal) + 1
                )
            );

            if(propertyMemberInfo != null)
            {
                if(propertyMemberInfo.GetCustomAttribute(typeof(DisplayAttribute)) is DisplayAttribute displayAttribute)
                {
                    fieldDisplayName = $"{displayAttribute.Name} ";
                }
            }

            // 클라이언트가 보낸 파일 이름을 신뢰하지 않는다.
            // 파일 이름을 표시하려면 값을 HTML로 인코딩한다.
            string trustedFileNameForDisplay = WebUtility.HtmlEncode(formFile.FileName);

            // 파일 길이를 확인한다.
            // 이 검사는 내용으로 BOM만 있는 파일을 포착하지 않는다.
            if(formFile.Length == 0)
            {
                modelStateDictionary.AddModelError
                (
                    formFile.Name,
                    $"{fieldDisplayName}({trustedFileNameForDisplay}) is empty."
                );

                return new byte[0];
            }
            
            if(formFile.Length > fileSizeLimit)
            {
                long fileSizeLimitMB = fileSizeLimit / 1048576;

                modelStateDictionary.AddModelError
                (
                    formFile.Name,
                    $"{fieldDisplayName}({trustedFileNameForDisplay}) exceeds {fileSizeLimitMB:N1} MB."
                );

                return new byte[0];
            }

            try
            {
                using(MemoryStream memoryStream = new MemoryStream())
                {
                    await formFile.CopyToAsync(memoryStream);

                    // 파일의 유일한 내용이 BOM이었고
                    // BOM을 제거한 후 내용이 실제로 비어있는 경우 내용 길이를 확인한다.
                    if(memoryStream.Length == 0)
                    {
                        modelStateDictionary.AddModelError
                        (
                            formFile.Name,
                            $"{fieldDisplayName}({trustedFileNameForDisplay}) is empty."
                        );
                    }

                    if(!IsValidFileExtensionAndSignature(formFile.FileName, memoryStream, sourceFileExtensionArray))
                    {
                        modelStateDictionary.AddModelError
                        (
                            formFile.Name,
                            $"{fieldDisplayName}({trustedFileNameForDisplay}) file type isn't permitted or " +
                                "the file's signature doesn't match the file's extension."
                        );
                    }
                    else
                    {
                        return memoryStream.ToArray();
                    }
                }
            }
            catch(Exception exception)
            {
                modelStateDictionary.AddModelError
                (
                    formFile.Name,
                    $"{fieldDisplayName}({trustedFileNameForDisplay}) upload failed. " +
                        "Please contact the Help Desk for support. Error : {exception.HResult}"
                );
            }

            return new byte[0];
        }

        #endregion
        #region 스트림 파일 처리하기 - ProcessStreamedFile(multipartSection, contentDispositionHeaderValue, modelStateDictionary, sourceFileExtensionArray, fileSizeLimit)

        /// <summary>
        /// 스트림 파일 처리하기
        /// </summary>
        /// <param name="multipartSection">멀티 파트 섹션</param>
        /// <param name="contentDispositionHeaderValue">컨텐트 배치 헤더 값</param>
        /// <param name="modelStateDictionary">모델 상태 딕셔너리</param>
        /// <param name="sourceFileExtensionArray">소스 파일 확장자 배열</param>
        /// <param name="fileSizeLimit">파일 크기 제한</param>
        /// <returns>바이트 배열 태스크</returns>
        public static async Task<byte[]> ProcessStreamedFile
        (
            MultipartSection              multipartSection,
            ContentDispositionHeaderValue contentDispositionHeaderValue,
            ModelStateDictionary          modelStateDictionary,
            string[]                      sourceFileExtensionArray,
            long                          fileSizeLimit
        )
        {
            try
            {
                using(MemoryStream memoryStream = new MemoryStream())
                {
                    await multipartSection.Body.CopyToAsync(memoryStream);

                    // 파일이 비어 있거나 크기 제한을 초과하는지 확인한다.
                    if(memoryStream.Length == 0)
                    {
                        modelStateDictionary.AddModelError("File", "The file is empty.");
                    }
                    else if(memoryStream.Length > fileSizeLimit)
                    {
                        long fileSizeLimitMB = fileSizeLimit / 1048576;

                        modelStateDictionary.AddModelError("File", $"The file exceeds {fileSizeLimitMB:N1} MB.");
                    }
                    else if
                    (
                        !IsValidFileExtensionAndSignature
                        (
                            contentDispositionHeaderValue.FileName.Value,
                            memoryStream,
                            sourceFileExtensionArray
                        )
                    )
                    {
                        modelStateDictionary.AddModelError
                        (
                            "File",
                            "The file type isn't permitted or the file's signature doesn't match the file's extension."
                        );
                    }
                    else
                    {
                        return memoryStream.ToArray();
                    }
                }
            }
            catch(Exception exception)
            {
                modelStateDictionary.AddModelError
                (
                    "File",
                    $"The upload failed. Please contact the Help Desk for support. Error: {exception.HResult}"
                );
            }

            return new byte[0];
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////// Private

        #region 파일 확장자/시그니처 유효 여부 구하기 - IsValidFileExtensionAndSignature(fileName, stream, sourceFileExtensionArray)

        /// <summary>
        /// 파일 확장자/시그니처 유효 여부 구하기
        /// </summary>
        /// <param name="fileName">파일명</param>
        /// <param name="stream">스트림</param>
        /// <param name="sourceFileExtensionArray">소스 파일 확장자 배열</param>
        /// <returns>파일 확장자/시그니처 유효 여부</returns>
        private static bool IsValidFileExtensionAndSignature(string fileName, Stream stream, string[] sourceFileExtensionArray)
        {
            if(string.IsNullOrEmpty(fileName) || stream == null || stream.Length == 0)
            {
                return false;
            }

            string fileExtension = Path.GetExtension(fileName).ToLowerInvariant();

            if(string.IsNullOrEmpty(fileExtension) || !sourceFileExtensionArray.Contains(fileExtension))
            {
                return false;
            }

            stream.Position = 0;

            using(BinaryReader reader = new BinaryReader(stream))
            {
                if(fileExtension.Equals(".txt") || fileExtension.Equals(".csv") || fileExtension.Equals(".prn"))
                {
                    if(_allowCharacterByteArray.Length == 0)
                    {
                        // 문자를 ASCII 인코딩으로 제한한다.
                        for(long i = 0L; i < stream.Length; i++)
                        {
                            if(reader.ReadByte() > sbyte.MaxValue)
                            {
                                return false;
                            }
                        }
                    }
                    else
                    {
                        // 문자를 ASCII 인코딩 및 _allowedCharacterByteArray 배열의 값으로 제한합니다.
                        for(long i = 0L; i < stream.Length; i++)
                        {
                            byte byteValue = reader.ReadByte();

                            if(byteValue > sbyte.MaxValue || !_allowCharacterByteArray.Contains(byteValue))
                            {
                                return false;
                            }
                        }
                    }

                    return true;
                }

                // _fileSignatureDictionary에 서명이 제공되지 않은 파일을 허용해야 하는 경우
                // 다음 코드 블록의 주석 처리를 제거한다.
                // 시스템에서 허용하려는 모든 파일 형식에 대해 가능한 경우
                // 파일에 대한 파일 서명을 추가하고 파일 서명 검사를 수행하는 것이 좋다.
                //if(!_fileSignatureDictionary.ContainsKey(fileExtension))
                //{
                //    return true;
                //}
                

                // 파일 서명 확인
                // --------------
                // _fileSignatureDictionary에 제공된 파일 서명을 사용하여
                // 다음 코드는 입력 콘텐츠의 파일 서명을 테스트한다.
                List<byte[]> signatureList = _fileSignatureDictionary[fileExtension];

                byte[] headerByteArray = reader.ReadBytes(signatureList.Max(m => m.Length));

                return signatureList.Any(signature => headerByteArray.Take(signature.Length).SequenceEqual(signature));
            }
        }

        #endregion
    }
}

 

728x90

 

▶ Utilities/MultipartRequestHelper.cs

using Microsoft.Net.Http.Headers;
using System;
using System.IO;

namespace TestProject.Utilities
{
    /// <summary>
    /// 멀티 파트 요청 헬퍼
    /// </summary>
    public static class MultipartRequestHelper
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Static
        //////////////////////////////////////////////////////////////////////////////// Public

        // Content-Type : multipart/form-data; boundary="----WebKitFormBoundarymx2fSWqWSd0OxQqq"
        // https://tools.ietf.org/html/rfc2046#section-5.1의 사양에는 70자가 합리적인 제한이라고 나와 있다.
        #region 경계 구하기 - GetBoundary(mediaTypeHeaderValue, boundaryLengthLimit)

        /// <summary>
        /// 경계 구하기
        /// </summary>
        /// <param name="mediaTypeHeaderValue">미디어 타입 헤더 값</param>
        /// <param name="boundaryLengthLimit">경계 길이 제한</param>
        /// <returns>경계</returns>
        public static string GetBoundary(MediaTypeHeaderValue mediaTypeHeaderValue, int boundaryLengthLimit)
        {
            string boundary = HeaderUtilities.RemoveQuotes(mediaTypeHeaderValue.Boundary).Value;

            if(string.IsNullOrWhiteSpace(boundary))
            {
                throw new InvalidDataException("Missing content-type boundary.");
            }

            if(boundary.Length > boundaryLengthLimit)
            {
                throw new InvalidDataException($"Multipart boundary length limit {boundaryLengthLimit} exceeded.");
            }

            return boundary;
        }

        #endregion
        #region 멀티 파트 컨텐트 타입 여부 구하기 - IsMultipartContentType(contentType)

        /// <summary>
        /// 멀티 파트 컨텐트 타입 여부 구하기
        /// </summary>
        /// <param name="contentType">컨텐트 타입</param>
        /// <returns>멀티 파트 컨텐트 타입 여부</returns>
        public static bool IsMultipartContentType(string contentType)
        {
            return !string.IsNullOrEmpty(contentType) && contentType.IndexOf("multipart/", StringComparison.OrdinalIgnoreCase) >= 0;
        }

        #endregion
        #region 폼 데이터 컨텐트 배치 소유 여부 구하기 - HasFormDataContentDisposition(contentDispositionHeaderValue)

        /// <summary>
        /// 폼 데이터 컨텐트 배치 소유 여부 구하기
        /// </summary>
        /// <param name="contentDispositionHeaderValue">컨텐트 배치 헤더 값</param>
        /// <returns>폼 데이터 컨텐트 배치 소유 여부</returns>
        public static bool HasFormDataContentDisposition(ContentDispositionHeaderValue contentDispositionHeaderValue)
        {
            // Content-Disposition: form-data; name="key";
            return contentDispositionHeaderValue != null                              &&
                   contentDispositionHeaderValue.DispositionType.Equals("form-data")  &&
                   string.IsNullOrEmpty(contentDispositionHeaderValue.FileName.Value) &&
                   string.IsNullOrEmpty(contentDispositionHeaderValue.FileNameStar.Value);
        }

        #endregion
        #region 파일 컨텐트 배치 소유 여부 구하기 - HasFileContentDisposition(contentDispositionHeaderValue)

        /// <summary>
        /// 파일 컨텐트 배치 소유 여부 구하기
        /// </summary>
        /// <param name="contentDispositionHeaderValue"></param>
        /// <returns>파일 컨텐트 배치 소유 여부</returns>
        public static bool HasFileContentDisposition(ContentDispositionHeaderValue contentDispositionHeaderValue)
        {
            // Content-Disposition: form-data; name="file1"; filename="sample.jpg"
            return contentDispositionHeaderValue != null                             &&
                   contentDispositionHeaderValue.DispositionType.Equals("form-data") &&
                   (!string.IsNullOrEmpty(contentDispositionHeaderValue.FileName.Value) || !string.IsNullOrEmpty(contentDispositionHeaderValue.FileNameStar.Value));
        }

        #endregion
    }
}

 

300x250

 

▶ Models/FileItemModel.cs

using System;
using System.ComponentModel.DataAnnotations;

namespace TestProject.Models
{
    /// <summary>
    /// 파일 항목 모델
    /// </summary>
    public class FileItemModel
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Property
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region ID - ID

        /// <summary>
        /// ID
        /// </summary>
        public int ID { get; set; }

        #endregion
        #region 내용 - Content

        /// <summary>
        /// 내용
        /// </summary>
        public byte[] Content { get; set; }

        #endregion
        #region 파일명 - FileName

        /// <summary>
        /// 파일명
        /// </summary>
        [Display(Name = "파일명")]
        public string FileName { get; set; }

        #endregion
        #region 노트 - Note

        /// <summary>
        /// 노트
        /// </summary>
        [Display(Name = "노트")]
        public string Note { get; set; }

        #endregion
        #region 크기 - Size

        /// <summary>
        /// 크기
        /// </summary>
        [Display(Name = "크기(바이트)")]
        [DisplayFormat(DataFormatString = "{0:N0}")]
        public long Size { get; set; }

        #endregion
        #region 업로드 시간 - UploadTime

        /// <summary>
        /// 업로드 시간
        /// </summary>
        [Display(Name = "업로드 시간(UTC)")]
        [DisplayFormat(DataFormatString = "{0:G}")]
        public DateTime UploadTime { get; set; }

        #endregion
    }
}

 

▶ Data/DatabaseContext.cs

using Microsoft.EntityFrameworkCore;

using TestProject.Models;

namespace TestProject.Data
{
    /// <summary>
    /// 데이터베이스 컨텍스트
    /// </summary>
    public class DatabaseContext : DbContext
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 파일 항목 DB 세트 - FileItemDBSet

        /// <summary>
        /// 파일 항목 DB 세트
        /// </summary>
        public DbSet<FileItemModel> FileItemDBSet { get; set; }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 생성자 - DatabaseContext(option)

        /// <summary>
        /// 생성자
        /// </summary>
        /// <param name="option">옵션</param>
        public DatabaseContext(DbContextOptions<DatabaseContext> option) : base(option)
        {
        }

        #endregion
    }
}

 

▶ Filters/DisableFormValueModelBindingAttribute.cs

using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using System;
using System.Collections.Generic;

namespace TestProject.Filters
{
    /// <summary>
    /// 폼 값 모델 바인딩 비활성 어트리뷰트
    /// </summary>
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
    public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 리소스 실행전 처리하기 - OnResourceExecuting(context)

        /// <summary>
        /// 리소스 실행전 처리하기
        /// </summary>
        /// <param name="context">리소스 실행전 컨텍스트</param>
        public void OnResourceExecuting(ResourceExecutingContext context)
        {
            IList<IValueProviderFactory> list = context.ValueProviderFactories;

            list.RemoveType<FormValueProviderFactory>();
            list.RemoveType<FormFileValueProviderFactory>();
            list.RemoveType<JQueryFormValueProviderFactory>();
        }

        #endregion
        #region 리소스 실행후 처리하기 - OnResourceExecuted(context)

        /// <summary>
        /// 리소스 실행후 처리하기
        /// </summary>
        /// <param name="context">리소스 실행후 컨텍스트</param>
        public void OnResourceExecuted(ResourceExecutedContext context)
        {
        }

        #endregion
    }
}

 

▶ Filters/GenerateAntiforgeryTokenCookieAttribute.cs

using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;

namespace TestProject.Filters
{
    /// <summary>
    /// 위조 방지 토큰 쿠키 생성 어트리뷰트
    /// </summary>
    public class GenerateAntiforgeryTokenCookieAttribute : ResultFilterAttribute
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 결과 실행전 처리하기 - OnResultExecuting(context)

        /// <summary>
        /// 결과 실행전 처리하기
        /// </summary>
        /// <param name="context">결과 실행전 컨텍스트</param>
        public override void OnResultExecuting(ResultExecutingContext context)
        {
            IAntiforgery antiforgery = context.HttpContext.RequestServices.GetService<IAntiforgery>();

            // 요청 토큰을 JavaScript에서 읽을 수 있는 쿠키로 보낸다.
            AntiforgeryTokenSet tokens = antiforgery.GetAndStoreTokens(context.HttpContext);

            context.HttpContext.Response.Cookies.Append
            (
                "RequestVerificationToken",
                tokens.RequestToken,
                new CookieOptions() { HttpOnly = false }
            );
        }

        #endregion
        #region 결과 실행후 처리하기 - OnResultExecuted(context)

        /// <summary>
        /// 결과 실행후 처리하기
        /// </summary>
        /// <param name="context">결과 실행후 컨텍스트</param>
        public override void OnResultExecuted(ResultExecutedContext context)
        {
        }

        #endregion
    }
}

 

▶ Controllers/FormData.cs

namespace TestProject.Controllers
{
    /// <summary>
    /// 폼 데이터
    /// </summary>
    public class FormData
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Property
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 노트 - Note

        /// <summary>
        /// 노트
        /// </summary>
        public string Note { get; set; }

        #endregion
    }
}

 

▶ Controllers/StreamingController.cs

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
using System;
using System.Globalization;
using System.IO;
using System.Net;
using System.Text;
using System.Threading.Tasks;

using TestProject.Data;
using TestProject.Filters;
using TestProject.Models;
using TestProject.Utilities;

namespace TestProject.Controllers
{
    /// <summary>
    /// 스트리밍 컨트롤러
    /// </summary>
    public class StreamingController : Controller
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Field
        ////////////////////////////////////////////////////////////////////////////////////////// Static
        //////////////////////////////////////////////////////////////////////////////// Private

        #region Field

        /// <summary>
        /// 디폴트 폼 옵션
        /// </summary>
        /// <remarks>
        /// 요청 본문 데이터에 대한 기본 제한을 설정하는 데 사용할 수 있도록 기본 양식 옵션을 가져온다.
        /// </remarks>
        private static readonly FormOptions _defaultFormOption = new FormOptions();

        #endregion

        ////////////////////////////////////////////////////////////////////////////////////////// Instance
        //////////////////////////////////////////////////////////////////////////////// Private

        #region Field

        /// <summary>
        /// 데이터베이스 컨텍스트
        /// </summary>
        private readonly DatabaseContext databaseContext;

        /// <summary>
        /// 파일 크기 제한
        /// </summary>
        private readonly long fileSizeLimit;

        /// <summary>
        /// 로그 기록기
        /// </summary>
        private readonly ILogger<StreamingController> logger;

        /// <summary>
        /// 허용 파일 확장자 배열
        /// </summary>
        private readonly string[] allowFileExtensionArray = { ".txt", ".zip", ".jpg" };

        /// <summary>
        /// 타겟 디렉토리 경로
        /// </summary>
        private readonly string targetDirectoryPath;

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 생성자 - StreamingController(logger, databaseContext, configuration)

        /// <summary>
        /// 생성자
        /// </summary>
        /// <param name="logger">로그 기록기</param>
        /// <param name="databaseContext">데이터베이스  컨텍스트</param>
        /// <param name="configuration">구성</param>
        public StreamingController(ILogger<StreamingController> logger, DatabaseContext databaseContext, IConfiguration configuration)
        {
            this.logger              = logger;
            this.databaseContext     = databaseContext;
            this.fileSizeLimit       = configuration.GetValue<long>("FileSizeLimit");
            this.targetDirectoryPath = configuration.GetValue<string>("UploadDirectoryPath");
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        // 다음 업로드 메소드 :
        //
        // 1. 잠재적으로 큰 파일 처리를 제어하려면 양식 값 모델 바인딩을 비활성화한다.
        //
        // 2. 일반적으로 위조 방지 토큰은 요청 본문으로 전송된다.
        //    요청 본문을 일찍 읽고 싶지 않기 때문에 토큰은 헤더를 통해 전송된다.
        //    위조 방지 토큰 필터는 먼저 요청 헤더에서 토큰을 찾은 다음 본문 읽기로 돌아간다.

        #region 데이터베이스 업로드하기 - UploadDatabase()

        /// <summary>
        /// 데이터베이스 업로드하기
        /// </summary>
        /// <returns>액션 결과 태스크</returns>
        [HttpPost]
        [DisableFormValueModelBinding]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> UploadDatabase()
        {
            if(!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
            {
                ModelState.AddModelError("File", $"The request couldn't be processed (Error 1).");

                return BadRequest(ModelState);
            }

            // 요청(formAccumulator)에서 양식 데이터 키-값 쌍을 누적한다.
            KeyValueAccumulator formAccumulator              = new KeyValueAccumulator();
            string              sourceFileName               = string.Empty;
            string              targetFileName               = string.Empty;
            byte[]              streamedFileContentByteArray = new byte[0];

            string boundary = MultipartRequestHelper.GetBoundary
            (
                MediaTypeHeaderValue.Parse(Request.ContentType),
                _defaultFormOption.MultipartBoundaryLengthLimit
            );

            MultipartReader multipartReader = new MultipartReader(boundary, HttpContext.Request.Body);

            MultipartSection multipartSection = await multipartReader.ReadNextSectionAsync();

            while(multipartSection != null)
            {
                bool hasContentDispositionHeaderValue = ContentDispositionHeaderValue.TryParse
                (
                    multipartSection.ContentDisposition,
                    out var contentDispositionHeaderValue
                );

                if(hasContentDispositionHeaderValue)
                {
                    if(MultipartRequestHelper.HasFileContentDisposition(contentDispositionHeaderValue))
                    {
                        targetFileName = contentDispositionHeaderValue.FileName.Value;

                        // 클라이언트가 보낸 파일 이름을 신뢰하지 않는다.
                        // 파일 이름을 표시하려면 값을 HTML로 인코딩한다.
                        sourceFileName = WebUtility.HtmlEncode(contentDispositionHeaderValue.FileName.Value);

                        streamedFileContentByteArray = await FileHelper.ProcessStreamedFile
                        (
                            multipartSection,
                            contentDispositionHeaderValue,
                            ModelState,
                            this.allowFileExtensionArray,
                            this.fileSizeLimit
                        );

                        if(!ModelState.IsValid)
                        {
                            return BadRequest(ModelState);
                        }
                    }
                    else if(MultipartRequestHelper.HasFormDataContentDisposition(contentDispositionHeaderValue))
                    {
                        // 다중 부분 헤더 길이 제한이 이미 적용되었으므로 키 이름 길이를 제한하지 않는다.
                        string   key      = HeaderUtilities.RemoveQuotes(contentDispositionHeaderValue.Name).Value;
                        Encoding encoding = GetEncoding(multipartSection);

                        if(encoding == null)
                        {
                            ModelState.AddModelError("File", $"The request couldn't be processed (Error 2).");

                            return BadRequest(ModelState);
                        }

                        using
                        (
                            StreamReader streamReader = new StreamReader
                            (
                                multipartSection.Body,
                                encoding,
                                detectEncodingFromByteOrderMarks : true,
                                bufferSize : 1024,
                                leaveOpen : true
                            )
                        )
                        {
                            // 값 길이 제한은 MultipartBodyLengthLimit에 의해 적용된다.
                            string value = await streamReader.ReadToEndAsync();

                            if(string.Equals(value, "undefined", StringComparison.OrdinalIgnoreCase))
                            {
                                value = string.Empty;
                            }

                            formAccumulator.Append(key, value);

                            if(formAccumulator.ValueCount > _defaultFormOption.ValueCountLimit)
                            {
                                // _defaultFormOption.ValueCountLimit의 폼 키 개수 제한이 초과되었습니다.
                                ModelState.AddModelError("File", $"The request couldn't be processed (Error 3).");

                                return BadRequest(ModelState);
                            }
                        }
                    }
                }

                // 소비되지 않은 나머지 섹션 본문을 비우고 다음 섹션의 헤더를 읽는다.
                multipartSection = await multipartReader.ReadNextSectionAsync();
            }

            // 폼 데이터를 모델에 바인딩 한다.
            FormData formData = new FormData();

            FormValueProvider formValueProvider = new FormValueProvider
            (
                BindingSource.Form,
                new FormCollection(formAccumulator.GetResults()),
                CultureInfo.CurrentCulture
            );

            bool isBindingSuccessful = await TryUpdateModelAsync(formData, prefix : "", valueProvider : formValueProvider);

            if(!isBindingSuccessful)
            {
                ModelState.AddModelError("File", "The request couldn't be processed (Error 5).");

                return BadRequest(ModelState);
            }

            // 경고!
            // 다음 예제에서는 파일 내용을 스캔하지 않고 파일이 저장된다.
            // 대부분의 프로덕션 시나리오에서 파일을 다운로드하거나 다른 시스템에서 사용할 수 있도록하기 전에
            // 바이러스 백신/멀웨어 방지 스캐너 API가 파일에 사용된다.

            FileItemModel fileItem = new FileItemModel()
            {
                Content    = streamedFileContentByteArray,
                FileName   = targetFileName,
                Note       = formData.Note,
                Size       = streamedFileContentByteArray.Length, 
                UploadTime = DateTime.UtcNow
            };

            this.databaseContext.FileItemDBSet.Add(fileItem);

            await this.databaseContext.SaveChangesAsync();

            return Created(nameof(StreamingController), null);
        }

        #endregion
        #region 물리적 저장소 업로드하기 - UploadPhysical()

        /// <summary>
        /// 물리적 저장소 업로드하기
        /// </summary>
        /// <returns>액션 결과 태스크</returns>
        [HttpPost]
        [DisableFormValueModelBinding]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> UploadPhysical()
        {
            if(!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
            {
                ModelState.AddModelError("File", $"The request couldn't be processed (Error 1).");

                return BadRequest(ModelState);
            }

            string boundary = MultipartRequestHelper.GetBoundary
            (
                MediaTypeHeaderValue.Parse(Request.ContentType),
                _defaultFormOption.MultipartBoundaryLengthLimit
            );

            MultipartReader multipartReader = new MultipartReader(boundary, HttpContext.Request.Body);

            MultipartSection multipartSection = await multipartReader.ReadNextSectionAsync();

            while(multipartSection != null)
            {
                bool hasContentDispositionHeaderValue = ContentDispositionHeaderValue.TryParse
                (
                    multipartSection.ContentDisposition,
                    out var contentDispositionHeaderValue
                );

                if(hasContentDispositionHeaderValue)
                {
                    if(!MultipartRequestHelper.HasFileContentDisposition(contentDispositionHeaderValue))
                    {
                        ModelState.AddModelError("File", $"The request couldn't be processed (Error 2).");

                        return BadRequest(ModelState);
                    }
                    else
                    {
                        string sourceFileName = WebUtility.HtmlEncode(contentDispositionHeaderValue.FileName.Value);
                        string targetFileName = sourceFileName;

                        // 경고!
                        // 다음 예제에서는 파일 내용을 스캔하지 않고 파일이 저장된다.
                        // 대부분의 프로덕션 시나리오에서 파일을 다운로드하거나 다른 시스템에서 사용할 수 있도록 하기 전에
                        // 바이러스 백신/멀웨어 방지 스캐너 API가 파일에 사용된다.

                        byte[] streamedFileContentByteArray = await FileHelper.ProcessStreamedFile
                        (
                            multipartSection,
                            contentDispositionHeaderValue,
                            ModelState, 
                            this.allowFileExtensionArray,
                            this.fileSizeLimit
                        );

                        if(!ModelState.IsValid)
                        {
                            return BadRequest(ModelState);
                        }

                        using(FileStream targetStream = System.IO.File.Create(Path.Combine(this.targetDirectoryPath, targetFileName)))
                        {
                            await targetStream.WriteAsync(streamedFileContentByteArray);

                            this.logger.LogInformation($"Uploaded file '{sourceFileName}' saved to '{this.targetDirectoryPath}' as {targetFileName}"); 
                        }
                    }
                }

                // 소비되지 않은 나머지 섹션 본문을 비우고 다음 섹션의 헤더를 읽는다.
                multipartSection = await multipartReader.ReadNextSectionAsync();
            }

            return Created(nameof(StreamingController), null);
        }

        #endregion

        ////////////////////////////////////////////////////////////////////////////////////////// Private

        #region 인코딩 구하기 - GetEncoding(multipartSection)

        /// <summary>
        /// 인코딩 구하기
        /// </summary>
        /// <param name="multipartSection">멀티 파트 섹션</param>
        /// <returns>인코딩</returns>
        private static Encoding GetEncoding(MultipartSection multipartSection)
        {
            var hasMediaTypeHeaderValue = MediaTypeHeaderValue.TryParse(multipartSection.ContentType, out var mediaTypeHeaderValue);

            // UTF-7은 안전하지 않으며 사용해서는 안된다.
            // UTF-8은 대부분의 경우 성공한다.
            if(!hasMediaTypeHeaderValue || Encoding.UTF7.Equals(mediaTypeHeaderValue.Encoding))
            {
                return Encoding.UTF8;
            }

            return mediaTypeHeaderValue.Encoding;
        }

        #endregion
    }
}

 

▶ Pages/Index.cshtml

@page
@model IndexModel
@{
    ViewData["Title"] = "대용량 파일 업로드하기";
}
<h1>데이터베이스 파일</h1>
@if(Model.FileItemList.Count == 0)
{
    <p>
        사용 가능한 파일이 없습니다.
        하나 이상의 파일을 업로드하려면 파일 업로드 시나리오 페이지 중 하나를 방문하십시오.
    </p>
}
else
{
    <table>
        <thead>
            <tr>
                <th></th>
                <th>
                    @Html.DisplayNameFor(model => model.FileItemList[0].FileName) /
                    @Html.DisplayNameFor(model => model.FileItemList[0].Note)
                </th>
                <th>@Html.DisplayNameFor(model => model.FileItemList[0].UploadTime)</th>
                <th>@Html.DisplayNameFor(model => model.FileItemList[0].Size)</th>
                <th><code>FileStreamResult</code> 사용</th>
            </tr>
        </thead>
        <tbody>
            @foreach(var file in Model.FileItemList)
            {
                <tr>
                    <td><a asp-page="./DeleteFileFromDatabase" asp-route-id="@file.ID">Delete</a></td>
                    <td><b>@file.FileName</b><br>@Html.DisplayFor(modelItem => file.Note)</td>
                    <td class="text-center">@Html.DisplayFor(modelItem => file.UploadTime)</td>
                    <td class="text-center">@Html.DisplayFor(modelItem => file.Size)</td>
                    <td class="text-center">
                        <a asp-page-handler="DownloadFromDatabase" asp-route-id="@file.ID">Download</a>
                    </td>
                </tr>
            }
        </tbody>
    </table>
}
<h1>물리적 저장소 파일</h1>
@if(Model.DirectoryContents.Count() == 0)
{
    <p>
        사용 가능한 파일이 없습니다.
        하나 이상의 파일을 업로드하려면 파일 업로드 시나리오 페이지 중 하나를 방문하십시오.
    </p>
}
else
{
    <table>
        <thead>
            <tr>
                <th></th>
                <th>파일명 / 경로</th>
                <th>크기 (단위 : 바이트)</th>
                <th><code>PhysicalFileResult</code> 사용</th>
            </tr>
        </thead>
        <tbody>
            @foreach(var file in Model.DirectoryContents)
            {
                <tr>
                    <td><a asp-page="./DeleteFileFromPhysicalStorage" asp-route-fileName="@file.Name">Delete</a></td>
                    <td><b>@file.Name</b><br>@file.PhysicalPath</td>
                    <td class="text-center">@file.Length.ToString("N0")</td>
                    <td class="text-center">
                        <a asp-page-handler="DownloadFromPhysicalStorage" asp-route-fileName="@file.Name">Download</a>
                    </td>
                </tr>
            }
        </tbody>
    </table>
}

 

▶ Pages/Index.cshtml.cs

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.FileProviders;
using System.Collections.Generic;
using System.Net;
using System.Net.Mime;
using System.Threading.Tasks;

using TestProject.Data;
using TestProject.Models;

namespace TestProject.Pages
{
    /// <summary>
    /// 인덱스 모델
    /// </summary>
    public class IndexModel : PageModel
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Field
        ////////////////////////////////////////////////////////////////////////////////////////// Private

        #region Field

        /// <summary>
        /// DB 컨텍스트
        /// </summary>
        private readonly DatabaseContext dbContext;

        /// <summary>
        /// 파일 제공자
        /// </summary>
        private readonly IFileProvider fileProvider;

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Property
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 파일 항목 리스트 - FileItemList

        /// <summary>
        /// 파일 항목 리스트
        /// </summary>
        public IList<FileItemModel> FileItemList { get; private set; }

        #endregion
        #region 디렉토리 컨텐츠 - DirectoryContents

        /// <summary>
        /// 디렉토리 컨텐츠
        /// </summary>
        public IDirectoryContents DirectoryContents { get; private set; }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 생성자 - IndexModel(dbContext, fileProvider)

        /// <summary>
        /// 생성자
        /// </summary>
        /// <param name="dbContext">DB 컨텍스트</param>
        /// <param name="fileProvider">파일 공급자</param>
        public IndexModel(DatabaseContext dbContext, IFileProvider fileProvider)
        {
            this.dbContext    = dbContext;
            this.fileProvider = fileProvider;
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region GET 요청시 처리하기 (비동기) - OnGetAsync()

        /// <summary>
        /// GET 요청시 처리하기 (비동기)
        /// </summary>
        /// <returns>태스크</returns>
        public async Task OnGetAsync()
        {
            FileItemList      = await this.dbContext.FileItemDBSet.AsNoTracking().ToListAsync();
            DirectoryContents = this.fileProvider.GetDirectoryContents(string.Empty);
        }

        #endregion
        #region 데이터베이스에서 다운로드 GET 요청시 처리하기 (비동기) - OnGetDownloadFromDatabaseAsync(id)

        /// <summary>
        /// 데이터베이스에서 다운로드 GET 요청시 처리하기 (비동기)
        /// </summary>
        /// <param name="id">ID</param>
        /// <returns>액션 결과 태스크</returns>
        public async Task<IActionResult> OnGetDownloadFromDatabaseAsync(int? id)
        {
            if(id == null)
            {
                return Page();
            }

            FileItemModel fileItem = await this.dbContext.FileItemDBSet.SingleOrDefaultAsync(m => m.ID == id);

            if(fileItem == null)
            {
                return Page();
            }

            // UI에 신뢰할 수 없는 파일 이름을 표시하지 마십시오.
            // 값을 HTML로 인코딩합니다.
            return File(fileItem.Content, MediaTypeNames.Application.Octet, WebUtility.HtmlEncode(fileItem.FileName));
        }

        #endregion
        #region 물리적 저장소에서 다운로드 GET 처리하기 - OnGetDownloadFromPhysicalStorage(fileName)

        /// <summary>
        /// 물리적 저장소에서 다운로드 GET 처리하기
        /// </summary>
        /// <param name="fileName">파일명</param>
        /// <returns>액션 결과</returns>
        public IActionResult OnGetDownloadFromPhysicalStorage(string fileName)
        {
            IFileInfo fileInfo = this.fileProvider.GetFileInfo(fileName);

            return PhysicalFile(fileInfo.PhysicalPath, MediaTypeNames.Application.Octet, fileName);
        }

        #endregion
    }
}

 

▶ Pages/UploadBufferMultipleFileToPhysicalStorage.cshtml

@page
@model UploadBufferMultipleFileToPhysicalStorageModel
@{
    ViewData["Title"] = "Buffered Multiple File Upload (Physical)";
}
<h1>물리적 저장소에 버퍼 방식 다수 파일 업로드하기</h1>
<form enctype="multipart/form-data" method="post">
    <dl>
        <dt><label asp-for="FileUpload.FormFileList"></label></dt>
        <dd>
            <input type="file" asp-for="FileUpload.FormFileList"
                multiple />
            <span asp-validation-for="FileUpload.FormFileList"></span>
        </dd>
    </dl>
    <input type="submit" asp-page-handler="Upload"
        class="btn"
        value="제출" />
</form>
<p class="result">
    @Model.Result
</p>
@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

 

▶ Pages/UploadBufferMultipleFileToPhysicalStorage.cshtml.cs

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Configuration;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Threading.Tasks;

using TestProject.Utilities;

namespace TestProject.Pages
{
    /// <summary>
    /// 물리적 저장소에 버퍼 방식 다수 파일 업로드 모델
    /// </summary>
    public class UploadBufferMultipleFileToPhysicalStorageModel : PageModel
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Field
        ////////////////////////////////////////////////////////////////////////////////////////// Private

        #region Field

        /// <summary>
        /// 타겟 디렉토리 경로
        /// </summary>
        private readonly string targetDirectoryPath;

        /// <summary>
        /// 파일 크기 제한
        /// </summary>
        private readonly long fileSizeLimit;

        /// <summary>
        /// 허용 파일 확장자 배열
        /// </summary>
        private readonly string[] allowFileExtensionArray = { ".txt" };

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Property
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 파일 업로드 - FileUpload

        /// <summary>
        /// 파일 업로드
        /// </summary>
        [BindProperty]
        public UploadBufferMultipleFileToPhysicalStorageModelFileUploadInfo FileUpload { get; set; }

        #endregion
        #region 결과 - Result

        /// <summary>
        /// 결과
        /// </summary>
        public string Result { get; private set; }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 생성자 - UploadBufferMultipleFileToPhysicalStorageModel(configuration)

        /// <summary>
        /// 생성자
        /// </summary>
        /// <param name="configuration">구성</param>
        public UploadBufferMultipleFileToPhysicalStorageModel(IConfiguration configuration)
        {
            this.targetDirectoryPath = configuration.GetValue<string>("UploadDirectoryPath");

            this.fileSizeLimit = configuration.GetValue<long>("FileSizeLimit");
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region GET 요청시 처리하기 - OnGet()

        /// <summary>
        /// GET 요청시 처리하기
        /// </summary>
        public void OnGet()
        {
        }

        #endregion
        #region 업로드 POST 요청시 처리하기 (비동기) - OnPostUploadAsync()

        /// <summary>
        /// 업로드 POST 요청시 처리하기 (비동기)
        /// </summary>
        /// <returns>액션 결과 태스크</returns>
        public async Task<IActionResult> OnPostUploadAsync()
        {
            // FileUpload 클래스 속성 위반을 포착하려면 초기 검사를 수행한다.
            if(!ModelState.IsValid)
            {
                Result = "Please correct the form.";

                return Page();
            }

            foreach(IFormFile formFile in FileUpload.FormFileList)
            {
                byte[] formFileContentByteArray = await FileHelper.ProcessFormFile<UploadBufferMultipleFileToPhysicalStorageModelFileUploadInfo>
                (
                    formFile,
                    ModelState,
                    this.allowFileExtensionArray, 
                    this.fileSizeLimit
                );

                // 두 번째 검사를 수행하여 ProcessFormFile 메서드 위반을 포착한다.
                // 유효성 검사가 실패하면 페이지로 돌아간다.
                if(!ModelState.IsValid)
                {
                    Result = "Please correct the form.";

                    return Page();
                }

                var sourceFileName = formFile.FileName;
                var targetFilePath = Path.Combine(this.targetDirectoryPath, sourceFileName);

                // 경고!
                // 다음 예제에서는 파일 내용을 스캔하지 않고 파일이 저장된다.
                // 대부분의 프로덕션 시나리오에서 파일을 다운로드하거나 다른 시스템에서 사용할 수 있도록하기 전에
                // 바이러스 백신/멀웨어 방지 스캐너 API가 파일에 사용된다.

                using(FileStream fileStream = System.IO.File.Create(targetFilePath))
                {
                    await fileStream.WriteAsync(formFileContentByteArray);

                    // FormFile로 직접 작업하려면 대신 다음을 사용한다 :
                    //await formFile.CopyToAsync(fileStream);
                }
            }

            return RedirectToPage("./Index");
        }

        #endregion
    }

    /// <summary>
    /// 물리적 저장소에 버퍼 방식 다수 파일 업로드 모델 파일 업로드 정보
    /// </summary>
    public class UploadBufferMultipleFileToPhysicalStorageModelFileUploadInfo
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Property
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 폼 파일 리스트 - FormFileList

        /// <summary>
        /// 폼 파일 리스트
        /// </summary>
        [Required]
        [Display(Name="파일")]
        public List<IFormFile> FormFileList { get; set; }

        #endregion
        #region 노트 - Note

        /// <summary>
        /// 노트
        /// </summary>
        [Display(Name="노트")]
        [StringLength(50, MinimumLength = 0)]
        public string Note { get; set; }

        #endregion
    }
}

 

▶ Pages/UploadStreamOneFileToPhysicalStorage.cshtml

@page
@model UploadStreamOneFileToPhysicalStorageModel
@{
    ViewData["Title"] = "Streamed Single File Upload with AJAX (Physical)";
}
<h1>물리적 저장소에 스트림 방식 1개 파일 업로드하기</h1>
<form id="uploadForm" action="Streaming/UploadPhysical"
    method="post" 
    enctype="multipart/form-data"
    onsubmit="AJAXSubmit(this);return false;">
    <dl>
        <dt><label for="file">파일</label></dt>
        <dd><input type="file" id="file" name="file" /></dd>
    </dl>
    <input type="submit"
        class="btn"
        value="제출" />
    <div style="margin-top:15px">
        <output name="result" form="uploadForm"></output>
    </div>
</form>
@section Scripts {
    <script>
        "use strict";

        async function AJAXSubmit(oFormElement)
        {
            const formData = new FormData(oFormElement);

            try
            {
                const response = await fetch
                (
                    oFormElement.action,
                    {
                        method  : 'POST',
                        headers : { 'RequestVerificationToken' : getCookie('RequestVerificationToken') },
                        body    : formData
                    }
                );

                oFormElement.elements.namedItem("result").value = '결과 : ' + response.status + ' ' + response.statusText;
            }
            catch(error)
            {
                console.error('에러 :', error);
            }
        }

        function getCookie(name)
        {
            var value = "; " + document.cookie;
            var parts = value.split("; " + name + "=");

            if(parts.length == 2)
            {
                return parts.pop().split(";").shift();
            }
        }
    </script>
}

 

▶ Pages/UploadStreamOneFileToPhysicalStorage.cshtml.cs

using Microsoft.AspNetCore.Mvc.RazorPages;

namespace TestProject.Pages
{
    /// <summary>
    /// 물리적 저상소에 스트림 방식 1개 파일 업로드 모델
    /// </summary>
    public class UploadStreamOneFileToPhysicalStorageModel : PageModel
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region GET 요청시 처리하기 - OnGet()

        /// <summary>
        /// GET 요청시 처리하기
        /// </summary>
        public void OnGet()
        {
        }

        #endregion
    }
}

 

▶ appsettings.json

{
    "Logging" :
    {
        "LogLevel" :
        {
            "Default"                    : "Information",
            "Microsoft"                  : "Warning",
            "Microsoft.Hosting.Lifetime" : "Information"
        }
    },
    "AllowedHosts"        : "*",
    "UploadDirectoryPath" : "D:\\UPLOAD",
    "FileSizeLimit"       : 1073741824
}

 

▶ web.config

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <system.webServer>
        <security>
            <requestFiltering>
                <requestLimits maxAllowedContentLength="1073741824" />
            </requestFiltering>
        </security>
    </system.webServer>
</configuration>

 

▶ Startup.cs

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;

using TestProject.Data;
using TestProject.Filters;

namespace TestProject
{
    /// <summary>
    /// 시작
    /// </summary>
    public class Startup
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Property
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 구성 - Configuration

        /// <summary>
        /// 구성
        /// </summary>
        public IConfiguration Configuration { get; }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Constructor
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 생성자 - Startup(configuration)

        /// <summary>
        /// 생성자
        /// </summary>
        /// <param name="configuration">구성</param>
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        #endregion

        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Public

        #region 서비스 컬렉션 구성하기 - ConfigureServices(services)

        /// <summary>
        /// 서비스 컬렉션 구성하기
        /// </summary>
        /// <param name="services">서비스 컬렉션</param>
        public void ConfigureServices(IServiceCollection services)
        {
            long fileSizeLimit = Configuration.GetValue<long>("FileSizeLimit");

            services.AddControllers();

            services.AddRazorPages
            (
                options =>
                {
                    options.Conventions.AddPageApplicationModelConvention
                    (
                        "/UploadStreamOneFileToDatabase",
                        model =>
                        {
                            model.Filters.Add(new GenerateAntiforgeryTokenCookieAttribute());
                            model.Filters.Add(new DisableFormValueModelBindingAttribute());
                        }
                    );

                    options.Conventions.AddPageApplicationModelConvention
                    (
                        "/UploadStreamOneFileToPhysicalStorage",
                        model =>
                        {
                            model.Filters.Add(new GenerateAntiforgeryTokenCookieAttribute());
                            model.Filters.Add(new DisableFormValueModelBindingAttribute());
                        }
                    );
                }
            );

            // Kestrel 사용시
            services.Configure<FormOptions>(options => { options.MultipartBodyLengthLimit = fileSizeLimit; });

            // IIS Express 사용시
            //services.Configure<IISServerOptions>(options => { options.MaxRequestBodySize = fileSizeLimit; });

            PhysicalFileProvider physicalFileProvider = new PhysicalFileProvider(Configuration.GetValue<string>("UploadDirectoryPath"));

            services.AddSingleton<IFileProvider>(physicalFileProvider);

            services.AddDbContext<DatabaseContext>(options => options.UseInMemoryDatabase("InMemoryDb"));
        }

        #endregion
        #region 구성하기 - Configure(app, environment)

        /// <summary>
        /// 구성하기
        /// </summary>
        /// <param name="app">애플리케이션 빌더</param>
        /// <param name="environment">웹 호스트 환경</param>
        public void Configure(IApplicationBuilder app, IWebHostEnvironment environment)
        {
            if(environment.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
            }

            app.UseStaticFiles();

            app.UseRouting();

            app.UseEndpoints
            (
                endpoints =>
                {
                    endpoints.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");

                    endpoints.MapRazorPages();
                }
            );
        }

        #endregion
    }
}

 

▶ Program.cs

using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;

namespace TestProject
{
    /// <summary>
    /// 프로그램
    /// </summary>
    public class Program
    {
        //////////////////////////////////////////////////////////////////////////////////////////////////// Method
        ////////////////////////////////////////////////////////////////////////////////////////// Static
        //////////////////////////////////////////////////////////////////////////////// Private

        #region 프로그램 시작하기 - Main(argumentArray)

        /// <summary>
        /// 프로그램 시작하기
        /// </summary>
        /// <param name="argumentArray">인자 배열</param>
        public static void Main(string[] argumentArray)
        {
            CreateHostBuilder(argumentArray).Build().Run();
        }

        #endregion
        #region 호스트 빌더 생성하기 - CreateHostBuilder(argumentArray)

        /// <summary>
        /// 호스트 빌더 생성하기
        /// </summary>
        /// <param name="argumentArray">인자 배열</param>
        /// <returns>호스트 빌더</returns>
        public static IHostBuilder CreateHostBuilder(string[] argumentArray) =>
            Host.CreateDefaultBuilder(argumentArray)
                .ConfigureWebHostDefaults
                (
                    builder =>
                    {
                        // Kestrel 사용시
                        builder.ConfigureKestrel
                        (
                            (context, options) =>
                            {
                                options.Limits.MaxRequestBodySize = 1073741824L;
                            }
                        )
                        .UseStartup<Startup>();

                        // IIS Express 사용시
                        //builder.UseStartup<Startup>();
                    }
                );

        #endregion
    }
}
728x90
반응형
그리드형
Posted by 사용자 icodebroker

댓글을 달아 주세요