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