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 |