2020-11-19 19:55:03
View
Create a HTML form.
<form id="uploadForm" action="UploadFile " method="post" enctype="multipart/form-data" onsubmit="DataFile_Submit(this, 'file was not saved.', 'errorId', 'result', '/document/image ');return false;"> </form>
Within the form add an upload button, inside the form:
<input type="file" accept="image/*" name="file" id="file" style="display: none;"> <label for="file" style="margin-left: 0px; margin-top: 0px;" class="btn btn-secondary">Upload...</label>
In this instance accept
attribute will allow all image type. I can also be set a particular extension. For example .script
so that the open dialogue will filter to only *.script
file types.
This markup will cause the a file Open... dialogue to appear when the button is clicked.
Add The Following Elements
<div> <div id="errorId" style="display:none"></div> @if (!ViewContext.ModelState.IsValid) { @Html.ValidationSummary(false, "", new { @class = "text-danger" }) } <p id="result" class="text-danger" style="display:none"></p> </div> <div></div> <div> <input class="btn btn-secondary" type="submit" value="Save" /> <a class="btn btn-secondary" href="/Image">Cancel</a> <div style="margin-top:15px"> <output form="uploadForm"name="result" ></output> </div> </div>
JavaScript
"use strict"; async function DataFile_Submit( formElement, messageFileNotFound, elementErrorId, elementResult, returnURL) { const logPrefix = 'DataFile_Submit: '; // Guard Clause if(!formElement) { console.log(`${logPrefix}formElement parameter missing`); return; } if (!messageFileNotFound) messageFileNotFound = 'file was not saved.'; if (!elementErrorId) elementErrorId = 'errorId'; if (!elementResult) elementResult = 'result'; if (!returnURL) returnURL = '/image'; const formData = new FormData(formElement); const requestVerificationToken = GetCookie('RequestVerificationToken'); try { const response = await fetch(formElement.action, { method: 'POST', headers: { 'RequestVerificationToken': requestVerificationToken }, body: formData }) .then((response) => { return response.json() }) .then((json) => { if (json === undefined) { let result = document.getElementById(elementResult); result.style.display = 'block'; result.innerText = messageFileNotFound; return; } else { if (json.error !== undefined) { let elementError = '<ul class="text-danger list_Error">'; let length = json.error.length; for (var i = 0; i < length; i++) { elementError += '<li>' + json.error[i] + '</li>'; } elementError += '</ul>' let errorId = document.getElementById(elementErrorId); if (errorId !== null) { errorId.style.display = 'block'; errorId.innerHTML = elementError } return; } } window.location.href = returnURL; }); formElement.elements .namedItem(elementResult) .value = 'Result: ' + response.status + ' ' + response.statusText; } catch (error) { console.error('Error:', error); } }
C# File Upload Helper Class
This helper class will be used by the controller method in the next section below.
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; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Net.Http.Headers; namespace delaney.Utilities { public static class FileHelpers {// If you require a check on specific characters in the IsValidFileExtensionAndSignature // method, supply the characters in the _allowedChars field. private static readonly byte[] _allowedChars = { }; // For more file signatures, see the File Signatures Database (https://www.filesignatures.net/) // and the official specifications for the file types you wish to add. private static readonly Dictionary<string, List<byte[]>> _fileSignature = 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 }, } }, };// **WARNING!** // In the following file processing methods, the file's content isn't scanned. // In most production scenarios, an anti-virus/anti-malware scanner API is // used on the file before making the file available to users or other // systems. For more information, see the topic that accompanies this sample // app. public static async Task<byte[]> ProcessFormFile<T>(IFormFile formFile, ModelStateDictionary modelState, string[] permittedExtensions, long sizeLimit) { var fieldDisplayName = string.Empty;// Use reflection to obtain the display name for the model // property associated with this IFormFile. If a display // name isn't found, error messages simply won't show // a display name. MemberInfo property = typeof(T).GetProperty( formFile.Name.Substring(formFile.Name.IndexOf(".", StringComparison.Ordinal) + 1)); if (property != null) { if (property.GetCustomAttribute(typeof(DisplayAttribute)) is DisplayAttribute displayAttribute) { fieldDisplayName = $"{displayAttribute.Name} "; } }// Don't trust the file name sent by the client. To display // the file name, HTML-encode the value. var trustedFileNameForDisplay = WebUtility.HtmlEncode( formFile.FileName);// Check the file length. This check doesn't catch files that only have // a BOM as their content. if (formFile.Length == 0) { modelState.AddModelError(formFile.Name, $"{fieldDisplayName}({trustedFileNameForDisplay}) is empty."); return new byte[0]; } if (formFile.Length > sizeLimit) { var megabyteSizeLimit = sizeLimit / 1048576; modelState.AddModelError(formFile.Name, $"{fieldDisplayName}({trustedFileNameForDisplay}) exceeds " + $"{megabyteSizeLimit:N1} MB."); return new byte[0]; } try { using (var memoryStream = new MemoryStream()) { await formFile.CopyToAsync(memoryStream);// Check the content length in case the file's only // content was a BOM and the content is actually // empty after removing the BOM. if (memoryStream.Length == 0) { modelState.AddModelError(formFile.Name, $"{fieldDisplayName}({trustedFileNameForDisplay}) is empty."); } if (!IsValidFileExtensionAndSignature( formFile.FileName, memoryStream, permittedExtensions)) { modelState.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 ex) { modelState.AddModelError(formFile.Name, $"{fieldDisplayName}({trustedFileNameForDisplay}) upload failed. " + $"Please contact the Help Desk for support. Error: {ex.HResult}"); // Log the exception } return new byte[0]; } public static async Task<byte[]> ProcessStreamedFile(MultipartSection section, ContentDispositionHeaderValue contentDisposition, ModelStateDictionary modelState, string[] permittedExtensions, long sizeLimit) { try { using (var memoryStream = new MemoryStream()) { await section.Body.CopyToAsync(memoryStream);// Check if the file is empty or exceeds the size limit. if (memoryStream.Length == 0) { modelState.AddModelError("File", "The file is empty."); } else if (memoryStream.Length > sizeLimit) { var megabyteSizeLimit = sizeLimit / 1048576; modelState.AddModelError("File", $"The file exceeds {megabyteSizeLimit:N1} MB."); } else if (!IsValidFileExtensionAndSignature( contentDisposition.FileName.Value, memoryStream, permittedExtensions)) { modelState.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 ex) { modelState.AddModelError("File", $"The upload failed. Error: {ex.HResult}");// Log the exception } return new byte[0]; } private static bool IsValidFileExtensionAndSignature(string fileName, Stream data, string[] permittedExtensions) { if (string.IsNullOrEmpty(fileName) || data == null || data.Length == 0) return false; var ext = Path.GetExtension(fileName).ToLowerInvariant(); if (string.IsNullOrEmpty(ext) || !permittedExtensions.Contains(ext)) return false; data.Position = 0; using (var reader = new BinaryReader(data)) { if (ext.Equals(".txt") || ext.Equals(".csv") || ext.Equals(".prn") || ext.Equals(".svg")) { if (_allowedChars.Length == 0) {// Limits characters to ASCII encoding. for (var i = 0; i < data.Length; i++) if (reader.ReadByte() > byte.MaxValue) return false; } else {// Limits characters to ASCII encoding and // values of the _allowedChars array. for (var i = 0; i < data.Length; i++) { var b = reader.ReadByte(); if (b > sbyte.MaxValue || !_allowedChars.Contains(b)) return false; } } return true; }// Uncomment the following code block if you must permit // files whose signature isn't provided in the _fileSignature // dictionary. We recommend that you add file signatures // for files (when possible) for all file types you intend // to allow on the system and perform the file signature // check. /* if (!_fileSignature.ContainsKey(ext)) { return true; } */ // File signature check // -------------------- // With the file signatures provided in the _fileSignature // dictionary, the following code tests the input content's // file signature. if (ext == ".scriptbot" || ext == ".vsdx" || ext == ".vstx") ext = ".zip"; var signatures = _fileSignature[ext]; var headerBytes = reader.ReadBytes(signatures.Max(m => m.Length)); return signatures.Any(signature => headerBytes.Take(signature.Length).SequenceEqual(signature)); } } } }
C# Image Class
class Image { public string Id { get; set: } public string FilenameBackend { set; get; } public string Filename { set; get; } public bool IsNew { get { return string.IsNullOrWhitespace(Id); } } }
C# Controller [HttpPost]
Method
[HttpPost("/document/image/UploadFile")] [Authorize(Policy = "Can Edit Image")] public async Task<IActionResult> UploadFile() { try { var userId = GetUserId();// Guard clause if (userId == null) return BadRequest("UserId is missing.");// Guard clause if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType)) return BadRequest("The request could not be processed.");// Get the default form options so that we can use them to set the default // limits for request body data. var defaultFormOptions = new Microsoft.AspNetCore.Http.Features.FormOptions(); var boundary = MultipartRequestHelper.GetBoundary( Microsoft.Net.Http.Headers.MediaTypeHeaderValue.Parse(Request.ContentType), defaultFormOptions.MultipartBoundaryLengthLimit); var reader = new MultipartReader(boundary, HttpContext.Request.Body); var section = await reader.ReadNextSectionAsync(); var model = new Models.Image(); string filename = ""; string filenameOld = ""; string filenameBackend = ""; string filenameBackendOld = ""; while (section != null) { var hasContentDispositionHeader = Microsoft.Net .Http .Headers .ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out var contentDisposition); if (hasContentDispositionHeader) { if (contentDisposition.Name.Value.ToLower() == "file") {// Is the filename present? if (string.IsNullOrEmpty(contentDisposition.FileName.Value)) { section = await reader.ReadNextSectionAsync(); continue; }// This check assumes that there's a file // present without form data. If form data // is present, this method immediately fails // and returns the model error. if (!MultipartRequestHelper.HasFileContentDisposition(contentDisposition)) return BadRequest("The request could not be processed.");// Don't trust the file name sent by the client. To display // the file name, HTML-encode the value. //model.Filename = WebUtility.HtmlEncode(contentDisposition.FileName.Value); filename = contentDisposition.FileName.Value; filenameBackend = Path.GetRandomFileName(); var setting = new Setting(_unitOfWork);// **WARNING!** // The file is saved without scanning the file's contents. // An anti-virus/anti-malware scanner API should be // used on the file before making the file available // for download or for use by other systems. var streamedFileContent = await FileHelpers.ProcessStreamedFile(section, contentDisposition, ModelState, _permittedExtensions, setting.ImageFileSize); if (!ModelState.IsValid) return BadRequest(ModelState); var path = Services.Data.Core.Domain.Image.Path; if (!System.IO.Directory.Exists(path)) System.IO.Directory.CreateDirectory(path); using (var targetStream = System.IO .File .Create(Path.Combine(path, filenameBackend))) { await targetStream.WriteAsync(streamedFileContent); } } else { var form = section.AsFormDataSection(); var s = await form.GetValueAsync(); if (contentDisposition.Name.Value.ToLower() == "id") model.Id = s; if (contentDisposition.Name.Value.ToLower() == "filenameold") filenameOld = s; if (contentDisposition.Name.Value.ToLower() == "filenamebackend") filenameBackendOld = s; } } section = await reader.ReadNextSectionAsync(); } if (string.IsNullOrEmpty(filenameBackend) && string.IsNullOrEmpty(filenameBackendOld)) { string message = "The filename is missing."; _unitOfWork.Log(message, Services.Data.Severity.Warning); return BadRequest("ImageController.UploadPhysical() - HttpPost", message); } if (model.IsNew) { var core = _unitOfWork.Images.SingleOrDefault(x => x.Filename == filename); if(core != null) { Services.Data.Core.Domain.Image.DeleteImage(filenameBackend); ModelState.AddModelError("Error", "The image filename is already used with another image record."); return BadRequest(ModelState); } model.Filename = filename; model.FilenameBackend = filenameBackend; } else { var core = _unitOfWork.Images.SingleOrDefault(x => x.Filename == filename && x.Id != model.Id); if (core != null) { Services.Data.Core.Domain.Image.DeleteImage(filenameBackend); ModelState.AddModelError("Error", "The image filename is already used with another image record."); return BadRequest(ModelState); } if(string.IsNullOrEmpty(filenameBackend)) { model.Filename = filenameOld; model.FilenameBackend = filenameBackendOld; } else {// We have a new image. Services.Data.Core.Domain.Image.DeleteImage(filenameBackendOld); model.FilenameBackend = filenameBackend; model.Filename = filename; } } if(!model.Validate(_unitOfWork, (int)userId, out List<string> errors)) { foreach (var s in errors) ModelState.AddModelError("Error", s); return BadRequest(ModelState); } await _unitOfWork.AddAsync(model.GetCore()); await _unitOfWork.CompleteAsync(); return Json("Success"); } catch (System.Exception ex) { _unitOfWork.Log(ex, Services.Data.Severity.Warning); ModelState.AddModelError("Error", "The request could not be processed."); return BadRequest(ModelState); } }