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