■ 대용량 파일을 업로드하는 방법을 보여준다.
▶ Utilities/FileHelper.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 |
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 } } |
▶ Utilities/MultipartRequestHelper.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
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 } } |
▶ Models/FileItemModel.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
namespace TestProject.Controllers { /// <summary> /// 폼 데이터 /// </summary> public class FormData { //////////////////////////////////////////////////////////////////////////////////////////////////// Property ////////////////////////////////////////////////////////////////////////////////////////// Public #region 노트 - Note /// <summary> /// 노트 /// </summary> public string Note { get; set; } #endregion } } |
▶ Controllers/StreamingController.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
@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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
@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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
@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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
{ "Logging" : { "LogLevel" : { "Default" : "Information", "Microsoft" : "Warning", "Microsoft.Hosting.Lifetime" : "Information" } }, "AllowedHosts" : "*", "UploadDirectoryPath" : "D:\\UPLOAD", "FileSizeLimit" : 1073741824 } |
▶ web.config
1 2 3 4 5 6 7 8 9 10 11 12 |
<?xml version="1.0" encoding="utf-8"?> <configuration> <system.webServer> <security> <requestFiltering> <requestLimits maxAllowedContentLength="1073741824" /> </requestFiltering> </security> </system.webServer> </configuration> |
▶ Startup.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
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 } } |