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