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 |