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