Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to Implement a Web API controller to accept chunked uploads using JQuery File Upload?

As the title states, I need some help implementing a Web API controller to accept chunked uploads using JQuery File Upload. Any help (including links to existing articles/tutorials) will be much appreciated.

like image 579
COBOL Avatar asked Oct 24 '14 10:10

COBOL


People also ask

How do you upload a 5 GB file through your spa to Web API?

After our Web API loaded, we can come to postman tool and using POST method we can send a request to Web API. We must choose “form-data” in the body part and choose “File” as type. We can click the “Send” button now. After some time, we will get a result.

What is a chunked upload?

What Is Chunked Upload? Chunked upload is the process of breaking a file into smaller pieces, upload 'as is' and glue pieces into original file on the server side. The process includes several subsequent POST calls (transferring chunks), which mimics regular file upload mentioned in section above.

How do I upload files to IFormFile?

Upload Single FileTo add view, right click on action method and click on add view. Then select View from left side filter and select Razor View – Empty. Then click on Add button. Create design for your view as per your requirements.


1 Answers

First let start with the client side. You must set the maxChunkSize option for chunked uploads. After that you need a unique identifier per file in order to identify each chunk on the server and append the corresponding chunk data to the correct file.

$('#fileupload')
        .bind('fileuploadsubmit', function (e, data) {
            data.headers = $.extend(data.headers,
                {"X-File-Identifier": generateFileUniqueIdentifier(data)})
            });
        });

generateFileUniqueIdentifier = function(data){
    var file=data.files[0],
    var result = file.relativePath||file.webkitRelativePath||file.fileName||file.name;
    
    return result.replace(/[^0-9a-zA-Z_-]/img,"") + "-" + i.size + "-" + $.now()
} 

Now on the server side: ApiController

 public class UploadController : ApiController
 {
        [HttpPost]
        [Route("upload/{targetFolder:int}")]
        [ValidateMimeMultipartContentFilter]
        public async Task<IHttpActionResult> UploadDocument(int targetFolder)
        {
            var uploadFileService = new UploadFileService();
            UploadProcessingResult uploadResult = await uploadFileService.HandleRequest(Request);

            if (uploadResult.IsComplete)
            {
                // do other stuff here after file upload complete    
                return Ok();
            }

            return Ok(HttpStatusCode.Continue);

        }
}

The service class which actually upload the file. This support chunks or a whole file.

public class UploadFileService
{
        private readonly string _uploadPath;
        private readonly MultipartFormDataStreamProvider _streamProvider;

        public UploadFileService()
        {
            _uploadPath = UserLocalPath;
            _streamProvider = new MultipartFormDataStreamProvider(_uploadPath);
        }
    
        #region Interface

        public async Task<UploadProcessingResult> HandleRequest(HttpRequestMessage request)
        {
            await request.Content.ReadAsMultipartAsync(_streamProvider);
            return await ProcessFile(request);
        }

        #endregion    

        #region Private implementation

        private async Task<UploadProcessingResult> ProcessFile(HttpRequestMessage request)
        {
            if (request.IsChunkUpload())
            {
                return await ProcessChunk(request);
            }

            return new UploadProcessingResult()
            {
                IsComplete = true,
                FileName = OriginalFileName,
                LocalFilePath = LocalFileName,
                FileMetadata = _streamProvider.FormData
            };
        }

        private async Task<UploadProcessingResult> ProcessChunk(HttpRequestMessage request)
        {
            //use the unique identifier sent from client to identify the file
            FileChunkMetaData chunkMetaData = request.GetChunkMetaData();
            string filePath = Path.Combine(_uploadPath, string.Format("{0}.temp", chunkMetaData.ChunkIdentifier));

            //append chunks to construct original file
            using (FileStream fileStream = new FileStream(filePath, FileMode.OpenOrCreate | FileMode.Append))
            {
                var localFileInfo = new FileInfo(LocalFileName);
                var localFileStream = localFileInfo.OpenRead();

                await localFileStream.CopyToAsync(fileStream);
                await fileStream.FlushAsync();

                fileStream.Close();
                localFileStream.Close();

                //delete chunk
                localFileInfo.Delete();
            }

            return new UploadProcessingResult()
            {
                IsComplete = chunkMetaData.IsLastChunk,
                FileName = OriginalFileName,
                LocalFilePath = chunkMetaData.IsLastChunk ? filePath : null,
                FileMetadata = _streamProvider.FormData
            };

        }

        #endregion    

        #region Properties

        private string LocalFileName
        {
            get
            {
                MultipartFileData fileData = _streamProvider.FileData.FirstOrDefault();
                return fileData.LocalFileName;
            }
        }

        private string OriginalFileName
        {
            get
            {
                MultipartFileData fileData = _streamProvider.FileData.FirstOrDefault();
                return fileData.Headers.ContentDisposition.FileName.Replace("\"", string.Empty);
            }
        }

        private string UserLocalPath
        {
            get
            {
               //return the path where you want to upload the file                   
            }
        }

        #endregion    
    }

The extensions over HttpRequestMessagge used to identify a chunk request

public static class HttpRequestMessageExtensions
{
        public static bool IsChunkUpload(this HttpRequestMessage request)
        {
            return request.Content.Headers.ContentRange != null;
        }

        public static FileChunkMetaData GetChunkMetaData(this HttpRequestMessage request)
        {
            return new FileChunkMetaData()
            {
                ChunkIdentifier = request.Headers.Contains("X-DS-Identifier") ? request.Headers.GetValues("X-File-Identifier").FirstOrDefault() : null,
                ChunkStart = request.Content.Headers.ContentRange.From,
                ChunkEnd = request.Content.Headers.ContentRange.To,
                TotalLength = request.Content.Headers.ContentRange.Length
            };
        }
    }

And at the end the service response model and chunk metadata

public class FileChunkMetaData
{
    public string ChunkIdentifier { get; set; }

    public long? ChunkStart { get; set; }

    public long? ChunkEnd { get; set; }

    public long? TotalLength { get; set; }

    public bool IsLastChunk
    {
        get { return ChunkEnd + 1 >= TotalLength; }
    }
}

public class UploadProcessingResult
{
    public bool IsComplete { get; set; }

    public string FileName { get; set; }

    public string LocalFilePath { get; set; }

    public NameValueCollection FileMetadata { get; set; }
}

The MultiPartContentFilter is just an ActionFilter to validate the content (from damienbod)

public class ValidateMimeMultipartContentFilter : ActionFilterAttribute
{
    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        if (!actionContext.Request.Content.IsMimeMultipartContent())
        {
            throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
        }
    }

    public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
    {

    }

}
like image 157
Trasnea Daniel Avatar answered Sep 20 '22 05:09

Trasnea Daniel