📜 ⬆️ ⬇️

FileSystem API & File API: we understand and use

HTML5 Powered with Performance & Integration, and Offline & Storage
In this article I want to review the FileSystem API and File API, deal with its methods and show a couple of useful pieces. This article is a compilation of materials with html5rocks ( 1 , 2 , 3 ). All the demos below can be viewed at the first two links. The third link also offers a number of interesting demos. Well, now let's study the material.

As an introduction


Using the FileSystem API and File API, a web application can create, read, view and write files located in the user sandbox area.

The API description is divided into the following sections:

A few words about the objects with which to work:
  1. File - the file itself; allows you to get read-only information such as name, size, mimetype, and more.
  2. FileList is an “array” of File objects.
  3. Blob is an entity that allows to parse a file by bytes.

Browser support and storage restrictions

At the time of this writing, only Google Chrome 9+ has a working implementation of the FileSystem API. And at the moment there are no dialog boxes for managing files and quotas on storage, so you will need to use the - unlimited-quota-for-files flag (if you develop applications for the Chrome Web Store, a manifest with unlimitedStorage permission will be enough). But everything changes and users will soon have the opportunity to manage the rights to work with the files that will be required by the application.

You may need to use the --allow-file-access-from-files flag if you debug an application using file: // . If this flag is not used, then exceptions of type SECURITY_ERR or QUOTA_EXCEEDED_ERR will be thrown.
')

Accessing the file system


Let's check the browser support for the functions we need.
//   File API if (window.File && window.FileReader && window.FileList && window.Blob) { //  } else { alert('File API    '); } 

A web application can access the file system (of course in a restricted sandbox) by calling the following window.requestFileSystem () method:
 window.requestFileSystem(type, size, successCallback, opt_errorCallback) 

type
Retention rules, available window.TEMPORARY and window.PERSISTENT values . data stored using the TEMPORARY key may be deleted at the discretion of the browser (for example, if there is not enough space). If the PERSISTENT key is set, then the data can be cleared only after user or application actions.

size
The size (in bytes) of the storage that the application will need.

successCallback
Callback function performed in case of successful access to the file system. Its argument is an object of type FileSystem .

opt_errorCallback
Optional callback function for error handling. It is also called when a file system access error occurs. The parameter is an object of type FileError .

If you call the requestFileSystem () method within your application for the first time, then at that moment the storage will be created. It is very important to remember that this storage is closed and another application will not have access to it. It also means that the application cannot change other files and folders located on the hard disk.

Usage example
 function onInitFs(fs) { console.log('Opened file system: ' + fs.name); } window.requestFileSystem(window.PERSISTENT, 5*1024*1024 /*5MB*/, onInitFs, errorHandler); 

The FileSystem specification also describes an API for synchronous operation, namely the LocalFileSystemSync interface, which is supposed to be used together by Web Workers. But in this article this API will not be considered.

Returning to the requestFileSystem () method, it is worth describing the possible errors that occur:
 function errorHandler(e) { var msg = ''; switch (e.code) { case FileError.QUOTA_EXCEEDED_ERR: msg = 'QUOTA_EXCEEDED_ERR'; break; case FileError.NOT_FOUND_ERR: msg = 'NOT_FOUND_ERR'; break; case FileError.SECURITY_ERR: msg = 'SECURITY_ERR'; break; case FileError.INVALID_MODIFICATION_ERR: msg = 'INVALID_MODIFICATION_ERR'; break; case FileError.INVALID_STATE_ERR: msg = 'INVALID_STATE_ERR'; break; default: msg = 'Unknown Error'; break; }; console.log('Error: ' + msg); } 

The described example is very simple, but in fact is a blank for further work with errors that occur.

Work with files


FileEntry interface is provided for working with files. It has a number of methods and properties that we used to associate with regular files. We give them below:
 fileEntry.isFile === true fileEntry.isDirectory === false fileEntry.name fileEntry.fullPath ... fileEntry.getMetadata(successCallback, opt_errorCallback); fileEntry.remove(successCallback, opt_errorCallback); fileEntry.moveTo(dirEntry, opt_newName, opt_successCallback, opt_errorCallback); fileEntry.copyTo(dirEntry, opt_newName, opt_successCallback, opt_errorCallback); fileEntry.getParent(successCallback, opt_errorCallback); fileEntry.toURI(opt_mimeType); // Currently not implemented in Google Chrome 9. fileEntry.file(successCallback, opt_errorCallback); fileEntry.createWriter(successCallback, opt_errorCallback); ... 

Let's take as examples the basics of working with FileEntry.

File creation

You can get or create a file using the getFile () method from the DirectoryEntry interface. After accessing the repository, callback returns us a FileSystem object containing DirectoryEntry (fs.root), which refers to the repository root folder.

The following code will create an empty log.txt file:
 function onInitFs(fs) { fs.root.getFile('log.txt', {create: true, exclusive: true}, function(fileEntry) { // fileEntry     // fileEntry.isFile === true // fileEntry.name == 'log.txt' // fileEntry.fullPath == '/log.txt' }, errorHandler); } window.requestFileSystem(window.PERSISTENT, 1024*1024, onInitFs, errorHandler); 

So, after accessing the file storage, we have FileSystem in our hands. Inside the callback function, we can refer to the fs.root.getFile () method, passing the name of the file to be created. You can pass both relative and absolute path - the main thing is that it is correct. For example, creating a file is erroneous if its parent folder does not exist. The second argument of the getFile () method is an object that describes the parameters of the object that will be applied to it if it has not yet been created. More information can be found in the documentation.

Reading a file by name

The following code accesses the “log.txt” file and reads its contents using the FileReader API, and then writes the entire contents to the <textarea> block. If the file does not exist, an error will be thrown.
 function onInitFs(fs) { fs.root.getFile('log.txt', {}, function(fileEntry) { fileEntry.file(function(file) { var reader = new FileReader(); reader.onloadend = function(e) { var txtArea = document.createElement('textarea'); txtArea.value = this.result; document.body.appendChild(txtArea); }; reader.readAsText(file); }, errorHandler); }, errorHandler); } window.requestFileSystem(window.PERSISTENT, 1024*1024, onInitFs, errorHandler); 

FileReader, among other things, provides the following methods for reading:

In the following example, we will read the image and display its thumbnail:
 <style> .thumb { height: 75px; border: 1px solid #000; margin: 10px 5px 0 0; } </style> <input type="file" id="files" name="files[]" multiple /> <output id="list"></output> <script> function handleFileSelect(evt) { var files = evt.target.files; // FileList object // Loop through the FileList and render image files as thumbnails. for (var i = 0, f; f = files[i]; i++) { // Only process image files. if (!f.type.match('image.*')) { continue; } var reader = new FileReader(); // Closure to capture the file information. reader.onload = (function(theFile) { return function(e) { // Render thumbnail. var span = document.createElement('span'); span.innerHTML = ['<img class="thumb" src="', e.target.result, '" title="', theFile.name, '"/>'].join(''); document.getElementById('list').insertBefore(span, null); }; })(f); // Read in the image file as a data URL. reader.readAsDataURL(f); } } document.getElementById('files').addEventListener('change', handleFileSelect, false); </script> 

Sometimes we may need not the whole file, but only a part of it, for this it is convenient to use File.slice (start_byte, length) .
It looks like this:
 var blob = file.slice(startingByte, length); reader.readAsBinaryString(blob); 

In the following example, we can read either the necessary bytes, or the entire file. Pay particular attention to onloadend and evt.target.readyState , which in this case will replace the onload event . (About the events below).
 <style> #byte_content { margin: 5px 0; max-height: 100px; overflow-y: auto; overflow-x: hidden; } #byte_range { margin-top: 5px; } </style> <input type="file" id="file" name="file" /> Read bytes: <span class="readBytesButtons"> <button data-startbyte="0" data-endbyte="4">1-5</button> <button data-startbyte="5" data-endbyte="14">6-15</button> <button data-startbyte="6" data-endbyte="7">7-8</button> <button>entire file</button> </span> <div id="byte_range"></div> <div id="byte_content"></div> <script> function readBlob(opt_startByte, opt_stopByte) { var files = document.getElementById('files').files; if (!files.length) { alert('Please select a file!'); return; } var file = files[0]; var start = opt_startByte || 0; var stop = opt_stopByte || file.size - 1; var reader = new FileReader(); // If we use onloadend, we need to check the readyState. reader.onloadend = function(evt) { if (evt.target.readyState == FileReader.DONE) { // DONE == 2 document.getElementById('byte_content').textContent = evt.target.result; document.getElementById('byte_range').textContent = ['Read bytes: ', start + 1, ' - ', stop + 1, ' of ', file.size, ' byte file'].join(''); } }; var length = (stop - start) + 1; var blob = file.slice(start, length); reader.readAsBinaryString(blob); } document.querySelector('.readBytesButtons').addEventListener('click', function(evt) { if (evt.target.tagName.toLowerCase() == 'button') { var startByte = evt.target.getAttribute('data-startbyte'); var endByte = evt.target.getAttribute('data-endbyte'); readBlob(startByte, endByte); } }, false); </script> 

Now about the events. FileReader provides us with the following types of events:
You can use them when you need to display the file upload process. For example:
 <style> #progress_bar { margin: 10px 0; padding: 3px; border: 1px solid #000; font-size: 14px; clear: both; opacity: 0; -moz-transition: opacity 1s linear; -o-transition: opacity 1s linear; -webkit-transition: opacity 1s linear; } #progress_bar.loading { opacity: 1.0; } #progress_bar .percent { background-color: #99ccff; height: auto; width: 0; } </style> <input type="file" id="file" name="file" /> <button onclick="abortRead();">Cancel read</button> <div id="progress_bar"><div class="percent">0%</div></div> <script> var reader; var progress = document.querySelector('.percent'); function abortRead() { reader.abort(); } function errorHandler(evt) { switch(evt.target.error.code) { case evt.target.error.NOT_FOUND_ERR: alert('File Not Found!'); break; case evt.target.error.NOT_READABLE_ERR: alert('File is not readable'); break; case evt.target.error.ABORT_ERR: break; // noop default: alert('An error occurred reading this file.'); }; } function updateProgress(evt) { // evt is an ProgressEvent. if (evt.lengthComputable) { var percentLoaded = Math.round((evt.loaded / evt.total) * 100); // Increase the progress bar length. if (percentLoaded < 100) { progress.style.width = percentLoaded + '%'; progress.textContent = percentLoaded + '%'; } } } function handleFileSelect(evt) { // Reset progress indicator on new file selection. progress.style.width = '0%'; progress.textContent = '0%'; reader = new FileReader(); reader.onerror = errorHandler; reader.onprogress = updateProgress; reader.onabort = function(e) { alert('File read cancelled'); }; reader.onloadstart = function(e) { document.getElementById('progress_bar').className = 'loading'; }; reader.onload = function(e) { // Ensure that the progress bar displays 100% at the end. progress.style.width = '100%'; progress.textContent = '100%'; setTimeout("document.getElementById('progress_bar').className='';", 2000); } // Read in the image file as a binary string. reader.readAsBinaryString(evt.target.files[0]); } document.getElementById('files').addEventListener('change', handleFileSelect, false); </script> 


Write to file

Using the following code, we will create a file “log.txt” (if it does not exist) and write 'Ipsum Lorem' into it.
 function onInitFs(fs) { fs.root.getFile('log.txt', {create: true}, function(fileEntry) { fileEntry.createWriter(function(fileWriter) { fileWriter.onwriteend = function(e) { console.log('Write completed.'); }; fileWriter.onerror = function(e) { console.log('Write failed: ' + e.toString()); }; var bb = new BlobBuilder(); bb.append('Ipsum Lorem'); fileWriter.write(bb.getBlob('text/plain')); }, errorHandler); }, errorHandler); } window.requestFileSystem(window.PERSISTENT, 1024*1024, onInitFs, errorHandler); 

As you can see, we call the createWriter () method to get the object. In addition, we handle the event of the end of writing to the file and possible creation of an error.

We add data to the file

The following code will add 'Hello World' to the end of the file. If there is no file, then an error is thrown.
 function onInitFs(fs) { fs.root.getFile('log.txt', {create: false}, function(fileEntry) { fileEntry.createWriter(function(fileWriter) { fileWriter.seek(fileWriter.length); // Start write position at EOF. var bb = new BlobBuilder(); bb.append('Hello World'); fileWriter.write(bb.getBlob('text/plain')); }, errorHandler); }, errorHandler); } window.requestFileSystem(window.PERSISTENT, 1024*1024, onInitFs, errorHandler); 


Create copies of selected files

The following code allows the user to select multiple files using <input type = "file" multiple> and creates copies of these files.
 <input type="file" id="myfile" multiple /> 

 document.querySelector('#myfile').onchange = function(e) { var files = this.files; window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) { for (var i = 0, file; file = files[i]; ++i) { (function(f) { fs.root.getFile(file.name, {create: true, exclusive: true}, function(fileEntry) { fileEntry.createWriter(function(fileWriter) { fileWriter.write(f); // Note: write() can take a File or Blob object. }, errorHandler); }, errorHandler); })(file); } }, errorHandler); }; 

To simplify file selection, you can use HTML5 Drag and Drop.

 <div id="drop_zone">Drop files here</div> <output id="list"></output> <script> function handleFileSelect(evt) { evt.stopPropagation(); evt.preventDefault(); var files = evt.dataTransfer.files; // FileList object. // files is a FileList of File objects. List some properties. var output = []; for (var i = 0, f; f = files[i]; i++) { output.push('<li><strong>', f.name, '</strong> (', f.type || 'n/a', ') - ', f.size, ' bytes, last modified: ', f.lastModifiedDate.toLocaleDateString(), '</li>'); } document.getElementById('list').innerHTML = '<ul>' + output.join('') + '</ul>'; } function handleDragOver(evt) { evt.stopPropagation(); evt.preventDefault(); } // Setup the dnd listeners. var dropZone = document.getElementById('drop_zone'); dropZone.addEventListener('dragover', handleDragOver, false); dropZone.addEventListener('drop', handleFileSelect, false); </script> 


Deleting files

The following code will remove 'log.txt'.
 window.requestFileSystem(window.PERSISTENT, 1024*1024, function(fs) { fs.root.getFile('log.txt', {create: false}, function(fileEntry) { fileEntry.remove(function() { console.log('File removed.'); }, errorHandler); }, errorHandler); }, errorHandler); 


Work with directories


Working with directories is accomplished through the use of DirectoryEntry, which has most of the FileEntry properties (they both inherit the Entry interface). Listed below are the properties and methods of DirectoryEntry.
 dirEntry.isDirectory === true //      FileEntry ... var dirReader = dirEntry.createReader(successCallback, opt_errorCallback); dirEntry.getFile(path, opt_flags, opt_successCallback, opt_errorCallback); dirEntry.getDirectory(path, opt_flags, opt_successCallback, opt_errorCallback); dirEntry.removeRecursively(successCallback, opt_errorCallback); ... 


Creating directories

To create and access directories, getDirectory () is used for the DirectoryEntry interface. You can send both the name and the path to the directory.

Create the MyPictures directory at the root:
 window.requestFileSystem(window.PERSISTENT, 1024*1024, function(fs) { fs.root.getDirectory('MyPictures', {create: true}, function(dirEntry) { ... }, errorHandler); }, errorHandler); 


Subdirectories

Creating subdirectories is essentially the same as creating directories, but be aware that if the parent directory does not exist, an error will be thrown. The following code shows how to get around this limitation:
 var path = 'music/genres/jazz/'; function createDir(rootDirEntry, folders) { // './'  '/' if (folders[0] == '.' || folders[0] == '') { folders = folders.slice(1); } rootDirEntry.getDirectory(folders[0], {create: true}, function(dirEntry) { if (folders.length) { createDir(dirEntry, folders.slice(1)); } }, errorHandler); }; function onInitFs(fs) { createDir(fs.root, path.split('/')); // fs.root is a DirectoryEntry. } window.requestFileSystem(window.PERSISTENT, 1024*1024, onInitFs, errorHandler); 

Now we have created the “music / genres / jazz” directory and we can access any of its levels and create new files in them. For example:
 window.requestFileSystem(window.PERSISTENT, 1024*1024, function(fs) { fs.root.getFile('/music/genres/jazz/song.mp3', {create: true}, function(fileEntry) { ... }, errorHandler); }, errorHandler); 


Parse the contents of the directory

To find out what is contained in the directory, you need to create a DirectoryReader and call its readEntries () method, but there is no guarantee that the entire contents of the selected directory will be returned (!!!). This means that you need to access the DirectoryReader.readEntries () method until the result is empty. Example:
 <ul id="filelist"></ul> 

 function toArray(list) { return Array.prototype.slice.call(list || [], 0); } function listResults(entries) { var fragment = document.createDocumentFragment(); entries.forEach(function(entry, i) { var img = entry.isDirectory ? '<img src="folder-icon.gif">' : '<img src="file-icon.gif">'; var li = document.createElement('li'); li.innerHTML = [img, '<span>', entry.name, '</span>'].join(''); fragment.appendChild(li); }); document.querySelector('#filelist').appendChild(fragment); } function onInitFs(fs) { var dirReader = fs.root.createReader(); var entries = []; var readEntries = function() { dirReader.readEntries (function(results) { if (!results.length) { listResults(entries.sort()); } else { entries = entries.concat(toArray(results)); readEntries(); } }, errorHandler); }; readEntries(); } window.requestFileSystem(window.PERSISTENT, 1024*1024, onInitFs, errorHandler); 


Delete the directory

To delete, call the DirectoryEntry.remove () method. It is important to know that if you try to delete a non-empty directory, an error will be thrown.

Delete the empty jazz directory from "/ music / genres /":
 window.requestFileSystem(window.PERSISTENT, 1024*1024, function(fs) { fs.root.getDirectory('music/genres/jazz', {}, function(dirEntry) { dirEntry.remove(function() { console.log('Directory removed.'); }, errorHandler); }, errorHandler); }, errorHandler); 


Recursively delete directories

If you have a non-empty directory and you still want to remove it, then the removeRecursively () method will help you, which will remove both the directory and all its contents.

Perform this operation with the “music” directory:
 window.requestFileSystem(window.PERSISTENT, 1024*1024, function(fs) { fs.root.getDirectory('/misc/../music', {}, function(dirEntry) { dirEntry.removeRecursively(function() { console.log('Directory removed.'); }, errorHandler); }, errorHandler); }, errorHandler); 


Copy, rename and move


FileEntry and DirectoryEntry are completely identical in this aspect.

We copy

Both FileEntry and DirectoryEntry have a copyTo () method for copying. In the case of directories, the method will recursively create all the content.

Copy "me.png" from one directory to another:
 function copy(cwd, src, dest) { cwd.getFile(src, {}, function(fileEntry) { cwd.getDirectory(dest, {}, function(dirEntry) { dirEntry.copyTo(dirEntry); }, errorHandler); }, errorHandler); } window.requestFileSystem(window.PERSISTENT, 1024*1024, function(fs) { copy(fs.root, '/folder1/me.png', 'folder2/mypics/'); }, errorHandler); 


Move or rename

FileEntry and DirectoryEntry have a moveTo () method that allows you to move and rename files and directories. The first argument is the parent folder, the second (optional) parameter is the new name.

Rename "me.png" to "you.png":
 function rename(cwd, src, newName) { cwd.getFile(src, {}, function(fileEntry) { fileEntry.moveTo(cwd, newName); }, errorHandler); } window.requestFileSystem(window.PERSISTENT, 1024*1024, function(fs) { rename(fs.root, 'me.png', 'you.png'); }, errorHandler); 


Move “me.png” from the root directory to “newfolder”.
 function move(src, dirName) { fs.root.getFile(src, {}, function(fileEntry) { fs.root.getDirectory(dirName, {}, function(dirEntry) { fileEntry.moveTo(dirEntry); }, errorHandler); }, errorHandler); } window.requestFileSystem(window.PERSISTENT, 1024*1024, function(fs) { move('/me.png', 'newfolder/'); }, errorHandler); 


Use Cases

HTML5 offers several options for local data storage, but FileSystem is different in that it allows you to satisfy those needs that do not satisfy the database. Basically, these are application needs that store large amounts of binary data and / or provide file access to applications outside the browser.

We give a list of possible applications:
  1. Uploader files
  2. Games and application that work with media files
  3. Audio / photo editor, offline mode
  4. Offlane video player
  5. Offline email client

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


All Articles