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