OLD | NEW |
---|---|
1 // Copyright 2014 The Chromium Authors. All rights reserved. | 1 // Copyright 2014 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 /** | 5 /** |
6 * Class representing the results of a scan operation. | 6 * Class representing the results of a scan operation. |
7 * | 7 * |
8 * @interface | 8 * @interface |
9 */ | 9 */ |
10 importer.MediaScanner = function() {}; | 10 importer.MediaScanner = function() {}; |
(...skipping 76 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
87 * @constructor | 87 * @constructor |
88 * @struct | 88 * @struct |
89 * @implements {importer.MediaScanner} | 89 * @implements {importer.MediaScanner} |
90 * | 90 * |
91 * @param {function(!FileEntry): !Promise.<string>} hashGenerator | 91 * @param {function(!FileEntry): !Promise.<string>} hashGenerator |
92 * @param {!importer.HistoryLoader} historyLoader | 92 * @param {!importer.HistoryLoader} historyLoader |
93 * @param {!importer.DirectoryWatcherFactory} watcherFactory | 93 * @param {!importer.DirectoryWatcherFactory} watcherFactory |
94 */ | 94 */ |
95 importer.DefaultMediaScanner = function( | 95 importer.DefaultMediaScanner = function( |
96 hashGenerator, historyLoader, watcherFactory) { | 96 hashGenerator, historyLoader, watcherFactory) { |
97 | |
98 /** @private {!importer.HistoryLoader} */ | |
99 this.historyLoader_ = historyLoader; | |
100 | |
101 /** @private {function(!FileEntry): !Promise.<string>} */ | |
102 this.createHashcode_ = hashGenerator; | |
103 | |
97 /** | 104 /** |
98 * A little factory for DefaultScanResults which allows us to forgo | 105 * A little factory for DefaultScanResults which allows us to forgo |
99 * the saving it's dependencies in our fields. | 106 * the saving it's dependencies in our fields. |
100 * @return {!importer.DefaultScanResult} | 107 * @return {!importer.DefaultScanResult} |
101 */ | 108 */ |
102 this.createScanResult_ = function() { | 109 this.createScanResult_ = function() { |
103 return new importer.DefaultScanResult(hashGenerator, historyLoader); | 110 return new importer.DefaultScanResult(); |
104 }; | 111 }; |
105 | 112 |
106 /** @private {!Array.<!importer.ScanObserver>} */ | 113 /** @private {!Array.<!importer.ScanObserver>} */ |
107 this.observers_ = []; | 114 this.observers_ = []; |
108 | 115 |
109 /** | 116 /** |
110 * @private {!importer.DirectoryWatcherFactory} | 117 * @private {!importer.DirectoryWatcherFactory} |
111 * @const | 118 * @const |
112 */ | 119 */ |
113 this.watcherFactory_ = watcherFactory; | 120 this.watcherFactory_ = watcherFactory; |
(...skipping 18 matching lines...) Expand all Loading... | |
132 importer.DefaultMediaScanner.prototype.scan = function(entries) { | 139 importer.DefaultMediaScanner.prototype.scan = function(entries) { |
133 if (entries.length == 0) { | 140 if (entries.length == 0) { |
134 throw new Error('Cannot scan empty list of entries.'); | 141 throw new Error('Cannot scan empty list of entries.'); |
135 } | 142 } |
136 | 143 |
137 var scanResult = this.createScanResult_(); | 144 var scanResult = this.createScanResult_(); |
138 var watcher = this.watcherFactory_( | 145 var watcher = this.watcherFactory_( |
139 /** @this {importer.DefaultMediaScanner} */ | 146 /** @this {importer.DefaultMediaScanner} */ |
140 function() { | 147 function() { |
141 scanResult.invalidateScan(); | 148 scanResult.invalidateScan(); |
142 this.observers_.forEach( | 149 this.notify_(importer.ScanEvent.INVALIDATED, scanResult); |
143 /** @param {!importer.ScanObserver} observer */ | |
144 function(observer) { | |
145 observer(importer.ScanEvent.INVALIDATED, scanResult); | |
146 }); | |
147 }.bind(this)); | 150 }.bind(this)); |
148 var scanPromises = entries.map( | 151 var scanPromises = entries.map( |
149 this.scanEntry_.bind(this, scanResult, watcher)); | 152 this.scanEntry_.bind(this, scanResult, watcher)); |
150 | 153 |
151 Promise.all(scanPromises) | 154 Promise.all(scanPromises) |
152 .then(scanResult.resolve) | 155 .then(scanResult.resolve) |
153 .catch(scanResult.reject); | 156 .catch(scanResult.reject); |
154 | 157 |
155 scanResult.whenFinal() | 158 scanResult.whenFinal() |
156 .then( | 159 .then( |
160 /** @this {importer.DefaultMediaScanner} */ | |
157 function() { | 161 function() { |
158 this.onScanFinished_(scanResult); | 162 this.notify_(importer.ScanEvent.FINALIZED, scanResult); |
159 }.bind(this)); | 163 }.bind(this)); |
160 | 164 |
161 return scanResult; | 165 return scanResult; |
162 }; | 166 }; |
163 | 167 |
164 /** | 168 /** |
165 * Called when a scan is finished. | 169 * Notifies all listeners at some point in the near future. |
166 * | 170 * |
171 * @param {!importer.ScanEvent} event | |
167 * @param {!importer.DefaultScanResult} result | 172 * @param {!importer.DefaultScanResult} result |
168 * @private | 173 * @private |
169 */ | 174 */ |
170 importer.DefaultMediaScanner.prototype.onScanFinished_ = function(result) { | 175 importer.DefaultMediaScanner.prototype.notify_ = function(event, result) { |
171 this.observers_.forEach( | 176 this.observers_.forEach( |
172 /** @param {!importer.ScanObserver} observer */ | 177 /** @param {!importer.ScanObserver} observer */ |
173 function(observer) { | 178 function(observer) { |
174 observer(importer.ScanEvent.FINALIZED, result); | 179 observer(event, result); |
175 }); | 180 }); |
176 }; | 181 }; |
177 | 182 |
178 /** | 183 /** |
179 * Resolves the entry to a list of {@code FileEntry}. | 184 * Resolves the entry by either: |
185 * a) recursing on it (when a directory) | |
186 * b) adding it to the results (when a media type file) | |
187 * c) ignoring it, if neither a or b | |
180 * | 188 * |
181 * @param {!importer.DefaultScanResult} result | 189 * @param {!importer.DefaultScanResult} scan |
182 * @param {!importer.DirectoryWatcher} watcher | 190 * @param {!importer.DirectoryWatcher} watcher |
183 * @param {!Entry} entry | 191 * @param {!Entry} entry |
192 * | |
184 * @return {!Promise} | 193 * @return {!Promise} |
185 * @private | 194 * @private |
186 */ | 195 */ |
187 importer.DefaultMediaScanner.prototype.scanEntry_ = | 196 importer.DefaultMediaScanner.prototype.scanEntry_ = |
188 function(result, watcher, entry) { | 197 function(scan, watcher, entry) { |
189 return entry.isFile ? | 198 if (entry.isDirectory) { |
190 result.onFileEntryFound(/** @type {!FileEntry} */ (entry)) : | 199 return this.scanDirectory_( |
191 this.scanDirectory_( | 200 scan, |
192 result, watcher, /** @type {!DirectoryEntry} */ (entry)); | 201 watcher, |
202 /** @type {!DirectoryEntry} */ (entry)); | |
203 } | |
204 | |
205 console.assert(entry.isFile); | |
Ben Kwa
2015/02/05 14:54:26
Suggestion: include an explanatory string in the a
Steve McKay
2015/02/05 15:46:46
Removed.
| |
206 return this.onFileEntryFound_(scan, /** @type {!FileEntry} */ (entry)); | |
193 }; | 207 }; |
194 | 208 |
195 /** | 209 /** |
196 * Finds all files beneath directory. | 210 * Finds all files beneath directory. |
197 * | 211 * |
198 * @param {!importer.DefaultScanResult} result | 212 * @param {!importer.DefaultScanResult} scan |
199 * @param {!importer.DirectoryWatcher} watcher | 213 * @param {!importer.DirectoryWatcher} watcher |
200 * @param {!DirectoryEntry} entry | 214 * @param {!DirectoryEntry} entry |
201 * @return {!Promise} | 215 * @return {!Promise} |
202 * @private | 216 * @private |
203 */ | 217 */ |
204 importer.DefaultMediaScanner.prototype.scanDirectory_ = | 218 importer.DefaultMediaScanner.prototype.scanDirectory_ = |
205 function(result, watcher, entry) { | 219 function(scan, watcher, entry) { |
206 // Collect promises for all files being added to results. | 220 // Collect promises for all files being added to results. |
207 // The directory scan promise can't resolve until all | 221 // The directory scan promise can't resolve until all |
208 // file entries are completely promised. | 222 // file entries are completely promised. |
209 var promises = []; | 223 var promises = []; |
210 | 224 |
211 return fileOperationUtil.findEntriesRecursively( | 225 return fileOperationUtil.findEntriesRecursively( |
212 entry, | 226 entry, |
213 /** @param {!Entry} entry */ | 227 /** |
228 * @param {!Entry} entry | |
229 * @this {importer.DefaultMediaScanner} | |
230 */ | |
214 function(entry) { | 231 function(entry) { |
215 if (watcher.triggered) { | 232 if (watcher.triggered) { |
233 console.log('Skipping file entry...watched directory was modified.'); | |
hirono
2015/02/05 10:51:43
Is it intentionally left here?
Steve McKay
2015/02/05 15:46:46
It was, but on second thought...removed it.
| |
216 return; | 234 return; |
217 } | 235 } |
236 | |
218 if (entry.isDirectory) { | 237 if (entry.isDirectory) { |
238 // Note, there is no need for us to recurse, the utility | |
239 // funciton findEntriesRecursively does that. So we | |
Ben Kwa
2015/02/05 14:54:26
function
Steve McKay
2015/02/05 15:46:46
Done.
| |
240 // just watch the directory for modifications, and that's it. | |
219 watcher.addDirectory(/** @type {!DirectoryEntry} */(entry)); | 241 watcher.addDirectory(/** @type {!DirectoryEntry} */(entry)); |
242 return; | |
243 } | |
244 | |
245 promises.push( | |
246 this.onFileEntryFound_(scan, /** @type {!FileEntry} */(entry))); | |
247 | |
248 }.bind(this)) | |
249 .then(Promise.all.bind(Promise, promises)); | |
250 }; | |
251 | |
252 /** | |
253 * Finds all files beneath directory. | |
254 * | |
255 * @param {!importer.DefaultScanResult} scan | |
256 * @param {!FileEntry} entry | |
257 * @return {!Promise} | |
258 * @private | |
259 */ | |
260 importer.DefaultMediaScanner.prototype.onFileEntryFound_ = | |
261 function(scan, entry) { | |
262 | |
263 if (!FileType.isImageOrVideo(entry)) { | |
264 return Promise.resolve(false); | |
Ben Kwa
2015/02/05 14:54:26
I think this should just be Promise.resolve(). Th
Steve McKay
2015/02/05 15:46:46
Done.
| |
265 } | |
266 return this.hasHistoryDuplicate_(entry).then( | |
Ben Kwa
2015/02/05 14:54:26
nit: line break before .then
Steve McKay
2015/02/05 15:46:46
Done.
| |
267 /** | |
268 * @param {boolean} duplicate | |
269 * @return {!Promise} | |
270 * @this {importer.DefaultMediaScanner} | |
271 */ | |
272 function(duplicate) { | |
273 if (duplicate) { | |
274 return false; | |
220 } else { | 275 } else { |
221 promises.push( | 276 return this.createHashcode_(entry) |
222 result.onFileEntryFound(/** @type {!FileEntry} */(entry))); | 277 .then( |
278 function(hashcode) { | |
279 return scan.addFileEntry(entry, hashcode); | |
280 }); | |
223 } | 281 } |
224 }) | 282 }.bind(this)).then( |
Ben Kwa
2015/02/05 14:54:26
nit: line break before .then
Steve McKay
2015/02/05 15:46:46
Done.
| |
225 .then(Promise.all.bind(Promise, promises)); | 283 /** |
284 * @param {boolean} added | |
285 * @this {importer.DefaultMediaScanner} | |
286 */ | |
287 function(added) { | |
288 if (added) { | |
289 this.notify_(importer.ScanEvent.UPDATED, scan); | |
290 } | |
291 }.bind(this)); | |
292 }; | |
293 | |
294 /** | |
295 * @param {!FileEntry} entry | |
296 * @return {!Promise.<boolean>} True if there is a history-entry-duplicate | |
297 * for the file. | |
298 * @private | |
299 */ | |
300 importer.DefaultMediaScanner.prototype.hasHistoryDuplicate_ = function(entry) { | |
301 return this.historyLoader_.getHistory() | |
302 .then( | |
303 /** | |
304 * @param {!importer.ImportHistory} history | |
305 * @return {!Promise} | |
306 * @this {importer.DefaultMediaScanner} | |
307 */ | |
308 function(history) { | |
309 return Promise.all([ | |
310 history.wasCopied(entry, importer.Destination.GOOGLE_DRIVE), | |
311 history.wasImported(entry, importer.Destination.GOOGLE_DRIVE) | |
312 ]).then( | |
313 /** | |
314 * @param {!Array.<boolean>} results | |
315 * @return {!Promise} | |
316 * @this {importer.DefaultMediaScanner} | |
317 */ | |
318 function(results) { | |
319 return results[0] || results[1]; | |
320 }.bind(this)); | |
321 }.bind(this)); | |
226 }; | 322 }; |
227 | 323 |
228 /** | 324 /** |
229 * Results of a scan operation. The object is "live" in that data can and | 325 * Results of a scan operation. The object is "live" in that data can and |
230 * will change as the scan operation discovers files. | 326 * will change as the scan operation discovers files. |
231 * | 327 * |
232 * <p>The scan is complete, and the object will become static once the | 328 * <p>The scan is complete, and the object will become static once the |
233 * {@code whenFinal} promise resolves. | 329 * {@code whenFinal} promise resolves. |
234 * | 330 * |
235 * @constructor | 331 * @constructor |
236 * @struct | 332 * @struct |
237 * @implements {importer.ScanResult} | 333 * @implements {importer.ScanResult} |
238 * | |
239 * @param {function(!FileEntry): !Promise.<string>} hashGenerator | |
240 * @param {!importer.HistoryLoader} historyLoader | |
241 */ | 334 */ |
242 importer.DefaultScanResult = function(hashGenerator, historyLoader) { | 335 importer.DefaultScanResult = function() { |
243 | |
244 /** @private {function(!FileEntry): !Promise.<string>} */ | |
245 this.createHashcode_ = hashGenerator; | |
246 | |
247 /** @private {!importer.HistoryLoader} */ | |
248 this.historyLoader_ = historyLoader; | |
249 | 336 |
250 /** | 337 /** |
251 * List of file entries found while scanning. | 338 * List of file entries found while scanning. |
252 * @private {!Array.<!FileEntry>} | 339 * @private {!Array.<!FileEntry>} |
253 */ | 340 */ |
254 this.fileEntries_ = []; | 341 this.fileEntries_ = []; |
255 | 342 |
256 /** | 343 /** |
257 * @private {boolean} | |
258 */ | |
259 this.invalidated_ = false; | |
260 | |
261 /** | |
262 * Hashcodes of all files included captured by this result object so-far. | 344 * Hashcodes of all files included captured by this result object so-far. |
263 * Used to dedupe newly discovered files against other files withing | 345 * Used to dedupe newly discovered files against other files withing |
264 * the ScanResult. | 346 * the ScanResult. |
265 * @private {!Object.<string, !FileEntry>} | 347 * @private {!Object.<string, !FileEntry>} |
266 */ | 348 */ |
267 this.fileHashcodes_ = {}; | 349 this.fileHashcodes_ = {}; |
268 | 350 |
269 /** @private {number} */ | 351 /** @private {number} */ |
270 this.totalBytes_ = 0; | 352 this.totalBytes_ = 0; |
271 | 353 |
272 /** | 354 /** |
273 * The point in time when the scan was started. | 355 * The point in time when the scan was started. |
274 * @type {Date} | 356 * @type {Date} |
275 */ | 357 */ |
276 this.scanStarted_ = new Date(); | 358 this.scanStarted_ = new Date(); |
277 | 359 |
278 /** | 360 /** |
279 * The point in time when the last scan activity occured. | 361 * The point in time when the last scan activity occured. |
280 * @type {Date} | 362 * @type {Date} |
281 */ | 363 */ |
282 this.lastScanActivity_ = this.scanStarted_; | 364 this.lastScanActivity_ = this.scanStarted_; |
283 | 365 |
366 /** | |
367 * @private {boolean} | |
368 */ | |
369 this.invalidated_ = false; | |
370 | |
284 /** @private {!importer.Resolver.<!importer.ScanResult>} */ | 371 /** @private {!importer.Resolver.<!importer.ScanResult>} */ |
285 this.resolver_ = new importer.Resolver(); | 372 this.resolver_ = new importer.Resolver(); |
286 }; | 373 }; |
287 | 374 |
288 /** @struct */ | 375 /** @struct */ |
289 importer.DefaultScanResult.prototype = { | 376 importer.DefaultScanResult.prototype = { |
290 | 377 |
291 /** @return {function()} */ | 378 /** @return {function()} */ |
292 get resolve() { return this.resolver_.resolve.bind(null, this); }, | 379 get resolve() { return this.resolver_.resolve.bind(null, this); }, |
293 | 380 |
(...skipping 31 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
325 }; | 412 }; |
326 | 413 |
327 /** | 414 /** |
328 * Invalidates this scan. | 415 * Invalidates this scan. |
329 */ | 416 */ |
330 importer.DefaultScanResult.prototype.invalidateScan = function() { | 417 importer.DefaultScanResult.prototype.invalidateScan = function() { |
331 this.invalidated_ = true; | 418 this.invalidated_ = true; |
332 }; | 419 }; |
333 | 420 |
334 /** | 421 /** |
335 * Handles files discovered during scanning. | |
336 * | |
337 * @param {!FileEntry} entry | |
338 * @return {!Promise} Resolves once file entry has been processed | |
339 * and is represented in results. | |
340 */ | |
341 importer.DefaultScanResult.prototype.onFileEntryFound = function(entry) { | |
342 this.lastScanActivity_ = new Date(); | |
343 | |
344 if (!FileType.isImageOrVideo(entry)) { | |
345 return Promise.resolve(); | |
346 } | |
347 | |
348 return this.historyLoader_.getHistory() | |
349 .then( | |
350 /** | |
351 * @param {!importer.ImportHistory} history | |
352 * @return {!Promise} | |
353 * @this {importer.DefaultScanResult} | |
354 */ | |
355 function(history) { | |
356 return Promise.all([ | |
357 history.wasCopied(entry, importer.Destination.GOOGLE_DRIVE), | |
358 history.wasImported(entry, importer.Destination.GOOGLE_DRIVE) | |
359 ]).then( | |
360 /** | |
361 * @param {!Array.<boolean>} results | |
362 * @return {!Promise} | |
363 * @this {importer.DefaultScanResult} | |
364 */ | |
365 function(results) { | |
366 return results[0] || results[1] ? | |
367 Promise.resolve() : | |
368 this.addFileEntry_(entry); | |
369 }.bind(this)); | |
370 }.bind(this)); | |
371 }; | |
372 | |
373 /** | |
374 * Adds a file to results. | 422 * Adds a file to results. |
375 * | 423 * |
376 * @param {!FileEntry} entry | 424 * @param {!FileEntry} entry |
377 * @return {!Promise} Resolves once file entry has been processed | 425 * @param {string} hashcode |
378 * and is represented in results. | 426 * @return {!Promise.<boolean>} True if the file as added, false if it was |
379 * @private | 427 * rejected as a dupe. |
380 */ | 428 */ |
381 importer.DefaultScanResult.prototype.addFileEntry_ = function(entry) { | 429 importer.DefaultScanResult.prototype.addFileEntry = function(entry, hashcode) { |
382 return new Promise( | 430 return new Promise(entry.getMetadata.bind(entry)).then( |
383 function(resolve, reject) { | 431 /** |
384 this.createHashcode_(entry).then( | 432 * @param {!Metadata} metadata |
385 /** | 433 * @this {importer.DefaultScanResult} |
386 * @param {string} hashcode | 434 */ |
387 * @this {importer.DefaultScanResult} | 435 function(metadata) { |
388 */ | 436 console.assert( |
389 function(hashcode) { | 437 'size' in metadata, |
390 // Ignore the entry if it is a duplicate. | 438 'size attribute missing from metadata.'); |
391 if (hashcode in this.fileHashcodes_) { | 439 this.lastScanActivity_ = new Date(); |
392 resolve(); | |
393 return; | |
394 } | |
395 | 440 |
396 entry.getMetadata( | 441 // We wait to check the hashcode until after all |
Ben Kwa
2015/02/05 14:54:26
I find this comment confusing. Which async data a
Steve McKay
2015/02/05 15:46:46
Rewrote to make the thought clearer.
| |
397 /** | 442 // async data is loaded. This avoids a possible race. |
398 * @param {!Metadata} metadata | 443 if (hashcode in this.fileHashcodes_) { |
399 * @this {importer.DefaultScanResult} | 444 return false; |
400 */ | 445 } |
401 function(metadata) { | |
402 console.assert( | |
403 'size' in metadata, | |
404 'size attribute missing from metadata.'); | |
405 this.lastScanActivity_ = new Date(); | |
406 | 446 |
407 // Double check that a dupe entry wasn't added while we were | 447 entry.size = metadata.size; |
408 // busy looking up metadata. | 448 this.totalBytes_ += metadata['size']; |
409 if (hashcode in this.fileHashcodes_) { | 449 this.fileHashcodes_[hashcode] = entry; |
410 resolve(); | 450 this.fileEntries_.push(entry); |
411 return; | 451 return true; |
412 } | 452 |
413 entry.size = metadata.size; | 453 }.bind(this)); |
414 this.totalBytes_ += metadata['size']; | |
415 this.fileHashcodes_[hashcode] = entry; | |
416 this.fileEntries_.push(entry); | |
417 resolve(); | |
418 }.bind(this)); | |
419 }.bind(this)); | |
420 }.bind(this)); | |
421 }; | 454 }; |
422 | 455 |
423 /** | 456 /** |
424 * Watcher for directories. | 457 * Watcher for directories. |
425 * @interface | 458 * @interface |
426 */ | 459 */ |
427 importer.DirectoryWatcher = function() {}; | 460 importer.DirectoryWatcher = function() {}; |
428 | 461 |
429 /** | 462 /** |
430 * Registers new directory to be watched. | 463 * Registers new directory to be watched. |
(...skipping 59 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
490 if (!this.watchedDirectories_[event.entry.toURL()]) | 523 if (!this.watchedDirectories_[event.entry.toURL()]) |
491 return; | 524 return; |
492 this.triggered = true; | 525 this.triggered = true; |
493 for (var url in this.watchedDirectories_) { | 526 for (var url in this.watchedDirectories_) { |
494 chrome.fileManagerPrivate.removeFileWatch(url, function() {}); | 527 chrome.fileManagerPrivate.removeFileWatch(url, function() {}); |
495 } | 528 } |
496 chrome.fileManagerPrivate.onDirectoryChanged.removeListener( | 529 chrome.fileManagerPrivate.onDirectoryChanged.removeListener( |
497 assert(this.listener_)); | 530 assert(this.listener_)); |
498 this.callback_(); | 531 this.callback_(); |
499 }; | 532 }; |
OLD | NEW |