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