Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(903)

Side by Side Diff: ui/file_manager/file_manager/background/js/media_scanner.js

Issue 899943002: Rework update model to eliminate a "flicker" resulting from the brief update to zero results when a… (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Created 5 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
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
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
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
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
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 };
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698