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.
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).
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:
- If the Request is valid:
context.Request("chunk") <> "" And context.Request.Files.Count > 0
- If the file type is allowed (by file extension)
- If user has permission to upload
- If the custom parameters are valid
- 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).