// If directory files changes too often, don't rescan directory more than once
// per specified interval
-var SIMULTANEOUS_RESCAN_INTERVAL = 1000;
+var SIMULTANEOUS_RESCAN_INTERVAL = 500;
// Used for operations that require almost instant rescan.
var SHORT_RESCAN_INTERVAL = 100;
this.scanFailures_ = 0;
this.changeDirectorySequence_ = 0;
+ this.directoryChangeQueue_ = new AsyncUtil.Queue();
+
this.fileFilter_ = fileFilter;
this.fileFilter_.addEventListener('changed',
this.onFilterChanged_.bind(this));
/**
* Invoked when a change in the directory is detected by the watcher.
+ * @param {Event} event Event object.
* @private
*/
-DirectoryModel.prototype.onWatcherDirectoryChanged_ = function() {
- // Clear the metadata cache since something in this directory has changed.
+DirectoryModel.prototype.onWatcherDirectoryChanged_ = function(event) {
var directoryEntry = this.getCurrentDirEntry();
- if (!util.isFakeEntry(directoryEntry))
- this.metadataCache_.clearRecursively(directoryEntry, '*');
- this.rescanSoon();
+ if (event.changedFiles) {
+ var urls = event.changedFiles.map(function(change) { return change.url; });
+ util.URLsToEntries(urls).then(function(result) {
+ // Removes the metadata of invalid entries.
+ if (result.failureUrls.length > 0)
+ this.metadataCache_.clearByUrl(result.failureUrls, '*');
+
+ // Rescans after force-refreshing the metadata of the changed entries.
+ var entries = result.entries;
+ if (entries.length) {
+ this.currentDirContents_.prefetchMetadata(entries, true, function() {
+ this.rescanSoon(false);
+ }.bind(this));
+ } else {
+ this.rescanSoon(false);
+ }
+ }.bind(this)).catch(function(error) {
+ console.error('Error in proceeding the changed event.', error,
+ 'Fallback to force-refresh');
+ this.rescanSoon(true);
+ }.bind(this));
+ } else {
+ // Invokes force refresh if the detailed information isn't provided.
+ // This can occur very frequently (e.g. when copying files into Downlaods)
+ // and rescan is heavy operation, so we keep some interval for each rescan.
+ this.rescanLater(true);
+ }
};
/**
* @private
*/
DirectoryModel.prototype.onFilterChanged_ = function() {
- this.rescanSoon();
+ this.rescanSoon(false);
};
/**
/**
* Schedule rescan with short delay.
+ * @param {boolean} refresh True to refresh metadata, or false to use cached
+ * one.
*/
-DirectoryModel.prototype.rescanSoon = function() {
- this.scheduleRescan(SHORT_RESCAN_INTERVAL);
+DirectoryModel.prototype.rescanSoon = function(refresh) {
+ this.scheduleRescan(SHORT_RESCAN_INTERVAL, refresh);
};
/**
* Schedule rescan with delay. Designed to handle directory change
* notification.
+ * @param {boolean} refresh True to refresh metadata, or false to use cached
+ * one.
*/
-DirectoryModel.prototype.rescanLater = function() {
- this.scheduleRescan(SIMULTANEOUS_RESCAN_INTERVAL);
+DirectoryModel.prototype.rescanLater = function(refresh) {
+ this.scheduleRescan(SIMULTANEOUS_RESCAN_INTERVAL, refresh);
};
/**
* nothing. File operation may cause a few notifications what should cause
* a single refresh.
* @param {number} delay Delay in ms after which the rescan will be performed.
+ * @param {boolean} refresh True to refresh metadata, or false to use cached
+ * one.
*/
-DirectoryModel.prototype.scheduleRescan = function(delay) {
+DirectoryModel.prototype.scheduleRescan = function(delay, refresh) {
if (this.rescanTime_) {
if (this.rescanTime_ <= Date.now() + delay)
return;
clearTimeout(this.rescanTimeoutId_);
}
+ var sequence = this.changeDirectorySequence_;
+
this.rescanTime_ = Date.now() + delay;
- this.rescanTimeoutId_ = setTimeout(this.rescan.bind(this), delay);
+ this.rescanTimeoutId_ = setTimeout(function() {
+ this.rescanTimeoutId_ = null;
+ if (sequence === this.changeDirectorySequence_)
+ this.rescan(refresh);
+ }.bind(this), delay);
};
/**
* preserving the select element etc.
*
* This should be to scan the contents of current directory (or search).
+ *
+ * @param {boolean} refresh True to refresh metadata, or false to use cached
+ * one.
*/
-DirectoryModel.prototype.rescan = function() {
+DirectoryModel.prototype.rescan = function(refresh) {
this.clearRescanTimeout_();
if (this.runningScan_) {
this.pendingRescan_ = true;
var dirContents = this.currentDirContents_.clone();
dirContents.setFileList([]);
+ var sequence = this.changeDirectorySequence_;
+
var successCallback = (function() {
- this.replaceDirectoryContents_(dirContents);
- cr.dispatchSimpleEvent(this, 'rescan-completed');
+ if (sequence === this.changeDirectorySequence_) {
+ this.replaceDirectoryContents_(dirContents);
+ cr.dispatchSimpleEvent(this, 'rescan-completed');
+ }
}).bind(this);
this.scan_(dirContents,
+ refresh,
successCallback, function() {}, function() {}, function() {});
};
*
* @param {DirectoryContentes} newDirContents New DirectoryContents instance to
* replace currentDirContents_.
- * @param {function()=} opt_callback Called on success.
+ * @param {function(boolean)} callback Callback with result. True if the scan
+ * is completed successfully, false if the scan is failed.
* @private
*/
DirectoryModel.prototype.clearAndScan_ = function(newDirContents,
- opt_callback) {
+ callback) {
if (this.currentDirContents_.isScanning())
this.currentDirContents_.cancelScan();
this.currentDirContents_ = newDirContents;
this.runningScan_ = null;
}
+ var sequence = this.changeDirectorySequence_;
+ var cancelled = false;
+
var onDone = function() {
+ if (cancelled)
+ return;
+
cr.dispatchSimpleEvent(this, 'scan-completed');
- if (opt_callback)
- opt_callback();
+ callback(true);
}.bind(this);
var onFailed = function() {
+ if (cancelled)
+ return;
+
cr.dispatchSimpleEvent(this, 'scan-failed');
+ callback(false);
}.bind(this);
var onUpdated = function() {
+ if (cancelled)
+ return;
+
+ if (this.changeDirectorySequence_ !== sequence) {
+ cancelled = true;
+ cr.dispatchSimpleEvent(this, 'scan-cancelled');
+ callback(false);
+ return;
+ }
+
cr.dispatchSimpleEvent(this, 'scan-updated');
}.bind(this);
var onCancelled = function() {
+ if (cancelled)
+ return;
+
+ cancelled = true;
cr.dispatchSimpleEvent(this, 'scan-cancelled');
+ callback(false);
}.bind(this);
// Clear the table, and start scanning.
cr.dispatchSimpleEvent(this, 'scan-started');
var fileList = this.getFileList();
fileList.splice(0, fileList.length);
- this.scan_(this.currentDirContents_,
+ this.scan_(this.currentDirContents_, false,
onDone, onFailed, onUpdated, onCancelled);
};
*
* @param {DirectoryContents} dirContents DirectoryContents instance on which
* the scan will be run.
+ * @param {boolean} refresh True to refresh metadata, or false to use cached
+ * one.
* @param {function()} successCallback Callback on success.
* @param {function()} failureCallback Callback on failure.
* @param {function()} updatedCallback Callback on update. Only on the last
*/
DirectoryModel.prototype.scan_ = function(
dirContents,
+ refresh,
successCallback, failureCallback, updatedCallback, cancelledCallback) {
var self = this;
*/
var maybeRunPendingRescan = function() {
if (this.pendingRescan_) {
- this.rescanSoon();
+ this.rescanSoon(refresh);
this.pendingRescan_ = false;
return true;
}
return;
if (this.scanFailures_ <= 1)
- this.rescanLater();
+ this.rescanLater(refresh);
}.bind(this);
this.runningScan_ = dirContents;
dirContents.addEventListener('scan-updated', updatedCallback);
dirContents.addEventListener('scan-failed', onFailure);
dirContents.addEventListener('scan-cancelled', cancelledCallback);
- dirContents.scan();
+ dirContents.scan(refresh);
};
/**
var leadIndex = this.fileListSelection_.leadIndex;
var leadEntry = this.getLeadEntry_();
+ this.currentDirContents_.dispose();
this.currentDirContents_ = dirContents;
dirContents.replaceContextFileList();
/**
* Callback when an entry is changed.
* @param {util.EntryChangedKind} kind How the entry is changed.
- * @param {Entry} entry The changed entry.
+ * @param {Array.<Entry>} entries The changed entries.
*/
-DirectoryModel.prototype.onEntryChanged = function(kind, entry) {
+DirectoryModel.prototype.onEntriesChanged = function(kind, entries) {
// TODO(hidehiko): We should update directory model even the search result
// is shown.
var rootType = this.getCurrentRootType();
switch (kind) {
case util.EntryChangedKind.CREATED:
- // Refresh the cache.
- this.metadataCache_.clear([entry], '*');
- entry.getParent(function(parentEntry) {
- if (!util.isSameEntry(this.getCurrentDirEntry(), parentEntry)) {
- // Do nothing if current directory changed during async operations.
- return;
- }
- this.currentDirContents_.prefetchMetadata([entry], function() {
- if (!util.isSameEntry(this.getCurrentDirEntry(), parentEntry)) {
- // Do nothing if current directory changed during async operations.
- return;
- }
-
- var index = this.findIndexByEntry_(entry);
+ var parentPromises = [];
+ for (var i = 0; i < entries.length; i++) {
+ parentPromises.push(new Promise(function(resolve, reject) {
+ entries[i].getParent(resolve, reject);
+ }));
+ }
+ Promise.all(parentPromises).then(function(parents) {
+ var entriesToAdd = [];
+ for (var i = 0; i < parents.length; i++) {
+ if (!util.isSameEntry(parents[i], this.getCurrentDirEntry()))
+ continue;
+ var index = this.findIndexByEntry_(entries[i]);
if (index >= 0) {
this.getFileList().replaceItem(
- this.getFileList().item(index), entry);
+ this.getFileList().item(index), entries[i]);
} else {
- this.getFileList().push(entry);
+ entriesToAdd.push(entries[i]);
}
- }.bind(this));
- }.bind(this));
+ }
+ this.getFileList().push.apply(this.getFileList(), entriesToAdd);
+ }.bind(this)).catch(function(error) {
+ console.error(error.stack || error);
+ });
break;
case util.EntryChangedKind.DELETED:
// This is the delete event.
- var index = this.findIndexByEntry_(entry);
- if (index >= 0)
- this.getFileList().splice(index, 1);
+ for (var i = 0; i < entries.length; i++) {
+ var index = this.findIndexByEntry_(entries[i]);
+ if (index >= 0)
+ this.getFileList().splice(index, 1);
+ }
break;
default:
*/
DirectoryModel.prototype.onRenameEntry = function(
oldEntry, newEntry, opt_callback) {
- this.currentDirContents_.prefetchMetadata([newEntry], function() {
+ this.currentDirContents_.prefetchMetadata([newEntry], true, function() {
// If the current directory is the old entry, then quietly change to the
// new one.
if (util.isSameEntry(oldEntry, this.getCurrentDirEntry()))
return;
}
- var tracker = this.createDirectoryChangeTracker();
- tracker.start();
+ var sequence = this.changeDirectorySequence_;
new Promise(entry.getDirectory.bind(
entry, name, {create: true, exclusive: true})).
// Refresh the cache.
this.metadataCache_.clear([newEntry], '*');
return new Promise(function(onFulfilled, onRejected) {
- this.metadataCache_.get([newEntry],
- 'filesystem',
- onFulfilled.bind(null, newEntry));
+ this.metadataCache_.getOne(newEntry,
+ 'filesystem',
+ onFulfilled.bind(null, newEntry));
}.bind(this));
}.bind(this)).
then(function(newEntry) {
// Do not change anything or call the callback if current
// directory changed.
- tracker.stop();
- if (tracker.hasChanged) {
+ if (this.changeDirectorySequence_ !== sequence) {
abortCallback();
return;
}
successCallback(newEntry);
}
}.bind(this), function(reason) {
- tracker.stop();
errorCallback(reason);
});
};
/**
- * Change the current directory to the directory represented by
+ * Changes the current directory to the directory represented by
* a DirectoryEntry or a fake entry.
*
* Dispatches the 'directory-changed' event when the directory is successfully
* changed.
*
+ * Note : if this is called from UI, please consider to use DirectoryModel.
+ * activateDirectoryEntry instead of this, which is higher-level function and
+ * cares about the selection.
+ *
* @param {DirectoryEntry|Object} dirEntry The entry of the new directory to
* be opened.
* @param {function()=} opt_callback Executed if the directory loads
this.changeDirectorySequence_++;
this.clearSearch_();
- var promise = new Promise(
- function(onFulfilled, onRejected) {
- this.fileWatcher_.changeWatchedDirectory(dirEntry, onFulfilled);
- }.bind(this)).
-
- then(function(sequence) {
- return new Promise(function(onFulfilled, onRejected) {
- if (this.changeDirectorySequence_ !== sequence)
+ this.directoryChangeQueue_.run(function(sequence, queueTaskCallback) {
+ this.fileWatcher_.changeWatchedDirectory(
+ dirEntry,
+ function() {
+ if (this.changeDirectorySequence_ !== sequence) {
+ queueTaskCallback();
return;
+ }
var newDirectoryContents = this.createDirectoryContents_(
this.currentFileListContext_, dirEntry, '');
- if (!newDirectoryContents)
+ if (!newDirectoryContents) {
+ queueTaskCallback();
return;
+ }
- var previousDirEntry = this.currentDirContents_.getDirectoryEntry();
- this.clearAndScan_(newDirectoryContents, opt_callback);
-
- // For tests that open the dialog to empty directories, everything is
- // loaded at this point.
+ var previousDirEntry =
+ this.currentDirContents_.getDirectoryEntry();
+ this.clearAndScan_(
+ newDirectoryContents,
+ function(result) {
+ // Calls the callback of the method when successful.
+ if (result && opt_callback)
+ opt_callback();
+
+ // Notify that the current task of this.directoryChangeQueue_
+ // is completed.
+ setTimeout(queueTaskCallback);
+ });
+
+ // For tests that open the dialog to empty directories, everything
+ // is loaded at this point.
util.testSendMessage('directory-change-complete');
var event = new Event('directory-changed');
event.newDirEntry = dirEntry;
this.dispatchEvent(event);
}.bind(this));
- }.bind(this, this.changeDirectorySequence_));
+ }.bind(this, this.changeDirectorySequence_));
+};
+
+/**
+ * Activates the given directory.
+ * This method:
+ * - Changes the current directory, if the given directory is the current
+ * directory.
+ * - Clears the selection, if the given directory is the current directory.
+ *
+ * @param {DirectoryEntry|Object} dirEntry The entry of the new directory to
+ * be opened.
+ * @param {function()=} opt_callback Executed if the directory loads
+ * successfully.
+ */
+DirectoryModel.prototype.activateDirectoryEntry = function(
+ dirEntry, opt_callback) {
+ var currentDirectoryEntry = this.getCurrentDirEntry();
+ if (currentDirectoryEntry &&
+ util.isSameEntry(dirEntry, currentDirectoryEntry)) {
+ // On activating the current directory, clear the selection on the filelist.
+ this.clearSelection();
+ } else {
+ // Otherwise, changes the current directory.
+ this.changeDirectoryEntry(dirEntry, opt_callback);
+ }
};
/**
return;
}
- if (!(query || '').trimLeft()) {
- if (this.isSearching()) {
- var newDirContents = this.createDirectoryContents_(
- this.currentFileListContext_,
- currentDirEntry);
- this.clearAndScan_(newDirContents);
+ this.changeDirectorySequence_++;
+ this.directoryChangeQueue_.run(function(sequence, callback) {
+ if (this.changeDirectorySequence_ !== sequence) {
+ callback();
+ return;
}
- return;
- }
- var newDirContents = this.createDirectoryContents_(
- this.currentFileListContext_, currentDirEntry, query);
- if (!newDirContents)
- return;
+ if (!(query || '').trimLeft()) {
+ if (this.isSearching()) {
+ var newDirContents = this.createDirectoryContents_(
+ this.currentFileListContext_,
+ currentDirEntry);
+ this.clearAndScan_(newDirContents,
+ callback);
+ } else {
+ callback();
+ }
+ return;
+ }
+
+ var newDirContents = this.createDirectoryContents_(
+ this.currentFileListContext_, currentDirEntry, query);
+ if (!newDirContents) {
+ callback();
+ return;
+ }
- this.onSearchCompleted_ = onSearchRescan;
- this.onClearSearch_ = onClearSearch;
- this.addEventListener('scan-completed', this.onSearchCompleted_);
- this.clearAndScan_(newDirContents);
+ this.onSearchCompleted_ = onSearchRescan;
+ this.onClearSearch_ = onClearSearch;
+ this.addEventListener('scan-completed', this.onSearchCompleted_);
+ this.clearAndScan_(newDirContents,
+ callback);
+ }.bind(this, this.changeDirectorySequence_));
};
/**