📜 ⬆️ ⬇️

Uploading files to the server in 2012

At one point, I was faced with the task of creating an API for working with files on the client and uploading them to the server.

I work in Mail.Ru Mail, and my direct responsibility is to work with JavaScript in all its manifestations. Attaching files to a letter is one of the main functions of any mail. We are no exception here: we already had a Flash downloader, which worked quite well and for a long time was fine for us. However, he had a number of drawbacks. The entire layout, graphics, business logic, and even localization were sewn into it, as a result of which the decision was cumbersome, and only the Flash developer could make the edit. At some point, we realized that we needed a new mechanism. How to create it will be discussed in this article.

Those who wrote the Flash downloader understand the challenges they face:
')

And so, looking at all this, we decided: it's time, the moment has come - and we have formed the following requirements:



The last 4 years, the possibilities of HTML5, including the File API, are being increasingly discussed. Many articles have been written on this subject, there are working examples. It would seem that here is a ready-made tool for solving the set tasks. But is everything as simple as it seems at first glance? Consider the statistics of Mail.Ru users by browsers. Only the versions that are supported by the File API are selected from the list, albeit not 100%.



As can be seen from the diagram, just over 63% of browsers used support the File API:


Also, do not forget about mobile devices, whose share is growing day by day. iOS 6 already supports File API.

Internet Explorer promises support from version 10.

But 63% is not 100%. So to abandon Flash'a early.

Thus, the task was reduced to creating a mechanism that combines both technologies (File API and Flash) and was implemented so that the final developer would not care how the files are loaded. In the course of the work, the idea arose of arranging all the developments in the form of a separate library (unified API) that would work regardless of the environment and could be used not only within the Mail.Ru Mail, but anywhere.

Consider specific examples of how the development process.


Getting a list of files


This is how getting a list of files on HTML5. Everything is very simple.

<input id="file" type="file" multiple /> <script> var input = document.getElementById("file"); input.addEventListener("change", function (){ var files = input.files; }, false); </script> 

But what to do when the File API is not supported, but is Flash supported? The basic principle of working with Flash is that all interaction takes place directly through it. You can not just take and call the file selection dialog.

This requires the user to click on Flash. Only at this moment can a dialogue be opened - such is the security policy.

Therefore, the Flash object is placed above the desired input. This is done very simply: the mouseover event is hung up on the entire document, and when you hover over the input [type = "file"] in the "parent", a Flash object is published and occupies its entire space.

When clicking on the flash drive, the file dialog opens, the user selects something in it and clicks OK. Then the data is transferred from Flash to JS via ExternalInterface. JS connects the received data with the necessary input and emulates the "change" event.
Flash -> js
 [[Flash]] --> jsFunc([{ id: "346515436346", //   name: "hello-world.png", //   type: "image/png", // mime-type size: 43325 //  }, { // etc. }]) 

All further interaction between JS and Flash will be carried out through the only available method of the Flash object. The first argument is the name of the command, the second is the parameter object with two required fields: the file id and callback. The callback will be called from Flash when the command completes.

 flash.cmd("imageTransform", { id: "346515436346", //   matrix: { }, //   callback: "__UNIQ_NAME__" }); 

After combining the two methods, the API turned out to be as close as possible to Native JS. The only difference is the way to get the files. Now we use the API method, since the input files have a file property only if the browser supports the HTML5 / File API; in the case of Flash, the list is taken from its associated data.

 <span class="js-fileapi-wrapper" style="position: relative"> <input id="file" type="file" multiple /> </span> <script> var input = document.getElementById("file"); FileAPI.event.on(input, "change", function (){ var files = FileAPI.getFiles(input); }); </script> 

or so
 <span class="js-fileapi-wrapper" style="position: relative"> <input id="file" type="file" multiple /> </span> <script> var input = document.getElementById("file"); FileAPI.event.on(input, "change", function (evt){ var files = FileAPI.getFiles(evt); }); </script> 


Filtration


As a rule, when downloading files there are a number of restrictions. One of the most popular ones is file size, image type and width / height. The standard scheme for solving such tasks: first upload the file to the server, and after validation, inform the user that the file did not fit, “try again”. Creating a filtering method, I tried to solve this problem, giving the opportunity to operate during filtering with more detailed information about the file.

What is the difficulty? The whole point is that initially, after receiving the list of files, we have only minimal information, such as name, size and type. In order to get more detailed information, the file must be read. This can be done via FileReader .
IE10 & FileReader
 ​//    , IE10  HTML5/FileAPI: var reader = new File​Reader; reader.readAsBinaryString(file); // error: Object doesn't support method or property "readAsBinaryString" ​// ,  ! var reader = new FileReader; reader.onload = function (evt){ var base64 = evt.result.replace(/^data:[^,]+,/, ''); var binaryString = window.atob(base64); // bingo! }; reader.readAsDataURL(file); 

The result is the following filtering method:

 FileAPI.filterFiles(files, function (file, info){ if( /^image/.test(file.type) ){ return info.width > 320 && info.height > 240; } else if( file.size ){ return file.size < 10 * FileAPI.MB; } else { // ,   File API  Flash,    . //   ,     ,    . return true; } }, function (files, ignore){ if( files.length > 0 ){ // ... } }); 


Also, out of the box, the definition of the height and width of the image is supported, and it is also possible to realize the collection of the necessary information:

 FileAPI.addInfoReader(/^audio/, function (file, callback){ //    //    callback( false, //    { artist: "...", album: "...", title: "...", ... } ); }); 



Work with images


In the process of creating an API, I wanted to get a tool for working with an image — for example, creating a preview — and that the basic functionality was supported by HTML5 and Flash.


Flash
First of all, it was necessary to understand how to do this through Flash, i.e. what to transfer to JS to build an image. As you understand, this is done using the Data URI. Flash reads the file as Base64, transfers to JS. We add “data: image / png; base64,” to the beginning and use the resulting string as “src”.

Happy end? Alas, but in IE6-7 there is no support for the Data URI, and IE8 +, which supports the Data URI, does not process more than 32 KB. In these cases, JS publishes the second flash drive, which transfers Base64, and it restores the image.


HTML5
Here you need to first get the original, and then through the Canvas to carry out the necessary transformation. The original can be obtained in two ways. The first is to read the file as a DataURL using FileReader. The second - URL.createObjectURL creates a link to the file associated with the current tab. Of course, to create a preview, the second method is enough, but not all browsers support it. And some do not support the companion URL.revokeObjectURL, which tells the browser that you no longer need to keep a link to the file.

After combining all these methods, the FileAPI.Image class turned out:



All of these methods fill the transformation matrix, and only when the get method is called, will it be applied. Transformation occurs through the Canvas or inside Flash, when working through it.

Matrix description
 { //    sx: Number, sy: Number, sw: Number, sh: Number, //   dw: Number, dh: Number, deg: Number } 

Usage example
 FileAPI.Image(imageFle) .crop(300, 300) .resize(100, 100) .get(function (err, img){ if( !err ){ images.appendChild(img); } }) ; 


Resize process


In our life, mirrors and soapboxes, which, given a price of 1.2K rubles, give out from 10Mpx, have long been entered. And when we began to compress these images, we got something like this:



As you can see, nothing good - continuous distortion. But, if you compress twice, then another and another, until we get the required size, the result is much better.



Here, compare, the difference is obvious:



If you add a little sharpe, it will be perfect

We also tried other methods, such as bicubic interpolation and Lanczos algorithm. They give a slightly better result, but very slow: 1.5s vs. 200-300ms. Also, this method gives the same result in Canvas and Flash.


File upload


I will summarize the ways in which you can now upload a file to the server.


iframe
Yes, and after years he is still in the ranks:
 <form target="__UNIQ__" action="/upload" method="post" enctype="multipart/form-data"> <iframe name="__UNIQ__"></iframe> <input name="files" type="file" /> <input name="foo" value="bar" type="hidden" /> </form> 

First, a transport form is created with the iframe inside (the target attribute of the form and the name iframe must match). Then you need to move the input [type = "file"] into it, because if you put a clone, it will be “empty”. That is why we subscribe to events through API methods to save them when cloning. Then call form.submit () and the entire contents of the form is sent via the iframe. The answer is obtained using JSONP.


Flash / Silverlight
Flash first appeared, followed by Silverlight, who was about to become a Flash killer, but something didn’t grow together. In general, everything is simple there: JS calls the method on the Flash object and passes the id of the file to be loaded, and Flash, in turn, duplicates all states and events in JS.


XMLHttpRequest + FormData
Now you can send not just text data, but binary. This is done very simply:
 //     var form = new FormData form.append("foo", "bar"); //    POST-, form.append("attach", file); //  ,   blob //    var xhr = new XMLHttpRequest; xhr.open("POST", "/upload", true); xhr.send(form) 

But what to do when you need to send not a file, but, for example, a Canvas? There are two ways. The first, which is the most correct and simple one, is, of course, to convert the Canvas to Blob:
 canvasToBlob(canvas, function (blob){ var form = new FormData form.append("foo", "bar"); form.append("attach", blob, "filename.png"); //      // ... }); 

As you understand, not everywhere there is an opportunity to turn such a focus. If the Canvas has no Canvas.toBlob method (or it cannot be implemented), go the other way. It is also suitable for those browsers that do not support FormData.

The essence of this method is to make a multipart request with your hands and send it to the server. For Canvas, the code will look like this:

 var dataURL = canvas.toDataURL("image/png"); //    FileReader var base64 = dataURL.replace(/^data:[^,]+,/, ""); //   var binaryString = window.atob(base64); //  Base64 //    muptipart,   var uniq = '1234567890'; var data = [ '--_'+ uniq , 'Content-Disposition: form-data; name="my-file"; filename="hello-world.png"' , 'Content-Type: image/png' , '' , binaryString , '--_'+ uniq +'--' ].join('\r\n'); var xhr = new XMLHttpRequest; xhr.open('POST', '/upload', true); xhr.setRequestHeader('Content-Type', 'multipart/form-data; boundary=_'+uniq); xhr.sendAsBinary(data); 

If the browser does not support xhr.sendAsBinary
 if( xhr.sendAsBinary ){ // ... } else { var bytes = Array.prototype.map.call(data, function(c){ return c.charCodeAt(0) & 0xff; }); xhr.send(new Uint8Array(bytes).buffer); } 


As a result, the method was born:
 var xhr = ​FileAPI.upload({ url: '/upload', data: { foo: 'bar' }, headers: { 'Session-Id': '...' }, files: { images: imageFiles, others: otherFiles }, imageTransform: { maxWidth: 1024, maxHeight: 768 }, upload: function (xhr){}, progress: function (event, file){}, complete: function (err, xhr, file){}, fileupload: function (file, xhr){}, fileprogress: function (event, file){}, filecomplete: function (err, xhr, file){} });​ 


It has a lot of parameters, but I will pay special attention to imageTransform. Through it the information for image transformation on the client is set. It works through both Flash and HTML5. But that's not all: imageTransform can also be multiple:
 { huge: { maxWidth: 800, maxHeight: 600, rotate: 90 }, medium: { width: 320, height: 240, preview: true }, small: { width: 100, height: 120, preview: true } } 

Those. in addition to the original, three copies of it will also be sent to the server. What for? My opinion is this: if you can transfer the load from the server to the client - do it. The server should remain only minimal validation of incoming data.

The upload function also returns an xhr-shaped object, i.e. it implements some of the properties and methods of XMLHttpRequest, such as:


Although HTML5 can download files with a single request, the standard Flash engine allows loading only one at a time. In addition, loading all at once is not a very good idea - the user may change his mind.

So, the xhr that the upload returned is actually proxyXHR. Its methods and properties reflect the state for the file that is currently being loaded. If the user decides to cancel the download, then the action will be performed for the file that is currently being loaded.



Epilogue


Finally, I want to show you a small example of loading files with drag'n'drop:
 <div id="el" class="dropzone"></div> <script> if( FileAPI.support.dnd ){ // ,     var el = document.getElementById("el"); //     Drag'n'Drop FileAPI.event.dnd(el, function (over){ //  ,    enter/leave   if( over ){ el.classList.add("dropzone_hover"); } else { el.classList.remove("dropzone_hover"); } }, function (dropFiles){ //    FileAPI.upload({ url: "/upload", files: { attaches: dropFiles }, complete: function (err, xhr){ if( !err ){ //   } } }); }); } </script> 


The library is on github, bug reports and pull requests are welcome.

useful links
- https://github.com/mailru/FileAPI ( demo )
- Mail.ru github (Tarantool, fest and more)
- input [type = "file" multiple]
- File API support
- FileReader
- URL.createObjectURL , URL.revokeObjectURL
- XMLHttpRequest
- FormData

Source: https://habr.com/ru/post/159587/


All Articles