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