So We Wanted To Build A File Uploader… A Case Study
One day I discovered that I needed to design an API that would upload files from a client to a server. I work on the Russian Web mail provider at Mail.Ru and deal with JavaScript in all its aspects. A basic feature of any Web mail service is of course attaching a file to an email.
Mail.ru is no exception: We used to have a Flash uploader, which was rather good, but still had some problems. HTML markup, graphics, business logic and even localization were all built into it, and it made the uploader pretty bloated. Furthermore, only a Flash developer could make any changes to it. We realized that we needed to build something new and different. This article covers all of our steps in creating what we consider to be a better tool for the job.
Anyone who has ever written a Flash uploader knows what problems it always brings:
- Cookies for authentication are difficult to manage because depending on browser and operating system they have erratic behaviour in Flash (i.e. cookies are not shared between HTTP requests and FileReference upload/download). Officially Flash supports cookies only in IE and they will not be shared among other browsers, or they will be retrieved from IE;
- There are assumptions that with Flash, cookies are read from Internet Explorer although it’s not officially confirmed;
- Proxy settings are quite inconvenient to update; with Flash, they are always retrieved from IE, independent of the browser used;
- Errors #2038 and #2048, elusive errors that appear in some combinations of network settings, browser and Flash player version;
- AdBlock and the like (no comment).
So, we decided that it was the right time for a change. Here’s a list of features that we wanted to have with a new approach to this problem:
- Select multiple files;
- Get file information (name, type and mini-type);
- Preview images before uploading;
- Resize, crop and rotate images client-side;
- Upload results to the server, plus CORS;
- Make it independent of external libraries;
- Make it extensible.
Over the last four years, we’ve all read heated debates about various features and options of HTML5, including the File API. Many publications touch on this API, and we have a few functioning examples of it. One would think, “Here’s a tool that solves the problem.” But is it as easy as it looks?
Well, let’s look at the browser statistics for Mail.Ru. We have selected only browser versions that support the File API, although in some cases these browsers do not provide full support for the API.
The diagram shows that a bit more than a whopping 87% of browsers indeed support the File API:
- Chrome 10+
- Firefox 3.6+
- Opera 11.10+
- Safari 5.4+
- IE 10+
Also, we shouldn’t forget about mobile browsers, which are becoming more popular by the day. Take iOS 6+, for example, which already supports the File API. However, 87% is not 100% and in our case it wasn’t feasible to entirely abandon Flash at this point.
So, our task evolved to building a tool that combines two techniques (File API and Flash) and that lets the developer kind of… ignore the way files are actually uploaded. During the development process we decided to combine all preliminary development into a separate library (a unified API) that would work independent of the environment and could be used wherever you like, not only in our service.
So let’s go into detail on a few specifics of the development process and see what we’ve built, how we built it and what we’ve learned along the way.
Retrieve File List #
Basics first. Here is how files are received in HTML5. Very simple.
But what do you do if you have only Flash support and no File API? The basic idea that we had for users with Flash support was to make all interactions go through Flash. You couldn’t simply call up a file-selection dialog. Due to the security policy, the dialog would open only after the Flash object has been clicked.
This is why the Flash object would be positioned above your target input. Then, you would attach a mouseover
event handler to the document, and put the Flash object into the input’s parent element when the user hovers over it.
The user would click the Flash object, open the file-selection dialog and select a file. Data would be transferred from Flash to JavaScript using ExternalInterface
. The JavaScript would bind the data received with the input element and emulate the change
event.
All further interactions between JavaScript and Flash are performed through the only available method in Flash. The first argument is a command name. The second parameter is an object with two mandatory fields: the file id
and the callback
function. The callback
is called from Flash once the command is executed.
The combination of the two methods results in the API, which is very similar to native JavaScript. The only difference is in the way files are received. Now we use the API method because the input has the files
property only when the browser supports HTML5 and the File API. In the case of Flash, the list is taken from the data associated with it.
Filter #
Usually, file uploading comes with a set of restrictions. The most common restrictions are on file size, image type and dimensions (width and height). If you look around solutions for this issue, you’ll notice that validation is usually done on the server, and the user would receive an error message if the file doesn’t match any restrictions. I tried to solve this problem in another way, by validating files on the client side — before the file has started uploading.
What’s the catch? The catch is that when we initially get the list of files, we have only the bare minimum of information about the files: name, size and type. To get more detailed information, we need to actually read the files. To do that, we can use FileReader.
So if we play around with FileReader, we’ll probably come up with the following filtering technique:
You can get the file’s dimensions “out of the box,” as well as a way to collect all of the data you’ll need:
Process Images #
In developing the API, we also wanted a convenient and powerful tool that would allow us to work with images — to create previews, crop, rotate and resize, for example — and whose functionality would be supported in both HTML5 and Flash.
Flash #
First, we needed to understand how to do this via Flash — that is, what to send to JavaScript to build the image. As we of course know, this is usually done using the data URI. Flash reads the file as Base64 and transfers it to JavaScript. So we add data:image/png;base64
to it, and use this string as the src
.
A happy ending? Unfortunately, IE 6 and 7 do not support the data URI, and IE 8+, which supports the data URI, cannot process more than 32 KB. In this case, JavaScript would create a second Flash object and transfer the Base64-encoded content into it. This Flash object would restore the image.
HTML5 #
In the case of HTML5, we would get the original image first, and then perform all required transformations using the canvas. Getting the original image can be done in one of two ways. The first is to read the file as a dataURI using FileReader
. The second is to use URL.createObjectURL to create a link to the file, which is bound to the current tab. Of course, the second option is good and is enough to generate a preview, but not all browsers support it. Opera 12, for example, does not support the accompanying URL.revokeObjectURL
, which informs the browser that there is no need to keep a link to the file anymore.
When we combine all of these methods, we get a class of FileAPI.Image
:
crop(x, y, width, height)
resize(width,[height])
rotate(deg)
preview(width, height)
— crop and resizeget(callback)
— get final image
All of these techniques fill the transformation matrix, which is applied only when the get()
method is called. Transformations are performed using the HTML5 canvas
or in Flash (when the file is uploaded through the Flash interface).
Here is our description of the matrix:
And here is a short example:
Resize #
Digital cameras emerged long ago and are still very popular. Some cost about $20 to $30 and can take photos with a resolution of 10 MP and up. We tried to downsize photos taken with such cameras, and this is what we ended up with:
As you can see, the quality is rather poor. However, if we first resize it in half and then do it again several times until we get the desired dimensions, then the quality is much better. This method is actually quite old, and in fact a consequence of the “nearest neighbor” interpolation; when compressing images at once, we are losing the quality of the image really “quickly”.
The difference is evident:
Apply a slight sharpening effect, and the image will be ideal.
We also tried other variations, such as bicubic interpolation and the Lanczos algorithm. The result was a bit better, but the process took more time: 1.5 seconds versus 200 to 300 milliseconds. This method also yielded the same results in canvas and Flash.
Uploading Files #
Now let’s sum up our various options for uploading a file to the server.
iframe #
Yes, we still use this many years later:
At first, we create а form
element with a nested iframe
inside. (The form
’s target attribute and the name of the iframe
should be the same.) After that, we move the input[type=“file”]
into it because if you put a clone there, it will turn up empty.
To illustrate this issue, imagine that you load a file via iframe
. We could use something like this:
However, such input would be “empty” in IE, i.e. it wouldn’t contain the selected file, which is why we need to “send” the original file and replace it with a clone.
That is why we subscribe to events via API methods, to save them during cloning. Then, we call form.submit()
, and put the contents of the form through the iframe
. We’ll get the results using JSONP.
Yes, erratic indeed.
Flash #
In principle, everything is quite simple: JavaScript calls the method from the Flash object and passes the ID of the file to be uploaded. Flash, in turn, duplicates all states and events in JavaScript.
XMLHttpRequest and FormData #
Now we can send binary data, not just text data. This is easy:
What if, for example, we want to send not a file, but canvas
data? There are two options. The first, which is easiest and correct, is to convert canvas
to Blob
:
As you can see, this trick is not universal. In case canvas doesn’t have Canvas.toBlob()
(or it cannot be implemented), we will choose another option. This option is also good for browsers that do not support FormData
. The point is to create the multipart request manually and then send it to the server. The code for the canvas would look like this:
Finally, our efforts result in the following method:
This has a lot of parameters, but the most important one is imageTransform
. It transforms images on the client, and it operates via both Flash and HTML5.
And that’s not even half of the story. We can have multiple imageTransforms
:
This means that three copies (besides the original) will be sent to the server. What for? If you can transfer the load from the server to the client, it’s a good idea to do it. The server should probably only minimally validate input files. First of all, you are not only removing the load, but also avoid logic on the server, completely moving it to the client.
Second, if the file doesn’t have to get uploaded to the server, we save bandwidth. In addition, there are often problems when it isn’t possible to make further processing on the server, such as integration with third-party services (Amazon S3, for example). In our experience, it’s OK to move the additional logic that previously was managed server-side to the client.
The upload function also calls back an XMLHttpRequest
-like object; that is, it assumes some properties and methods of XMLHttpRequest
, such as:
status
HTTP status codestatusText
HTTP status textresponseText
server’s replygetResponseHeader(name)
get header of the server’s replygetAllResponseHeaders()
get all headersabort()
abort upload
Although HTML5 allows you to upload several files in one request, standard Flash allows only file-by-file uploading. Moreover, in our opinion, uploading files in a batch proved not to be a good idea. For one, Flash doesn’t support this, and we wanted to have an identical behavior for both Flash and HTML5. Second, the user might simply run out of memory and the browser will fail.
XMLHttpRequest
, which has called back the upload, is a proxy XMLHttpRequest
, in fact. Its methods and properties reflect states in the file currently being uploaded.
Final Word #
I’ll end with a small example of how we let users upload files using drag’n’drop:
It took us quite some time to develop the library. We worked on it for about 5 months since it was a little side thing that we had to finish aside from the regular work. The main headache was caused by the little details that different browsers had. Chrome, Firefox and IE10+ were just fine, but Safari and Opera had very different behaviors from version to version, including inconsistencies on Win/Mac platforms. Still, the main problem was to actually combine all three technologies — iframe, Flash, HTML5 — to create a bulletproof file uploader.
The library is available on GitHub and we’ve published a documentation as well. Bug reports and pull requests are more than welcome!
Useful Links #
- FileAPI (and demo), Mail.ru, GitHub
- Mail.ru, GitHub Find Tarantool, fest and much else.
- “HTML5 Form Features,” Can I Use…? See the support for
input[type="file" multiple]
. - “File API,” Can I Use…?
- “FileReader,” Mozilla Developer Network
- “URL.createObjectURL” and “URL.revokeObjectURL,” Mozilla Developer Network
- “XMLHttpRequest,” Mozilla Developer Network
- “FormData,” Mozilla Developer Network
This article has been reviewed and edited by Andrew Sumin, a front-end engineer working on Mail.ru front-end team.
Further Reading #
- How Mail.Ru Reduced Email Storage From 50 To 32 PB
- Product Design Unification Case Study: Mobile Web Framework
- How To Improve Your Email Workflow With Modular Design
- Making Responsive HTML Email Coding Easy With MJML
![Smashing Editorial](https://www.smashingmagazine.com/images/logo/logo--red.png)
— Comments 2
The major problem with file uploads regards security. In my opinion the client-side aspect of this task is less important than the server-side one. HTML5, old iframe-based approach or Flash are not important. The key aspect here is how to process files on the server and how to store them safely. You can build a killer uploading system in the browser but if your security checks on the server fail, you'll probably end up with facing several security holes. It doesn't matter that you're handling image files here: all HTTP input is potentially tainted. I'd like to know a little bit more about your server-side procedure to ensure the safety and consistency of this kind of data. Browsers only are not enough. Excellent article for the rest.
Great post! Thanks!