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