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);
            }
        }
Copyright © 2025 delaney. All rights reserved.