Leon's Weblog

November 19, 2013

Multi-File Upload Using Plupload in ASP.NET

Filed under: Software Dev — Leon @ 7:34 pm

File_Upload After a failed attempt at getting multi-file upload functionality on an intranet site using the AjaxFileUpload control from the Ajax Control Toolkit I was forced to look for alternatives. The AjaxFileUpload control was designed for ASP.NET so I originally though that it would have been the ideal solution. Unfortunately, I kept getting authentication errors in certain usage scenarios on legacy browsers. As an alternative, I ended using Plupload which is used in content management systems like WordPress. While not designed specifically for ASP.NET, I found Plupload flexible enough to work well with different kinds of server side technologies as well as most browsers. The Plupload does not interfere with ASP.NET’s partial page rendering and can be used on sites with master pages. For legacy browsers, it could be configured to use it’s Flash interface or an even more basic HTML 4 interface (instead of HTML 5 which I used by default). Here is how I set it up.

Plupload requires setting up two files to work. On the client side (in the ASPX page) we need to load the appropriate JavaScript libraries and configure the plugin. On the server side, we need to handle the data stream that will be submitted from the client. There are many examples online of configuring the client side script so I won’t go into great detail here. I found the multipart_params parameter very useful for passing server side attributes for files being uploaded. I defined some of the parameters (e.g. AllowedFileTypes) on the server side to be able to be consistent later when it comes time to validate the data stream submitted from the client. Also, I found it simpler to implement the FileUploaded event handler on the client side to be able to indicate the status of the upload to the user (otherwise all uploads look successful unless something crashes on the server side).

    <script language="javascript" type="text/javascript">
        $(document).ready(function () {
            $("#Uploader").pluploadQueue({
                runtimes: 'flash,html5,html4',
                url: 'UploadHandler.ashx',
                max_file_size: '20mb',
                chunk_size: '1mb',
                unique_names: false,
                // Specify what files to browse for
                filters: [
                    { title: 'All Files', extensions: '<%= AllowedFileTypes %>' }
                    ],
                flash_swf_url: '../Scripts/plupload/Moxie.swf',
                multiple_queues: true,
                multipart_params: {
                    'FileID': '<%= FileID %>'
                }
            });

            // get uploader instance
            var uploader = $("#Uploader").pluploadQueue();

            uploader.bind("Error", function (upload, error) {
                alert('An error has occured on the server. Please contact administrator.');
            });

            uploader.bind("FileUploaded", function (upload, file, response) {
                //display error if file upload failed
                if (response.response != 'success') {
                    file.status = plupload.FAILED;
                    alert(file.name + ': ' + response.response);
                }

                //refresh page if all files have been uploaded
                if (upload.total.uploaded == upload.files.length) {
                    document.location.reload(); 
                }
            });
        });
    </script>

For the server side upload logic I used a Web Handler (ASHX file) which has less overhead than a typical ASP.NET web page. The ProcessRequest function is where all the magic happens. Note that, unlike in regular ASP.NET web pages, the Request object in a Web Handler is not globally accessible and gets passed into the ProcessRequest method as part of the context object parameter. On each request of the Web Handler, we need to validate the data passed in and then process the particular chunk of the file being uploaded (with a few additional steps for the first and last chunks). Note that I chose to store the file chunks in a temporary folder until the upload of each individual file completes and then move the finished file to the desired destination. The most important parameters that Plupload passes to the upload handler let you know the name of the file being uploaded and which chunk of the total number of chunks in the current file is being processed. Also note that you can access your custom parameters that were specified in the multipart_params section of the configuration the same way (as I did below with the FileID). As with any user entered data, be sure to validate the data that you retrieve from the Request object.

Dim chunk As Integer = IIf(context.Request("chunk") <> "", Integer.Parse(context.Request("chunk")), 0)
Dim chunks As Integer = IIf(context.Request("chunks") <> "", Integer.Parse(context.Request("chunks")), 0)
Dim fileID As Integer = IIf(context.Request("FileID") <> "", Integer.Parse(context.Request("FileID")), 0)
Dim fileName As String = IIf(context.Request("name") <> "", Path.GetFileName(context.Request("name")), "")

Among the many conditions that you can validate are:

  1. If the Request is valid: context.Request("chunk") <> "" And context.Request.Files.Count > 0
  2. If the file type is allowed (by file extension)
  3. If user has permission to upload
  4. If the custom parameters are valid
  5. If the chunk size is correct: context.Request.Files(0).InputStream.Length <= 2097152

We can test when we are on the first chunk by checking if chunk = 0 Or chunks = 0 and perform additional steps. In my case, I check the temp upload folder and purge stale files. This is also a good time to check if the file being upload duplicates one that is already on the server (at that point you can either stop, make a copy, or override the file on the server). Likewise, we can test when we are on the last chunk by checking if chunks = 0 Or chunk = chunks - 1. In my case, I index the file, log the upload to the database, and finally move the file from the temp folder to its destination.

The actual upload of each chunk is handled by writing the data from the Request object to disk as follows. Note that, for the first chunk, a new file needs to be created and for each subsequent chunk the data must be appended to the existing file.

'Load file chunk into buffer
Dim fileUpload As HttpPostedFile = context.Request.Files(0)
Dim buffer(fileUpload.InputStream.Length) As Byte
fileUpload.InputStream.Read(buffer, 0, buffer.Length - 1)

'Write buffer to disk
Dim fs As New FileStream(uploadPathTemp & fileName, IIf(chunk = 0, FileMode.Create, FileMode.Append))
fs.Write(buffer, 0, buffer.Length - 1)
fs.Close()

At the end, I chose to report the status of the uploads back to the client using a plain text (e.g. “success”). You can alternately use JSON but I did not want to over-complicate this process.This is where that client side FileUploaded event handler comes in (flagging a file with file.status = plupload.FAILED will highlight the problem on Plupload’s user interface).

Code
The complete logic for the file upload page and the upload handler can be found below:
Upload.aspx
UploadHandler.ashx

2 Comments »

  1. Hi,

    Do you have complete working project?

    Comment by Steve Dyson — November 2, 2015 @ 11:16 am
  2. Would you mind to share your DocumentUtils class, or a complete set of class which can manage the upload? I followed your code but found some missing class where I don’t know what they are using for and how they are coded?

    Best regards,

    Sokhawin

    Comment by Sokhawin — June 24, 2016 @ 12:04 am

RSS feed for comments on this post. TrackBack URI

Leave a comment