Commit 9ec42b88 authored by hirono's avatar hirono Committed by Commit bot

Files.app: Invalidate ScanResult when the scanned directory is changed.

 * Add whenInvalidated method to ScanResult.
 * Watch the invalidation in ImportController.

BUG=420680
TEST=FileManagerJsTest.MediaScannerTest

Review URL: https://codereview.chromium.org/865343002

Cr-Commit-Position: refs/heads/master@{#312977}
parent 27ac8082
......@@ -79,7 +79,8 @@ function FileBrowserBackground() {
*/
this.mediaScanner = new importer.DefaultMediaScanner(
importer.createMetadataHashcode,
this.historyLoader);
this.historyLoader,
importer.DefaultDirectoryWatcher.create);
/**
* Handles importing of user media (e.g. photos, videos) from removable
......
......@@ -44,6 +44,11 @@ importer.ScanResult = function() {};
*/
importer.ScanResult.prototype.isFinal;
/**
* @return {boolean} true if scanning is invalidated.
*/
importer.ScanResult.prototype.isInvalidated;
/**
* Returns all files entries discovered so far. The list will be
* complete only after scanning has completed and {@code isFinal}
......@@ -85,12 +90,14 @@ importer.ScanResult.prototype.whenFinal;
*
* @param {function(!FileEntry): !Promise.<string>} hashGenerator
* @param {!importer.HistoryLoader} historyLoader
* @param {!importer.DirectoryWatcherFactory} watcherFactory
*/
importer.DefaultMediaScanner = function(hashGenerator, historyLoader) {
importer.DefaultMediaScanner = function(
hashGenerator, historyLoader, watcherFactory) {
/**
* A little factory for DefaultScanResults which allows us to forgo
* the saving it's dependencies in our fields.
* @private {function(): !importer.DefaultScanResult}
* @return {!importer.DefaultScanResult}
*/
this.createScanResult_ = function() {
return new importer.DefaultScanResult(hashGenerator, historyLoader);
......@@ -98,6 +105,12 @@ importer.DefaultMediaScanner = function(hashGenerator, historyLoader) {
/** @private {!Array.<!importer.ScanObserver>} */
this.observers_ = [];
/**
* @private {!importer.DirectoryWatcherFactory}
* @const
*/
this.watcherFactory_ = watcherFactory;
};
/** @override */
......@@ -122,7 +135,16 @@ importer.DefaultMediaScanner.prototype.scan = function(entries) {
}
var scanResult = this.createScanResult_();
var scanPromises = entries.map(this.scanEntry_.bind(this, scanResult));
var watcher = this.watcherFactory_(function() {
scanResult.invalidateScan();
this.observers_.forEach(
/** @param {!importer.ScanObserver} observer */
function(observer) {
observer(importer.ScanEvent.INVALIDATED, scanResult);
});
}.bind(this));
var scanPromises = entries.map(
this.scanEntry_.bind(this, scanResult, watcher));
Promise.all(scanPromises)
.then(scanResult.resolveScan.bind(scanResult))
......@@ -155,45 +177,50 @@ importer.DefaultMediaScanner.prototype.onScanFinished_ = function(result) {
* Resolves the entry to a list of {@code FileEntry}.
*
* @param {!importer.DefaultScanResult} result
* @param {!importer.DirectoryWatcher} watcher
* @param {!Entry} entry
* @return {!Promise}
* @private
*/
importer.DefaultMediaScanner.prototype.scanEntry_ =
function(result, entry) {
function(result, watcher, entry) {
return entry.isFile ?
result.onFileEntryFound(/** @type {!FileEntry} */ (entry)) :
this.scanDirectory_(result, /** @type {!DirectoryEntry} */ (entry));
this.scanDirectory_(
result, watcher, /** @type {!DirectoryEntry} */ (entry));
};
/**
* Finds all files beneath directory.
*
* @param {!importer.DefaultScanResult} result
* @param {!importer.DirectoryWatcher} watcher
* @param {!DirectoryEntry} entry
* @return {!Promise}
* @private
*/
importer.DefaultMediaScanner.prototype.scanDirectory_ =
function(result, entry) {
return new Promise(
function(resolve, reject) {
// Collect promises for all files being added to results.
// The directory scan promise can't resolve until all
// file entries are completely promised.
var promises = [];
fileOperationUtil.findFilesRecursively(
entry,
/** @param {!FileEntry} fileEntry */
function(fileEntry) {
promises.push(result.onFileEntryFound(fileEntry));
})
.then(
/** @this {importer.DefaultScanResult} */
function() {
Promise.all(promises).then(resolve).catch(reject);
});
});
function(result, watcher, entry) {
// Collect promises for all files being added to results.
// The directory scan promise can't resolve until all
// file entries are completely promised.
var promises = [];
return fileOperationUtil.findEntriesRecursively(
entry,
/** @param {!Entry} entry */
function(entry) {
if (watcher.triggered) {
return;
}
if (entry.isDirectory) {
watcher.addDirectory(/** @type {!DirectoryEntry} */(entry));
} else {
promises.push(
result.onFileEntryFound(/** @type {!FileEntry} */(entry)));
}
})
.then(Promise.all.bind(Promise, promises));
};
/**
......@@ -224,6 +251,11 @@ importer.DefaultScanResult = function(hashGenerator, historyLoader) {
*/
this.fileEntries_ = [];
/**
* @private {boolean}
*/
this.invalidated_ = false;
/**
* Hashcodes of all files included captured by this result object so-far.
* Used to dedupe newly discovered files against other files withing
......@@ -276,6 +308,10 @@ importer.DefaultScanResult.prototype.isFinal = function() {
return this.settled_;
};
importer.DefaultScanResult.prototype.isInvalidated = function() {
return this.invalidated_;
};
/** @override */
importer.DefaultScanResult.prototype.getFileEntries = function() {
return this.fileEntries_;
......@@ -296,6 +332,13 @@ importer.DefaultScanResult.prototype.whenFinal = function() {
return this.finishedPromise_;
};
/**
* Invalidates this scan.
*/
importer.DefaultScanResult.prototype.invalidateScan = function() {
this.invalidated_ = true;
};
/**
* Handles files discovered during scanning.
*
......@@ -385,3 +428,80 @@ importer.DefaultScanResult.prototype.addFileEntry_ = function(entry) {
}.bind(this));
};
/**
* Watcher for directories.
* @interface
*/
importer.DirectoryWatcher = function() {};
/**
* Registers new directory to be watched.
* @param {!DirectoryEntry} entry
*/
importer.DirectoryWatcher.prototype.addDirectory = function(entry) {};
/**
* @typedef {function()}
*/
importer.DirectoryWatcherFactoryCallback;
/**
* @typedef {function(importer.DirectoryWatcherFactoryCallback):
* !importer.DirectoryWatcher}
*/
importer.DirectoryWatcherFactory;
/**
* Watcher for directories.
* @param {function()} callback Callback to be invoked when one of watched
* directories is changed.
* @implements {importer.DirectoryWatcher}
* @constructor
*/
importer.DefaultDirectoryWatcher = function(callback) {
this.callback_ = callback;
this.watchedDirectories_ = {};
this.triggered = false;
this.listener_ = null;
};
/**
* Creates new directory watcher.
* @param {function()} callback Callback to be invoked when one of watched
* directories is changed.
* @return {!importer.DirectoryWatcher}
*/
importer.DefaultDirectoryWatcher.create = function(callback) {
return new importer.DefaultDirectoryWatcher(callback);
};
/**
* Registers new directory to be watched.
* @param {!DirectoryEntry} entry
*/
importer.DefaultDirectoryWatcher.prototype.addDirectory = function(entry) {
if (!this.listener_) {
this.listener_ = this.onWatchedDirectoryModified_.bind(this);
chrome.fileManagerPrivate.onDirectoryChanged.addListener(
assert(this.listener_));
}
this.watchedDirectories_[entry.toURL()] = true;
chrome.fileManagerPrivate.addFileWatch(entry.toURL(), function() {});
};
/**
* @param {FileWatchEvent} event
* @private
*/
importer.DefaultDirectoryWatcher.prototype.onWatchedDirectoryModified_ =
function(event) {
if (!this.watchedDirectories_[event.entry.toURL()])
return;
this.triggered = true;
for (var url in this.watchedDirectories_) {
chrome.fileManagerPrivate.removeFileWatch(url, function() {});
}
chrome.fileManagerPrivate.onDirectoryChanged.removeListener(
assert(this.listener_));
this.callback_();
};
......@@ -12,16 +12,17 @@
<script src="../../../../../ui/webui/resources/js/load_time_data.js"></script>
<script src="../../common/js/volume_manager_common.js"></script>
<script src="../../common/js/importer_common.js"></script>
<script src="../../common/js/file_type.js"></script>
<script src="../../common/js/importer_common.js"></script>
<script src="../../common/js/mock_entry.js"></script>
<script src="../../common/js/mock_file_system.js"></script>
<script src="../../common/js/unittest_util.js"></script>
<script src="../../common/js/util.js"></script>
<script src="file_operation_util.js"></script>
<script src="import_history.js"></script>
<script src="test_import_history.js"></script>
<script src="media_scanner.js"></script>
<script src="mock_media_scanner.js"></script>
<script src="test_import_history.js"></script>
<script src="media_scanner_unittest.js"></script>
......
......@@ -17,6 +17,9 @@ var scanner;
/** @type {!importer.TestImportHistory} */
var importHistory;
/** @type {!importer.TestDirectoryWatcher} */
var watcher;
// Set up the test components.
function setUp() {
......@@ -26,7 +29,11 @@ function setUp() {
function(entry) {
return Promise.resolve(entry.name);
},
importHistory);
importHistory,
function(callback) {
watcher = new TestDirectoryWatcher(callback);
return watcher;
});
}
/**
......@@ -311,6 +318,25 @@ function testDedupesFiles(callback) {
callback);
}
function testInvalidation(callback) {
var invalidatePromise = new Promise(function(fulfill) {
scanner.addObserver(fulfill);
});
reportPromise(
makeTestFileSystemRoot('testInvalidation')
.then(populateDir.bind(null, ['DCIM']))
.then(
/**
* Scans the directories.
* @param {!DirectoryEntry} root
*/
function(root) {
scan = scanner.scan([root]);
watcher.callback();
return invalidatePromise;
}),
callback);
}
/**
* Verifies the results of the media scan are as expected.
......
......@@ -154,3 +154,31 @@ TestScanResult.prototype.whenFinal = function() {
TestScanResult.prototype.isFinal = function() {
return this.settled_;
};
/** @override */
TestScanResult.prototype.isInvalidated = function() {
return false;
};
/**
* @constructor
* @implements {importer.DirectoryWatcher}
*/
function TestDirectoryWatcher(callback) {
/**
* @public {function()}
* @const
*/
this.callback = callback;
/**
* @public {boolean}
*/
this.triggered = false;
};
/**
* @override
*/
TestDirectoryWatcher.prototype.addDirectory = function() {
};
......@@ -7,7 +7,8 @@ var importer = importer || {};
/** @enum {string} */
importer.ScanEvent = {
FINALIZED: 'finalized'
FINALIZED: 'finalized',
INVALIDATED: 'invalidated'
};
/**
......
......@@ -60,7 +60,6 @@ importer.ImportController =
this.cachedScans_ = {};
this.scanner_.addObserver(this.onScanEvent_.bind(this));
this.environment_.addVolumeUnmountListener(
this.onVolumeUnmounted_.bind(this));
};
......@@ -72,8 +71,17 @@ importer.ImportController =
* @private
*/
importer.ImportController.prototype.onScanEvent_ = function(event, result) {
// TODO(smckay): only do this if this is a directory scan.
if (event === importer.ScanEvent.FINALIZED) {
if (event === importer.ScanEvent.INVALIDATED) {
for (var key in this.cachedScans_) {
for (var url in this.cachedScans_[key]) {
if (this.cachedScans_[key][url].isInvalidated()) {
delete this.cachedScans_[key][url];
}
}
}
}
if (event === importer.ScanEvent.FINALIZED ||
event === importer.ScanEvent.INVALIDATED) {
this.updateCommands_();
}
};
......@@ -104,7 +112,6 @@ importer.ImportController.prototype.execute = function() {
* @return {!importer.CommandUpdate} response
*/
importer.ImportController.prototype.getCommandUpdate = function() {
// If there is no Google Drive mount, Drive may be disabled
// or the machine may be running in guest mode.
if (this.environment_.isGoogleDriveMounted()) {
......@@ -240,6 +247,7 @@ importer.ImportController.prototype.getCurrentDirectoryScan_ = function() {
scan = this.scanner_.scan([directory]);
this.cachedScans_[volumeId][url] = scan;
}
assert(!scan.isInvalidated());
return scan;
};
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment