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.
8 * Scanner of the entries.
11 function ContentScanner() {
12 this.cancelled_ = false;
16 * Starts to scan the entries. For example, starts to read the entries in a
17 * directory, or starts to search with some query on a file system.
18 * Derived classes must override this method.
20 * @param {function(Array.<Entry>)} entriesCallback Called when some chunk of
21 * entries are read. This can be called a couple of times until the
23 * @param {function()} successCallback Called when the scan is completed
25 * @param {function(FileError)} errorCallback Called an error occurs.
27 ContentScanner.prototype.scan = function(
28 entriesCallback, successCallback, errorCallback) {
32 * Request cancelling of the running scan. When the cancelling is done,
33 * an error will be reported from errorCallback passed to scan().
35 ContentScanner.prototype.cancel = function() {
36 this.cancelled_ = true;
40 * Scanner of the entries in a directory.
41 * @param {DirectoryEntry} entry The directory to be read.
43 * @extends {ContentScanner}
45 function DirectoryContentScanner(entry) {
46 ContentScanner.call(this);
51 * Extends ContentScanner.
53 DirectoryContentScanner.prototype.__proto__ = ContentScanner.prototype;
56 * Starts to read the entries in the directory.
59 DirectoryContentScanner.prototype.scan = function(
60 entriesCallback, successCallback, errorCallback) {
62 (util.isFakeEntry(this.entry_) &&
63 this.entry_.rootType === RootType.DRIVE)) {
64 // If entry is not specified or a fake, we cannot read it.
65 errorCallback(util.createDOMError(
66 util.FileError.INVALID_MODIFICATION_ERR));
70 metrics.startInterval('DirectoryScan');
71 var reader = this.entry_.createReader();
72 var readEntries = function() {
75 if (this.cancelled_) {
76 errorCallback(util.createDOMError(util.FileError.ABORT_ERR));
80 if (entries.length === 0) {
81 // All entries are read.
82 metrics.recordInterval('DirectoryScan');
87 entriesCallback(entries);
96 * Scanner of the entries for the search results on Drive File System.
97 * @param {string} query The query string.
99 * @extends {ContentScanner}
101 function DriveSearchContentScanner(query) {
102 ContentScanner.call(this);
107 * Extends ContentScanner.
109 DriveSearchContentScanner.prototype.__proto__ = ContentScanner.prototype;
112 * Delay in milliseconds to be used for drive search scan, in order to reduce
113 * the number of server requests while user is typing the query.
118 DriveSearchContentScanner.SCAN_DELAY_ = 200;
121 * Maximum number of results which is shown on the search.
126 DriveSearchContentScanner.MAX_RESULTS_ = 100;
129 * Starts to search on Drive File System.
132 DriveSearchContentScanner.prototype.scan = function(
133 entriesCallback, successCallback, errorCallback) {
134 var numReadEntries = 0;
135 var readEntries = function(nextFeed) {
136 chrome.fileBrowserPrivate.searchDrive(
137 {query: this.query_, nextFeed: nextFeed},
138 function(entries, nextFeed) {
139 if (this.cancelled_) {
140 errorCallback(util.createDOMError(util.FileError.ABORT_ERR));
144 // TODO(tbarzic): Improve error handling.
146 console.error('Drive search encountered an error.');
147 errorCallback(util.createDOMError(
148 util.FileError.INVALID_MODIFICATION_ERR));
152 var numRemainingEntries =
153 DriveSearchContentScanner.MAX_RESULTS_ - numReadEntries;
154 if (entries.length >= numRemainingEntries) {
155 // The limit is hit, so quit the scan here.
156 entries = entries.slice(0, numRemainingEntries);
160 numReadEntries += entries.length;
161 if (entries.length > 0)
162 entriesCallback(entries);
167 readEntries(nextFeed);
171 // Let's give another search a chance to cancel us before we begin.
174 // Check cancelled state before read the entries.
175 if (this.cancelled_) {
176 errorCallback(util.createDOMError(util.FileError.ABORT_ERR));
181 DriveSearchContentScanner.SCAN_DELAY_);
185 * Scanner of the entries of the file name search on the directory tree, whose
187 * @param {DirectoryEntry} entry The root of the search target directory tree.
188 * @param {string} query The query of the search.
190 * @extends {ContentScanner}
192 function LocalSearchContentScanner(entry, query) {
193 ContentScanner.call(this);
195 this.query_ = query.toLowerCase();
199 * Extedns ContentScanner.
201 LocalSearchContentScanner.prototype.__proto__ = ContentScanner.prototype;
204 * Starts the file name search.
207 LocalSearchContentScanner.prototype.scan = function(
208 entriesCallback, successCallback, errorCallback) {
209 var numRunningTasks = 0;
211 var maybeRunCallback = function() {
212 if (numRunningTasks === 0) {
214 errorCallback(util.createDOMError(util.FileError.ABORT_ERR));
216 errorCallback(error);
222 var processEntry = function(entry) {
224 var onError = function(fileError) {
231 var onSuccess = function(entries) {
232 if (this.cancelled_ || error || entries.length === 0) {
238 // Filters by the query, and if found, run entriesCallback.
239 var foundEntries = entries.filter(function(entry) {
240 return entry.name.toLowerCase().indexOf(this.query_) >= 0;
242 if (foundEntries.length > 0)
243 entriesCallback(foundEntries);
245 // Start to process sub directories.
246 for (var i = 0; i < entries.length; i++) {
247 if (entries[i].isDirectory)
248 processEntry(entries[i]);
251 // Read remaining entries.
252 reader.readEntries(onSuccess, onError);
255 var reader = entry.createReader();
256 reader.readEntries(onSuccess, onError);
259 processEntry(this.entry_);
263 * Scanner of the entries for the metadata search on Drive File System.
264 * @param {string} query The query of the search.
265 * @param {DriveMetadataSearchContentScanner.SearchType} searchType The option
268 * @extends {ContentScanner}
270 function DriveMetadataSearchContentScanner(query, searchType) {
271 ContentScanner.call(this);
273 this.searchType_ = searchType;
277 * Extends ContentScanner.
279 DriveMetadataSearchContentScanner.prototype.__proto__ =
280 ContentScanner.prototype;
283 * The search types on the Drive File System.
286 DriveMetadataSearchContentScanner.SearchType = Object.freeze({
288 SEARCH_SHARED_WITH_ME: 'SHARED_WITH_ME',
289 SEARCH_RECENT_FILES: 'EXCLUDE_DIRECTORIES',
290 SEARCH_OFFLINE: 'OFFLINE'
294 * Starts to metadata-search on Drive File System.
297 DriveMetadataSearchContentScanner.prototype.scan = function(
298 entriesCallback, successCallback, errorCallback) {
299 chrome.fileBrowserPrivate.searchDriveMetadata(
300 {query: this.query_, types: this.searchType_, maxResults: 500},
302 if (this.cancelled_) {
303 errorCallback(util.createDOMError(util.FileError.ABORT_ERR));
308 console.error('Drive search encountered an error.');
309 errorCallback(util.createDOMError(
310 util.FileError.INVALID_MODIFICATION_ERR));
314 var entries = results.map(function(result) { return result.entry; });
315 if (entries.length > 0)
316 entriesCallback(entries);
322 * This class manages filters and determines a file should be shown or not.
323 * When filters are changed, a 'changed' event is fired.
325 * @param {MetadataCache} metadataCache Metadata cache service.
326 * @param {boolean} showHidden If files starting with '.' are shown.
328 * @extends {cr.EventTarget}
330 function FileFilter(metadataCache, showHidden) {
332 * @type {MetadataCache}
335 this.metadataCache_ = metadataCache;
338 * @type Object.<string, Function>
342 this.setFilterHidden(!showHidden);
344 // Do not show entries marked as 'deleted'.
345 this.addFilter('deleted', function(entry) {
346 var internal = this.metadataCache_.getCached(entry, 'internal');
347 return !(internal && internal.deleted);
352 * FileFilter extends cr.EventTarget.
354 FileFilter.prototype = {__proto__: cr.EventTarget.prototype};
357 * @param {string} name Filter identifier.
358 * @param {function(Entry)} callback A filter — a function receiving an Entry,
359 * and returning bool.
361 FileFilter.prototype.addFilter = function(name, callback) {
362 this.filters_[name] = callback;
363 cr.dispatchSimpleEvent(this, 'changed');
367 * @param {string} name Filter identifier.
369 FileFilter.prototype.removeFilter = function(name) {
370 delete this.filters_[name];
371 cr.dispatchSimpleEvent(this, 'changed');
375 * @param {boolean} value If do not show hidden files.
377 FileFilter.prototype.setFilterHidden = function(value) {
381 function(entry) { return entry.name.substr(0, 1) !== '.'; }
384 this.removeFilter('hidden');
389 * @return {boolean} If the files with names starting with "." are not shown.
391 FileFilter.prototype.isFilterHiddenOn = function() {
392 return 'hidden' in this.filters_;
396 * @param {Entry} entry File entry.
397 * @return {boolean} True if the file should be shown, false otherwise.
399 FileFilter.prototype.filter = function(entry) {
400 for (var name in this.filters_) {
401 if (!this.filters_[name](entry))
408 * A context of DirectoryContents.
409 * TODO(yoshiki): remove this. crbug.com/224869.
411 * @param {FileFilter} fileFilter The file-filter context.
412 * @param {MetadataCache} metadataCache Metadata cache service.
415 function FileListContext(fileFilter, metadataCache) {
417 * @type {cr.ui.ArrayDataModel}
419 this.fileList = new cr.ui.ArrayDataModel([]);
422 * @type {MetadataCache}
424 this.metadataCache = metadataCache;
429 this.fileFilter = fileFilter;
433 * This class is responsible for scanning directory (or search results),
434 * and filling the fileList. Different descendants handle various types of
435 * directory contents shown: basic directory, drive search results, local search
437 * TODO(hidehiko): Remove EventTarget from this.
439 * @param {FileListContext} context The file list context.
440 * @param {boolean} isSearch True for search directory contents, otherwise
442 * @param {DirectoryEntry} directoryEntry The entry of the current directory.
443 * @param {DirectoryEntry} lastNonSearchDirectoryEntry The entry of the last
444 * non-search directory.
445 * @param {function():ContentScanner} scannerFactory The factory to create
446 * ContentScanner instance.
448 * @extends {cr.EventTarget}
450 function DirectoryContents(context, isSearch, directoryEntry,
451 lastNonSearchDirectoryEntry,
453 this.context_ = context;
454 this.fileList_ = context.fileList;
456 this.isSearch_ = isSearch;
457 this.directoryEntry_ = directoryEntry;
458 this.lastNonSearchDirectoryEntry_ = lastNonSearchDirectoryEntry;
460 this.scannerFactory_ = scannerFactory;
461 this.scanner_ = null;
462 this.prefetchMetadataQueue_ = new AsyncUtil.Queue();
463 this.scanCancelled_ = false;
467 * DirectoryContents extends cr.EventTarget.
469 DirectoryContents.prototype.__proto__ = cr.EventTarget.prototype;
472 * Create the copy of the object, but without scan started.
473 * @return {DirectoryContents} Object copy.
475 DirectoryContents.prototype.clone = function() {
476 return new DirectoryContents(
477 this.context_, this.isSearch_, this.directoryEntry_,
478 this.lastNonSearchDirectoryEntry_, this.scannerFactory_);
482 * Use a given fileList instead of the fileList from the context.
483 * @param {Array|cr.ui.ArrayDataModel} fileList The new file list.
485 DirectoryContents.prototype.setFileList = function(fileList) {
486 if (fileList instanceof cr.ui.ArrayDataModel)
487 this.fileList_ = fileList;
489 this.fileList_ = new cr.ui.ArrayDataModel(fileList);
490 this.context_.metadataCache.setCacheSize(this.fileList_.length);
494 * Use the filelist from the context and replace its contents with the entries
495 * from the current fileList.
497 DirectoryContents.prototype.replaceContextFileList = function() {
498 if (this.context_.fileList !== this.fileList_) {
499 var spliceArgs = this.fileList_.slice();
500 var fileList = this.context_.fileList;
501 spliceArgs.unshift(0, fileList.length);
502 fileList.splice.apply(fileList, spliceArgs);
503 this.fileList_ = fileList;
504 this.context_.metadataCache.setCacheSize(this.fileList_.length);
509 * @return {boolean} If the scan is active.
511 DirectoryContents.prototype.isScanning = function() {
512 return this.scanner_ || this.prefetchMetadataQueue_.isRunning();
516 * @return {boolean} True if search results (drive or local).
518 DirectoryContents.prototype.isSearch = function() {
519 return this.isSearch_;
523 * @return {DirectoryEntry} A DirectoryEntry for current directory. In case of
524 * search -- the top directory from which search is run.
526 DirectoryContents.prototype.getDirectoryEntry = function() {
527 return this.directoryEntry_;
531 * @return {DirectoryEntry} A DirectoryEntry for the last non search contents.
533 DirectoryContents.prototype.getLastNonSearchDirectoryEntry = function() {
534 return this.lastNonSearchDirectoryEntry_;
538 * Start directory scan/search operation. Either 'scan-completed' or
539 * 'scan-failed' event will be fired upon completion.
541 DirectoryContents.prototype.scan = function() {
542 // TODO(hidehiko,mtomasz): this scan method must be called at most once.
543 // Remove such a limitation.
544 this.scanner_ = this.scannerFactory_();
545 this.scanner_.scan(this.onNewEntries_.bind(this),
546 this.onScanCompleted_.bind(this),
547 this.onScanError_.bind(this));
551 * Cancels the running scan.
553 DirectoryContents.prototype.cancelScan = function() {
554 if (this.scanCancelled_)
556 this.scanCancelled_ = true;
558 this.scanner_.cancel();
560 this.prefetchMetadataQueue_.cancel();
561 cr.dispatchSimpleEvent(this, 'scan-cancelled');
565 * Called when the scanning by scanner_ is done.
568 DirectoryContents.prototype.onScanCompleted_ = function() {
569 this.scanner_ = null;
570 if (this.scanCancelled_)
573 this.prefetchMetadataQueue_.run(function(callback) {
574 // Call callback first, so isScanning() returns false in the event handlers.
576 cr.dispatchSimpleEvent(this, 'scan-completed');
581 * Called in case scan has failed. Should send the event.
584 DirectoryContents.prototype.onScanError_ = function() {
585 this.scanner_ = null;
586 if (this.scanCancelled_)
589 this.prefetchMetadataQueue_.run(function(callback) {
590 // Call callback first, so isScanning() returns false in the event handlers.
592 cr.dispatchSimpleEvent(this, 'scan-failed');
597 * Called when some chunk of entries are read by scanner.
598 * @param {Array.<Entry>} entries The list of the scanned entries.
601 DirectoryContents.prototype.onNewEntries_ = function(entries) {
602 if (this.scanCancelled_)
605 var entriesFiltered = [].filter.call(
606 entries, this.context_.fileFilter.filter.bind(this.context_.fileFilter));
608 // Update the filelist without waiting the metadata.
609 this.fileList_.push.apply(this.fileList_, entriesFiltered);
610 cr.dispatchSimpleEvent(this, 'scan-updated');
612 this.context_.metadataCache.setCacheSize(this.fileList_.length);
614 // Because the prefetchMetadata can be slow, throttling by splitting entries
615 // into smaller chunks to reduce UI latency.
616 // TODO(hidehiko,mtomasz): This should be handled in MetadataCache.
617 var MAX_CHUNK_SIZE = 50;
618 for (var i = 0; i < entriesFiltered.length; i += MAX_CHUNK_SIZE) {
619 var chunk = entriesFiltered.slice(i, i + MAX_CHUNK_SIZE);
620 this.prefetchMetadataQueue_.run(function(chunk, callback) {
621 this.prefetchMetadata(chunk, function() {
622 if (this.scanCancelled_) {
623 // Do nothing if the scanning is cancelled.
628 // TODO(yoshiki): Here we should fire the update event of changed
629 // items. Currently we have a method this.fileList_.updateIndex() to
630 // fire an event, but this method takes only 1 argument and invokes sort
631 // one by one. It is obviously time wasting. Instead, we call sort
633 // In future, we should implement a good method like updateIndexes and
635 var status = this.fileList_.sortStatus;
636 this.fileList_.sort(status.field, status.direction);
638 cr.dispatchSimpleEvent(this, 'scan-updated');
641 }.bind(this, chunk));
646 * @param {Array.<Entry>} entries Files.
647 * @param {function(Object)} callback Callback on done.
649 DirectoryContents.prototype.prefetchMetadata = function(entries, callback) {
650 this.context_.metadataCache.get(entries, 'filesystem', callback);
654 * @param {Array.<Entry>} entries Files.
655 * @param {function(Object)} callback Callback on done.
657 DirectoryContents.prototype.reloadMetadata = function(entries, callback) {
658 this.context_.metadataCache.clear(entries, '*');
659 this.context_.metadataCache.get(entries, 'filesystem', callback);
663 * @param {string} name Directory name.
664 * @param {function(DirectoryEntry)} successCallback Called on success.
665 * @param {function(FileError)} errorCallback On error.
667 DirectoryContents.prototype.createDirectory = function(
668 name, successCallback, errorCallback) {
669 // TODO(hidehiko): createDirectory should not be the part of
671 if (this.isSearch_ || !this.directoryEntry_) {
672 errorCallback(util.createDOMError(
673 util.FileError.INVALID_MODIFICATION_ERR));
677 var onSuccess = function(newEntry) {
678 this.reloadMetadata([newEntry], function() {
679 successCallback(newEntry);
683 this.directoryEntry_.getDirectory(name, {create: true, exclusive: true},
684 onSuccess.bind(this), errorCallback);
688 * Creates a DirectoryContents instance to show entries in a directory.
690 * @param {FileListContext} context File list context.
691 * @param {DirectoryEntry} directoryEntry The current directory entry.
692 * @return {DirectoryContents} Created DirectoryContents instance.
694 DirectoryContents.createForDirectory = function(context, directoryEntry) {
695 return new DirectoryContents(
697 false, // Non search.
701 return new DirectoryContentScanner(directoryEntry);
706 * Creates a DirectoryContents instance to show the result of the search on
709 * @param {FileListContext} context File list context.
710 * @param {DirectoryEntry} directoryEntry The current directory entry.
711 * @param {DirectoryEntry} previousDirectoryEntry The DirectoryEntry that was
712 * current before the search.
713 * @param {string} query Search query.
714 * @return {DirectoryContents} Created DirectoryContents instance.
716 DirectoryContents.createForDriveSearch = function(
717 context, directoryEntry, previousDirectoryEntry, query) {
718 return new DirectoryContents(
722 previousDirectoryEntry,
724 return new DriveSearchContentScanner(query);
729 * Creates a DirectoryContents instance to show the result of the search on
732 * @param {FileListContext} context File list context.
733 * @param {DirectoryEntry} directoryEntry The current directory entry.
734 * @param {string} query Search query.
735 * @return {DirectoryContents} Created DirectoryContents instance.
737 DirectoryContents.createForLocalSearch = function(
738 context, directoryEntry, query) {
739 return new DirectoryContents(
745 return new LocalSearchContentScanner(directoryEntry, query);
750 * Creates a DirectoryContents instance to show the result of metadata search
751 * on Drive File System.
753 * @param {FileListContext} context File list context.
754 * @param {DirectoryEntry} fakeDirectoryEntry Fake directory entry representing
755 * the set of result entries. This serves as a top directory for the
757 * @param {DirectoryEntry} driveDirectoryEntry Directory for the actual drive.
758 * @param {string} query Search query.
759 * @param {DriveMetadataSearchContentScanner.SearchType} searchType The type of
760 * the search. The scanner will restricts the entries based on the given
762 * @return {DirectoryContents} Created DirectoryContents instance.
764 DirectoryContents.createForDriveMetadataSearch = function(
765 context, fakeDirectoryEntry, driveDirectoryEntry, query, searchType) {
766 return new DirectoryContents(
772 return new DriveMetadataSearchContentScanner(query, searchType);