| OLD | NEW |
| (Empty) |
| 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, customIconUrl, etc. } | |
| 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 imageWidth: data.imageWidth, | |
| 819 imageHeight: data.imageHeight, | |
| 820 imageRotation: data.imageRotation, | |
| 821 availableOffline: DriveProvider.isAvailableOffline(data, url), | |
| 822 availableWhenMetered: DriveProvider.isAvailableWhenMetered(data), | |
| 823 customIconUrl: data.customIconUrl || '', | |
| 824 contentMimeType: data.contentMimeType || '', | |
| 825 sharedWithMe: data.sharedWithMe | |
| 826 }; | |
| 827 | |
| 828 if (!data.isPresent) { | |
| 829 // Block the local fetch for drive files, which require downloading. | |
| 830 result.thumbnail = {url: '', transform: null}; | |
| 831 result.media = {}; | |
| 832 } | |
| 833 | |
| 834 if ('thumbnailUrl' in data) { | |
| 835 result.thumbnail = { | |
| 836 url: data.thumbnailUrl.replace(/s220/, 's500'), | |
| 837 transform: null | |
| 838 }; | |
| 839 } | |
| 840 if (!data.isPresent) { | |
| 841 // Indicate that the data is not available in local cache. | |
| 842 // It used to have a field 'url' for streaming play, but it is | |
| 843 // derprecated. See crbug.com/174560. | |
| 844 result.streaming = {}; | |
| 845 } | |
| 846 return result; | |
| 847 }; | |
| 848 | |
| 849 | |
| 850 /** | |
| 851 * Provider of content metadata. | |
| 852 * This provider returns the following objects: | |
| 853 * thumbnail: { url, transform } | |
| 854 * media: { artist, album, title, width, height, imageTransform, etc. } | |
| 855 * fetchedMedia: { same fields here } | |
| 856 * @constructor | |
| 857 */ | |
| 858 function ContentProvider() { | |
| 859 MetadataProvider.call(this); | |
| 860 | |
| 861 // Pass all URLs to the metadata reader until we have a correct filter. | |
| 862 this.urlFilter_ = /.*/; | |
| 863 | |
| 864 var path = document.location.pathname; | |
| 865 var workerPath = document.location.origin + | |
| 866 path.substring(0, path.lastIndexOf('/') + 1) + | |
| 867 'js/metadata/metadata_dispatcher.js'; | |
| 868 | |
| 869 if (ContentProvider.USE_SHARED_WORKER) { | |
| 870 this.dispatcher_ = new SharedWorker(workerPath).port; | |
| 871 this.dispatcher_.start(); | |
| 872 } else { | |
| 873 this.dispatcher_ = new Worker(workerPath); | |
| 874 } | |
| 875 | |
| 876 this.dispatcher_.onmessage = this.onMessage_.bind(this); | |
| 877 this.dispatcher_.postMessage({verb: 'init'}); | |
| 878 | |
| 879 // Initialization is not complete until the Worker sends back the | |
| 880 // 'initialized' message. See below. | |
| 881 this.initialized_ = false; | |
| 882 | |
| 883 // Map from url to callback. | |
| 884 // Note that simultaneous requests for same url are handled in MetadataCache. | |
| 885 this.callbacks_ = {}; | |
| 886 } | |
| 887 | |
| 888 /** | |
| 889 * Flag defining which kind of a worker to use. | |
| 890 * TODO(kaznacheev): Observe for some time and remove if SharedWorker does not | |
| 891 * cause any problems. | |
| 892 */ | |
| 893 ContentProvider.USE_SHARED_WORKER = true; | |
| 894 | |
| 895 ContentProvider.prototype = { | |
| 896 __proto__: MetadataProvider.prototype | |
| 897 }; | |
| 898 | |
| 899 /** | |
| 900 * @param {string} url The url. | |
| 901 * @return {boolean} Whether this provider supports the url. | |
| 902 */ | |
| 903 ContentProvider.prototype.supportsUrl = function(url) { | |
| 904 return url.match(this.urlFilter_); | |
| 905 }; | |
| 906 | |
| 907 /** | |
| 908 * @param {string} type The metadata type. | |
| 909 * @return {boolean} Whether this provider provides this metadata. | |
| 910 */ | |
| 911 ContentProvider.prototype.providesType = function(type) { | |
| 912 return type == 'thumbnail' || type == 'fetchedMedia' || type == 'media'; | |
| 913 }; | |
| 914 | |
| 915 /** | |
| 916 * @return {string} Unique provider id. | |
| 917 */ | |
| 918 ContentProvider.prototype.getId = function() { return 'content'; }; | |
| 919 | |
| 920 /** | |
| 921 * Fetches the metadata. | |
| 922 * @param {string} url File url. | |
| 923 * @param {string} type Requested metadata type. | |
| 924 * @param {function(Object)} callback Callback expects a map from metadata type | |
| 925 * to metadata value. | |
| 926 * @param {Entry=} opt_entry The file entry if present. | |
| 927 */ | |
| 928 ContentProvider.prototype.fetch = function(url, type, callback, opt_entry) { | |
| 929 if (opt_entry && opt_entry.isDirectory) { | |
| 930 callback({}); | |
| 931 return; | |
| 932 } | |
| 933 this.callbacks_[url] = callback; | |
| 934 this.dispatcher_.postMessage({verb: 'request', arguments: [url]}); | |
| 935 }; | |
| 936 | |
| 937 /** | |
| 938 * Dispatch a message from a metadata reader to the appropriate on* method. | |
| 939 * @param {Object} event The event. | |
| 940 * @private | |
| 941 */ | |
| 942 ContentProvider.prototype.onMessage_ = function(event) { | |
| 943 var data = event.data; | |
| 944 | |
| 945 var methodName = | |
| 946 'on' + data.verb.substr(0, 1).toUpperCase() + data.verb.substr(1) + '_'; | |
| 947 | |
| 948 if (!(methodName in this)) { | |
| 949 console.error('Unknown message from metadata reader: ' + data.verb, data); | |
| 950 return; | |
| 951 } | |
| 952 | |
| 953 this[methodName].apply(this, data.arguments); | |
| 954 }; | |
| 955 | |
| 956 /** | |
| 957 * @return {boolean} Whether provider is ready. | |
| 958 */ | |
| 959 ContentProvider.prototype.isInitialized = function() { | |
| 960 return this.initialized_; | |
| 961 }; | |
| 962 | |
| 963 /** | |
| 964 * Handles the 'initialized' message from the metadata reader Worker. | |
| 965 * @param {Object} regexp Regexp of supported urls. | |
| 966 * @private | |
| 967 */ | |
| 968 ContentProvider.prototype.onInitialized_ = function(regexp) { | |
| 969 this.urlFilter_ = regexp; | |
| 970 | |
| 971 // Tests can monitor for this state with | |
| 972 // ExtensionTestMessageListener listener("worker-initialized"); | |
| 973 // ASSERT_TRUE(listener.WaitUntilSatisfied()); | |
| 974 // Automated tests need to wait for this, otherwise we crash in | |
| 975 // browser_test cleanup because the worker process still has | |
| 976 // URL requests in-flight. | |
| 977 var test = chrome.test || window.top.chrome.test; | |
| 978 test.sendMessage('worker-initialized'); | |
| 979 this.initialized_ = true; | |
| 980 }; | |
| 981 | |
| 982 /** | |
| 983 * Converts content metadata from parsers to the internal format. | |
| 984 * @param {Object} metadata The content metadata. | |
| 985 * @param {Object=} opt_result The internal metadata object ot put result in. | |
| 986 * @return {Object!} Converted metadata. | |
| 987 */ | |
| 988 ContentProvider.ConvertContentMetadata = function(metadata, opt_result) { | |
| 989 var result = opt_result || {}; | |
| 990 | |
| 991 if ('thumbnailURL' in metadata) { | |
| 992 metadata.thumbnailTransform = metadata.thumbnailTransform || null; | |
| 993 result.thumbnail = { | |
| 994 url: metadata.thumbnailURL, | |
| 995 transform: metadata.thumbnailTransform | |
| 996 }; | |
| 997 } | |
| 998 | |
| 999 for (var key in metadata) { | |
| 1000 if (metadata.hasOwnProperty(key)) { | |
| 1001 if (!('media' in result)) result.media = {}; | |
| 1002 result.media[key] = metadata[key]; | |
| 1003 } | |
| 1004 } | |
| 1005 | |
| 1006 if ('media' in result) { | |
| 1007 result.fetchedMedia = result.media; | |
| 1008 } | |
| 1009 | |
| 1010 return result; | |
| 1011 }; | |
| 1012 | |
| 1013 /** | |
| 1014 * Handles the 'result' message from the worker. | |
| 1015 * @param {string} url File url. | |
| 1016 * @param {Object} metadata The metadata. | |
| 1017 * @private | |
| 1018 */ | |
| 1019 ContentProvider.prototype.onResult_ = function(url, metadata) { | |
| 1020 var callback = this.callbacks_[url]; | |
| 1021 delete this.callbacks_[url]; | |
| 1022 callback(ContentProvider.ConvertContentMetadata(metadata)); | |
| 1023 }; | |
| 1024 | |
| 1025 /** | |
| 1026 * Handles the 'error' message from the worker. | |
| 1027 * @param {string} url File url. | |
| 1028 * @param {string} step Step failed. | |
| 1029 * @param {string} error Error description. | |
| 1030 * @param {Object?} metadata The metadata, if available. | |
| 1031 * @private | |
| 1032 */ | |
| 1033 ContentProvider.prototype.onError_ = function(url, step, error, metadata) { | |
| 1034 if (MetadataCache.log) // Avoid log spam by default. | |
| 1035 console.warn('metadata: ' + url + ': ' + step + ': ' + error); | |
| 1036 metadata = metadata || {}; | |
| 1037 // Prevent asking for thumbnail again. | |
| 1038 metadata.thumbnailURL = ''; | |
| 1039 this.onResult_(url, metadata); | |
| 1040 }; | |
| 1041 | |
| 1042 /** | |
| 1043 * Handles the 'log' message from the worker. | |
| 1044 * @param {Array.<*>} arglist Log arguments. | |
| 1045 * @private | |
| 1046 */ | |
| 1047 ContentProvider.prototype.onLog_ = function(arglist) { | |
| 1048 if (MetadataCache.log) // Avoid log spam by default. | |
| 1049 console.log.apply(console, ['metadata:'].concat(arglist)); | |
| 1050 }; | |
| OLD | NEW |