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 '<'; 96 case '>': return '>'; 97 case '&': return '&'; 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 '<': return '<'; 110 case '>': return '>'; 111 case '&': 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