There are plenty of tasks connected with creating, parsing, editing, saving and removing files. Most of them are performed on the server side. First of all, we do that because Java, C# or whatever language we use on the backend can easily handle all these tasks. However, a huge problem persists: there’s only one server for all your users and each of them wants to perform some actions.

It may cause increasing server resource utilization or even tear your server down because of the huge amount of requests. The second reason to perform these actions on the server side is that you haven’t read this article yet and you don’t know all the possibilities the modern browsers provide. But that’s fine. We are here to reveal all secrets and mysteries about it. This topic can be divided into three parts: past, present and future.

Past. ActiveX

There were only two browsers in the past – Netscape and IE. One day Microsoft decided to make the IE users happy and added the coolest feature for that time that allowed communicating JavaScript code and Window OS. It was called ActiveX. This technology can be used even nowadays in the latest version of IE browser (it’s turned off by default). We won’t talk a lot about it because it’s only for IE, and a user would have to do a lot of strange magic to his browser to run the scripts that contain ActiveXObjects.

In the present section, we will talk about File, Drag-and-Drop and FileReader APIs as well as about some interesting examples. The most amazing part of the story is in the ‘future’ section where all the secrets of the browser file system will be revealed. Let’s start our journey.

Present. Playing with Added Files

There are two ways to pass files to a browser in all modern browsers (IE10+): 1) Input tag with “file” type; 2) Dragging files over some DOM element.

Good Old Days with Input Element

The first option can be used even in old browsers. The only difference is that we can’t get files’ content in IE9 and lower. It means we still need to use the server to do some file manipulations. To get the files that are chosen by the user, we need to add an event handler „change“ for this input, and when it is called, check the property „files“ of this element.

var template = "<div><span>Name: </span><span>{{Name}}</span></div><div><span>Size: </span><span>{{Size}}</span></div><div><span>Data: </span><span>{{Data}}</span></div>", data = document.getElementById("fileData"); document.getElementById("fileElement").addEventListener("change", function(e){ var file = this.files ? this.files[0] : { name: this.value }, fileReader = window.FileReader ? new FileReader() : null; if (file){ if (fileReader){ fileReader.addEventListener("loadend", function(e){ data.innerHTML = template.replace("{{Name}}", file.name).replace("{{Size}}", file.size).replace("{{Data}}", e.target.result.substring(0, 10)); }, false); fileReader.readAsText(file); } else { data.innerHTML = template.replace("{{Name}}", file.name).replace("{{Size}}", "Don't know").replace("{{Data}}", "This browser isn't smart enough!"); } } }, false); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 var template = "<div><span>Name: </span><span>{{Name}}</span></div><div><span>Size: </span><span>{{Size}}</span></div><div><span>Data: </span><span>{{Data}}</span></div>" , data = document . getElementById ( "fileData" ) ; document . getElementById ( "fileElement" ) . addEventListener ( "change" , function ( e ) { var file = this . files ? this . files [ 0 ] : { name : this . value } , fileReader = window . FileReader ? new FileReader ( ) : null ; if ( file ) { if ( fileReader ) { fileReader . addEventListener ( "loadend" , function ( e ) { data . innerHTML = template . replace ( "{{Name}}" , file . name ) . replace ( "{{Size}}" , file . size ) . replace ( "{{Data}}" , e . target . result . substring ( 0 , 10 ) ) ; } , false ) ; fileReader . readAsText ( file ) ; } else { data . innerHTML = template . replace ( "{{Name}}" , file . name ) . replace ( "{{Size}}" , "Don't know" ) . replace ( "{{Data}}" , "This browser isn't smart enough!" ) ; } } } , false ) ;

As you can see, here we used the method „readAsText“ of the „FileReader“ object to get the file content. It also has methods that read the selected file (or files) and returns results in different formats. You may use the method „readAsArrayBuffer“ that reads the file and returns files as binary data in array; „readAsDataURL“ that returns a URL like “data:image/png,base64:” that you may place in the src attribute of the IMG tag, for example (see demo #2). Be careful with the method “readAsBinaryString” because it isn’t supported in IE10-11 browsers.

You may also want to control the process of file reading, show some progress bars or waiting elements, or even abort it, if files are too “heavy”. Especially for you, FileReader has events like onprogress, onabort, onloadstart, onloadend, onerror, and method abort, that stops the reading process and throws an error that can be handled. We’ve made a small fallback for the IE9 in this example, that doesn’t have any way for reading files. The only information that we can get about the chosen file (it doesn’t support multiselection) is its name that is stored in the „value“ property of the input element.

Please, select an image

fileReader.addEventListener("loadend", function(e){ img.src = e.target.result; }, false); fileReader.readAsDataURL(file); 1 2 3 4 fileReader . addEventListener ( "loadend" , function ( e ) { img . src = e . target . result ; } , false ) ; fileReader . readAsDataURL ( file ) ;

Drag Me to Browser!

That was an old way of adding files to the browser. It can be used even in IE6 with some modifications (addEventListener is supported in IE9+). There is another way how we can do it in modern browsers (IE10+). All you need is to add an event handler “drop” to some DOM element on your web page. It can be a special element with text inside or just a body. After that, a user may drag some file and drop it into the element that has a “drop” event handler. The chosen file can be found in “dataTransfer” of the event object. Let’s take a look at the demo:

Drop your file here

var template = "<div><span>Name: </span><span>{{Name}}</span></div><div><span>Size: </span><span>{{Size}}</span></div><div><span>Data: </span><span>{{Data}}</span></div>", data = document.getElementById("fileData"), preventDefault = function(e){ e.preventDefault(); }, highlight = { add: function(e){ preventDefault(e); e.target.classList.add("hoverClass"); }, remove: function(e){ preventDefault(e); e.target.classList.remove("hoverClass"); } }, uploader = document.getElementById("fileElement"); if (window.FileReader){ uploader.addEventListener("dragover", highlight.add, false); uploader.addEventListener("mouseout", highlight.remove, false); uploader.addEventListener("drop", function(e){ var file = e.dataTransfer.files[0], fileReader = new FileReader(); if (file) { if (fileReader){ fileReader.addEventListener("loadend", function(e){ data.innerHTML = template.replace("{{Name}}", file.name).replace("{{Size}}", file.size).replace("{{Data}}", e.target.result.substring(0, 10)); }, false); fileReader.readAsText(file); } else { alert("Sorry, but this browser isn't smart enough( Choose another one"); } } preventDefault(e); }, false); } else { alert("Sorry, but this browser isn't smart enough( Choose another one"); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 var template = "<div><span>Name: </span><span>{{Name}}</span></div><div><span>Size: </span><span>{{Size}}</span></div><div><span>Data: </span><span>{{Data}}</span></div>" , data = document . getElementById ( "fileData" ) , preventDefault = function ( e ) { e . preventDefault ( ) ; } , highlight = { add : function ( e ) { preventDefault ( e ) ; e . target . classList . add ( "hoverClass" ) ; } , remove : function ( e ) { preventDefault ( e ) ; e . target . classList . remove ( "hoverClass" ) ; } } , uploader = document . getElementById ( "fileElement" ) ; if ( window . FileReader ) { uploader . addEventListener ( "dragover" , highlight . add , false ) ; uploader . addEventListener ( "mouseout" , highlight . remove , false ) ; uploader . addEventListener ( "drop" , function ( e ) { var file = e . dataTransfer . files [ 0 ] , fileReader = new FileReader ( ) ; if ( file ) { if ( fileReader ) { fileReader . addEventListener ( "loadend" , function ( e ) { data . innerHTML = template . replace ( "{{Name}}" , file . name ) . replace ( "{{Size}}" , file . size ) . replace ( "{{Data}}" , e . target . result . substring ( 0 , 10 ) ) ; } , false ) ; fileReader . readAsText ( file ) ; } else { alert ( "Sorry, but this browser isn't smart enough( Choose another one" ) ; } } preventDefault ( e ) ; } , false ) ; } else { alert ( "Sorry, but this browser isn't smart enough( Choose another one" ) ; }

A few important remarks about this demo:

1) First of all, you shouldn’t check if your browser supports the “ondrop” event because of IE9. It supports this event but doesn’t have files in the dataTransfer property.

2) If you want to catch the “ondrop” event, you have to add an event handler to the “ondragover” event and call the preventDefault function. By default, the browser will try to download this file or render it on the page (if it’s an image or a pdf file) and you should stop it.

A minute of graceful degradation: for the old browsers like IE9 you may place an input element from the previous examples over the current element, stretch it, set zero opacity to it and change the text to: “Click and select file”. When a user clicks on this element, he will see a usual dialog for choosing files. This solution also works for tablets and phones that provide no possibility to split screens and drag files between them.

Drag Me out of Browser!

The previous example could have triggered one question about drag and drop, namely: “Can we drag files out of the browser to the desktop?”. The answer is “YES”, but it works only in Chrome. Nevertheless, it’s a cool feature for your web page, and we should give it a try. In the next example, you may drag the PDF image to your desktop, and it will be saved as a PDF file.

Drag it to your desktop

document.getElementById("fileData").addEventListener("dragstart", function(e){ e.dataTransfer.setData("DownloadURL", "application/pdf:doc.pdf:https://mywebsite.com"); }, false); 1 2 3 document . getElementById ( "fileData" ) . addEventListener ( "dragstart" , function ( e ) { e . dataTransfer . setData ( "DownloadURL" , "application/pdf:doc.pdf:https://mywebsite.com" ) ; } , false ) ;

The code of this example is pretty easy. All you need is to add a handler to the “dragstart” event and set data to transfer. It can be a link to some documents on the server or binary data. If you want to get a file from the server, you need to pass a special string that consists of content type, name of the file that will be saved and the full path to it. For example, “application/pdf:newDoc.pdf:https://mywebsite.com”. As has been mentioned, it works only in Chrome. However, that is not a problem because it’s not the basic functionality of your web application. It’s just a cool feature for Chrome users that might be replaced with a usual link for downloading this file from the server.

“Everything changes and nothing remains still … and … you cannot step twice into the same stream” (Heraclitus)

Now we can get files and their content in JavaScript. But what can we do with them? How can we change them? That is a good question. First of all, we can work with the text. When we read the file as a text string, we will get all its text content and parse it. For example, a user may drop a new CSS file, and you can apply it to your page straight away. Or, if it’s a file with an article that should be published, you can read it, apply some styles to the titles and place it in the text editor on your page.

One of the examples can be parsing of an excel file. You don’t need to send it to the server, convert it to JSON and send it back. That can be done on the client-side without spending time on sending a request and waiting for the response from the server. There are already some libraries for parsing xl and xlsx files in your browser that you may find here: XLS and XLSX.

Another way of using it is to work with binary data. It could be images, videos, or some other files. If we want to process binary data, we need to use some new JavaScript features like Blob objects and typed arrays. We won’t dive deeply into this theme because it’s too big and deserves the whole article. The Blob object is an object that looks like a usual file in JavaScript and contains raw data.

The constructor for this object takes two parameters: parts of data and object with options. Parts of data can be ArrayBuffer, typed array, Blob, or strings. They represent data that will be stored in a new Blob. The options include the data type.

new Blob(["<div>Hello world</div>"], {type: "text/html"}) 1 new Blob ( [ "<div>Hello world</div>" ] , { type : "text/html" } )

Each Blob object has type and size properties as well as the slice method. All data stored in this Blob can be split with the method slice. This method returns another Blob object with a sliced part of the data. Typed arrays and buffers are used for storing fixed-length binary data of the particular type (Int8Array or Int32Array). This let us make some interesting stuff on the client-side, for example, compress files.

This library makes it possible to apply the deflate algorithm to all chosen files and collect them into one zip file. It uses WebWokers to run this process in the background. One more example of processing file binary data is PDF.js. It’s an open source project for rendering PDF documents on the web page. You just need to pass an array with binary data to it, and it will draw the content of the PDF document on the canvas.

Future. Make Your File System with Blackjack and …

For now, we know how to get files and change them. The time has come to reveal the secret of file storage. Yes, we are talking about creating and saving folders and files inside your browser.

At the time when this article was written only Chrome and Opera support this feature. And again it’s not a big deal because it can be used as an additional tool for caching files in your browser. Let’s look closer at its API. First of all, we need to request a file system object with the webkitRequestFileSystem method. It takes four parameters: type, size, success callback, and error callback.

There are two types of storages in the browser: temporary and persistent. The first one can be created without asking a user for permission. You just create and use it. However, if your browser encounters the lack of memory or some other problem, it will remove all stored data. That’s why this type of storage may be used to cache data that can be requested from the server one more time or restored.

Another type of storage is persistent, and the browser will never remove files saved there. Nonetheless, if to request this type of storage without asking the user, it won’t work.

To request user permission to use persistent storage we need to call navigator.webkitPersistentStorage.requestQuota method. The constants for these types of storages are stored in the window object: window.TEMPORARY or window.PERSISTENT. The second parameter is the size of storage that we need in bytes. Now we can create our first 1Gb storage. Temporary:

window.webkitRequestFileSystem(window.TEMPORARY, 1000*1024*1024, function(fs){ alert("Storage is ready"); }, errorHandler); 1 2 3 window . webkitRequestFileSystem ( window . TEMPORARY , 1000 * 1024 * 1024 , function ( fs ) { alert ( "Storage is ready" ) ; } , errorHandler ) ;

Permanent:

navigator.webkitPersistentStorage.requestQuota(1000*1024*1024, function(bytes) { window.webkitRequestFileSystem(window.PERSISTENT, bytes, function(fs){ alert("storage is ready"); }, errorHandler); }, errorHandler); 1 2 3 4 5 navigator . webkitPersistentStorage . requestQuota ( 1000 * 1024 * 1024 , function ( bytes ) { window . webkitRequestFileSystem ( window . PERSISTENT , bytes , function ( fs ) { alert ( "storage is ready" ) ; } , errorHandler ) ; } , errorHandler ) ;

As you can see, we have lots of errorHandlers that are used for catching errors connected with storages creation. The object FileError contains the full list of errors’ types.

So, now we have access to the storage, and we can create our first folder and a file, and save them.

After the usage of the permanent storage (the same with the temporary storage) is requested and approved, we get the file system object as the first argument of success callback. This object has the following interface:

As you can see, there are special methods for creating, moving, removing, moving, and getting files and directories. The fs object that we got has a reference to the root directory of the browser file system. This one and all other directories that we will create are represented by the DirectoryEntry interface. All files in this file system are represented by the FileEntry interface. We will use the getFile and the getDirectory methods to create and get the already created files and folders.

The first parameter of this object is a file or a directory path. It can be a relative or an absolute path. The second parameter of these methods is an object with options, in case of absence of a file with such path. It includes the properties “create” and “exclusive”. If the property “create” is set to “true”, a new file or directory will be created, and the old file will be overwritten.

If we set it to “false”, it will return a FileEntry instance or throw an error, if there is no such file. The property “exclusive” set to “true” will prevent files overwriting. To distinguish files and folders, each of the two interfaces has flags: isDirectory and isFile. There is also a special class for adding content to file: FileWriter. It has to be requested for each file that needs to be changed. Let’s add some files:

//create folder "files" in the root folder fs.root.getDirectory("files", {create: true}, function(directory){ //create in folder "files" file newFile.txt directory.getFile('newFile.txt', {create: true}, function(file) { //add some text to "newFile/txt" file.createWriter(function(writer) { writer.onwriteend = function(e) { alert("done"); }; // Create a new Blob and write it to log.txt. var blob = new Blob(['hello world!!!!'], {type: 'text/plain'}); writer.write(blob); }); }, errorHandler); }); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 //create folder "files" in the root folder fs . root . getDirectory ( "files" , { create : true } , function ( directory ) { //create in folder "files" file newFile.txt directory . getFile ( 'newFile.txt' , { create : true } , function ( file ) { //add some text to "newFile/txt" file . createWriter ( function ( writer ) { writer . onwriteend = function ( e ) { alert ( "done" ) ; } ; // Create a new Blob and write it to log.txt. var blob = new Blob ( [ 'hello world!!!!' ] , { type : 'text/plain' } ) ; writer . write ( blob ) ; } ) ; } , errorHandler ) ; } ) ;

Everything passed well, and it seems that there are no errors. Let’s check our file and folder. You should enable some experimental features in Chrome DevTool to see the content of the browser files system. That’s why you have to go to the chrome://flags/#enable-devtools-experiments page and enable it. After reloading Chrome, you will see a tab “Experiments” in the Chrome DevTool options. There you should enable File System inspection and relaunch the DevTool:

After that, you will see the FileSystem option on the “Resources” tab of the DevTool. Here all created files and storages are displayed:

We may also get these files by a special internal browser URL that can be set as the src attribute of the IMG tag. It starts with “filesystem:” and looks like this: “filesystem:{URL of site}/persistent/files/newFile.txt”. We used the “toURL” method to get it.

Now we have all the information that we need to create a web application to cache audio and video files, and listen and watch them without the Internet.

Thanks for your attention and feel free to leave your comments and questions.