Asp Net Core Api file stream upload

Keywords: ASP.NET C#

Tip: after the article is written, the directory can be generated automatically. Please refer to the help document on the right for how to generate it

1, File stream

Next, let's see how to upload large files with file stream to avoid loading all uploaded files into the server memory at one time. The trouble of uploading with file stream is that you can't use the model binder of ASP.NET Core MVC to deserialize the uploaded file into C# objects (like the IFormFile interface described earlier). First, we need to define the class MultipartRequestHelper to identify each section type in the Http request (whether it is a form key value pair section or an uploaded file section)

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

namespace AspNetCore.MultipartRequest
{
    public static class MultipartRequestHelper
    {
        // Content-Type: multipart/form-data; boundary="----WebKitFormBoundarymx2fSWqWSd0OxQqq"
        // The spec says 70 characters is a reasonable limit.
        public static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit)
        {
            //var boundary = Microsoft.Net.Http.Headers.HeaderUtilities.RemoveQuotes(contentType.Boundary);// .NET Core <2.0
            var boundary = Microsoft.Net.Http.Headers.HeaderUtilities.RemoveQuotes(contentType.Boundary).Value; //.NET Core 2.0
            if (string.IsNullOrWhiteSpace(boundary))
            {
                throw new InvalidDataException("Missing content-type boundary.");
            }

            //Note that the boundary.Length here refers to the length of the string after the equal sign in boundary = ---------------------------------------- 99614912995, that is, the length of the section separator. As mentioned above, it is reasonable that the length will not exceed 70 characters
            if (boundary.Length > lengthLimit)
            {
                throw new InvalidDataException(
                    $"Multipart boundary length limit {lengthLimit} exceeded.");
            }

            return boundary;
        }

        public static bool IsMultipartContentType(string contentType)
        {
            return !string.IsNullOrEmpty(contentType)
                    && contentType.IndexOf("multipart/", StringComparison.OrdinalIgnoreCase) >= 0;
        }

        //If section is a form key value pair section, this method returns true
        public static bool HasFormDataContentDisposition(ContentDispositionHeaderValue contentDisposition)
        {
            // Content-Disposition: form-data; name="key";
            return contentDisposition != null
                    && contentDisposition.DispositionType.Equals("form-data")
                    && string.IsNullOrEmpty(contentDisposition.FileName.Value) // For .NET Core <2.0 remove ".Value"
                    && string.IsNullOrEmpty(contentDisposition.FileNameStar.Value); // For .NET Core <2.0 remove ".Value"
        }

        //If section is the uploaded file section, this method returns true
        public static bool HasFileContentDisposition(ContentDispositionHeaderValue contentDisposition)
        {
            // Content-Disposition: form-data; name="myfile1"; filename="Misc 002.jpg"
            return contentDisposition != null
                    && contentDisposition.DispositionType.Equals("form-data")
                    && (!string.IsNullOrEmpty(contentDisposition.FileName.Value) // For .NET Core <2.0 remove ".Value"
                        || !string.IsNullOrEmpty(contentDisposition.FileNameStar.Value)); // For .NET Core <2.0 remove ".Value"
        }

        // If the Header of a section is: content disposition: form data; name="files";  filename="F:\Misc 002.jpg"
        // Then this method returns: files
        public static string GetFileContentInputName(ContentDispositionHeaderValue contentDisposition)
        {
            return contentDisposition.Name.Value;
        }

        // If the Header of a section is: content disposition: form data; name="myfile1";  filename="F:\Misc 002.jpg"
        // Then this method returns Misc 002.jpg
        public static string GetFileName(ContentDispositionHeaderValue contentDisposition)
        {
            return Path.GetFileName(contentDisposition.FileName.Value);
        }
    }
}

Then we need to define an extension class called FileStreamingHelper. The StreamFiles extension method is used to read the file stream data of the uploaded file and write the data to the hard disk of the server. It accepts a parameter targetDirectory to declare which folder the uploaded file is stored in the server.

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

namespace AspNetCore.MultipartRequest
{
    public static class FileStreamingHelper
    {
        private static readonly FormOptions _defaultFormOptions = new FormOptions();

        public static async Task<FileReturnType> StreamFiles(this HttpRequest request, string targetDirectory = null)
        {
            if (!MultipartRequestHelper.IsMultipartContentType(request.ContentType))
            {
                throw new Exception($"Expected a multipart request, but got {request.ContentType}");
            }

            // Used to accumulate all the form url encoded key value pairs in the 
            // request.
            var formAccumulator = new KeyValueAccumulator();
			var fileEntity = new List<FileEntity>();
			
            var boundary = MultipartRequestHelper.GetBoundary(
                MediaTypeHeaderValue.Parse(request.ContentType),
                _defaultFormOptions.MultipartBoundaryLengthLimit);
            var reader = new MultipartReader(boundary, request.Body);

            var section = await reader.ReadNextSectionAsync();//Used to read the first section data in the Http request
            while (section != null)
            {
                ContentDispositionHeaderValue contentDisposition;
                var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out contentDisposition);

                if (hasContentDispositionHeader)
                {
                    /*
                    The section used to process the uploaded file type
                    -----------------------------99614912995
                    Content - Disposition: form - data; name = "files"; filename = "Misc 002.jpg"

                    ASAADSDSDJXCKDSDSDSHAUSAUASAASSDSDFDSFJHSIHFSDUIASUI+/==
                    -----------------------------99614912995
                    */
                    if (MultipartRequestHelper.HasFileContentDisposition(contentDisposition))
                    {
                       var fileName = MultipartRequestHelper.GetFileName(contentDisposition);
                       var streamdFileContent = await FileHelpers.ProcessStreamedFile(section, fileName,targetDirectory)
                       fileEntity.Add( new FileEntity
                       {
                       		Name = fileName;
                       		AttachmentType = Path.GetExtension(fileName).ToLower(),
                       		SavePath = streamdFileContent.Item2,
                       		FileSize = FileHelper.GetLength(streamdFileContent.Item.Length); 
                       		FileFormat= Path.GetExtension(fileName).ToLower(),
                       }); 

                    }
                    /*
                    section used to process form key value data
                    -----------------------------99614912995
                    Content - Disposition: form - data; name = "SOMENAME"

                    Formulaire de Quota
                    -----------------------------99614912995
                    */
                    else if (MultipartRequestHelper.HasFormDataContentDisposition(contentDisposition))
                    {
                        // Content-Disposition: form-data; name="key"
                        //
                        // value

                        // Do not limit the key name length here because the 
                        // multipart headers length limit is already in effect.
                        var key = HeaderUtilities.RemoveQuotes(contentDisposition.Name);
                        var encoding = GetEncoding(section);
                        using (var streamReader = new StreamReader(
                            section.Body,
                            encoding,
                            detectEncodingFromByteOrderMarks: true,
                            bufferSize: 1024,
                            leaveOpen: true))
                        {
                            // The value length limit is enforced by MultipartBodyLengthLimit
                            var value = await streamReader.ReadToEndAsync();
                            if (String.Equals(value, "undefined", StringComparison.OrdinalIgnoreCase))
                            {
                                value = String.Empty;
                            }
                            formAccumulator.Append(key.Value, value); // For .NET Core <2.0 remove ".Value" from key

                            if (formAccumulator.ValueCount > _defaultFormOptions.ValueCountLimit)
                            {
                                throw new InvalidDataException($"Form key count limit {_defaultFormOptions.ValueCountLimit} exceeded.");
                            }
                        }
                    }
                }

                // Drains any remaining section body that has not been consumed and
                // reads the headers for the next section.
                section = await reader.ReadNextSectionAsync();//Used to read the next section data in the Http request
            }

            // Bind form data to a model
            var fileReturnType = new FileReturnType();
            fileReturnType.formValueProvider = new FormValueProvider(
                BindingSource.Form,
                new FormCollection(formAccumulator.GetResults()),
                CultureInfo.CurrentCulture);
			fileReturnType.fileEntity = fileEntity;
            return fileReturnType;
        }

        private static Encoding GetEncoding(MultipartSection section)
        {
            MediaTypeHeaderValue mediaType;
            var hasMediaTypeHeader = MediaTypeHeaderValue.TryParse(section.ContentType, out mediaType);
            // UTF-7 is insecure and should not be honored. UTF-8 will succeed in 
            // most cases.
            if (!hasMediaTypeHeader || Encoding.UTF7.Equals(mediaType.Encoding))
            {
                return Encoding.UTF8;
            }
            return mediaType.Encoding;
        }
    }
}

Now we also need to create a custom interceptor DisableFormValueModelBindingAttribute for ASP.NET Core MVC, which implements the interface IResourceFilter to disable the model binder of ASP.NET Core MVC. In this way, when an Http request arrives at the server, ASP.NET Core MVC will not load all the requested uploaded file data into the server memory, Instead, the Action method of the Controller is executed immediately when the Http request reaches the server.

ASP.NET Core 3.X uses the following code:

using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using System;
using System.Linq;

namespace AspNetCore.MultipartRequest
{
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
    public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
    {
        public void OnResourceExecuting(ResourceExecutingContext context)
        {
            var factories = context.ValueProviderFactories;
            factories.RemoveType<FormValueProviderFactory>();
            factories.RemoveType<FormFileValueProviderFactory>();
            factories.RemoveType<JQueryFormValueProviderFactory>();
        }

        public void OnResourceExecuted(ResourceExecutedContext context)
        {
        }
    }
}

Finally, we define an Action method called Index in the Controller and register the DisableFormValueModelBindingAttribute interceptor we defined to disable the model binding of Action. The Index method will call the StreamFiles method in the FileStreamingHelper class we defined earlier, and its parameter is the folder path used to store uploaded files. The StreamFiles method will return a FormValueProvider to store the form key data in the Http request. Then we will bind it to the MVC view model viewModel, and then return the viewModel to the client browser to report the success of the client browser file upload.

[HttpPost]
[DisableFormValueModelBinding]
[DisableRequestSizeLimit]
public async Task<IActionResult> Index()
{
    FormValueProvider formModel;
   // formModel = await Request.StreamFiles(@"F:\UploadingFiles");
      formModel = await Request.StreamFiles();
      
    var viewModel = new MyViewModel();

    var bindingSuccessful = await TryUpdateModelAsync(viewModel, prefix: "",
        valueProvider: formModel);

	

    if (!bindingSuccessful)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }
    }
//bindingSuccessful accepts the attribute of the uploaded value and saves it to the database

    return Ok(viewModel);
}
public static class FileHelpers
{	
	private static string _filePath = _filePath ?? AppContext.BaseDirectory;//Save video file path
	private static string _dbFilePath; //Save the folder path in the database

	///<summary>
	///File return type
	///</summary>
	///< param name = "section" > file stream < / param >
	///< param name = "filename" > file name < / param >	
	///< param name = "targetdirectory" > storage location < / param >
	///<returns></returns>
	public static async Task<Tuple<byte[],string>> ProcessStreamedFile(MultipartSection section,string fileName,
		string targetDirectory = null)
	{
		try
		{
			if(string.IsNullOrEmpty(fileName))
			{
				throw new Exception("File name cannot be empty");
			}
			
			using(var memoryStream = new MemoryStream())
			{
				//This is the size of the file data read from the section requested by Http every time. The unit is Byte, that is, Byte. Here, it is set to 1024, which means that each time 1024 bytes of data are read from the section data stream requested by Http to the server memory, and then written to the file stream of targetFileStream below. This value can be adjusted according to the memory size of the server. This avoids loading the data of all uploaded files into the server memory at one time, resulting in server crash.
				var loadBufferBytes = 1024;
				//section.Body is a System.IO.Stream type, which represents the data stream of a section in the Http request. All data of each section can be read from this data stream. Therefore, we can use the section.Body.Read method to read the data in a loop instead of the section.Body.CopyToAsync method (if the section.Body.Read method returns 0, Indicates that the data stream has reached the end and all the data has been read), and then writes the data to the targetFileStream
				await section.Body.CopyToAsync(memoryStream ,loadBufferBytes);
				var ext = Path.GetExtension(fileName).ToLower();
				if(memoryStream.Length == 0 )
				{
					throw new Exception("The file is empty");
				}
				else if(memoryStream.Length > 2147483648 )
				{
					throw new Exception("File size exceeds 2 GB");
				}
				else if(memoryStream.Length > 2147483648 )
				{
					throw new Exception("File size exceeds 2 GB");
				}
				else if(ext.Contains(".mp4") || ext.Contains(".avi") || ext.Contains(".wmv")
				 || ext.Contains(".3gp") || ext.Contains(".dv"))
				{
					SaveFile(memoryStream, fileName, targetDirectory);
					Tuple<byte[],string> tuple = new Tuple<byte[],string>(memoryStream.ToArray(), _dbFilePath);
					return tuple;
				}
			}
		}
		 catch (Exception ex)
    	 {
        	  throw new Exception(ex.InnerException?.Message ?? ex.Message);
    	 }
		
	}

  private static void SaveFile(MultipartSection memoryStream, string fileName,
        string targetDirectory = null)
        {
            string currentDate = DateTime.Now.ToString("yyyy");
            string subfolder = "Video";

            //F:\Practice\DOTNET CORE\StudentManagement\StudentManagement\bin\Debug\net5.0\20210901\Video
            var folder = Path.Combine(currentDate, subfolder);
            var uploadPath = Path.Combine(_filePath,folder);

            //Custom storage path
            if (!string.IsNullOrEmpty(targetDirectory))
            {
                if (!Directory.Exists(uploadPath))
                {
                    Directory.CreateDirectory(uploadPath);
                }
                uploadPath = targetDirectory;
            }
            //Specified path
            else if (!string.IsNullOrEmpty(uploadPath))
            {
                Directory.CreateDirectory(uploadPath);
            }

            var ext = Path.GetExtension(fileName).ToLower();
            var newName = GenerateId.GenerateOrderNumber() + ext;
            using (var fs = new FileStream(Path.Combine(uploadPath, newName), FileMode.Create))
            {
                fs.Write(memoryStream.ToArray(), 0, memoryStream.ToArray().Length);
                fs.Close();
                _dbFilePath = Path.Combine(folder,newName);
            }

        }

//GenerateId class
		 /// <summary>
        ///Gets the file size displayed as a string
        /// </summary>
        ///< param name = "lengthofdocument" > file size unit: Byte type: long < / param >
        /// <returns></returns>
        public static string GetLength(long lengthOfDocument)
        {
            //If the file size is within 0-1024B, the display is in B
            //If the file size is within 1KB-1024KB, it is displayed in kilobytes
            //If the file size is within 1M-1024M, the display is in M
            //If the file size is within 1024 MB, the display is in GB
            if (lengthOfDocument <1024)
                return string.Format(lengthOfDocument.ToString() + 'B');
            else if(lengthOfDocument >1024 && lengthOfDocument <= Math.Pow(1024, 2))
                return string.Format((lengthOfDocument / 1024.0).ToString("F2") + "KB");
            else if (lengthOfDocument > Math.Pow(1024, 2) && lengthOfDocument <= Math.Pow(1024, 3))
                return string.Format((lengthOfDocument / 1024.0 / 1024.0).ToString("F2") + "M");
            else 
                return string.Format((lengthOfDocument / 1024.0 / 1024.0 / 1024.0).ToString("F2") + "GB");

        }

	///<summary>
	///File return type
	///</summary>
	public class FileReturnType
	{
		public FileReturnType()
		{
			fileEntity = new List<>(FileEntity);
		}
		public FormValueProvider formValueProvider  {get;set;}
		public List<FileEntity> fileEntity {get;set;}
	}

}	

2, Class

public class FileEntity
{
	public string Name {get;set;}
	//Attachment type
	public string AttachmentType{get;set;}
	public string SavePath {get;set;}
	//file format
	public string FileFormat{get;set;}
	public string FileSize{get;set;}
}

summary

Provide convenience for yourself. Please point out any mistakes (a little messy). Thank you.

reference:
https://www.cnblogs.com/OpenCoder/p/9785031.html File stream upload
https://hub.fastgit.org/dotnet/AspNetCore.Docs/blob/main/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/Controllers/StreamingController.cs Official upload file
https://blog.csdn.net/qq_22098679/article/details/81327074 file creation permission problem

Posted by qazwsx on Wed, 01 Sep 2021 15:02:30 -0700