1 /* global 
  2 /**
  3  * @package webfs
  4  * @copyright  Copyright(c) 2011 Ajax.org B.V. <info AT ajax.org>
  5  * @author Tane Piper <tane AT ajax DOT org>
  6  * @license http://github.com/ajaxorg/webfs/blob/master/LICENSE MIT License
  7  */
  8 
  9 /**
 10  * We need to ensure that we have the correct version of requestFileSystem and
 11  * BlobBuilder available to the script
 12  */
 13 window.requestFileSystem = window.requestFileSystem || window.webkitRequestFileSystem;
 14 window.BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder;
 15 
 16 
 17 /**
 18  * @class WebFS object
 19  * @description Can take a pre-existing native local filesystem as a
 20  *              parameter.  If not parameter passed, filesystem can be set with
 21  *              setFileSystem 
 22  */
 23 var WebFS = (function() {
 24     
 25     /**
 26      * @constructor
 27      * @description Can take a pre-existing native local filesystem as a
 28      *              parameter.  If not parameter passed, filesystem can be set with
 29      *              setFileSystem
 30      * @param   {DOMFileSystem}     fs
 31      * @type    {void}
 32      */
 33     function WebFS(fs) {
 34         this.fs = fs;
 35         if (fs)
 36             this.root = fs.root;
 37     }
 38     
 39     // Filesystem system types flags
 40     WebFS.prototype.TEMPORARY                   = 0;
 41     WebFS.prototype.PERSISTENT                  = 1;
 42     // Filesystem progress flags
 43     WebFS.prototype.EMPTY                       = 0;
 44     WebFS.prototype.LOADING                     = 1;
 45     WebFS.prototype.DONE                        = 2;
 46     // Filesystem error flags
 47     WebFS.prototype.NOT_FOUND_ERR               = 1;
 48     WebFS.prototype.SECURITY_ERR                = 2;
 49     WebFS.prototype.ABORT_ERR                   = 3;
 50     WebFS.prototype.NOT_READABLE_ERR            = 4;
 51     WebFS.prototype.ENCODING_ERR                = 5;
 52     WebFS.prototype.NO_MODIFICATION_ALLOWED_ERR = 6;
 53     WebFS.prototype.INVALID_STATE_ERR           = 7;
 54     WebFS.prototype.SYNTAX_ERR                  = 8;
 55     WebFS.prototype.INVALID_MODIFICATION_ERR    = 9;
 56     WebFS.prototype.QUOTA_EXCEEDED_ERR          = 10;
 57     WebFS.prototype.TYPE_MISMATCH_ERR           = 11;
 58     WebFS.prototype.PATH_EXISTS_ERR             = 12;
 59     
 60     WebFS.prototype.DIR_SEPARATOR = '/';
 61     WebFS.prototype.DIR_BLACKLIST = ['.', './', '..', '../', '/'];
 62     WebFS.prototype.TYPE_FILE = 'file';
 63     WebFS.prototype.TYPE_DIR = 'dir';
 64     
 65     
 66     /**
 67      * Creates a stats object
 68      * @private
 69      * @param   {FileEntry, DirectoryEntry}   entry
 70      * @param   {Function}                    callback
 71      * @type    {void}
 72      */
 73     var Stats = function(entry, callback) {
 74         var _self = {};
 75         
 76         if (entry.isFile) {
 77             
 78             entry.file(function(file) {
 79                 
 80                 var t = file.lastModifiedDate;
 81                 var m = t.getMonth() + 1;
 82                 var month = m < 10 ? "0" + m : m;
 83             
 84                 var time = [t.getFullYear(), month, t.getDate()].join('-') + 'T' + [t.getHours(), t.getMinutes(), t.getSeconds()].join(':') + 'Z';
 85             
 86                 _self.mtime = time;
 87                 _self.atime = time;
 88                 _self.ctime = time;
 89                 
 90                 _self.size = file.fileSize;
 91                 
 92                 afterMetaData()
 93             });
 94         } else {
 95             entry.getMetadata(function(metadata) {
 96                 var t = metadata.modificationTime;
 97                 var m = t.getMonth() + 1;
 98                 var month = m < 10 ? "0" + m : m;
 99                 
100                 var time = [t.getFullYear(), month, t.getDate()].join('-') + 'T' + [t.getHours(), t.getMinutes(), t.getSeconds()].join(':') + 'Z';
101                 
102                 _self.mtime = time;
103                 _self.atime = time;
104                 _self.ctime = time;
105                 
106                 _self.size = 0;
107                 
108                 afterMetaData()
109             });
110         }
111         
112         function afterMetaData() {
113             
114             _self.dev = 0;
115             _self.ino = 0;
116             _self.mode = 0;
117             _self.nlink = 0;
118             _self.uid = 0;
119             _self.gid = 0;
120             _self.rdev = 0;
121             _self.blocks = 0;
122             
123             
124             _self.isDirectory = entry.isDirectory;
125             _self.isFile = entry.isFile;
126             
127             /**
128              * These next stats functions all return false for nodejs compatibility
129              */
130             _self.isBlockDevice = false;
131             _self.isCharacterDevice = false;
132             _self.isSymbolicLink = false;
133             _self.isFIFO = false;
134             _self.isSocket = false;
135             
136             callback(null, _self);
137         }
138     };
139 
140     /**
141      * Returns if requestFileSystem is available
142      * @type    {void}
143      */
144     WebFS.prototype.isAvailable = function(){
145         return !!window.requestFileSystem;
146     };
147     
148     /**
149      * Error handler for file system operations
150      * @param   {Error}     error
151      * @type    {void}
152      */
153     WebFS.prototype.errorHandler = function(error) {
154         var msg;
155         
156         switch(error.code) {
157             case this.NOT_FOUND_ERR:
158                 error.message = "The file or directory has not been found";
159                 break;
160             case this.SECURITY_ERR:
161                 error.message = "The file you are attempting to access is unsafe for web access or may be being accessed too many times.";
162                 break;
163             case this.ABORT_ERR:
164                 error.message = "The current operation has been aborted";
165                 break;
166             case this.NOT_READABLE_ERR:
167                 error.message = "The file you are attempting to read is not readable, this may be a permissions issue.";
168                 break;
169             case this.ENCODING_ERR:
170                 error.message = "The data or URL passed is malformed";
171                 break;
172             case this.NO_MODIFICATION_ALLOWED_ERR:
173                 error.message = "The file or directory cannot be modified.";
174                 break;
175             case this.INVALID_STATE_ERR:
176                 error.message = "The file or directory state has changed since the last operation.";
177                 break;
178             case this.SYNTAX_ERR:
179                 error.message = "There is a syntax error with this file operation.";
180                 break;
181             case this.INVALID_MODIFICATION_ERR:
182                 error.message = "Invalid file operation.";
183                 break;
184             case this.QUOTA_EXCEEDED_ERR:
185                 msg = "The quota for the filesystem has been exceeded.";
186                 break;
187             case this.TYPE_MISMATCH_ERR:
188                 error.message = "Incorrect file operation on file or directory.";
189                 break;
190             case this.PATH_EXISTS_ERR:
191                 error.message = "This path already exists";
192                 break;
193         }
194         
195         return error;
196     };
197     
198     /**
199      * If the user does not use an external fs object, we can call this method
200      * to create a new file system object
201      * @param   {Number}    type
202      * @param   {Number}    size
203      * @param   {Function}  callback
204      * @type    {void}
205      */
206     WebFS.prototype.setFileSystem = function(type, size, callback) {
207         var _self = this;
208         
209         var successHandler = function(fs) {
210             _self.fs = fs;
211             _self.root = fs.root;
212             callback(null, _self);
213         };
214         
215         var errorHandler = function(error) {
216             callback(_self.errorHandler(error));
217         };
218         
219         requestFileSystem(type, (size * 1024 *1024), successHandler, errorHandler);
220     };
221     
222     /**
223      * Get the current raw filesystem for this WebFS object
224      * @type    {void}
225      */
226     WebFS.prototype.getFileSystem = function() {
227         return this.fs;
228     };
229     
230     /**
231      * Rename or move src to dest.  If dest is a directory, must contain a trailing '/' char.
232      * @param   {String}    src 
233      * @param   {String}    dest
234      * @param   {Function}  callback
235      * @type    {void}
236      */
237     WebFS.prototype.rename = function(src, dest, callback) {
238         var _self = this;
239         
240         var errorHandler = function(error) {
241             callback(_self.errorHandler(error));
242         };
243         
244         var doMove = function(srcDirEntry, destDirEntry, newName) {
245             var name = newName || null;
246             srcDirEntry.moveTo(destDirEntry, name, function(newDirEntry) {
247                 callback(null, newDirEntry);
248             }, errorHandler);
249         };
250         
251         if (dest[dest.length - 1] == _self.DIR_SEPARATOR) {
252             _self.root.getDirectory(src, {}, function(srcDirEntry) {
253                 // Create blacklist for dirs we can't re-create.
254                 var create = _self.DIR_BLACKLIST.indexOf(dest) != -1 ? false : true;
255              
256                 _self.root.getDirectory(dest, {create: create}, function(destDirEntry) {
257                     doMove(srcDirEntry, destDirEntry);
258                 }, errorHandler);
259              }, function(error) {
260                  // Try the src entry as a file instead.
261                 _self.root.getFile(src, {}, function(srcDirEntry) {
262                     _self.root.getDirectory(dest, {}, function(destDirEntry) {
263                         doMove(srcDirEntry, destDirEntry);
264                     }, errorHandler);
265                 }, errorHandler);
266             });
267         } else {
268             // Treat src/destination as files.
269             _self.root.getFile(src, {}, function(srcFileEntry) {
270                 srcFileEntry.getParent(function(parentDirEntry) {
271                     doMove(srcFileEntry, parentDirEntry, dest);
272               }, errorHandler);
273             }, errorHandler);
274         }
275     };
276     
277     /**
278      * Takes a file handler and truncates the content to the passed length
279      * @param   {FileEntry} fileEntry
280      * @param   {Number}    len
281      * @param   {Function}  callback
282      * @type    {void}
283      */
284     WebFS.prototype.truncate = function(fileEntry, len, callback) {
285         var _self = this;
286         
287         var errorHandler = function(error) {
288             callback(_self.errorHandler(error));
289         };
290     
291         fileEntry.createWriter(function(fileWriter) {
292             fileWriter.onwriteend = function(e) {
293                 callback(null, fileEntry, e);
294             };
295             fileWriter.onerror = errorHandler;
296                 
297             fileWriter.truncate(len);
298         });
299     };
300     
301     /**
302      * Stub chmod function for nodejs compatiblity
303      * @param   {String}    path
304      * @param   {Number}    mode
305      * @param   {Function}  callback
306      * @type    {void}
307      */
308     WebFS.prototype.chmod = function(path, mode, callback) {
309         callback();
310     };
311     
312     /**
313      * Returns a stat object from a path
314      * @param   {String}    path
315      * @param   {Function}  callback
316      * @type    {void}
317      */
318     WebFS.prototype.stat = function(path, callback) {
319         var _self = this;
320         
321         var errorHandler = function(error) {
322             callback(_self.errorHandler(error));
323         };
324         
325         this.open(path, function(error, fileHandler) {
326             if (error && error.code == _self.TYPE_MISMATCH_ERR) {
327                 // Get a directory instead
328                 _self.root.getDirectory(path, {}, function(dirHandler) {
329                     Stats(dirHandler, callback);
330                 }, errorHandler);
331             } else if (error) {
332                 errorHandler(error);
333             } else {
334                 Stats(fileHandler, callback);
335             }
336         });
337     };
338     
339     /**
340      * Returns a stat object from a path
341      * @param   {String}    path
342      * @param   {Function}  callback
343      * @type    {void}
344      * @function
345      */
346     WebFS.prototype.lstat = WebFS.prototype.stat;
347     
348     /**
349      * Returns a stat object from a file descriptor
350      * @param   {FileEntry, DirectoryEntry} fd
351      * @param   {Function}                  callback
352      * @type    {void}
353      */
354     WebFS.prototype.fstat = function(fd, callback) {
355         Stats(fd, callback);
356     };
357     
358     /**
359      * Stub link function for nodejs compatibility
360      * @param   {String}    srcpath
361      * @param   {String}    destpath
362      * @param   {Function}  callback
363      * @type    {void}
364      */
365     WebFS.prototype.link = function(srcpath, destpath, callback) {
366         callback();
367     };
368     
369     /**
370      * Stub symlink function for nodejs compatibility
371      * @param   {String}    linkdata
372      * @param   {String}    path
373      * @param   {Function}  callback
374      * @type    {void}
375      */
376     WebFS.prototype.symlink = function(linkdata, path, callback) {
377         callback();
378     };
379     
380     /**
381      * Stub readlink function for nodejs compatibility
382      * @param   {String}    path
383      * @param   {Function}  callback
384      * @type    {void}
385      */
386     WebFS.prototype.readlink = function(path, callback) {
387         callback();
388     };
389     
390     /**
391      * Stub realpath function for nodejs compatibility
392      * @param   {String}    path
393      * @param   {Function}  callback
394      * @type    {void}
395      */
396     WebFS.prototype.realpath = function(path, callback) {
397         callback();
398     };
399     
400     /**
401      * Deletes a file from the path.  Directories are removed recursivly
402      * @param   {String}    path
403      * @param   {Function}  callback
404      * @type    {void}
405      */
406     WebFS.prototype.unlink = function(path, callback) {
407         var _self = this;
408         
409         var errorHandler = function(error) {
410             callback(_self.errorHandler(error));
411         };
412         
413         _self.root.getFile(path, {}, function(fileEntry) {
414             fileEntry.remove(callback, errorHandler);
415         }, function(error) {
416             if (e.code == FileError.TYPE_MISMATCH_ERR) {
417                 _self.root.getDirectory(path, {}, function(dirEntry) {
418                     dirEntry.removeRecursively(callback, errorHandler);
419                 }, errorHandler);
420             } else {
421                 errorHandler(error);
422             }
423         });
424     };
425     
426     /**
427      * Deletes a directory from the path.  Directories are removed recursivly
428      * @param   {String}    path
429      * @param   {Function}  callback
430      * @type    {void}
431      * @function
432      */
433     WebFS.prototype.rmdir= WebFS.prototype.unlink;
434     
435     /**
436      * Creates a directory on the filesystem, will recursivly create paths
437      * @param   {String}    path
438      * @param   {Number}    mode
439      * @param   {Function}  callback
440      * @type    {void}
441      */
442     WebFS.prototype.mkdir = function(path, mode, callback) {
443         var _self = this;
444         
445         if (typeof callback != "function") {
446             callback = mode;
447         }
448         
449         var errorHandler = function(error) {
450             callback(_self.errorHandler(error));
451         };
452         
453         var createDir = function(rootDir, folders) {
454             if (folders[0] == '.' || folders[0] == '') {
455                 folders = folders.slice(1);
456             }
457 
458             rootDir.getDirectory(folders[0], {create: true}, function(dirEntry) {
459                 if (folders.length) {
460                     createDir(dirEntry, folders.slice(1));
461                 } else {
462                     callback(null, dirEntry);
463                 }
464             }, errorHandler);
465         };
466         createDir(this.fs.root, path.split('/'));
467     };
468     
469     /**
470      * Reads the contents of a directory, returns the result as an array of entries
471      * @param   {String}    path
472      * @param   {Function}  callback
473      * @type    {void}
474      */
475     WebFS.prototype.readdir = function(path, callback) {
476         var _self = this;
477         
478         var errorHandler = function(error) {
479             callback(_self.errorHandler(error));
480         };
481         
482         var listHandler = function(dirHandler) {
483             var dirReader = dirHandler.createReader();
484             var entries = [];
485             var readEntries = function() {
486                 dirReader.readEntries(function(results) {
487                     if (!results.length) {
488                         callback(null, entries.sort());
489                     } else {
490                         for (var i = 0, j = results.length; i < j; i++) {
491                             if (results[i].isDirectory)
492                                 entries.push(results[i].fullPath + '/');
493                             else
494                                 entries.push(results[i].fullPath);
495                         }
496                         //entries = entries.concat(Array.prototype.slice.call(results || [], 0));
497                         readEntries();
498                     }
499                 }, errorHandler);
500             };
501             readEntries();
502         };
503         
504         _self.root.getDirectory(path, {}, listHandler, errorHandler);
505     };
506     
507     /**
508      * 'Close' a file or directory handler by setting it to null
509      * @param   {FileEntry, DirectoryEntry} fd
510      * @param   {Function}                  callback
511      * @type    {void}
512      */
513     WebFS.prototype.close = function(fd, callback) {
514         fd = null;  // Set to null for GC
515         callback();
516     };
517     
518     /**
519      * Opens a file or directory and return a handler
520      * @param   {String}    path
521      * @param   {String}    flags
522      * @param   {Number}    mode
523      * @param   {Function}  callback
524      * @type    {void}
525      */
526     WebFS.prototype.open = function(path, flags, mode, callback) { 
527         var _self = this;
528         
529         if (typeof callback != "function") {
530             callback = mode;
531         }
532         if (typeof callback != "function") {
533             callback = flags;
534         }
535         
536         var errorHandler = function(error) {
537             if (error && error.code == _self.TYPE_MISMATCH_ERR) {
538                 _self.root.getDirectory(path, options, successHandler, function(error) {
539                     callback(_self.errorHandler(error));
540                 });
541             }
542         };
543         
544         var successHandler = function(fileHandler) {
545             callback(null, fileHandler);
546         };
547         
548         var options = {};
549         // If the flag is to write or append, and the file does not exist
550         // then we need to ensure it's created
551         if (['w', 'w+', 'a', 'a+'].indexOf(flags) > -1)
552             options.create = true;
553 
554         _self.root.getFile(path, options, successHandler, errorHandler);
555     };
556     
557     /**
558      * Writes the contents of a Blob or File to a FileEntry on the filesystem
559      * @param   {FileEntry} fileHandler
560      * @param   {Mixed}     buffer
561      * @param   {Number}    offset
562      * @param   {Number}    length
563      * @param   {Number}    position
564      * @param   {Function}  callback
565      * @type    {void}
566      */
567     WebFS.prototype.write = function(fileHandler, buffer, offset, length, position, callback) {
568         var _self = this,
569             data;
570             
571         if (typeof callback != "function") {
572             callback = position;
573         }
574         if (typeof callback != "function") {
575             callback = length;
576         }
577         if (typeof callback != "function") {
578             callback = offset;
579         }
580             
581         var errorHandler = function(error) {
582             callback(_self.errorHandler(error));
583         };
584                 
585         var data = (typeof buffer == 'string') ? _self.createBlob(buffer) : buffer;
586         
587         var writerHandler = function(fileWriter) {
588             
589             fileWriter.onwriteend = function(e) {
590                 callback(null, e.loaded, buffer);
591             };
592             
593             fileWriter.onerror = errorHandler;
594           
595             fileWriter.write(data);
596         };
597             
598         fileHandler.createWriter(writerHandler, errorHandler);
599     };
600     
601     /**
602      * Asynchronously writes data to a file, replacing the file if it already exists.  Data can be a string or a buffer.
603      * @param   {String}    filename
604      * @param   {Mixed}     data
605      * @param   {String}    encoding
606      * @param   {Function}  callback
607      * @type    {void}
608      */
609     WebFS.prototype.writeFile = function(filename, data, encoding, callback) {
610         var _self = this;
611         
612         if (typeof callback != 'function') {
613             callback = encoding;
614         }
615         
616         var buffer = (typeof data == 'string') ? _self.createBlob(data) : data;
617         
618         var openFileHandler = function(error, fileHandler) {
619             _self.truncate(fileHandler, 0, function(error) {
620                 if (error)
621                     return callback(error);
622                 _self.write(fileHandler, buffer, null, null, null, function(error, written, buffer_) {
623                     callback(error, buffer_);  
624                 });
625             });
626         };
627         
628         _self.open(filename, 'w', null, openFileHandler);  
629     };
630     
631     /**
632      * Read data from the file specified by file handler.
633      * @param   {FileEntry} fileHandler
634      * @param   {Mixed}     buffer
635      * @param   {Number}    offset
636      * @param   {Number}    length
637      * @param   {Number}    position
638      * @type    {void}
639      */
640     WebFS.prototype.read = function(fileHandler, buffer, offset, length, position, callback) {
641         var _self = this,
642             data;
643             
644         if (typeof callback != "function") {
645             callback = position;
646         }
647         if (typeof callback != "function") {
648             callback = length;
649         }
650         if (typeof callback != "function") {
651             callback = offset;
652         }
653         
654         var errorHandler = function(error) {
655             callback(_self.errorHandler(error));
656         };
657         
658         fileHandler.file(function(file) {
659             var reader = new FileReader();
660             
661             reader.onloadend = function(e) {
662                 buffer.append(this.result);
663                 callback(null, buffer.getBlob().size, buffer.getBlob());
664             };
665             
666             reader.onerror = errorHandler;
667             
668             // Since we want to support binary or string data, we should read
669             // as an array buffer and allow the user to determine the output
670             // from the Blob/File interface buffer
671             reader.readAsArrayBuffer(file);
672         });
673     };
674     
675     /**
676      * Asynchronously reads the entire contents of a file and returns a buffer
677      * @param   {String}    filename
678      * @param   {String}    encoding
679      * @param   {Function}  callback
680      * @type    {void}
681      */
682     WebFS.prototype.readFile = function(filename, encoding, callback) {
683         var _self = this;
684         
685         if (typeof callback != 'function') {
686             callback = encoding;
687         }
688                 
689         var successHandler = function(error, fileHandler) {
690             if (error)
691                 return callback(error);
692                 
693             _self.read(fileHandler, new BlobBuilder(), null, null, null, callback);
694         };
695         
696         this.open(filename, null, null, successHandler);
697     };
698     
699     /**
700      * Takes data, string or binary, and creates a binary blob.
701      * @param   {Mixed}     data
702      * @param   {String}    encoding
703      * @type    {void}
704      */
705     WebFS.prototype.createBlob = function(data, encoding) {
706         var bb = new BlobBuilder();
707         bb.append(data);
708         if (encoding)
709             return bb.getBlob(encoding);
710         else
711             return bb.getBlob();
712     };
713     
714     /**
715      * Method to get content of a blob or file as a string
716      * @param   {File, Blob}    data
717      * @param   {String}        encoding
718      * @param   {Function}      callback
719      * @type    {void}
720      */
721     WebFS.prototype.readString = function(data, encoding, callback) {
722         
723         var reader = new FileReader(),
724             encoding_;
725             
726         if (typeof callback != 'function') {
727             callback = encoding;
728             encoding_ = 'UTF-8';
729         } else {
730             encoding_ = encoding;
731         }
732         
733         reader.onloadend = function(event) {
734             callback(null, this.result);
735         };
736         
737         reader.onerror = function(error) {
738             callback(error);
739         };
740         
741         data = reader.readAsText(data, encoding_);
742     };
743     
744     /**
745      * Method to get content as a binary string
746      * @param   {File, Blob}    data
747      * @param   {Function}      callback
748      * @type    {void}
749      */
750     WebFS.prototype.readBinaryString = function(data, callback) {
751         var reader = new FileReader();
752         reader.onloadend = function(event) {
753             callback(null, this.result);
754         };
755         reader.onerror = function(error) {
756             callback(error);
757         };
758         
759         reader.readAsBinaryString(data);
760     };
761     
762     /**
763      * Method to get content as a array buffer
764      * @param   {File, Blob}    data
765      * @param   {Function}      callback
766      * @type    {void}
767      */
768     WebFS.prototype.readArrayBuffer = function(data, callback) {
769         var reader;
770         reader = new FileReader();
771         reader.onloadend = function(event) {
772             callback(null, this.result);
773         };
774         reader.onerror = function(error) {
775             callback(error);
776         };
777         
778         reader.readAsArrayBuffer(data);
779     };
780     
781     /**
782      * Method to get content as a data url
783      * @param   {File, Blob}    data
784      * @param   {Function}      callback
785      * @type    {void}
786      */
787     WebFS.prototype.readDataUrl = function(data, callback) {
788         var reader = new FileReader();
789         reader.onloadend = function(event) {
790             callback(null, this.result);
791         };
792         reader.onerror = function(error) {
793             callback(error);
794         };
795         
796         reader.readAsDataURL(data);
797     };
798     
799     return WebFS;
800 })();