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 */ 11function 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 */ 27ContentScanner.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 */ 35ContentScanner.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 */ 45function DirectoryContentScanner(entry) { 46 ContentScanner.call(this); 47 this.entry_ = entry; 48} 49 50/** 51 * Extends ContentScanner. 52 */ 53DirectoryContentScanner.prototype.__proto__ = ContentScanner.prototype; 54 55/** 56 * Starts to read the entries in the directory. 57 * @override 58 */ 59DirectoryContentScanner.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 */ 99function DriveSearchContentScanner(query) { 100 ContentScanner.call(this); 101 this.query_ = query; 102} 103 104/** 105 * Extends ContentScanner. 106 */ 107DriveSearchContentScanner.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 */ 116DriveSearchContentScanner.SCAN_DELAY_ = 200; 117 118/** 119 * Maximum number of results which is shown on the search. 120 * @type {number} 121 * @private 122 * @const 123 */ 124DriveSearchContentScanner.MAX_RESULTS_ = 100; 125 126/** 127 * Starts to search on Drive File System. 128 * @override 129 */ 130DriveSearchContentScanner.prototype.scan = function( 131 entriesCallback, successCallback, errorCallback) { 132 var numReadEntries = 0; 133 var readEntries = function(nextFeed) { 134 chrome.fileManagerPrivate.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 */ 190function LocalSearchContentScanner(entry, query) { 191 ContentScanner.call(this); 192 this.entry_ = entry; 193 this.query_ = query.toLowerCase(); 194} 195 196/** 197 * Extends ContentScanner. 198 */ 199LocalSearchContentScanner.prototype.__proto__ = ContentScanner.prototype; 200 201/** 202 * Starts the file name search. 203 * @override 204 */ 205LocalSearchContentScanner.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 */ 267function DriveMetadataSearchContentScanner(searchType) { 268 ContentScanner.call(this); 269 this.searchType_ = searchType; 270} 271 272/** 273 * Extends ContentScanner. 274 */ 275DriveMetadataSearchContentScanner.prototype.__proto__ = 276 ContentScanner.prototype; 277 278/** 279 * The search types on the Drive File System. 280 * @enum {string} 281 */ 282DriveMetadataSearchContentScanner.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 */ 293DriveMetadataSearchContentScanner.prototype.scan = function( 294 entriesCallback, successCallback, errorCallback) { 295 chrome.fileManagerPrivate.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 '.' or ending with 323 * '.crdownlaod' are shown. 324 * @constructor 325 * @extends {cr.EventTarget} 326 */ 327function FileFilter(metadataCache, showHidden) { 328 /** 329 * @type {MetadataCache} 330 * @private 331 */ 332 this.metadataCache_ = metadataCache; 333 334 /** 335 * @type {Object.<string, Function>} 336 * @private 337 */ 338 this.filters_ = {}; 339 this.setFilterHidden(!showHidden); 340 341 // Do not show entries marked as 'deleted'. 342 this.addFilter('deleted', function(entry) { 343 var internal = this.metadataCache_.getCached(entry, 'internal'); 344 return !(internal && internal.deleted); 345 }.bind(this)); 346} 347 348/* 349 * FileFilter extends cr.EventTarget. 350 */ 351FileFilter.prototype = {__proto__: cr.EventTarget.prototype}; 352 353/** 354 * @param {string} name Filter identifier. 355 * @param {function(Entry)} callback A filter — a function receiving an Entry, 356 * and returning bool. 357 */ 358FileFilter.prototype.addFilter = function(name, callback) { 359 this.filters_[name] = callback; 360 cr.dispatchSimpleEvent(this, 'changed'); 361}; 362 363/** 364 * @param {string} name Filter identifier. 365 */ 366FileFilter.prototype.removeFilter = function(name) { 367 delete this.filters_[name]; 368 cr.dispatchSimpleEvent(this, 'changed'); 369}; 370 371/** 372 * @param {boolean} value If do not show hidden files. 373 */ 374FileFilter.prototype.setFilterHidden = function(value) { 375 var regexpCrdownloadExtension = /\.crdownload$/i; 376 if (value) { 377 this.addFilter( 378 'hidden', 379 function(entry) { 380 return entry.name.substr(0, 1) !== '.' && 381 !regexpCrdownloadExtension.test(entry.name); 382 } 383 ); 384 } else { 385 this.removeFilter('hidden'); 386 } 387}; 388 389/** 390 * @return {boolean} If the files with names starting with "." are not shown. 391 */ 392FileFilter.prototype.isFilterHiddenOn = function() { 393 return 'hidden' in this.filters_; 394}; 395 396/** 397 * @param {Entry} entry File entry. 398 * @return {boolean} True if the file should be shown, false otherwise. 399 */ 400FileFilter.prototype.filter = function(entry) { 401 for (var name in this.filters_) { 402 if (!this.filters_[name](entry)) 403 return false; 404 } 405 return true; 406}; 407 408/** 409 * File list. 410 * @param {MetadataCache} metadataCache Metadata cache. 411 * @constructor 412 * @extends {cr.ui.ArrayDataModel} 413 */ 414function FileListModel(metadataCache) { 415 cr.ui.ArrayDataModel.call(this, []); 416 417 /** 418 * Metadata cache. 419 * @type {MetadataCache} 420 * @private 421 */ 422 this.metadataCache_ = metadataCache; 423 424 // Initialize compare functions. 425 this.setCompareFunction('name', util.compareName); 426 this.setCompareFunction('modificationTime', this.compareMtime_.bind(this)); 427 this.setCompareFunction('size', this.compareSize_.bind(this)); 428 this.setCompareFunction('type', this.compareType_.bind(this)); 429} 430 431FileListModel.prototype = { 432 __proto__: cr.ui.ArrayDataModel.prototype 433}; 434 435/** 436 * Compare by mtime first, then by name. 437 * @param {Entry} a First entry. 438 * @param {Entry} b Second entry. 439 * @return {number} Compare result. 440 * @private 441 */ 442FileListModel.prototype.compareMtime_ = function(a, b) { 443 var aCachedFilesystem = this.metadataCache_.getCached(a, 'filesystem'); 444 var aTime = aCachedFilesystem ? aCachedFilesystem.modificationTime : 0; 445 446 var bCachedFilesystem = this.metadataCache_.getCached(b, 'filesystem'); 447 var bTime = bCachedFilesystem ? bCachedFilesystem.modificationTime : 0; 448 449 if (aTime > bTime) 450 return 1; 451 452 if (aTime < bTime) 453 return -1; 454 455 return util.compareName(a, b); 456}; 457 458/** 459 * Compare by size first, then by name. 460 * @param {Entry} a First entry. 461 * @param {Entry} b Second entry. 462 * @return {number} Compare result. 463 * @private 464 */ 465FileListModel.prototype.compareSize_ = function(a, b) { 466 var aCachedFilesystem = this.metadataCache_.getCached(a, 'filesystem'); 467 var aSize = aCachedFilesystem ? aCachedFilesystem.size : 0; 468 469 var bCachedFilesystem = this.metadataCache_.getCached(b, 'filesystem'); 470 var bSize = bCachedFilesystem ? bCachedFilesystem.size : 0; 471 472 return aSize !== bSize ? aSize - bSize : util.compareName(a, b); 473}; 474 475/** 476 * Compare by type first, then by subtype and then by name. 477 * @param {Entry} a First entry. 478 * @param {Entry} b Second entry. 479 * @return {number} Compare result. 480 * @private 481 */ 482FileListModel.prototype.compareType_ = function(a, b) { 483 // Directories precede files. 484 if (a.isDirectory !== b.isDirectory) 485 return Number(b.isDirectory) - Number(a.isDirectory); 486 487 var aType = FileType.typeToString(FileType.getType(a)); 488 var bType = FileType.typeToString(FileType.getType(b)); 489 490 var result = util.collator.compare(aType, bType); 491 return result !== 0 ? result : util.compareName(a, b); 492}; 493 494/** 495 * A context of DirectoryContents. 496 * TODO(yoshiki): remove this. crbug.com/224869. 497 * 498 * @param {FileFilter} fileFilter The file-filter context. 499 * @param {MetadataCache} metadataCache Metadata cache service. 500 * @constructor 501 */ 502function FileListContext(fileFilter, metadataCache) { 503 /** 504 * @type {FileListModel} 505 */ 506 this.fileList = new FileListModel(metadataCache); 507 508 /** 509 * @type {MetadataCache} 510 */ 511 this.metadataCache = metadataCache; 512 513 /** 514 * @type {FileFilter} 515 */ 516 this.fileFilter = fileFilter; 517} 518 519/** 520 * This class is responsible for scanning directory (or search results), 521 * and filling the fileList. Different descendants handle various types of 522 * directory contents shown: basic directory, drive search results, local search 523 * results. 524 * TODO(hidehiko): Remove EventTarget from this. 525 * 526 * @param {FileListContext} context The file list context. 527 * @param {boolean} isSearch True for search directory contents, otherwise 528 * false. 529 * @param {DirectoryEntry} directoryEntry The entry of the current directory. 530 * @param {function():ContentScanner} scannerFactory The factory to create 531 * ContentScanner instance. 532 * @constructor 533 * @extends {cr.EventTarget} 534 */ 535function DirectoryContents(context, 536 isSearch, 537 directoryEntry, 538 scannerFactory) { 539 this.context_ = context; 540 this.fileList_ = context.fileList; 541 542 this.isSearch_ = isSearch; 543 this.directoryEntry_ = directoryEntry; 544 545 this.scannerFactory_ = scannerFactory; 546 this.scanner_ = null; 547 this.processNewEntriesQueue_ = new AsyncUtil.Queue(); 548 this.scanCancelled_ = false; 549 550 this.lastSpaceInMetadataCache_ = 0; 551} 552 553/** 554 * DirectoryContents extends cr.EventTarget. 555 */ 556DirectoryContents.prototype.__proto__ = cr.EventTarget.prototype; 557 558/** 559 * Create the copy of the object, but without scan started. 560 * @return {DirectoryContents} Object copy. 561 */ 562DirectoryContents.prototype.clone = function() { 563 return new DirectoryContents( 564 this.context_, 565 this.isSearch_, 566 this.directoryEntry_, 567 this.scannerFactory_); 568}; 569 570/** 571 * Disposes the reserved metadata cache. 572 */ 573DirectoryContents.prototype.dispose = function() { 574 this.context_.metadataCache.resizeBy(-this.lastSpaceInMetadataCache_); 575 // Though the lastSpaceInMetadataCache_ is not supposed to be referred after 576 // dispose(), keep it synced with requested cache size just in case. 577 this.lastSpaceInMetadataCache_ = 0; 578}; 579 580/** 581 * Make a space for current directory size in the metadata cache. 582 * 583 * @param {number} size The cache size to be set. 584 * @private 585 */ 586DirectoryContents.prototype.makeSpaceInMetadataCache_ = function(size) { 587 this.context_.metadataCache.resizeBy(size - this.lastSpaceInMetadataCache_); 588 this.lastSpaceInMetadataCache_ = size; 589}; 590 591/** 592 * Use a given fileList instead of the fileList from the context. 593 * @param {Array|cr.ui.ArrayDataModel} fileList The new file list. 594 */ 595DirectoryContents.prototype.setFileList = function(fileList) { 596 if (fileList instanceof cr.ui.ArrayDataModel) 597 this.fileList_ = fileList; 598 else 599 this.fileList_ = new cr.ui.ArrayDataModel(fileList); 600 this.makeSpaceInMetadataCache_(this.fileList_.length); 601}; 602 603/** 604 * Use the filelist from the context and replace its contents with the entries 605 * from the current fileList. 606 */ 607DirectoryContents.prototype.replaceContextFileList = function() { 608 if (this.context_.fileList !== this.fileList_) { 609 var spliceArgs = this.fileList_.slice(); 610 var fileList = this.context_.fileList; 611 spliceArgs.unshift(0, fileList.length); 612 fileList.splice.apply(fileList, spliceArgs); 613 this.fileList_ = fileList; 614 this.makeSpaceInMetadataCache_(this.fileList_.length); 615 } 616}; 617 618/** 619 * @return {boolean} If the scan is active. 620 */ 621DirectoryContents.prototype.isScanning = function() { 622 return this.scanner_ || this.processNewEntriesQueue_.isRunning(); 623}; 624 625/** 626 * @return {boolean} True if search results (drive or local). 627 */ 628DirectoryContents.prototype.isSearch = function() { 629 return this.isSearch_; 630}; 631 632/** 633 * @return {DirectoryEntry} A DirectoryEntry for current directory. In case of 634 * search -- the top directory from which search is run. 635 */ 636DirectoryContents.prototype.getDirectoryEntry = function() { 637 return this.directoryEntry_; 638}; 639 640/** 641 * Start directory scan/search operation. Either 'scan-completed' or 642 * 'scan-failed' event will be fired upon completion. 643 * 644 * @param {boolean} refresh True to refresh metadata, or false to use cached 645 * one. 646 */ 647DirectoryContents.prototype.scan = function(refresh) { 648 /** 649 * Invoked when the scanning is completed successfully. 650 * @this {DirectoryContents} 651 */ 652 function completionCallback() { 653 this.onScanFinished_(); 654 this.onScanCompleted_(); 655 } 656 657 /** 658 * Invoked when the scanning is finished but is not completed due to error. 659 * @this {DirectoryContents} 660 */ 661 function errorCallback() { 662 this.onScanFinished_(); 663 this.onScanError_(); 664 } 665 666 // TODO(hidehiko,mtomasz): this scan method must be called at most once. 667 // Remove such a limitation. 668 this.scanner_ = this.scannerFactory_(); 669 this.scanner_.scan(this.onNewEntries_.bind(this, refresh), 670 completionCallback.bind(this), 671 errorCallback.bind(this)); 672}; 673 674/** 675 * Adds/removes/updates items of file list. 676 * @param {Array.<Entry>} updatedEntries Entries of updated/added files. 677 * @param {Array.<string>} removedUrls URLs of removed files. 678 */ 679DirectoryContents.prototype.update = function(updatedEntries, removedUrls) { 680 var removedMap = {}; 681 for (var i = 0; i < removedUrls.length; i++) { 682 removedMap[removedUrls[i]] = true; 683 } 684 685 var updatedMap = {}; 686 for (var i = 0; i < updatedEntries.length; i++) { 687 updatedMap[updatedEntries[i].toURL()] = updatedEntries[i]; 688 } 689 690 var updatedList = []; 691 for (var i = 0; i < this.fileList_.length; i++) { 692 var url = this.fileList_.item(i).toURL(); 693 694 if (url in removedMap) { 695 this.fileList_.splice(i, 1); 696 i--; 697 continue; 698 } 699 700 if (url in updatedMap) { 701 updatedList.push(updatedMap[url]); 702 delete updatedMap[url]; 703 } 704 } 705 706 var addedList = []; 707 for (var url in updatedMap) { 708 addedList.push(updatedMap[url]); 709 } 710 711 if (removedUrls.length > 0) 712 this.fileList_.metadataCache_.clearByUrl(removedUrls, '*'); 713 714 this.prefetchMetadata(updatedList, true, function() { 715 this.onNewEntries_(true, addedList); 716 this.onScanFinished_(); 717 this.onScanCompleted_(); 718 }.bind(this)); 719}; 720 721/** 722 * Cancels the running scan. 723 */ 724DirectoryContents.prototype.cancelScan = function() { 725 if (this.scanCancelled_) 726 return; 727 this.scanCancelled_ = true; 728 if (this.scanner_) 729 this.scanner_.cancel(); 730 731 this.onScanFinished_(); 732 733 this.processNewEntriesQueue_.cancel(); 734 cr.dispatchSimpleEvent(this, 'scan-cancelled'); 735}; 736 737/** 738 * Called when the scanning by scanner_ is done, even when the scanning is 739 * succeeded or failed. This is called before completion (or error) callback. 740 * 741 * @private 742 */ 743DirectoryContents.prototype.onScanFinished_ = function() { 744 this.scanner_ = null; 745 746 this.processNewEntriesQueue_.run(function(callback) { 747 // TODO(yoshiki): Here we should fire the update event of changed 748 // items. Currently we have a method this.fileList_.updateIndex() to 749 // fire an event, but this method takes only 1 argument and invokes sort 750 // one by one. It is obviously time wasting. Instead, we call sort 751 // directory. 752 // In future, we should implement a good method like updateIndexes and 753 // use it here. 754 var status = this.fileList_.sortStatus; 755 if (status) 756 this.fileList_.sort(status.field, status.direction); 757 758 callback(); 759 }.bind(this)); 760}; 761 762/** 763 * Called when the scanning by scanner_ is succeeded. 764 * @private 765 */ 766DirectoryContents.prototype.onScanCompleted_ = function() { 767 if (this.scanCancelled_) 768 return; 769 770 this.processNewEntriesQueue_.run(function(callback) { 771 // Call callback first, so isScanning() returns false in the event handlers. 772 callback(); 773 774 cr.dispatchSimpleEvent(this, 'scan-completed'); 775 }.bind(this)); 776}; 777 778/** 779 * Called in case scan has failed. Should send the event. 780 * @private 781 */ 782DirectoryContents.prototype.onScanError_ = function() { 783 if (this.scanCancelled_) 784 return; 785 786 this.processNewEntriesQueue_.run(function(callback) { 787 // Call callback first, so isScanning() returns false in the event handlers. 788 callback(); 789 cr.dispatchSimpleEvent(this, 'scan-failed'); 790 }.bind(this)); 791}; 792 793/** 794 * Called when some chunk of entries are read by scanner. 795 * 796 * @param {boolean} refresh True to refresh metadata, or false to use cached 797 * one. 798 * @param {Array.<Entry>} entries The list of the scanned entries. 799 * @private 800 */ 801DirectoryContents.prototype.onNewEntries_ = function(refresh, entries) { 802 if (this.scanCancelled_) 803 return; 804 805 var entriesFiltered = [].filter.call( 806 entries, this.context_.fileFilter.filter.bind(this.context_.fileFilter)); 807 808 // Caching URL to reduce a number of calls of toURL in sort. 809 // This is a temporary solution. We need to fix a root cause of slow toURL. 810 // See crbug.com/370908 for detail. 811 entriesFiltered.forEach(function(entry) { entry.cachedUrl = entry.toURL(); }); 812 813 if (entriesFiltered.length === 0) 814 return; 815 816 // Enlarge the cache size into the new filelist size. 817 var newListSize = this.fileList_.length + entriesFiltered.length; 818 this.makeSpaceInMetadataCache_(newListSize); 819 820 this.processNewEntriesQueue_.run(function(callbackOuter) { 821 var finish = function() { 822 if (!this.scanCancelled_) { 823 // Update the filelist without waiting the metadata. 824 this.fileList_.push.apply(this.fileList_, entriesFiltered); 825 cr.dispatchSimpleEvent(this, 'scan-updated'); 826 } 827 callbackOuter(); 828 }.bind(this); 829 // Because the prefetchMetadata can be slow, throttling by splitting entries 830 // into smaller chunks to reduce UI latency. 831 // TODO(hidehiko,mtomasz): This should be handled in MetadataCache. 832 var MAX_CHUNK_SIZE = 25; 833 var prefetchMetadataQueue = new AsyncUtil.ConcurrentQueue(4); 834 for (var i = 0; i < entriesFiltered.length; i += MAX_CHUNK_SIZE) { 835 if (prefetchMetadataQueue.isCancelled()) 836 break; 837 838 var chunk = entriesFiltered.slice(i, i + MAX_CHUNK_SIZE); 839 prefetchMetadataQueue.run(function(chunk, callbackInner) { 840 this.prefetchMetadata(chunk, refresh, function() { 841 if (!prefetchMetadataQueue.isCancelled()) { 842 if (this.scanCancelled_) 843 prefetchMetadataQueue.cancel(); 844 } 845 846 // Checks if this is the last task. 847 if (prefetchMetadataQueue.getWaitingTasksCount() === 0 && 848 prefetchMetadataQueue.getRunningTasksCount() === 1) { 849 // |callbackOuter| in |finish| must be called before 850 // |callbackInner|, to prevent double-calling. 851 finish(); 852 } 853 854 callbackInner(); 855 }.bind(this)); 856 }.bind(this, chunk)); 857 } 858 }.bind(this)); 859}; 860 861/** 862 * @param {Array.<Entry>} entries Files. 863 * @param {boolean} refresh True to refresh metadata, or false to use cached 864 * one. 865 * @param {function(Object)} callback Callback on done. 866 */ 867DirectoryContents.prototype.prefetchMetadata = 868 function(entries, refresh, callback) { 869 var TYPES = 'filesystem|external'; 870 if (refresh) 871 this.context_.metadataCache.getLatest(entries, TYPES, callback); 872 else 873 this.context_.metadataCache.get(entries, TYPES, callback); 874}; 875 876/** 877 * Creates a DirectoryContents instance to show entries in a directory. 878 * 879 * @param {FileListContext} context File list context. 880 * @param {DirectoryEntry} directoryEntry The current directory entry. 881 * @return {DirectoryContents} Created DirectoryContents instance. 882 */ 883DirectoryContents.createForDirectory = function(context, directoryEntry) { 884 return new DirectoryContents( 885 context, 886 false, // Non search. 887 directoryEntry, 888 function() { 889 return new DirectoryContentScanner(directoryEntry); 890 }); 891}; 892 893/** 894 * Creates a DirectoryContents instance to show the result of the search on 895 * Drive File System. 896 * 897 * @param {FileListContext} context File list context. 898 * @param {DirectoryEntry} directoryEntry The current directory entry. 899 * @param {string} query Search query. 900 * @return {DirectoryContents} Created DirectoryContents instance. 901 */ 902DirectoryContents.createForDriveSearch = function( 903 context, directoryEntry, query) { 904 return new DirectoryContents( 905 context, 906 true, // Search. 907 directoryEntry, 908 function() { 909 return new DriveSearchContentScanner(query); 910 }); 911}; 912 913/** 914 * Creates a DirectoryContents instance to show the result of the search on 915 * Local File System. 916 * 917 * @param {FileListContext} context File list context. 918 * @param {DirectoryEntry} directoryEntry The current directory entry. 919 * @param {string} query Search query. 920 * @return {DirectoryContents} Created DirectoryContents instance. 921 */ 922DirectoryContents.createForLocalSearch = function( 923 context, directoryEntry, query) { 924 return new DirectoryContents( 925 context, 926 true, // Search. 927 directoryEntry, 928 function() { 929 return new LocalSearchContentScanner(directoryEntry, query); 930 }); 931}; 932 933/** 934 * Creates a DirectoryContents instance to show the result of metadata search 935 * on Drive File System. 936 * 937 * @param {FileListContext} context File list context. 938 * @param {DirectoryEntry} fakeDirectoryEntry Fake directory entry representing 939 * the set of result entries. This serves as a top directory for the 940 * search. 941 * @param {DriveMetadataSearchContentScanner.SearchType} searchType The type of 942 * the search. The scanner will restricts the entries based on the given 943 * type. 944 * @return {DirectoryContents} Created DirectoryContents instance. 945 */ 946DirectoryContents.createForDriveMetadataSearch = function( 947 context, fakeDirectoryEntry, searchType) { 948 return new DirectoryContents( 949 context, 950 true, // Search 951 fakeDirectoryEntry, 952 function() { 953 return new DriveMetadataSearchContentScanner(searchType); 954 }); 955}; 956