Upstream version 5.34.104.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / file_manager / foreground / js / directory_contents.js
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.
4
5 'use strict';
6
7 /**
8  * Scanner of the entries.
9  * @constructor
10  */
11 function ContentScanner() {
12   this.cancelled_ = false;
13 }
14
15 /**
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.
19  *
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
22  *     completion.
23  * @param {function()} successCallback Called when the scan is completed
24  *     successfully.
25  * @param {function(FileError)} errorCallback Called an error occurs.
26  */
27 ContentScanner.prototype.scan = function(
28     entriesCallback, successCallback, errorCallback) {
29 };
30
31 /**
32  * Request cancelling of the running scan. When the cancelling is done,
33  * an error will be reported from errorCallback passed to scan().
34  */
35 ContentScanner.prototype.cancel = function() {
36   this.cancelled_ = true;
37 };
38
39 /**
40  * Scanner of the entries in a directory.
41  * @param {DirectoryEntry} entry The directory to be read.
42  * @constructor
43  * @extends {ContentScanner}
44  */
45 function DirectoryContentScanner(entry) {
46   ContentScanner.call(this);
47   this.entry_ = entry;
48 }
49
50 /**
51  * Extends ContentScanner.
52  */
53 DirectoryContentScanner.prototype.__proto__ = ContentScanner.prototype;
54
55 /**
56  * Starts to read the entries in the directory.
57  * @override
58  */
59 DirectoryContentScanner.prototype.scan = function(
60     entriesCallback, successCallback, errorCallback) {
61   if (!this.entry_ || util.isFakeEntry(this.entry_)) {
62     // If entry is not specified or a fake, we cannot read it.
63     errorCallback(util.createDOMError(
64         util.FileError.INVALID_MODIFICATION_ERR));
65     return;
66   }
67
68   metrics.startInterval('DirectoryScan');
69   var reader = this.entry_.createReader();
70   var readEntries = function() {
71     reader.readEntries(
72         function(entries) {
73           if (this.cancelled_) {
74             errorCallback(util.createDOMError(util.FileError.ABORT_ERR));
75             return;
76           }
77
78           if (entries.length === 0) {
79             // All entries are read.
80             metrics.recordInterval('DirectoryScan');
81             successCallback();
82             return;
83           }
84
85           entriesCallback(entries);
86           readEntries();
87         }.bind(this),
88         errorCallback);
89   }.bind(this);
90   readEntries();
91 };
92
93 /**
94  * Scanner of the entries for the search results on Drive File System.
95  * @param {string} query The query string.
96  * @constructor
97  * @extends {ContentScanner}
98  */
99 function DriveSearchContentScanner(query) {
100   ContentScanner.call(this);
101   this.query_ = query;
102 }
103
104 /**
105  * Extends ContentScanner.
106  */
107 DriveSearchContentScanner.prototype.__proto__ = ContentScanner.prototype;
108
109 /**
110  * Delay in milliseconds to be used for drive search scan, in order to reduce
111  * the number of server requests while user is typing the query.
112  * @type {number}
113  * @private
114  * @const
115  */
116 DriveSearchContentScanner.SCAN_DELAY_ = 200;
117
118 /**
119  * Maximum number of results which is shown on the search.
120  * @type {number}
121  * @private
122  * @const
123  */
124 DriveSearchContentScanner.MAX_RESULTS_ = 100;
125
126 /**
127  * Starts to search on Drive File System.
128  * @override
129  */
130 DriveSearchContentScanner.prototype.scan = function(
131     entriesCallback, successCallback, errorCallback) {
132   var numReadEntries = 0;
133   var readEntries = function(nextFeed) {
134     chrome.fileBrowserPrivate.searchDrive(
135         {query: this.query_, nextFeed: nextFeed},
136         function(entries, nextFeed) {
137           if (this.cancelled_) {
138             errorCallback(util.createDOMError(util.FileError.ABORT_ERR));
139             return;
140           }
141
142           // TODO(tbarzic): Improve error handling.
143           if (!entries) {
144             console.error('Drive search encountered an error.');
145             errorCallback(util.createDOMError(
146                 util.FileError.INVALID_MODIFICATION_ERR));
147             return;
148           }
149
150           var numRemainingEntries =
151               DriveSearchContentScanner.MAX_RESULTS_ - numReadEntries;
152           if (entries.length >= numRemainingEntries) {
153             // The limit is hit, so quit the scan here.
154             entries = entries.slice(0, numRemainingEntries);
155             nextFeed = '';
156           }
157
158           numReadEntries += entries.length;
159           if (entries.length > 0)
160             entriesCallback(entries);
161
162           if (nextFeed === '')
163             successCallback();
164           else
165             readEntries(nextFeed);
166         }.bind(this));
167   }.bind(this);
168
169   // Let's give another search a chance to cancel us before we begin.
170   setTimeout(
171       function() {
172         // Check cancelled state before read the entries.
173         if (this.cancelled_) {
174           errorCallback(util.createDOMError(util.FileError.ABORT_ERR));
175           return;
176         }
177         readEntries('');
178       }.bind(this),
179       DriveSearchContentScanner.SCAN_DELAY_);
180 };
181
182 /**
183  * Scanner of the entries of the file name search on the directory tree, whose
184  * root is entry.
185  * @param {DirectoryEntry} entry The root of the search target directory tree.
186  * @param {string} query The query of the search.
187  * @constructor
188  * @extends {ContentScanner}
189  */
190 function LocalSearchContentScanner(entry, query) {
191   ContentScanner.call(this);
192   this.entry_ = entry;
193   this.query_ = query.toLowerCase();
194 }
195
196 /**
197  * Extedns ContentScanner.
198  */
199 LocalSearchContentScanner.prototype.__proto__ = ContentScanner.prototype;
200
201 /**
202  * Starts the file name search.
203  * @override
204  */
205 LocalSearchContentScanner.prototype.scan = function(
206     entriesCallback, successCallback, errorCallback) {
207   var numRunningTasks = 0;
208   var error = null;
209   var maybeRunCallback = function() {
210     if (numRunningTasks === 0) {
211       if (this.cancelled_)
212         errorCallback(util.createDOMError(util.FileError.ABORT_ERR));
213       else if (error)
214         errorCallback(error);
215       else
216         successCallback();
217     }
218   }.bind(this);
219
220   var processEntry = function(entry) {
221     numRunningTasks++;
222     var onError = function(fileError) {
223       if (!error)
224         error = fileError;
225       numRunningTasks--;
226       maybeRunCallback();
227     };
228
229     var onSuccess = function(entries) {
230       if (this.cancelled_ || error || entries.length === 0) {
231         numRunningTasks--;
232         maybeRunCallback();
233         return;
234       }
235
236       // Filters by the query, and if found, run entriesCallback.
237       var foundEntries = entries.filter(function(entry) {
238         return entry.name.toLowerCase().indexOf(this.query_) >= 0;
239       }.bind(this));
240       if (foundEntries.length > 0)
241         entriesCallback(foundEntries);
242
243       // Start to process sub directories.
244       for (var i = 0; i < entries.length; i++) {
245         if (entries[i].isDirectory)
246           processEntry(entries[i]);
247       }
248
249       // Read remaining entries.
250       reader.readEntries(onSuccess, onError);
251     }.bind(this);
252
253     var reader = entry.createReader();
254     reader.readEntries(onSuccess, onError);
255   }.bind(this);
256
257   processEntry(this.entry_);
258 };
259
260 /**
261  * Scanner of the entries for the metadata search on Drive File System.
262  * @param {DriveMetadataSearchContentScanner.SearchType} searchType The option
263  *     of the search.
264  * @constructor
265  * @extends {ContentScanner}
266  */
267 function DriveMetadataSearchContentScanner(searchType) {
268   ContentScanner.call(this);
269   this.searchType_ = searchType;
270 }
271
272 /**
273  * Extends ContentScanner.
274  */
275 DriveMetadataSearchContentScanner.prototype.__proto__ =
276     ContentScanner.prototype;
277
278 /**
279  * The search types on the Drive File System.
280  * @enum {string}
281  */
282 DriveMetadataSearchContentScanner.SearchType = Object.freeze({
283   SEARCH_ALL: 'ALL',
284   SEARCH_SHARED_WITH_ME: 'SHARED_WITH_ME',
285   SEARCH_RECENT_FILES: 'EXCLUDE_DIRECTORIES',
286   SEARCH_OFFLINE: 'OFFLINE'
287 });
288
289 /**
290  * Starts to metadata-search on Drive File System.
291  * @override
292  */
293 DriveMetadataSearchContentScanner.prototype.scan = function(
294     entriesCallback, successCallback, errorCallback) {
295   chrome.fileBrowserPrivate.searchDriveMetadata(
296       {query: '', types: this.searchType_, maxResults: 500},
297       function(results) {
298         if (this.cancelled_) {
299           errorCallback(util.createDOMError(util.FileError.ABORT_ERR));
300           return;
301         }
302
303         if (!results) {
304           console.error('Drive search encountered an error.');
305           errorCallback(util.createDOMError(
306               util.FileError.INVALID_MODIFICATION_ERR));
307           return;
308         }
309
310         var entries = results.map(function(result) { return result.entry; });
311         if (entries.length > 0)
312           entriesCallback(entries);
313         successCallback();
314       }.bind(this));
315 };
316
317 /**
318  * This class manages filters and determines a file should be shown or not.
319  * When filters are changed, a 'changed' event is fired.
320  *
321  * @param {MetadataCache} metadataCache Metadata cache service.
322  * @param {boolean} showHidden If files starting with '.' are shown.
323  * @constructor
324  * @extends {cr.EventTarget}
325  */
326 function FileFilter(metadataCache, showHidden) {
327   /**
328    * @type {MetadataCache}
329    * @private
330    */
331   this.metadataCache_ = metadataCache;
332
333   /**
334    * @type Object.<string, Function>
335    * @private
336    */
337   this.filters_ = {};
338   this.setFilterHidden(!showHidden);
339
340   // Do not show entries marked as 'deleted'.
341   this.addFilter('deleted', function(entry) {
342     var internal = this.metadataCache_.getCached(entry, 'internal');
343     return !(internal && internal.deleted);
344   }.bind(this));
345 }
346
347 /*
348  * FileFilter extends cr.EventTarget.
349  */
350 FileFilter.prototype = {__proto__: cr.EventTarget.prototype};
351
352 /**
353  * @param {string} name Filter identifier.
354  * @param {function(Entry)} callback A filter â€” a function receiving an Entry,
355  *     and returning bool.
356  */
357 FileFilter.prototype.addFilter = function(name, callback) {
358   this.filters_[name] = callback;
359   cr.dispatchSimpleEvent(this, 'changed');
360 };
361
362 /**
363  * @param {string} name Filter identifier.
364  */
365 FileFilter.prototype.removeFilter = function(name) {
366   delete this.filters_[name];
367   cr.dispatchSimpleEvent(this, 'changed');
368 };
369
370 /**
371  * @param {boolean} value If do not show hidden files.
372  */
373 FileFilter.prototype.setFilterHidden = function(value) {
374   if (value) {
375     this.addFilter(
376         'hidden',
377         function(entry) { return entry.name.substr(0, 1) !== '.'; }
378     );
379   } else {
380     this.removeFilter('hidden');
381   }
382 };
383
384 /**
385  * @return {boolean} If the files with names starting with "." are not shown.
386  */
387 FileFilter.prototype.isFilterHiddenOn = function() {
388   return 'hidden' in this.filters_;
389 };
390
391 /**
392  * @param {Entry} entry File entry.
393  * @return {boolean} True if the file should be shown, false otherwise.
394  */
395 FileFilter.prototype.filter = function(entry) {
396   for (var name in this.filters_) {
397     if (!this.filters_[name](entry))
398       return false;
399   }
400   return true;
401 };
402
403 /**
404  * Array data model that can replace its item.
405  * @param {Array} array Array to be used internal.
406  */
407 function ReplacableArrayDataModel(array) {
408   cr.ui.ArrayDataModel.call(this, array);
409 }
410
411 ReplacableArrayDataModel.prototype = {
412   __proto__: cr.ui.ArrayDataModel.prototype
413 };
414
415 /**
416  * Replace the contents of item and publish a 'change' event.
417  * If the old item is not found in the list, the new item is just ignored.
418  * @param {*} oldItem Old item.
419  * @param {*} newItem New item.
420  */
421 ReplacableArrayDataModel.prototype.replaceItem = function(oldItem, newItem) {
422   var index = this.indexOf(oldItem);
423   if (index >= 0) {
424     this.array_[this.indexes_[index]] = newItem;
425     this.updateIndex(index);
426   }
427 };
428
429 /**
430  * A context of DirectoryContents.
431  * TODO(yoshiki): remove this. crbug.com/224869.
432  *
433  * @param {FileFilter} fileFilter The file-filter context.
434  * @param {MetadataCache} metadataCache Metadata cache service.
435  * @constructor
436  */
437 function FileListContext(fileFilter, metadataCache) {
438   /**
439    * @type {cr.ui.ArrayDataModel}
440    */
441   this.fileList = new ReplacableArrayDataModel([]);
442
443   /**
444    * @type {MetadataCache}
445    */
446   this.metadataCache = metadataCache;
447
448   /**
449    * @type {FileFilter}
450    */
451   this.fileFilter = fileFilter;
452 }
453
454 /**
455  * This class is responsible for scanning directory (or search results),
456  * and filling the fileList. Different descendants handle various types of
457  * directory contents shown: basic directory, drive search results, local search
458  * results.
459  * TODO(hidehiko): Remove EventTarget from this.
460  *
461  * @param {FileListContext} context The file list context.
462  * @param {boolean} isSearch True for search directory contents, otherwise
463  *     false.
464  * @param {DirectoryEntry} directoryEntry The entry of the current directory.
465  * @param {function():ContentScanner} scannerFactory The factory to create
466  *     ContentScanner instance.
467  * @constructor
468  * @extends {cr.EventTarget}
469  */
470 function DirectoryContents(context,
471                            isSearch,
472                            directoryEntry,
473                            scannerFactory) {
474   this.context_ = context;
475   this.fileList_ = context.fileList;
476
477   this.isSearch_ = isSearch;
478   this.directoryEntry_ = directoryEntry;
479
480   this.scannerFactory_ = scannerFactory;
481   this.scanner_ = null;
482   this.prefetchMetadataQueue_ = new AsyncUtil.Queue();
483   this.scanCancelled_ = false;
484 }
485
486 /**
487  * DirectoryContents extends cr.EventTarget.
488  */
489 DirectoryContents.prototype.__proto__ = cr.EventTarget.prototype;
490
491 /**
492  * Create the copy of the object, but without scan started.
493  * @return {DirectoryContents} Object copy.
494  */
495 DirectoryContents.prototype.clone = function() {
496   return new DirectoryContents(
497       this.context_,
498       this.isSearch_,
499       this.directoryEntry_,
500       this.scannerFactory_);
501 };
502
503 /**
504  * Use a given fileList instead of the fileList from the context.
505  * @param {Array|cr.ui.ArrayDataModel} fileList The new file list.
506  */
507 DirectoryContents.prototype.setFileList = function(fileList) {
508   if (fileList instanceof cr.ui.ArrayDataModel)
509     this.fileList_ = fileList;
510   else
511     this.fileList_ = new cr.ui.ArrayDataModel(fileList);
512   this.context_.metadataCache.setCacheSize(this.fileList_.length);
513 };
514
515 /**
516  * Use the filelist from the context and replace its contents with the entries
517  * from the current fileList.
518  */
519 DirectoryContents.prototype.replaceContextFileList = function() {
520   if (this.context_.fileList !== this.fileList_) {
521     var spliceArgs = this.fileList_.slice();
522     var fileList = this.context_.fileList;
523     spliceArgs.unshift(0, fileList.length);
524     fileList.splice.apply(fileList, spliceArgs);
525     this.fileList_ = fileList;
526     this.context_.metadataCache.setCacheSize(this.fileList_.length);
527   }
528 };
529
530 /**
531  * @return {boolean} If the scan is active.
532  */
533 DirectoryContents.prototype.isScanning = function() {
534   return this.scanner_ || this.prefetchMetadataQueue_.isRunning();
535 };
536
537 /**
538  * @return {boolean} True if search results (drive or local).
539  */
540 DirectoryContents.prototype.isSearch = function() {
541   return this.isSearch_;
542 };
543
544 /**
545  * @return {DirectoryEntry} A DirectoryEntry for current directory. In case of
546  *     search -- the top directory from which search is run.
547  */
548 DirectoryContents.prototype.getDirectoryEntry = function() {
549   return this.directoryEntry_;
550 };
551
552 /**
553  * Start directory scan/search operation. Either 'scan-completed' or
554  * 'scan-failed' event will be fired upon completion.
555  */
556 DirectoryContents.prototype.scan = function() {
557   // TODO(hidehiko,mtomasz): this scan method must be called at most once.
558   // Remove such a limitation.
559   this.scanner_ = this.scannerFactory_();
560   this.scanner_.scan(this.onNewEntries_.bind(this),
561                      this.onScanCompleted_.bind(this),
562                      this.onScanError_.bind(this));
563 };
564
565 /**
566  * Cancels the running scan.
567  */
568 DirectoryContents.prototype.cancelScan = function() {
569   if (this.scanCancelled_)
570     return;
571   this.scanCancelled_ = true;
572   if (this.scanner_)
573     this.scanner_.cancel();
574
575   this.prefetchMetadataQueue_.cancel();
576   cr.dispatchSimpleEvent(this, 'scan-cancelled');
577 };
578
579 /**
580  * Called when the scanning by scanner_ is done.
581  * @private
582  */
583 DirectoryContents.prototype.onScanCompleted_ = function() {
584   this.scanner_ = null;
585   if (this.scanCancelled_)
586     return;
587
588   this.prefetchMetadataQueue_.run(function(callback) {
589     // Call callback first, so isScanning() returns false in the event handlers.
590     callback();
591     cr.dispatchSimpleEvent(this, 'scan-completed');
592   }.bind(this));
593 };
594
595 /**
596  * Called in case scan has failed. Should send the event.
597  * @private
598  */
599 DirectoryContents.prototype.onScanError_ = function() {
600   this.scanner_ = null;
601   if (this.scanCancelled_)
602     return;
603
604   this.prefetchMetadataQueue_.run(function(callback) {
605     // Call callback first, so isScanning() returns false in the event handlers.
606     callback();
607     cr.dispatchSimpleEvent(this, 'scan-failed');
608   }.bind(this));
609 };
610
611 /**
612  * Called when some chunk of entries are read by scanner.
613  * @param {Array.<Entry>} entries The list of the scanned entries.
614  * @private
615  */
616 DirectoryContents.prototype.onNewEntries_ = function(entries) {
617   if (this.scanCancelled_)
618     return;
619
620   var entriesFiltered = [].filter.call(
621       entries, this.context_.fileFilter.filter.bind(this.context_.fileFilter));
622
623   // Update the filelist without waiting the metadata.
624   this.fileList_.push.apply(this.fileList_, entriesFiltered);
625   cr.dispatchSimpleEvent(this, 'scan-updated');
626
627   this.context_.metadataCache.setCacheSize(this.fileList_.length);
628
629   // Because the prefetchMetadata can be slow, throttling by splitting entries
630   // into smaller chunks to reduce UI latency.
631   // TODO(hidehiko,mtomasz): This should be handled in MetadataCache.
632   var MAX_CHUNK_SIZE = 50;
633   for (var i = 0; i < entriesFiltered.length; i += MAX_CHUNK_SIZE) {
634     var chunk = entriesFiltered.slice(i, i + MAX_CHUNK_SIZE);
635     this.prefetchMetadataQueue_.run(function(chunk, callback) {
636       this.prefetchMetadata(chunk, function() {
637         if (this.scanCancelled_) {
638           // Do nothing if the scanning is cancelled.
639           callback();
640           return;
641         }
642
643         // TODO(yoshiki): Here we should fire the update event of changed
644         // items. Currently we have a method this.fileList_.updateIndex() to
645         // fire an event, but this method takes only 1 argument and invokes sort
646         // one by one. It is obviously time wasting. Instead, we call sort
647         // directory.
648         // In future, we should implement a good method like updateIndexes and
649         // use it here.
650         var status = this.fileList_.sortStatus;
651         this.fileList_.sort(status.field, status.direction);
652
653         cr.dispatchSimpleEvent(this, 'scan-updated');
654         callback();
655       }.bind(this));
656     }.bind(this, chunk));
657   }
658 };
659
660 /**
661  * @param {Array.<Entry>} entries Files.
662  * @param {function(Object)} callback Callback on done.
663  */
664 DirectoryContents.prototype.prefetchMetadata = function(entries, callback) {
665   this.context_.metadataCache.get(entries, 'filesystem|drive', callback);
666 };
667
668 /**
669  * Creates a DirectoryContents instance to show entries in a directory.
670  *
671  * @param {FileListContext} context File list context.
672  * @param {DirectoryEntry} directoryEntry The current directory entry.
673  * @return {DirectoryContents} Created DirectoryContents instance.
674  */
675 DirectoryContents.createForDirectory = function(context, directoryEntry) {
676   return new DirectoryContents(
677       context,
678       false,  // Non search.
679       directoryEntry,
680       function() {
681         return new DirectoryContentScanner(directoryEntry);
682       });
683 };
684
685 /**
686  * Creates a DirectoryContents instance to show the result of the search on
687  * Drive File System.
688  *
689  * @param {FileListContext} context File list context.
690  * @param {DirectoryEntry} directoryEntry The current directory entry.
691  * @param {string} query Search query.
692  * @return {DirectoryContents} Created DirectoryContents instance.
693  */
694 DirectoryContents.createForDriveSearch = function(
695     context, directoryEntry, query) {
696   return new DirectoryContents(
697       context,
698       true,  // Search.
699       directoryEntry,
700       function() {
701         return new DriveSearchContentScanner(query);
702       });
703 };
704
705 /**
706  * Creates a DirectoryContents instance to show the result of the search on
707  * Local File System.
708  *
709  * @param {FileListContext} context File list context.
710  * @param {DirectoryEntry} directoryEntry The current directory entry.
711  * @param {string} query Search query.
712  * @return {DirectoryContents} Created DirectoryContents instance.
713  */
714 DirectoryContents.createForLocalSearch = function(
715     context, directoryEntry, query) {
716   return new DirectoryContents(
717       context,
718       true,  // Search.
719       directoryEntry,
720       function() {
721         return new LocalSearchContentScanner(directoryEntry, query);
722       });
723 };
724
725 /**
726  * Creates a DirectoryContents instance to show the result of metadata search
727  * on Drive File System.
728  *
729  * @param {FileListContext} context File list context.
730  * @param {DirectoryEntry} fakeDirectoryEntry Fake directory entry representing
731  *     the set of result entries. This serves as a top directory for the
732  *     search.
733  * @param {DriveMetadataSearchContentScanner.SearchType} searchType The type of
734  *     the search. The scanner will restricts the entries based on the given
735  *     type.
736  * @return {DirectoryContents} Created DirectoryContents instance.
737  */
738 DirectoryContents.createForDriveMetadataSearch = function(
739     context, fakeDirectoryEntry, searchType) {
740   return new DirectoryContents(
741       context,
742       true,  // Search
743       fakeDirectoryEntry,
744       function() {
745         return new DriveMetadataSearchContentScanner(searchType);
746       });
747 };