Home | History | Annotate | Download | only in js
      1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 'use strict';
      6 
      7 /**
      8  * Namespace for utility functions.
      9  */
     10 var util = {};
     11 
     12 /**
     13  * Returns a function that console.log's its arguments, prefixed by |msg|.
     14  *
     15  * @param {string} msg The message prefix to use in the log.
     16  * @param {function(...string)=} opt_callback A function to invoke after
     17  *     logging.
     18  * @return {function(...string)} Function that logs.
     19  */
     20 util.flog = function(msg, opt_callback) {
     21   return function() {
     22     var ary = Array.apply(null, arguments);
     23     console.log(msg + ': ' + ary.join(', '));
     24     if (opt_callback)
     25       opt_callback.apply(null, arguments);
     26   };
     27 };
     28 
     29 /**
     30  * Returns a function that throws an exception that includes its arguments
     31  * prefixed by |msg|.
     32  *
     33  * @param {string} msg The message prefix to use in the exception.
     34  * @return {function(...string)} Function that throws.
     35  */
     36 util.ferr = function(msg) {
     37   return function() {
     38     var ary = Array.apply(null, arguments);
     39     throw new Error(msg + ': ' + ary.join(', '));
     40   };
     41 };
     42 
     43 /**
     44  * Install a sensible toString() on the FileError object.
     45  *
     46  * FileError.prototype.code is a numeric code describing the cause of the
     47  * error.  The FileError constructor has a named property for each possible
     48  * error code, but provides no way to map the code to the named property.
     49  * This toString() implementation fixes that.
     50  */
     51 util.installFileErrorToString = function() {
     52   FileError.prototype.toString = function() {
     53     return '[object FileError: ' + util.getFileErrorMnemonic(this.code) + ']';
     54   };
     55 };
     56 
     57 /**
     58  * @param {number} code The file error code.
     59  * @return {string} The file error mnemonic.
     60  */
     61 util.getFileErrorMnemonic = function(code) {
     62   for (var key in FileError) {
     63     if (key.search(/_ERR$/) != -1 && FileError[key] == code)
     64       return key;
     65   }
     66 
     67   return code;
     68 };
     69 
     70 /**
     71  * @param {number} code File error code (from FileError object).
     72  * @return {string} Translated file error string.
     73  */
     74 util.getFileErrorString = function(code) {
     75   for (var key in FileError) {
     76     var match = /(.*)_ERR$/.exec(key);
     77     if (match && FileError[key] == code) {
     78       // This would convert 1 to 'NOT_FOUND'.
     79       code = match[1];
     80       break;
     81     }
     82   }
     83   console.warn('File error: ' + code);
     84   return loadTimeData.getString('FILE_ERROR_' + code) ||
     85       loadTimeData.getString('FILE_ERROR_GENERIC');
     86 };
     87 
     88 /**
     89  * @param {string} str String to escape.
     90  * @return {string} Escaped string.
     91  */
     92 util.htmlEscape = function(str) {
     93   return str.replace(/[<>&]/g, function(entity) {
     94     switch (entity) {
     95       case '<': return '&lt;';
     96       case '>': return '&gt;';
     97       case '&': return '&amp;';
     98     }
     99   });
    100 };
    101 
    102 /**
    103  * @param {string} str String to unescape.
    104  * @return {string} Unescaped string.
    105  */
    106 util.htmlUnescape = function(str) {
    107   return str.replace(/&(lt|gt|amp);/g, function(entity) {
    108     switch (entity) {
    109       case '&lt;': return '<';
    110       case '&gt;': return '>';
    111       case '&amp;': return '&';
    112     }
    113   });
    114 };
    115 
    116 /**
    117  * Given a list of Entries, recurse any DirectoryEntries if |recurse| is true,
    118  * and call back with a list of all file and directory entries encountered
    119  * (including the original set).
    120  * @param {Array.<Entry>} entries List of entries.
    121  * @param {boolean} recurse Whether to recurse.
    122  * @param {function(Object)} successCallback Object has the fields dirEntries,
    123  *     fileEntries and fileBytes.
    124  */
    125 util.recurseAndResolveEntries = function(entries, recurse, successCallback) {
    126   var pendingSubdirectories = 0;
    127   var pendingFiles = 0;
    128 
    129   var dirEntries = [];
    130   var fileEntries = [];
    131   var fileBytes = 0;
    132 
    133   var steps = {
    134     // Start operations.
    135     start: function() {
    136       for (var i = 0; i < entries.length; i++) {
    137         var parentPath = PathUtil.getParentDirectory(entries[i].fullPath);
    138         steps.tallyEntry(entries[i], parentPath);
    139       }
    140       steps.areWeThereYet();
    141     },
    142 
    143     // Process one entry.
    144     tallyEntry: function(entry, originalSourcePath) {
    145       entry.originalSourcePath = originalSourcePath;
    146       if (entry.isDirectory) {
    147         dirEntries.push(entry);
    148         if (!recurse)
    149           return;
    150         pendingSubdirectories++;
    151         util.forEachDirEntry(entry, function(inEntry) {
    152           if (inEntry == null) {
    153             // Null entry indicates we're done scanning this directory.
    154             pendingSubdirectories--;
    155             steps.areWeThereYet();
    156             return;
    157           }
    158           steps.tallyEntry(inEntry, originalSourcePath);
    159         });
    160       } else {
    161         fileEntries.push(entry);
    162         pendingFiles++;
    163         entry.getMetadata(function(metadata) {
    164           fileBytes += metadata.size;
    165           pendingFiles--;
    166           steps.areWeThereYet();
    167         });
    168       }
    169     },
    170 
    171     // We invoke this after each async callback to see if we've received all
    172     // the expected callbacks.  If so, we're done.
    173     areWeThereYet: function() {
    174       if (!successCallback || pendingSubdirectories != 0 || pendingFiles != 0)
    175         return;
    176       var pathCompare = function(a, b) {
    177         if (a.fullPath > b.fullPath)
    178           return 1;
    179         if (a.fullPath < b.fullPath)
    180           return -1;
    181         return 0;
    182       };
    183       var result = {
    184         dirEntries: dirEntries.sort(pathCompare),
    185         fileEntries: fileEntries.sort(pathCompare),
    186         fileBytes: fileBytes
    187       };
    188       successCallback(result);
    189     }
    190   };
    191 
    192   steps.start();
    193 };
    194 
    195 /**
    196  * Utility function to invoke callback once for each entry in dirEntry.
    197  * callback is called with 'null' after all entries are visited to indicate
    198  * the end of the directory scan.
    199  *
    200  * @param {DirectoryEntry} dirEntry The directory entry to enumerate.
    201  * @param {function(Entry)} callback The function to invoke for each entry in
    202  *     dirEntry.
    203  */
    204 util.forEachDirEntry = function(dirEntry, callback) {
    205   var reader;
    206 
    207   var onError = function(err) {
    208     console.error('Failed to read  dir entries at ' + dirEntry.fullPath);
    209   };
    210 
    211   var onReadSome = function(results) {
    212     if (results.length == 0)
    213       return callback(null);
    214 
    215     for (var i = 0; i < results.length; i++)
    216       callback(results[i]);
    217 
    218     reader.readEntries(onReadSome, onError);
    219   };
    220 
    221   reader = dirEntry.createReader();
    222   reader.readEntries(onReadSome, onError);
    223 };
    224 
    225 /**
    226  * Reads contents of directory.
    227  * @param {DirectoryEntry} root Root entry.
    228  * @param {string} path Directory path.
    229  * @param {function(Array.<Entry>)} callback List of entries passed to callback.
    230  */
    231 util.readDirectory = function(root, path, callback) {
    232   var onError = function(e) {
    233     callback([], e);
    234   };
    235   root.getDirectory(path, {create: false}, function(entry) {
    236     var reader = entry.createReader();
    237     var r = [];
    238     var readNext = function() {
    239       reader.readEntries(function(results) {
    240         if (results.length == 0) {
    241           callback(r, null);
    242           return;
    243         }
    244         r.push.apply(r, results);
    245         readNext();
    246       }, onError);
    247     };
    248     readNext();
    249   }, onError);
    250 };
    251 
    252 /**
    253  * Utility function to resolve multiple directories with a single call.
    254  *
    255  * The successCallback will be invoked once for each directory object
    256  * found.  The errorCallback will be invoked once for each
    257  * path that could not be resolved.
    258  *
    259  * The successCallback is invoked with a null entry when all paths have
    260  * been processed.
    261  *
    262  * @param {DirEntry} dirEntry The base directory.
    263  * @param {Object} params The parameters to pass to the underlying
    264  *     getDirectory calls.
    265  * @param {Array.<string>} paths The list of directories to resolve.
    266  * @param {function(!DirEntry)} successCallback The function to invoke for
    267  *     each DirEntry found.  Also invoked once with null at the end of the
    268  *     process.
    269  * @param {function(FileError)} errorCallback The function to invoke
    270  *     for each path that cannot be resolved.
    271  */
    272 util.getDirectories = function(dirEntry, params, paths, successCallback,
    273                                errorCallback) {
    274 
    275   // Copy the params array, since we're going to destroy it.
    276   params = [].slice.call(params);
    277 
    278   var onComplete = function() {
    279     successCallback(null);
    280   };
    281 
    282   var getNextDirectory = function() {
    283     var path = paths.shift();
    284     if (!path)
    285       return onComplete();
    286 
    287     dirEntry.getDirectory(
    288       path, params,
    289       function(entry) {
    290         successCallback(entry);
    291         getNextDirectory();
    292       },
    293       function(err) {
    294         errorCallback(err);
    295         getNextDirectory();
    296       });
    297   };
    298 
    299   getNextDirectory();
    300 };
    301 
    302 /**
    303  * Utility function to resolve multiple files with a single call.
    304  *
    305  * The successCallback will be invoked once for each directory object
    306  * found.  The errorCallback will be invoked once for each
    307  * path that could not be resolved.
    308  *
    309  * The successCallback is invoked with a null entry when all paths have
    310  * been processed.
    311  *
    312  * @param {DirEntry} dirEntry The base directory.
    313  * @param {Object} params The parameters to pass to the underlying
    314  *     getFile calls.
    315  * @param {Array.<string>} paths The list of files to resolve.
    316  * @param {function(!FileEntry)} successCallback The function to invoke for
    317  *     each FileEntry found.  Also invoked once with null at the end of the
    318  *     process.
    319  * @param {function(FileError)} errorCallback The function to invoke
    320  *     for each path that cannot be resolved.
    321  */
    322 util.getFiles = function(dirEntry, params, paths, successCallback,
    323                          errorCallback) {
    324   // Copy the params array, since we're going to destroy it.
    325   params = [].slice.call(params);
    326 
    327   var onComplete = function() {
    328     successCallback(null);
    329   };
    330 
    331   var getNextFile = function() {
    332     var path = paths.shift();
    333     if (!path)
    334       return onComplete();
    335 
    336     dirEntry.getFile(
    337       path, params,
    338       function(entry) {
    339         successCallback(entry);
    340         getNextFile();
    341       },
    342       function(err) {
    343         errorCallback(err);
    344         getNextFile();
    345       });
    346   };
    347 
    348   getNextFile();
    349 };
    350 
    351 /**
    352  * Resolve a path to either a DirectoryEntry or a FileEntry, regardless of
    353  * whether the path is a directory or file.
    354  *
    355  * @param {DirectoryEntry} root The root of the filesystem to search.
    356  * @param {string} path The path to be resolved.
    357  * @param {function(Entry)} resultCallback Called back when a path is
    358  *     successfully resolved. Entry will be either a DirectoryEntry or
    359  *     a FileEntry.
    360  * @param {function(FileError)} errorCallback Called back if an unexpected
    361  *     error occurs while resolving the path.
    362  */
    363 util.resolvePath = function(root, path, resultCallback, errorCallback) {
    364   if (path == '' || path == '/') {
    365     resultCallback(root);
    366     return;
    367   }
    368 
    369   root.getFile(
    370       path, {create: false},
    371       resultCallback,
    372       function(err) {
    373         if (err.code == FileError.TYPE_MISMATCH_ERR) {
    374           // Bah.  It's a directory, ask again.
    375           root.getDirectory(
    376               path, {create: false},
    377               resultCallback,
    378               errorCallback);
    379         } else {
    380           errorCallback(err);
    381         }
    382       });
    383 };
    384 
    385 /**
    386  * Locate the file referred to by path, creating directories or the file
    387  * itself if necessary.
    388  * @param {DirEntry} root The root entry.
    389  * @param {string} path The file path.
    390  * @param {function(FileEntry)} successCallback The callback.
    391  * @param {function(FileError)} errorCallback The callback.
    392  */
    393 util.getOrCreateFile = function(root, path, successCallback, errorCallback) {
    394   var dirname = null;
    395   var basename = null;
    396 
    397   var onDirFound = function(dirEntry) {
    398     dirEntry.getFile(basename, { create: true },
    399                      successCallback, errorCallback);
    400   };
    401 
    402   var i = path.lastIndexOf('/');
    403   if (i > -1) {
    404     dirname = path.substr(0, i);
    405     basename = path.substr(i + 1);
    406   } else {
    407     basename = path;
    408   }
    409 
    410   if (!dirname) {
    411     onDirFound(root);
    412     return;
    413   }
    414 
    415   util.getOrCreateDirectory(root, dirname, onDirFound, errorCallback);
    416 };
    417 
    418 /**
    419  * Locate the directory referred to by path, creating directories along the
    420  * way.
    421  * @param {DirEntry} root The root entry.
    422  * @param {string} path The directory path.
    423  * @param {function(FileEntry)} successCallback The callback.
    424  * @param {function(FileError)} errorCallback The callback.
    425  */
    426 util.getOrCreateDirectory = function(root, path, successCallback,
    427                                      errorCallback) {
    428   var names = path.split('/');
    429 
    430   var getOrCreateNextName = function(dir) {
    431     if (!names.length)
    432       return successCallback(dir);
    433 
    434     var name;
    435     do {
    436       name = names.shift();
    437     } while (!name || name == '.');
    438 
    439     dir.getDirectory(name, { create: true }, getOrCreateNextName,
    440                      errorCallback);
    441   };
    442 
    443   getOrCreateNextName(root);
    444 };
    445 
    446 /**
    447  * Remove a file or a directory.
    448  * @param {Entry} entry The entry to remove.
    449  * @param {function()} onSuccess The success callback.
    450  * @param {function(FileError)} onError The error callback.
    451  */
    452 util.removeFileOrDirectory = function(entry, onSuccess, onError) {
    453   if (entry.isDirectory)
    454     entry.removeRecursively(onSuccess, onError);
    455   else
    456     entry.remove(onSuccess, onError);
    457 };
    458 
    459 /**
    460  * Checks if an entry exists at |relativePath| in |dirEntry|.
    461  * If exists, tries to deduplicate the path by inserting parenthesized number,
    462  * such as " (1)", before the extension. If it still exists, tries the
    463  * deduplication again by increasing the number up to 10 times.
    464  * For example, suppose "file.txt" is given, "file.txt", "file (1).txt",
    465  * "file (2).txt", ..., "file (9).txt" will be tried.
    466  *
    467  * @param {DirectoryEntry} dirEntry The target directory entry.
    468  * @param {string} relativePath The path to be deduplicated.
    469  * @param {function(string)} onSuccess Called with the deduplicated path on
    470  *     success.
    471  * @param {function(FileError)} onError Called on error.
    472  */
    473 util.deduplicatePath = function(dirEntry, relativePath, onSuccess, onError) {
    474   // The trial is up to 10.
    475   var MAX_RETRY = 10;
    476 
    477   // Crack the path into three part. The parenthesized number (if exists) will
    478   // be replaced by incremented number for retry. For example, suppose
    479   // |relativePath| is "file (10).txt", the second check path will be
    480   // "file (11).txt".
    481   var match = /^(.*?)(?: \((\d+)\))?(\.[^.]*?)?$/.exec(relativePath);
    482   var prefix = match[1];
    483   var copyNumber = match[2] ? parseInt(match[2], 10) : 0;
    484   var ext = match[3] ? match[3] : '';
    485 
    486   // The path currently checking the existence.
    487   var trialPath = relativePath;
    488 
    489   var onNotResolved = function(err) {
    490     // We expect to be unable to resolve the target file, since we're going
    491     // to create it during the copy.  However, if the resolve fails with
    492     // anything other than NOT_FOUND, that's trouble.
    493     if (err.code != FileError.NOT_FOUND_ERR) {
    494       onError(err);
    495       return;
    496     }
    497 
    498     // Found a path that doesn't exist.
    499     onSuccess(trialPath);
    500   }
    501 
    502   var numRetry = MAX_RETRY;
    503   var onResolved = function(entry) {
    504     if (--numRetry == 0) {
    505       // Hit the limit of the number of retrial.
    506       // Note that we cannot create FileError object directly, so here we use
    507       // Object.create instead.
    508       onError(util.createFileError(FileError.PATH_EXISTS_ERR));
    509       return;
    510     }
    511 
    512     ++copyNumber;
    513     trialPath = prefix + ' (' + copyNumber + ')' + ext;
    514     util.resolvePath(dirEntry, trialPath, onResolved, onNotResolved);
    515   };
    516 
    517   // Check to see if the target exists.
    518   util.resolvePath(dirEntry, trialPath, onResolved, onNotResolved);
    519 };
    520 
    521 /**
    522  * Convert a number of bytes into a human friendly format, using the correct
    523  * number separators.
    524  *
    525  * @param {number} bytes The number of bytes.
    526  * @return {string} Localized string.
    527  */
    528 util.bytesToString = function(bytes) {
    529   // Translation identifiers for size units.
    530   var UNITS = ['SIZE_BYTES',
    531                'SIZE_KB',
    532                'SIZE_MB',
    533                'SIZE_GB',
    534                'SIZE_TB',
    535                'SIZE_PB'];
    536 
    537   // Minimum values for the units above.
    538   var STEPS = [0,
    539                Math.pow(2, 10),
    540                Math.pow(2, 20),
    541                Math.pow(2, 30),
    542                Math.pow(2, 40),
    543                Math.pow(2, 50)];
    544 
    545   var str = function(n, u) {
    546     // TODO(rginda): Switch to v8Locale's number formatter when it's
    547     // available.
    548     return strf(u, n.toLocaleString());
    549   };
    550 
    551   var fmt = function(s, u) {
    552     var rounded = Math.round(bytes / s * 10) / 10;
    553     return str(rounded, u);
    554   };
    555 
    556   // Less than 1KB is displayed like '80 bytes'.
    557   if (bytes < STEPS[1]) {
    558     return str(bytes, UNITS[0]);
    559   }
    560 
    561   // Up to 1MB is displayed as rounded up number of KBs.
    562   if (bytes < STEPS[2]) {
    563     var rounded = Math.ceil(bytes / STEPS[1]);
    564     return str(rounded, UNITS[1]);
    565   }
    566 
    567   // This loop index is used outside the loop if it turns out |bytes|
    568   // requires the largest unit.
    569   var i;
    570 
    571   for (i = 2 /* MB */; i < UNITS.length - 1; i++) {
    572     if (bytes < STEPS[i + 1])
    573       return fmt(STEPS[i], UNITS[i]);
    574   }
    575 
    576   return fmt(STEPS[i], UNITS[i]);
    577 };
    578 
    579 /**
    580  * Utility function to read specified range of bytes from file
    581  * @param {File} file The file to read.
    582  * @param {number} begin Starting byte(included).
    583  * @param {number} end Last byte(excluded).
    584  * @param {function(File, Uint8Array)} callback Callback to invoke.
    585  * @param {function(FileError)} onError Error handler.
    586  */
    587 util.readFileBytes = function(file, begin, end, callback, onError) {
    588   var fileReader = new FileReader();
    589   fileReader.onerror = onError;
    590   fileReader.onloadend = function() {
    591     callback(file, new ByteReader(fileReader.result));
    592   };
    593   fileReader.readAsArrayBuffer(file.slice(begin, end));
    594 };
    595 
    596 /**
    597  * Write a blob to a file.
    598  * Truncates the file first, so the previous content is fully overwritten.
    599  * @param {FileEntry} entry File entry.
    600  * @param {Blob} blob The blob to write.
    601  * @param {function(Event)} onSuccess Completion callback. The first argument is
    602  *     a 'writeend' event.
    603  * @param {function(FileError)} onError Error handler.
    604  */
    605 util.writeBlobToFile = function(entry, blob, onSuccess, onError) {
    606   var truncate = function(writer) {
    607     writer.onerror = onError;
    608     writer.onwriteend = write.bind(null, writer);
    609     writer.truncate(0);
    610   };
    611 
    612   var write = function(writer) {
    613     writer.onwriteend = onSuccess;
    614     writer.write(blob);
    615   };
    616 
    617   entry.createWriter(truncate, onError);
    618 };
    619 
    620 /**
    621  * Returns a string '[Ctrl-][Alt-][Shift-][Meta-]' depending on the event
    622  * modifiers. Convenient for writing out conditions in keyboard handlers.
    623  *
    624  * @param {Event} event The keyboard event.
    625  * @return {string} Modifiers.
    626  */
    627 util.getKeyModifiers = function(event) {
    628   return (event.ctrlKey ? 'Ctrl-' : '') +
    629          (event.altKey ? 'Alt-' : '') +
    630          (event.shiftKey ? 'Shift-' : '') +
    631          (event.metaKey ? 'Meta-' : '');
    632 };
    633 
    634 /**
    635  * @param {HTMLElement} element Element to transform.
    636  * @param {Object} transform Transform object,
    637  *                           contains scaleX, scaleY and rotate90 properties.
    638  */
    639 util.applyTransform = function(element, transform) {
    640   element.style.webkitTransform =
    641       transform ? 'scaleX(' + transform.scaleX + ') ' +
    642                   'scaleY(' + transform.scaleY + ') ' +
    643                   'rotate(' + transform.rotate90 * 90 + 'deg)' :
    644       '';
    645 };
    646 
    647 /**
    648  * Makes filesystem: URL from the path.
    649  * @param {string} path File or directory path.
    650  * @return {string} URL.
    651  */
    652 util.makeFilesystemUrl = function(path) {
    653   path = path.split('/').map(encodeURIComponent).join('/');
    654   var prefix = 'external';
    655   return 'filesystem:' + document.location.origin + '/' + prefix + path;
    656 };
    657 
    658 /**
    659  * Extracts path from filesystem: URL.
    660  * @param {string} url Filesystem URL.
    661  * @return {string} The path.
    662  */
    663 util.extractFilePath = function(url) {
    664   var match =
    665       /^filesystem:[\w-]*:\/\/[\w]*\/(external|persistent|temporary)(\/.*)$/.
    666       exec(url);
    667   var path = match && match[2];
    668   if (!path) return null;
    669   return decodeURIComponent(path);
    670 };
    671 
    672 /**
    673  * Traverses a tree up to a certain depth.
    674  * @param {FileEntry} root Root entry.
    675  * @param {function(Array.<Entry>)} callback The callback is called at the very
    676  *     end with a list of entries found.
    677  * @param {number?} max_depth Maximum depth. Pass zero to traverse everything.
    678  * @param {function(entry):boolean=} opt_filter Optional filter to skip some
    679  *     files/directories.
    680  */
    681 util.traverseTree = function(root, callback, max_depth, opt_filter) {
    682   var list = [];
    683   util.forEachEntryInTree(root, function(entry) {
    684     if (entry) {
    685       list.push(entry);
    686     } else {
    687       callback(list);
    688     }
    689     return true;
    690   }, max_depth, opt_filter);
    691 };
    692 
    693 /**
    694  * Traverses a tree up to a certain depth, and calls a callback for each entry.
    695  * callback is called with 'null' after all entries are visited to indicate
    696  * the end of the traversal.
    697  * @param {FileEntry} root Root entry.
    698  * @param {function(Entry):boolean} callback The callback is called for each
    699  *     entry, and then once with null passed. If callback returns false once,
    700  *     the whole traversal is stopped.
    701  * @param {number?} max_depth Maximum depth. Pass zero to traverse everything.
    702  * @param {function(entry):boolean=} opt_filter Optional filter to skip some
    703  *     files/directories.
    704  */
    705 util.forEachEntryInTree = function(root, callback, max_depth, opt_filter) {
    706   if (root.isFile) {
    707     if (opt_filter && !opt_filter(root)) {
    708       callback(null);
    709       return;
    710     }
    711     if (callback(root))
    712       callback(null);
    713     return;
    714   }
    715 
    716   var pending = 0;
    717   var cancelled = false;
    718 
    719   var maybeDone = function() {
    720     if (pending == 0 && !cancelled)
    721       callback(null);
    722   };
    723 
    724   var readEntry = function(entry, depth) {
    725     if (cancelled) return;
    726     if (opt_filter && !opt_filter(entry)) return;
    727 
    728     if (!callback(entry)) {
    729       cancelled = true;
    730       return;
    731     }
    732 
    733     // Do not recurse too deep and into files.
    734     if (entry.isFile || (max_depth != 0 && depth >= max_depth))
    735       return;
    736 
    737     pending++;
    738     util.forEachDirEntry(entry, function(childEntry) {
    739       if (childEntry == null) {
    740         // Null entry indicates we're done scanning this directory.
    741         pending--;
    742         maybeDone();
    743       } else {
    744         readEntry(childEntry, depth + 1);
    745       }
    746     });
    747   };
    748 
    749   readEntry(root, 0);
    750 };
    751 
    752 /**
    753  * A shortcut function to create a child element with given tag and class.
    754  *
    755  * @param {HTMLElement} parent Parent element.
    756  * @param {string=} opt_className Class name.
    757  * @param {string=} opt_tag Element tag, DIV is omitted.
    758  * @return {Element} Newly created element.
    759  */
    760 util.createChild = function(parent, opt_className, opt_tag) {
    761   var child = parent.ownerDocument.createElement(opt_tag || 'div');
    762   if (opt_className)
    763     child.className = opt_className;
    764   parent.appendChild(child);
    765   return child;
    766 };
    767 
    768 /**
    769  * Update the app state.
    770  *
    771  * @param {string} path Path to be put in the address bar after the hash.
    772  *   If null the hash is left unchanged.
    773  * @param {string|Object=} opt_param Search parameter. Used directly if string,
    774  *   stringified if object. If omitted the search query is left unchanged.
    775  */
    776 util.updateAppState = function(path, opt_param) {
    777   window.appState = window.appState || {};
    778   if (typeof opt_param == 'string')
    779     window.appState.params = {};
    780   else if (typeof opt_param == 'object')
    781     window.appState.params = opt_param;
    782   if (path)
    783     window.appState.defaultPath = path;
    784   util.saveAppState();
    785   return;
    786 };
    787 
    788 /**
    789  * Return a translated string.
    790  *
    791  * Wrapper function to make dealing with translated strings more concise.
    792  * Equivalent to loadTimeData.getString(id).
    793  *
    794  * @param {string} id The id of the string to return.
    795  * @return {string} The translated string.
    796  */
    797 function str(id) {
    798   return loadTimeData.getString(id);
    799 }
    800 
    801 /**
    802  * Return a translated string with arguments replaced.
    803  *
    804  * Wrapper function to make dealing with translated strings more concise.
    805  * Equivilant to loadTimeData.getStringF(id, ...).
    806  *
    807  * @param {string} id The id of the string to return.
    808  * @param {...string} var_args The values to replace into the string.
    809  * @return {string} The translated string with replaced values.
    810  */
    811 function strf(id, var_args) {
    812   return loadTimeData.getStringF.apply(loadTimeData, arguments);
    813 }
    814 
    815 /**
    816  * Adapter object that abstracts away the the difference between Chrome app APIs
    817  * v1 and v2. Is only necessary while the migration to v2 APIs is in progress.
    818  * TODO(mtomasz): Clean up this. crbug.com/240606.
    819  */
    820 util.platform = {
    821   /**
    822    * @return {boolean} True if Files.app is running via "chrome://files", open
    823    * files or select folder dialog. False otherwise.
    824    */
    825   runningInBrowser: function() {
    826     return !window.appID;
    827   },
    828 
    829   /**
    830    * @param {function(Object)} callback Function accepting a preference map.
    831    */
    832   getPreferences: function(callback) {
    833     chrome.storage.local.get(callback);
    834   },
    835 
    836   /**
    837    * @param {string} key Preference name.
    838    * @param {function(string)} callback Function accepting the preference value.
    839    */
    840   getPreference: function(key, callback) {
    841     chrome.storage.local.get(key, function(items) {
    842       callback(items[key]);
    843     });
    844   },
    845 
    846   /**
    847    * @param {string} key Preference name.
    848    * @param {string|Object} value Preference value.
    849    * @param {function()=} opt_callback Completion callback.
    850    */
    851   setPreference: function(key, value, opt_callback) {
    852     if (typeof value != 'string')
    853       value = JSON.stringify(value);
    854 
    855     var items = {};
    856     items[key] = value;
    857     chrome.storage.local.set(items, opt_callback);
    858   }
    859 };
    860 
    861 /**
    862  * Attach page load handler.
    863  * @param {function()} handler Application-specific load handler.
    864  */
    865 util.addPageLoadHandler = function(handler) {
    866   document.addEventListener('DOMContentLoaded', function() {
    867     handler();
    868   });
    869 };
    870 
    871 /**
    872  * Save app launch data to the local storage.
    873  */
    874 util.saveAppState = function() {
    875   if (window.appState)
    876     util.platform.setPreference(window.appID, window.appState);
    877 };
    878 
    879 /**
    880  *  AppCache is a persistent timestamped key-value storage backed by
    881  *  HTML5 local storage.
    882  *
    883  *  It is not designed for frequent access. In order to avoid costly
    884  *  localStorage iteration all data is kept in a single localStorage item.
    885  *  There is no in-memory caching, so concurrent access is _almost_ safe.
    886  *
    887  *  TODO(kaznacheev) Reimplement this based on Indexed DB.
    888  */
    889 util.AppCache = function() {};
    890 
    891 /**
    892  * Local storage key.
    893  */
    894 util.AppCache.KEY = 'AppCache';
    895 
    896 /**
    897  * Max number of items.
    898  */
    899 util.AppCache.CAPACITY = 100;
    900 
    901 /**
    902  * Default lifetime.
    903  */
    904 util.AppCache.LIFETIME = 30 * 24 * 60 * 60 * 1000;  // 30 days.
    905 
    906 /**
    907  * @param {string} key Key.
    908  * @param {function(number)} callback Callback accepting a value.
    909  */
    910 util.AppCache.getValue = function(key, callback) {
    911   util.AppCache.read_(function(map) {
    912     var entry = map[key];
    913     callback(entry && entry.value);
    914   });
    915 };
    916 
    917 /**
    918  * Update the cache.
    919  *
    920  * @param {string} key Key.
    921  * @param {string} value Value. Remove the key if value is null.
    922  * @param {number=} opt_lifetime Maximim time to keep an item (in milliseconds).
    923  */
    924 util.AppCache.update = function(key, value, opt_lifetime) {
    925   util.AppCache.read_(function(map) {
    926     if (value != null) {
    927       map[key] = {
    928         value: value,
    929         expire: Date.now() + (opt_lifetime || util.AppCache.LIFETIME)
    930       };
    931     } else if (key in map) {
    932       delete map[key];
    933     } else {
    934       return;  // Nothing to do.
    935     }
    936     util.AppCache.cleanup_(map);
    937     util.AppCache.write_(map);
    938   });
    939 };
    940 
    941 /**
    942  * @param {function(Object)} callback Callback accepting a map of timestamped
    943  *   key-value pairs.
    944  * @private
    945  */
    946 util.AppCache.read_ = function(callback) {
    947   util.platform.getPreference(util.AppCache.KEY, function(json) {
    948     if (json) {
    949       try {
    950         callback(JSON.parse(json));
    951       } catch (e) {
    952         // The local storage item somehow got messed up, start fresh.
    953       }
    954     }
    955     callback({});
    956   });
    957 };
    958 
    959 /**
    960  * @param {Object} map A map of timestamped key-value pairs.
    961  * @private
    962  */
    963 util.AppCache.write_ = function(map) {
    964   util.platform.setPreference(util.AppCache.KEY, JSON.stringify(map));
    965 };
    966 
    967 /**
    968  * Remove over-capacity and obsolete items.
    969  *
    970  * @param {Object} map A map of timestamped key-value pairs.
    971  * @private
    972  */
    973 util.AppCache.cleanup_ = function(map) {
    974   // Sort keys by ascending timestamps.
    975   var keys = [];
    976   for (var key in map) {
    977     if (map.hasOwnProperty(key))
    978       keys.push(key);
    979   }
    980   keys.sort(function(a, b) { return map[a].expire > map[b].expire });
    981 
    982   var cutoff = Date.now();
    983 
    984   var obsolete = 0;
    985   while (obsolete < keys.length &&
    986          map[keys[obsolete]].expire < cutoff) {
    987     obsolete++;
    988   }
    989 
    990   var overCapacity = Math.max(0, keys.length - util.AppCache.CAPACITY);
    991 
    992   var itemsToDelete = Math.max(obsolete, overCapacity);
    993   for (var i = 0; i != itemsToDelete; i++) {
    994     delete map[keys[i]];
    995   }
    996 };
    997 
    998 /**
    999  * Load an image.
   1000  *
   1001  * @param {Image} image Image element.
   1002  * @param {string} url Source url.
   1003  * @param {Object=} opt_options Hash array of options, eg. width, height,
   1004  *     maxWidth, maxHeight, scale, cache.
   1005  * @param {function()=} opt_isValid Function returning false iff the task
   1006  *     is not valid and should be aborted.
   1007  * @return {?number} Task identifier or null if fetched immediately from
   1008  *     cache.
   1009  */
   1010 util.loadImage = function(image, url, opt_options, opt_isValid) {
   1011   return ImageLoaderClient.loadToImage(url,
   1012                                       image,
   1013                                       opt_options || {},
   1014                                       function() {},
   1015                                       function() { image.onerror(); },
   1016                                       opt_isValid);
   1017 };
   1018 
   1019 /**
   1020  * Cancels loading an image.
   1021  * @param {number} taskId Task identifier returned by util.loadImage().
   1022  */
   1023 util.cancelLoadImage = function(taskId) {
   1024   ImageLoaderClient.getInstance().cancel(taskId);
   1025 };
   1026 
   1027 /**
   1028  * Finds proerty descriptor in the object prototype chain.
   1029  * @param {Object} object The object.
   1030  * @param {string} propertyName The property name.
   1031  * @return {Object} Property descriptor.
   1032  */
   1033 util.findPropertyDescriptor = function(object, propertyName) {
   1034   for (var p = object; p; p = Object.getPrototypeOf(p)) {
   1035     var d = Object.getOwnPropertyDescriptor(p, propertyName);
   1036     if (d)
   1037       return d;
   1038   }
   1039   return null;
   1040 };
   1041 
   1042 /**
   1043  * Calls inherited property setter (useful when property is
   1044  * overriden).
   1045  * @param {Object} object The object.
   1046  * @param {string} propertyName The property name.
   1047  * @param {*} value Value to set.
   1048  */
   1049 util.callInheritedSetter = function(object, propertyName, value) {
   1050   var d = util.findPropertyDescriptor(Object.getPrototypeOf(object),
   1051                                       propertyName);
   1052   d.set.call(object, value);
   1053 };
   1054 
   1055 /**
   1056  * Returns true if the board of the device matches the given prefix.
   1057  * @param {string} boardPrefix The board prefix to match against.
   1058  *     (ex. "x86-mario". Prefix is used as the actual board name comes with
   1059  *     suffix like "x86-mario-something".
   1060  * @return {boolean} True if the board of the device matches the given prefix.
   1061  */
   1062 util.boardIs = function(boardPrefix) {
   1063   // The board name should be lower-cased, but making it case-insensitive for
   1064   // backward compatibility just in case.
   1065   var board = str('CHROMEOS_RELEASE_BOARD');
   1066   var pattern = new RegExp('^' + boardPrefix, 'i');
   1067   return board.match(pattern) != null;
   1068 };
   1069 
   1070 /**
   1071  * Adds an isFocused method to the current window object.
   1072  */
   1073 util.addIsFocusedMethod = function() {
   1074   var focused = true;
   1075 
   1076   window.addEventListener('focus', function() {
   1077     focused = true;
   1078   });
   1079 
   1080   window.addEventListener('blur', function() {
   1081     focused = false;
   1082   });
   1083 
   1084   /**
   1085    * @return {boolean} True if focused.
   1086    */
   1087   window.isFocused = function() {
   1088     return focused;
   1089   };
   1090 };
   1091 
   1092 /**
   1093  * Makes a redirect to the specified Files.app's window from another window.
   1094  * @param {number} id Window id.
   1095  * @param {string} url Target url.
   1096  * @return {boolean} True if the window has been found. False otherwise.
   1097  */
   1098 util.redirectMainWindow = function(id, url) {
   1099   // TODO(mtomasz): Implement this for Apps V2, once the photo importer is
   1100   // restored.
   1101   return false;
   1102 };
   1103 
   1104 /**
   1105  * Checks, if the Files.app's window is in a full screen mode.
   1106  *
   1107  * @param {AppWindow} appWindow App window to be maximized.
   1108  * @return {boolean} True if the full screen mode is enabled.
   1109  */
   1110 util.isFullScreen = function(appWindow) {
   1111   if (appWindow) {
   1112     return appWindow.isFullscreen();
   1113   } else {
   1114     console.error('App window not passed. Unable to check status of ' +
   1115                   'the full screen mode.');
   1116     return false;
   1117   }
   1118 };
   1119 
   1120 /**
   1121  * Toggles the full screen mode.
   1122  *
   1123  * @param {AppWindow} appWindow App window to be maximized.
   1124  * @param {boolean} enabled True for enabling, false for disabling.
   1125  */
   1126 util.toggleFullScreen = function(appWindow, enabled) {
   1127   if (appWindow) {
   1128     if (enabled)
   1129       appWindow.fullscreen();
   1130     else
   1131       appWindow.restore();
   1132     return;
   1133   }
   1134 
   1135   console.error(
   1136       'App window not passed. Unable to toggle the full screen mode.');
   1137 };
   1138 
   1139 /**
   1140  * The type of a file operation error.
   1141  * @enum {number}
   1142  */
   1143 util.FileOperationErrorType = {
   1144   UNEXPECTED_SOURCE_FILE: 0,
   1145   TARGET_EXISTS: 1,
   1146   FILESYSTEM_ERROR: 2,
   1147 };
   1148 
   1149 /**
   1150  * The type of an entry changed event.
   1151  * @enum {number}
   1152  */
   1153 util.EntryChangedType = {
   1154   CREATED: 0,
   1155   DELETED: 1,
   1156 };
   1157 
   1158 /**
   1159  * @param {DirectoryEntry|Object} entry DirectoryEntry to be checked.
   1160  * @return {boolean} True if the given entry is fake.
   1161  */
   1162 util.isFakeDirectoryEntry = function(entry) {
   1163   // Currently, fake entry doesn't support createReader.
   1164   return !('createReader' in entry);
   1165 };
   1166 
   1167 /**
   1168  * Creates a FileError instance with given code.
   1169  * Note that we cannot create FileError instance by "new FileError(code)",
   1170  * unfortunately, so here we use Object.create.
   1171  * @param {number} code Error code for the FileError.
   1172  * @return {FileError} FileError instance
   1173  */
   1174 util.createFileError = function(code) {
   1175   return Object.create(FileError.prototype, {
   1176     code: { get: function() { return code; } }
   1177   });
   1178 };
   1179 
   1180 /**
   1181  * @param {Entry|Object} entry1 The entry to be compared. Can be a fake.
   1182  * @param {Entry|Object} entry2 The entry to be compared. Can be a fake.
   1183  * @return {boolean} True if the both entry represents a same file or directory.
   1184  */
   1185 util.isSameEntry = function(entry1, entry2) {
   1186   // Currently, we can assume there is only one root.
   1187   // When we support multi-file system, we need to look at filesystem, too.
   1188   return entry1.fullPath == entry2.fullPath;
   1189 };
   1190 
   1191 /**
   1192  * @param {Entry|Object} parent The parent entry. Can be a fake.
   1193  * @param {Entry|Object} child The child entry. Can be a fake.
   1194  * @return {boolean} True if parent entry is actualy the parent of the child
   1195  *     entry.
   1196  */
   1197 util.isParentEntry = function(parent, child) {
   1198   // Currently, we can assume there is only one root.
   1199   // When we support multi-file system, we need to look at filesystem, too.
   1200   return PathUtil.isParentPath(parent.fullPath, child.fullPath);
   1201 };
   1202