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 * MetadataCache is a map from Entry to an object containing properties. 9 * Properties are divided by types, and all properties of one type are accessed 10 * at once. 11 * Some of the properties: 12 * { 13 * filesystem: size, modificationTime 14 * internal: presence 15 * drive: pinned, present, hosted, availableOffline 16 * streaming: (no property) 17 * 18 * Following are not fetched for non-present drive files. 19 * media: artist, album, title, width, height, imageTransform, etc. 20 * thumbnail: url, transform 21 * 22 * Following are always fetched from content, and so force the downloading 23 * of remote drive files. One should use this for required content metadata, 24 * i.e. image orientation. 25 * fetchedMedia: width, height, etc. 26 * } 27 * 28 * Typical usages: 29 * { 30 * cache.get([entry1, entry2], 'drive|filesystem', function(metadata) { 31 * if (metadata[0].drive.pinned && metadata[1].filesystem.size === 0) 32 * alert("Pinned and empty!"); 33 * }); 34 * 35 * cache.set(entry, 'internal', {presence: 'deleted'}); 36 * 37 * cache.clear([fileEntry1, fileEntry2], 'filesystem'); 38 * 39 * // Getting fresh value. 40 * cache.clear(entry, 'thumbnail'); 41 * cache.getOne(entry, 'thumbnail', function(thumbnail) { 42 * img.src = thumbnail.url; 43 * }); 44 * 45 * var cached = cache.getCached(entry, 'filesystem'); 46 * var size = (cached && cached.size) || UNKNOWN_SIZE; 47 * } 48 * 49 * @param {Array.<MetadataProvider>} providers Metadata providers. 50 * @constructor 51 */ 52function MetadataCache(providers) { 53 /** 54 * Map from Entry (using Entry.toURL) to metadata. Metadata contains 55 * |properties| - an hierarchical object of values, and an object for each 56 * metadata provider: <prodiver-id>: {time, callbacks} 57 * @private 58 */ 59 this.cache_ = {}; 60 61 /** 62 * List of metadata providers. 63 * @private 64 */ 65 this.providers_ = providers; 66 67 /** 68 * List of observers added. Each one is an object with fields: 69 * re - regexp of urls; 70 * type - metadata type; 71 * callback - the callback. 72 * @private 73 */ 74 this.observers_ = []; 75 this.observerId_ = 0; 76 77 this.batchCount_ = 0; 78 this.totalCount_ = 0; 79 80 this.currentCacheSize_ = 0; 81 82 /** 83 * Time of first get query of the current batch. Items updated later than this 84 * will not be evicted. 85 * 86 * @type {Date} 87 * @private 88 */ 89 this.lastBatchStart_ = new Date(); 90 91 Object.seal(this); 92} 93 94/** 95 * Observer type: it will be notified if the changed Entry is exactly the same 96 * as the observed Entry. 97 */ 98MetadataCache.EXACT = 0; 99 100/** 101 * Observer type: it will be notified if the changed Entry is an immediate child 102 * of the observed Entry. 103 */ 104MetadataCache.CHILDREN = 1; 105 106/** 107 * Observer type: it will be notified if the changed Entry is a descendant of 108 * of the observer Entry. 109 */ 110MetadataCache.DESCENDANTS = 2; 111 112/** 113 * Margin of the cache size. This amount of caches may be kept in addition. 114 */ 115MetadataCache.EVICTION_THRESHOLD_MARGIN = 500; 116 117/** 118 * @param {VolumeManagerWrapper} volumeManager Volume manager instance. 119 * @return {MetadataCache!} The cache with all providers. 120 */ 121MetadataCache.createFull = function(volumeManager) { 122 // DriveProvider should be prior to FileSystemProvider, because it covers 123 // FileSystemProvider for files in Drive. 124 return new MetadataCache([ 125 new DriveProvider(volumeManager), 126 new FilesystemProvider(), 127 new ContentProvider() 128 ]); 129}; 130 131/** 132 * Clones metadata entry. Metadata entries may contain scalars, arrays, 133 * hash arrays and Date object. Other objects are not supported. 134 * @param {Object} metadata Metadata object. 135 * @return {Object} Cloned entry. 136 */ 137MetadataCache.cloneMetadata = function(metadata) { 138 if (metadata instanceof Array) { 139 var result = []; 140 for (var index = 0; index < metadata.length; index++) { 141 result[index] = MetadataCache.cloneMetadata(metadata[index]); 142 } 143 return result; 144 } else if (metadata instanceof Date) { 145 var result = new Date(); 146 result.setTime(metadata.getTime()); 147 return result; 148 } else if (metadata instanceof Object) { // Hash array only. 149 var result = {}; 150 for (var property in metadata) { 151 if (metadata.hasOwnProperty(property)) 152 result[property] = MetadataCache.cloneMetadata(metadata[property]); 153 } 154 return result; 155 } else { 156 return metadata; 157 } 158}; 159 160/** 161 * @return {boolean} Whether all providers are ready. 162 */ 163MetadataCache.prototype.isInitialized = function() { 164 for (var index = 0; index < this.providers_.length; index++) { 165 if (!this.providers_[index].isInitialized()) return false; 166 } 167 return true; 168}; 169 170/** 171 * Changes the size of cache by delta value. The actual cache size may be larger 172 * than the given value. 173 * 174 * @param {number} delta The delta size to be changed the cache size by. 175 */ 176MetadataCache.prototype.resizeBy = function(delta) { 177 this.currentCacheSize_ += delta; 178 if (this.currentCacheSize_ < 0) 179 this.currentCacheSize_ = 0; 180 181 if (this.totalCount_ > this.currentEvictionThreshold_()) 182 this.evict_(); 183}; 184 185/** 186 * Returns the current threshold to evict caches. When the number of caches 187 * exceeds this, the cache should be evicted. 188 * @return {number} Threshold to evict caches. 189 * @private 190 */ 191MetadataCache.prototype.currentEvictionThreshold_ = function() { 192 return this.currentCacheSize_ * 2 + MetadataCache.EVICTION_THRESHOLD_MARGIN; 193}; 194 195/** 196 * Fetches the metadata, puts it in the cache, and passes to callback. 197 * If required metadata is already in the cache, does not fetch it again. 198 * 199 * @param {Array.<Entry>} entries The list of entries. 200 * @param {string} type The metadata type. 201 * @param {function(Object)} callback The metadata is passed to callback. 202 */ 203MetadataCache.prototype.get = function(entries, type, callback) { 204 return this.getInternal_(entries, type, false, callback); 205}; 206 207/** 208 * Fetches the metadata, puts it in the cache, and passes to callback. 209 * Even if required metadata is already in the cache, fetches it again. 210 * 211 * @param {Array.<Entry>} entries The list of entries. 212 * @param {string} type The metadata type. 213 * @param {function(Object)} callback The metadata is passed to callback. 214 */ 215MetadataCache.prototype.getLatest = function(entries, type, callback, refresh) { 216 return this.getInternal_(entries, type, true, callback); 217}; 218 219/** 220 * Fetches the metadata, puts it in the cache. This is only for internal use. 221 * 222 * @param {Array.<Entry>} entries The list of entries. 223 * @param {string} type The metadata type. 224 * @param {boolean} refresh True to get the latest value and refresh the cache, 225 * false to get the value from the cache. 226 * @param {function(Object)} callback The metadata is passed to callback. 227 */ 228MetadataCache.prototype.getInternal_ = 229 function(entries, type, refresh, callback) { 230 if (entries.length === 0) { 231 if (callback) callback([]); 232 return; 233 } 234 235 var result = []; 236 var remaining = entries.length; 237 this.startBatchUpdates(); 238 239 var onOneItem = function(index, value) { 240 result[index] = value; 241 remaining--; 242 if (remaining === 0) { 243 this.endBatchUpdates(); 244 if (callback) setTimeout(callback, 0, result); 245 } 246 }; 247 248 for (var index = 0; index < entries.length; index++) { 249 result.push(null); 250 this.getOneInternal_(entries[index], 251 type, 252 refresh, 253 onOneItem.bind(this, index)); 254 } 255}; 256 257/** 258 * Fetches the metadata for one Entry. See comments to |get|. 259 * @param {Entry} entry The entry. 260 * @param {string} type Metadata type. 261 * @param {function(Object)} callback The callback. 262 */ 263MetadataCache.prototype.getOne = function(entry, type, callback, refresh) { 264 return this.getOneInternal_(entry, type, false, callback); 265}; 266 267/** 268 * Fetches the metadata for one Entry. This is only for internal use. 269 * 270 * @param {Entry} entry The entry. 271 * @param {string} type Metadata type. 272 * @param {boolean} refresh True to get the latest value and refresh the cache, 273 * false to get the value from the cache. 274 * @param {function(Object)} callback The callback. 275 * @private 276 */ 277MetadataCache.prototype.getOneInternal_ = 278 function(entry, type, refresh, callback) { 279 if (type.indexOf('|') !== -1) { 280 var types = type.split('|'); 281 var result = {}; 282 var typesLeft = types.length; 283 284 var onOneType = function(requestedType, metadata) { 285 result[requestedType] = metadata; 286 typesLeft--; 287 if (typesLeft === 0) callback(result); 288 }; 289 290 for (var index = 0; index < types.length; index++) { 291 this.getOneInternal_(entry, types[index], refresh, 292 onOneType.bind(null, types[index])); 293 } 294 return; 295 } 296 297 callback = callback || function() {}; 298 299 var entryURL = entry.toURL(); 300 if (!(entryURL in this.cache_)) { 301 this.cache_[entryURL] = this.createEmptyItem_(); 302 this.totalCount_++; 303 } 304 305 var item = this.cache_[entryURL]; 306 307 if (!refresh && type in item.properties) { 308 // Uses cache, if available and not on the 'refresh' mode. 309 callback(item.properties[type]); 310 return; 311 } 312 313 this.startBatchUpdates(); 314 var providers = this.providers_.slice(); 315 var currentProvider; 316 var self = this; 317 318 var queryProvider = function() { 319 var id = currentProvider.getId(); 320 321 // If on 'refresh'-mode, replaces the callback array. The previous 322 // array may be remaining in the closure captured by previous tasks. 323 if (refresh) 324 item[id].callbacks = []; 325 var fetchedCallbacks = item[id].callbacks; 326 327 var onFetched = function() { 328 if (type in item.properties) { 329 self.endBatchUpdates(); 330 // Got properties from provider. 331 callback(item.properties[type]); 332 } else { 333 tryNextProvider(); 334 } 335 }; 336 337 var onProviderProperties = function(properties) { 338 var callbacks = fetchedCallbacks.splice(0); 339 item.time = new Date(); 340 self.mergeProperties_(entry, properties); 341 342 for (var index = 0; index < callbacks.length; index++) { 343 callbacks[index](); 344 } 345 }; 346 347 fetchedCallbacks.push(onFetched); 348 349 // Querying now. 350 if (fetchedCallbacks.length === 1) 351 currentProvider.fetch(entry, type, onProviderProperties); 352 }; 353 354 var tryNextProvider = function() { 355 if (providers.length === 0) { 356 self.endBatchUpdates(); 357 callback(item.properties[type] || null); 358 return; 359 } 360 361 currentProvider = providers.shift(); 362 if (currentProvider.supportsEntry(entry) && 363 currentProvider.providesType(type)) { 364 queryProvider(); 365 } else { 366 tryNextProvider(); 367 } 368 }; 369 370 tryNextProvider(); 371}; 372 373/** 374 * Returns the cached metadata value, or |null| if not present. 375 * @param {Entry} entry Entry. 376 * @param {string} type The metadata type. 377 * @return {Object} The metadata or null. 378 */ 379MetadataCache.prototype.getCached = function(entry, type) { 380 // Entry.cachedUrl may be set in DirectoryContents.onNewEntries_(). 381 // See the comment there for detail. 382 var entryURL = entry.cachedUrl || entry.toURL(); 383 var cache = this.cache_[entryURL]; 384 return cache ? (cache.properties[type] || null) : null; 385}; 386 387/** 388 * Puts the metadata into cache 389 * @param {Entry|Array.<Entry>} entries The list of entries. May be just a 390 * single entry. 391 * @param {string} type The metadata type. 392 * @param {Array.<Object>} values List of corresponding metadata values. 393 */ 394MetadataCache.prototype.set = function(entries, type, values) { 395 if (!(entries instanceof Array)) { 396 entries = [entries]; 397 values = [values]; 398 } 399 400 this.startBatchUpdates(); 401 for (var index = 0; index < entries.length; index++) { 402 var entryURL = entries[index].toURL(); 403 if (!(entryURL in this.cache_)) { 404 this.cache_[entryURL] = this.createEmptyItem_(); 405 this.totalCount_++; 406 } 407 this.cache_[entryURL].properties[type] = values[index]; 408 this.notifyObservers_(entries[index], type); 409 } 410 this.endBatchUpdates(); 411}; 412 413/** 414 * Clears the cached metadata values. 415 * @param {Entry|Array.<Entry>} entries The list of entries. May be just a 416 * single entry. 417 * @param {string} type The metadata types or * for any type. 418 */ 419MetadataCache.prototype.clear = function(entries, type) { 420 if (!(entries instanceof Array)) 421 entries = [entries]; 422 423 var types = type.split('|'); 424 425 for (var index = 0; index < entries.length; index++) { 426 var entry = entries[index]; 427 var entryURL = entry.toURL(); 428 if (entryURL in this.cache_) { 429 if (type === '*') { 430 this.cache_[entryURL].properties = {}; 431 } else { 432 for (var j = 0; j < types.length; j++) { 433 var type = types[j]; 434 delete this.cache_[entryURL].properties[type]; 435 } 436 } 437 } 438 } 439}; 440 441/** 442 * Clears the cached metadata values recursively. 443 * @param {Entry} entry An entry to be cleared recursively from cache. 444 * @param {string} type The metadata types or * for any type. 445 */ 446MetadataCache.prototype.clearRecursively = function(entry, type) { 447 var types = type.split('|'); 448 var keys = Object.keys(this.cache_); 449 var entryURL = entry.toURL(); 450 451 for (var index = 0; index < keys.length; index++) { 452 var cachedEntryURL = keys[index]; 453 if (cachedEntryURL.substring(0, entryURL.length) === entryURL) { 454 if (type === '*') { 455 this.cache_[cachedEntryURL].properties = {}; 456 } else { 457 for (var j = 0; j < types.length; j++) { 458 var type = types[j]; 459 delete this.cache_[cachedEntryURL].properties[type]; 460 } 461 } 462 } 463 } 464}; 465 466/** 467 * Adds an observer, which will be notified when metadata changes. 468 * @param {Entry} entry The root entry to look at. 469 * @param {number} relation This defines, which items will trigger the observer. 470 * See comments to |MetadataCache.EXACT| and others. 471 * @param {string} type The metadata type. 472 * @param {function(Array.<Entry>, Object.<string, Object>)} observer Map of 473 * entries and corresponding metadata values are passed to this callback. 474 * @return {number} The observer id, which can be used to remove it. 475 */ 476MetadataCache.prototype.addObserver = function( 477 entry, relation, type, observer) { 478 var entryURL = entry.toURL(); 479 var re; 480 if (relation === MetadataCache.CHILDREN) 481 re = entryURL + '(/[^/]*)?'; 482 else if (relation === MetadataCache.DESCENDANTS) 483 re = entryURL + '(/.*)?'; 484 else 485 re = entryURL; 486 487 var id = ++this.observerId_; 488 this.observers_.push({ 489 re: new RegExp('^' + re + '$'), 490 type: type, 491 callback: observer, 492 id: id, 493 pending: {} 494 }); 495 496 return id; 497}; 498 499/** 500 * Removes the observer. 501 * @param {number} id Observer id. 502 * @return {boolean} Whether observer was removed or not. 503 */ 504MetadataCache.prototype.removeObserver = function(id) { 505 for (var index = 0; index < this.observers_.length; index++) { 506 if (this.observers_[index].id === id) { 507 this.observers_.splice(index, 1); 508 return true; 509 } 510 } 511 return false; 512}; 513 514/** 515 * Start batch updates. 516 */ 517MetadataCache.prototype.startBatchUpdates = function() { 518 this.batchCount_++; 519 if (this.batchCount_ === 1) 520 this.lastBatchStart_ = new Date(); 521}; 522 523/** 524 * End batch updates. Notifies observers if all nested updates are finished. 525 */ 526MetadataCache.prototype.endBatchUpdates = function() { 527 this.batchCount_--; 528 if (this.batchCount_ !== 0) return; 529 if (this.totalCount_ > this.currentEvictionThreshold_()) 530 this.evict_(); 531 for (var index = 0; index < this.observers_.length; index++) { 532 var observer = this.observers_[index]; 533 var entries = []; 534 var properties = {}; 535 for (var entryURL in observer.pending) { 536 if (observer.pending.hasOwnProperty(entryURL) && 537 entryURL in this.cache_) { 538 var entry = observer.pending[entryURL]; 539 entries.push(entry); 540 properties[entryURL] = 541 this.cache_[entryURL].properties[observer.type] || null; 542 } 543 } 544 observer.pending = {}; 545 if (entries.length > 0) { 546 observer.callback(entries, properties); 547 } 548 } 549}; 550 551/** 552 * Notifies observers or puts the data to pending list. 553 * @param {Entry} entry Changed entry. 554 * @param {string} type Metadata type. 555 * @private 556 */ 557MetadataCache.prototype.notifyObservers_ = function(entry, type) { 558 var entryURL = entry.toURL(); 559 for (var index = 0; index < this.observers_.length; index++) { 560 var observer = this.observers_[index]; 561 if (observer.type === type && observer.re.test(entryURL)) { 562 if (this.batchCount_ === 0) { 563 // Observer expects array of urls and map of properties. 564 var property = {}; 565 property[entryURL] = this.cache_[entryURL].properties[type] || null; 566 observer.callback( 567 [entry], property); 568 } else { 569 observer.pending[entryURL] = entry; 570 } 571 } 572 } 573}; 574 575/** 576 * Removes the oldest items from the cache. 577 * This method never removes the items from last batch. 578 * @private 579 */ 580MetadataCache.prototype.evict_ = function() { 581 var toRemove = []; 582 583 // We leave only a half of items, so we will not call evict_ soon again. 584 var desiredCount = this.currentEvictionThreshold_(); 585 var removeCount = this.totalCount_ - desiredCount; 586 for (var url in this.cache_) { 587 if (this.cache_.hasOwnProperty(url) && 588 this.cache_[url].time < this.lastBatchStart_) { 589 toRemove.push(url); 590 } 591 } 592 593 toRemove.sort(function(a, b) { 594 var aTime = this.cache_[a].time; 595 var bTime = this.cache_[b].time; 596 return aTime < bTime ? -1 : aTime > bTime ? 1 : 0; 597 }.bind(this)); 598 599 removeCount = Math.min(removeCount, toRemove.length); 600 this.totalCount_ -= removeCount; 601 for (var index = 0; index < removeCount; index++) { 602 delete this.cache_[toRemove[index]]; 603 } 604}; 605 606/** 607 * @return {Object} Empty cache item. 608 * @private 609 */ 610MetadataCache.prototype.createEmptyItem_ = function() { 611 var item = {properties: {}}; 612 for (var index = 0; index < this.providers_.length; index++) { 613 item[this.providers_[index].getId()] = {callbacks: []}; 614 } 615 return item; 616}; 617 618/** 619 * Caches all the properties from data to cache entry for the entry. 620 * @param {Entry} entry The file entry. 621 * @param {Object} data The properties. 622 * @private 623 */ 624MetadataCache.prototype.mergeProperties_ = function(entry, data) { 625 if (data === null) return; 626 var entryURL = entry.toURL(); 627 if (!(entryURL in this.cache_)) { 628 this.cache_[entryURL] = this.createEmptyItem_(); 629 this.totalCount_++; 630 } 631 var properties = this.cache_[entryURL].properties; 632 for (var type in data) { 633 if (data.hasOwnProperty(type)) { 634 properties[type] = data[type]; 635 this.notifyObservers_(entry, type); 636 } 637 } 638}; 639 640/** 641 * Base class for metadata providers. 642 * @constructor 643 */ 644function MetadataProvider() { 645} 646 647/** 648 * @param {Entry} entry The entry. 649 * @return {boolean} Whether this provider supports the entry. 650 */ 651MetadataProvider.prototype.supportsEntry = function(entry) { return false; }; 652 653/** 654 * @param {string} type The metadata type. 655 * @return {boolean} Whether this provider provides this metadata. 656 */ 657MetadataProvider.prototype.providesType = function(type) { return false; }; 658 659/** 660 * @return {string} Unique provider id. 661 */ 662MetadataProvider.prototype.getId = function() { return ''; }; 663 664/** 665 * @return {boolean} Whether provider is ready. 666 */ 667MetadataProvider.prototype.isInitialized = function() { return true; }; 668 669/** 670 * Fetches the metadata. It's suggested to return all the metadata this provider 671 * can fetch at once. 672 * @param {Entry} entry File entry. 673 * @param {string} type Requested metadata type. 674 * @param {function(Object)} callback Callback expects a map from metadata type 675 * to metadata value. 676 */ 677MetadataProvider.prototype.fetch = function(entry, type, callback) { 678 throw new Error('Default metadata provider cannot fetch.'); 679}; 680 681 682/** 683 * Provider of filesystem metadata. 684 * This provider returns the following objects: 685 * filesystem: { size, modificationTime } 686 * @constructor 687 */ 688function FilesystemProvider() { 689 MetadataProvider.call(this); 690} 691 692FilesystemProvider.prototype = { 693 __proto__: MetadataProvider.prototype 694}; 695 696/** 697 * @param {Entry} entry The entry. 698 * @return {boolean} Whether this provider supports the entry. 699 */ 700FilesystemProvider.prototype.supportsEntry = function(entry) { 701 return true; 702}; 703 704/** 705 * @param {string} type The metadata type. 706 * @return {boolean} Whether this provider provides this metadata. 707 */ 708FilesystemProvider.prototype.providesType = function(type) { 709 return type === 'filesystem'; 710}; 711 712/** 713 * @return {string} Unique provider id. 714 */ 715FilesystemProvider.prototype.getId = function() { return 'filesystem'; }; 716 717/** 718 * Fetches the metadata. 719 * @param {Entry} entry File entry. 720 * @param {string} type Requested metadata type. 721 * @param {function(Object)} callback Callback expects a map from metadata type 722 * to metadata value. 723 */ 724FilesystemProvider.prototype.fetch = function( 725 entry, type, callback) { 726 function onError(error) { 727 callback(null); 728 } 729 730 function onMetadata(entry, metadata) { 731 callback({ 732 filesystem: { 733 size: (entry.isFile ? (metadata.size || 0) : -1), 734 modificationTime: metadata.modificationTime 735 } 736 }); 737 } 738 739 entry.getMetadata(onMetadata.bind(null, entry), onError); 740}; 741 742/** 743 * Provider of drive metadata. 744 * This provider returns the following objects: 745 * drive: { pinned, hosted, present, customIconUrl, etc. } 746 * thumbnail: { url, transform } 747 * streaming: { } 748 * @param {VolumeManagerWrapper} volumeManager Volume manager instance. 749 * @constructor 750 */ 751function DriveProvider(volumeManager) { 752 MetadataProvider.call(this); 753 754 /** 755 * @type {VolumeManagerWrapper} 756 * @private 757 */ 758 this.volumeManager_ = volumeManager; 759 760 // We batch metadata fetches into single API call. 761 this.entries_ = []; 762 this.callbacks_ = []; 763 this.scheduled_ = false; 764 765 this.callApiBound_ = this.callApi_.bind(this); 766} 767 768DriveProvider.prototype = { 769 __proto__: MetadataProvider.prototype 770}; 771 772/** 773 * @param {Entry} entry The entry. 774 * @return {boolean} Whether this provider supports the entry. 775 */ 776DriveProvider.prototype.supportsEntry = function(entry) { 777 var locationInfo = this.volumeManager_.getLocationInfo(entry); 778 return locationInfo && locationInfo.isDriveBased; 779}; 780 781/** 782 * @param {string} type The metadata type. 783 * @return {boolean} Whether this provider provides this metadata. 784 */ 785DriveProvider.prototype.providesType = function(type) { 786 return type === 'drive' || type === 'thumbnail' || 787 type === 'streaming' || type === 'media' || type === 'filesystem'; 788}; 789 790/** 791 * @return {string} Unique provider id. 792 */ 793DriveProvider.prototype.getId = function() { return 'drive'; }; 794 795/** 796 * Fetches the metadata. 797 * @param {Entry} entry File entry. 798 * @param {string} type Requested metadata type. 799 * @param {function(Object)} callback Callback expects a map from metadata type 800 * to metadata value. 801 */ 802DriveProvider.prototype.fetch = function(entry, type, callback) { 803 this.entries_.push(entry); 804 this.callbacks_.push(callback); 805 if (!this.scheduled_) { 806 this.scheduled_ = true; 807 setTimeout(this.callApiBound_, 0); 808 } 809}; 810 811/** 812 * Schedules the API call. 813 * @private 814 */ 815DriveProvider.prototype.callApi_ = function() { 816 this.scheduled_ = false; 817 818 var entries = this.entries_; 819 var callbacks = this.callbacks_; 820 this.entries_ = []; 821 this.callbacks_ = []; 822 var self = this; 823 824 // TODO(mtomasz): Make getDriveEntryProperties accept Entry instead of URL. 825 var entryURLs = util.entriesToURLs(entries); 826 chrome.fileBrowserPrivate.getDriveEntryProperties( 827 entryURLs, 828 function(propertiesList) { 829 console.assert(propertiesList.length === callbacks.length); 830 for (var i = 0; i < callbacks.length; i++) { 831 callbacks[i](self.convert_(propertiesList[i], entries[i])); 832 } 833 }); 834}; 835 836/** 837 * @param {DriveEntryProperties} data Drive entry properties. 838 * @param {Entry} entry File entry. 839 * @return {boolean} True if the file is available offline. 840 */ 841DriveProvider.isAvailableOffline = function(data, entry) { 842 if (data.isPresent) 843 return true; 844 845 if (!data.isHosted) 846 return false; 847 848 // What's available offline? See the 'Web' column at: 849 // http://support.google.com/drive/answer/1628467 850 var subtype = FileType.getType(entry).subtype; 851 return (subtype === 'doc' || 852 subtype === 'draw' || 853 subtype === 'sheet' || 854 subtype === 'slides'); 855}; 856 857/** 858 * @param {DriveEntryProperties} data Drive entry properties. 859 * @return {boolean} True if opening the file does not require downloading it 860 * via a metered connection. 861 */ 862DriveProvider.isAvailableWhenMetered = function(data) { 863 return data.isPresent || data.isHosted; 864}; 865 866/** 867 * Converts API metadata to internal format. 868 * @param {Object} data Metadata from API call. 869 * @param {Entry} entry File entry. 870 * @return {Object} Metadata in internal format. 871 * @private 872 */ 873DriveProvider.prototype.convert_ = function(data, entry) { 874 var result = {}; 875 result.drive = { 876 present: data.isPresent, 877 pinned: data.isPinned, 878 hosted: data.isHosted, 879 imageWidth: data.imageWidth, 880 imageHeight: data.imageHeight, 881 imageRotation: data.imageRotation, 882 availableOffline: DriveProvider.isAvailableOffline(data, entry), 883 availableWhenMetered: DriveProvider.isAvailableWhenMetered(data), 884 customIconUrl: data.customIconUrl || '', 885 contentMimeType: data.contentMimeType || '', 886 sharedWithMe: data.sharedWithMe, 887 shared: data.shared 888 }; 889 890 result.filesystem = { 891 size: (entry.isFile ? (data.fileSize || 0) : -1), 892 modificationTime: new Date(data.lastModifiedTime) 893 }; 894 895 if ('thumbnailUrl' in data) { 896 result.thumbnail = { 897 url: data.thumbnailUrl, 898 transform: null 899 }; 900 } else if (data.isPresent) { 901 result.thumbnail = null; 902 } else { 903 // Block the local fetch for drive files, which require downloading. 904 result.thumbnail = {url: '', transform: null}; 905 } 906 907 result.media = data.isPresent ? null : {}; 908 // Indicate that the data is not available in local cache. 909 // It used to have a field 'url' for streaming play, but it is 910 // derprecated. See crbug.com/174560. 911 result.streaming = data.isPresent ? null : {}; 912 913 return result; 914}; 915 916 917/** 918 * Provider of content metadata. 919 * This provider returns the following objects: 920 * thumbnail: { url, transform } 921 * media: { artist, album, title, width, height, imageTransform, etc. } 922 * fetchedMedia: { same fields here } 923 * @constructor 924 */ 925function ContentProvider() { 926 MetadataProvider.call(this); 927 928 // Pass all URLs to the metadata reader until we have a correct filter. 929 this.urlFilter_ = /.*/; 930 931 var dispatcher = new SharedWorker(ContentProvider.WORKER_SCRIPT).port; 932 dispatcher.onmessage = this.onMessage_.bind(this); 933 dispatcher.postMessage({verb: 'init'}); 934 dispatcher.start(); 935 this.dispatcher_ = dispatcher; 936 937 // Initialization is not complete until the Worker sends back the 938 // 'initialized' message. See below. 939 this.initialized_ = false; 940 941 // Map from Entry.toURL() to callback. 942 // Note that simultaneous requests for same url are handled in MetadataCache. 943 this.callbacks_ = {}; 944} 945 946/** 947 * Path of a worker script. 948 * @type {string} 949 * @const 950 */ 951ContentProvider.WORKER_SCRIPT = 952 'chrome-extension://hhaomjibdihmijegdhdafkllkbggdgoj/' + 953 'foreground/js/metadata/metadata_dispatcher.js'; 954 955ContentProvider.prototype = { 956 __proto__: MetadataProvider.prototype 957}; 958 959/** 960 * @param {Entry} entry The entry. 961 * @return {boolean} Whether this provider supports the entry. 962 */ 963ContentProvider.prototype.supportsEntry = function(entry) { 964 return entry.toURL().match(this.urlFilter_); 965}; 966 967/** 968 * @param {string} type The metadata type. 969 * @return {boolean} Whether this provider provides this metadata. 970 */ 971ContentProvider.prototype.providesType = function(type) { 972 return type === 'thumbnail' || type === 'fetchedMedia' || type === 'media'; 973}; 974 975/** 976 * @return {string} Unique provider id. 977 */ 978ContentProvider.prototype.getId = function() { return 'content'; }; 979 980/** 981 * Fetches the metadata. 982 * @param {Entry} entry File entry. 983 * @param {string} type Requested metadata type. 984 * @param {function(Object)} callback Callback expects a map from metadata type 985 * to metadata value. 986 */ 987ContentProvider.prototype.fetch = function(entry, type, callback) { 988 if (entry.isDirectory) { 989 callback({}); 990 return; 991 } 992 var entryURL = entry.toURL(); 993 this.callbacks_[entryURL] = callback; 994 this.dispatcher_.postMessage({verb: 'request', arguments: [entryURL]}); 995}; 996 997/** 998 * Dispatch a message from a metadata reader to the appropriate on* method. 999 * @param {Object} event The event. 1000 * @private 1001 */ 1002ContentProvider.prototype.onMessage_ = function(event) { 1003 var data = event.data; 1004 1005 var methodName = 1006 'on' + data.verb.substr(0, 1).toUpperCase() + data.verb.substr(1) + '_'; 1007 1008 if (!(methodName in this)) { 1009 console.error('Unknown message from metadata reader: ' + data.verb, data); 1010 return; 1011 } 1012 1013 this[methodName].apply(this, data.arguments); 1014}; 1015 1016/** 1017 * @return {boolean} Whether provider is ready. 1018 */ 1019ContentProvider.prototype.isInitialized = function() { 1020 return this.initialized_; 1021}; 1022 1023/** 1024 * Handles the 'initialized' message from the metadata reader Worker. 1025 * @param {Object} regexp Regexp of supported urls. 1026 * @private 1027 */ 1028ContentProvider.prototype.onInitialized_ = function(regexp) { 1029 this.urlFilter_ = regexp; 1030 1031 // Tests can monitor for this state with 1032 // ExtensionTestMessageListener listener("worker-initialized"); 1033 // ASSERT_TRUE(listener.WaitUntilSatisfied()); 1034 // Automated tests need to wait for this, otherwise we crash in 1035 // browser_test cleanup because the worker process still has 1036 // URL requests in-flight. 1037 util.testSendMessage('worker-initialized'); 1038 this.initialized_ = true; 1039}; 1040 1041/** 1042 * Converts content metadata from parsers to the internal format. 1043 * @param {Object} metadata The content metadata. 1044 * @param {Object=} opt_result The internal metadata object ot put result in. 1045 * @return {Object!} Converted metadata. 1046 */ 1047ContentProvider.ConvertContentMetadata = function(metadata, opt_result) { 1048 var result = opt_result || {}; 1049 1050 if ('thumbnailURL' in metadata) { 1051 metadata.thumbnailTransform = metadata.thumbnailTransform || null; 1052 result.thumbnail = { 1053 url: metadata.thumbnailURL, 1054 transform: metadata.thumbnailTransform 1055 }; 1056 } 1057 1058 for (var key in metadata) { 1059 if (metadata.hasOwnProperty(key)) { 1060 if (!result.media) 1061 result.media = {}; 1062 result.media[key] = metadata[key]; 1063 } 1064 } 1065 1066 if (result.media) 1067 result.fetchedMedia = result.media; 1068 1069 return result; 1070}; 1071 1072/** 1073 * Handles the 'result' message from the worker. 1074 * @param {string} url File url. 1075 * @param {Object} metadata The metadata. 1076 * @private 1077 */ 1078ContentProvider.prototype.onResult_ = function(url, metadata) { 1079 var callback = this.callbacks_[url]; 1080 delete this.callbacks_[url]; 1081 callback(ContentProvider.ConvertContentMetadata(metadata)); 1082}; 1083 1084/** 1085 * Handles the 'error' message from the worker. 1086 * @param {string} url File entry. 1087 * @param {string} step Step failed. 1088 * @param {string} error Error description. 1089 * @param {Object?} metadata The metadata, if available. 1090 * @private 1091 */ 1092ContentProvider.prototype.onError_ = function(url, step, error, metadata) { 1093 if (MetadataCache.log) // Avoid log spam by default. 1094 console.warn('metadata: ' + url + ': ' + step + ': ' + error); 1095 metadata = metadata || {}; 1096 // Prevent asking for thumbnail again. 1097 metadata.thumbnailURL = ''; 1098 this.onResult_(url, metadata); 1099}; 1100 1101/** 1102 * Handles the 'log' message from the worker. 1103 * @param {Array.<*>} arglist Log arguments. 1104 * @private 1105 */ 1106ContentProvider.prototype.onLog_ = function(arglist) { 1107 if (MetadataCache.log) // Avoid log spam by default. 1108 console.log.apply(console, ['metadata:'].concat(arglist)); 1109}; 1110