| OLD | NEW |
| (Empty) |
| 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 // If directory files changes too often, don't rescan directory more than once | |
| 8 // per specified interval | |
| 9 var SIMULTANEOUS_RESCAN_INTERVAL = 1000; | |
| 10 // Used for operations that require almost instant rescan. | |
| 11 var SHORT_RESCAN_INTERVAL = 100; | |
| 12 | |
| 13 /** | |
| 14 * Data model of the file manager. | |
| 15 * | |
| 16 * @param {boolean} singleSelection True if only one file could be selected | |
| 17 * at the time. | |
| 18 * @param {FileFilter} fileFilter Instance of FileFilter. | |
| 19 * @param {FileWatcher} fileWatcher Instance of FileWatcher. | |
| 20 * @param {MetadataCache} metadataCache The metadata cache service. | |
| 21 * @param {VolumeManagerWrapper} volumeManager The volume manager. | |
| 22 * @param {boolean} showSpecialSearchRoots True if special-search roots are | |
| 23 * available. They should be hidden for the dialogs to save files. | |
| 24 * @constructor | |
| 25 */ | |
| 26 function DirectoryModel(singleSelection, fileFilter, fileWatcher, | |
| 27 metadataCache, volumeManager, | |
| 28 showSpecialSearchRoots) { | |
| 29 this.fileListSelection_ = singleSelection ? | |
| 30 new cr.ui.ListSingleSelectionModel() : new cr.ui.ListSelectionModel(); | |
| 31 | |
| 32 this.runningScan_ = null; | |
| 33 this.pendingScan_ = null; | |
| 34 this.rescanTime_ = null; | |
| 35 this.scanFailures_ = 0; | |
| 36 this.showSpecialSearchRoots_ = showSpecialSearchRoots; | |
| 37 | |
| 38 this.fileFilter_ = fileFilter; | |
| 39 this.fileFilter_.addEventListener('changed', | |
| 40 this.onFilterChanged_.bind(this)); | |
| 41 | |
| 42 this.currentFileListContext_ = new FileListContext( | |
| 43 fileFilter, metadataCache); | |
| 44 this.currentDirContents_ = | |
| 45 DirectoryContents.createForDirectory(this.currentFileListContext_, null); | |
| 46 | |
| 47 this.volumeManager_ = volumeManager; | |
| 48 this.volumeManager_.volumeInfoList.addEventListener( | |
| 49 'splice', this.onVolumeInfoListUpdated_.bind(this)); | |
| 50 | |
| 51 this.fileWatcher_ = fileWatcher; | |
| 52 this.fileWatcher_.addEventListener( | |
| 53 'watcher-directory-changed', | |
| 54 this.onWatcherDirectoryChanged_.bind(this)); | |
| 55 } | |
| 56 | |
| 57 /** | |
| 58 * Fake entry to be used in currentDirEntry_ when current directory is | |
| 59 * unmounted DRIVE. TODO(haruki): Support "drive/root" and "drive/other". | |
| 60 * @type {Object} | |
| 61 * @const | |
| 62 * @private | |
| 63 */ | |
| 64 DirectoryModel.fakeDriveEntry_ = { | |
| 65 fullPath: RootDirectory.DRIVE + '/' + DriveSubRootDirectory.ROOT, | |
| 66 isDirectory: true | |
| 67 }; | |
| 68 | |
| 69 /** | |
| 70 * Fake entry representing a psuedo directory, which contains Drive files | |
| 71 * available offline. This entry works as a trigger to start a search for | |
| 72 * offline files. | |
| 73 * @type {Object} | |
| 74 * @const | |
| 75 * @private | |
| 76 */ | |
| 77 DirectoryModel.fakeDriveOfflineEntry_ = { | |
| 78 fullPath: RootDirectory.DRIVE_OFFLINE, | |
| 79 isDirectory: true | |
| 80 }; | |
| 81 | |
| 82 /** | |
| 83 * Fake entry representing a pseudo directory, which contains shared-with-me | |
| 84 * Drive files. This entry works as a trigger to start a search for | |
| 85 * shared-with-me files. | |
| 86 * @type {Object} | |
| 87 * @const | |
| 88 * @private | |
| 89 */ | |
| 90 DirectoryModel.fakeDriveSharedWithMeEntry_ = { | |
| 91 fullPath: RootDirectory.DRIVE_SHARED_WITH_ME, | |
| 92 isDirectory: true | |
| 93 }; | |
| 94 | |
| 95 /** | |
| 96 * Fake entry representing a pseudo directory, which contains Drive files | |
| 97 * accessed recently. This entry works as a trigger to start a metadata search | |
| 98 * implemented as DirectoryContentsDriveRecent. | |
| 99 * DirectoryModel is responsible to start the search when the UI tries to open | |
| 100 * this fake entry (e.g. changeDirectory()). | |
| 101 * @type {Object} | |
| 102 * @const | |
| 103 * @private | |
| 104 */ | |
| 105 DirectoryModel.fakeDriveRecentEntry_ = { | |
| 106 fullPath: RootDirectory.DRIVE_RECENT, | |
| 107 isDirectory: true | |
| 108 }; | |
| 109 | |
| 110 /** | |
| 111 * List of fake entries for special searches. | |
| 112 * | |
| 113 * @type {Array.<Object>} | |
| 114 * @const | |
| 115 */ | |
| 116 DirectoryModel.FAKE_DRIVE_SPECIAL_SEARCH_ENTRIES = [ | |
| 117 DirectoryModel.fakeDriveSharedWithMeEntry_, | |
| 118 DirectoryModel.fakeDriveRecentEntry_, | |
| 119 DirectoryModel.fakeDriveOfflineEntry_ | |
| 120 ]; | |
| 121 | |
| 122 /** | |
| 123 * DirectoryModel extends cr.EventTarget. | |
| 124 */ | |
| 125 DirectoryModel.prototype.__proto__ = cr.EventTarget.prototype; | |
| 126 | |
| 127 /** | |
| 128 * Disposes the directory model by removing file watchers. | |
| 129 */ | |
| 130 DirectoryModel.prototype.dispose = function() { | |
| 131 this.fileWatcher_.dispose(); | |
| 132 }; | |
| 133 | |
| 134 /** | |
| 135 * @return {cr.ui.ArrayDataModel} Files in the current directory. | |
| 136 */ | |
| 137 DirectoryModel.prototype.getFileList = function() { | |
| 138 return this.currentFileListContext_.fileList; | |
| 139 }; | |
| 140 | |
| 141 /** | |
| 142 * Sort the file list. | |
| 143 * @param {string} sortField Sort field. | |
| 144 * @param {string} sortDirection "asc" or "desc". | |
| 145 */ | |
| 146 DirectoryModel.prototype.sortFileList = function(sortField, sortDirection) { | |
| 147 this.getFileList().sort(sortField, sortDirection); | |
| 148 }; | |
| 149 | |
| 150 /** | |
| 151 * @return {cr.ui.ListSelectionModel|cr.ui.ListSingleSelectionModel} Selection | |
| 152 * in the fileList. | |
| 153 */ | |
| 154 DirectoryModel.prototype.getFileListSelection = function() { | |
| 155 return this.fileListSelection_; | |
| 156 }; | |
| 157 | |
| 158 /** | |
| 159 * @return {RootType} Root type of current root. | |
| 160 */ | |
| 161 DirectoryModel.prototype.getCurrentRootType = function() { | |
| 162 var entry = this.currentDirContents_.getDirectoryEntry(); | |
| 163 return PathUtil.getRootType(entry ? entry.fullPath : ''); | |
| 164 }; | |
| 165 | |
| 166 /** | |
| 167 * @return {string} Root path. | |
| 168 */ | |
| 169 DirectoryModel.prototype.getCurrentRootPath = function() { | |
| 170 var entry = this.currentDirContents_.getDirectoryEntry(); | |
| 171 return entry ? PathUtil.getRootPath(entry.fullPath) : ''; | |
| 172 }; | |
| 173 | |
| 174 /** | |
| 175 * @return {string} Filesystem URL representing the mountpoint for the current | |
| 176 * contents. | |
| 177 */ | |
| 178 DirectoryModel.prototype.getCurrentMountPointUrl = function() { | |
| 179 var rootPath = this.getCurrentRootPath(); | |
| 180 // Special search roots are just showing a search results from DRIVE. | |
| 181 if (PathUtil.getRootType(rootPath) == RootType.DRIVE || | |
| 182 PathUtil.isSpecialSearchRoot(rootPath)) | |
| 183 return util.makeFilesystemUrl(RootDirectory.DRIVE); | |
| 184 | |
| 185 return util.makeFilesystemUrl(rootPath); | |
| 186 }; | |
| 187 | |
| 188 /** | |
| 189 * @return {boolean} on True if offline. | |
| 190 */ | |
| 191 DirectoryModel.prototype.isDriveOffline = function() { | |
| 192 var connection = this.volumeManager_.getDriveConnectionState(); | |
| 193 return connection.type == util.DriveConnectionType.OFFLINE; | |
| 194 }; | |
| 195 | |
| 196 /** | |
| 197 * TODO(haruki): This actually checks the current root. Fix the method name and | |
| 198 * related code. | |
| 199 * @return {boolean} True if the root for the current directory is read only. | |
| 200 */ | |
| 201 DirectoryModel.prototype.isReadOnly = function() { | |
| 202 return this.isPathReadOnly(this.getCurrentRootPath()); | |
| 203 }; | |
| 204 | |
| 205 /** | |
| 206 * @return {boolean} True if the a scan is active. | |
| 207 */ | |
| 208 DirectoryModel.prototype.isScanning = function() { | |
| 209 return this.currentDirContents_.isScanning(); | |
| 210 }; | |
| 211 | |
| 212 /** | |
| 213 * @return {boolean} True if search is in progress. | |
| 214 */ | |
| 215 DirectoryModel.prototype.isSearching = function() { | |
| 216 return this.currentDirContents_.isSearch(); | |
| 217 }; | |
| 218 | |
| 219 /** | |
| 220 * @param {string} path Path to check. | |
| 221 * @return {boolean} True if the |path| is read only. | |
| 222 */ | |
| 223 DirectoryModel.prototype.isPathReadOnly = function(path) { | |
| 224 // TODO(hidehiko): Migrate this into VolumeInfo. | |
| 225 switch (PathUtil.getRootType(path)) { | |
| 226 case RootType.REMOVABLE: | |
| 227 var volumeInfo = this.volumeManager_.getVolumeInfo( | |
| 228 PathUtil.getRootPath(path)); | |
| 229 // Returns true if the volume is actually read only, or if an error | |
| 230 // is found during the mounting. | |
| 231 // TODO(hidehiko): Remove "error" check here, by removing error'ed volume | |
| 232 // info from VolumeManager. | |
| 233 return volumeInfo && (volumeInfo.isReadOnly || !!volumeInfo.error); | |
| 234 case RootType.ARCHIVE: | |
| 235 return true; | |
| 236 case RootType.DOWNLOADS: | |
| 237 return false; | |
| 238 case RootType.DRIVE: | |
| 239 // TODO(haruki): Maybe add DRIVE_OFFLINE as well to allow renaming in the | |
| 240 // offline tab. | |
| 241 return this.isDriveOffline(); | |
| 242 default: | |
| 243 return true; | |
| 244 } | |
| 245 }; | |
| 246 | |
| 247 /** | |
| 248 * Updates the selection by using the updateFunc and publish the change event. | |
| 249 * If updateFunc returns true, it force to dispatch the change event even if the | |
| 250 * selection index is not changed. | |
| 251 * | |
| 252 * @param {cr.ui.ListSelectionModel|cr.ui.ListSingleSelectionModel} selection | |
| 253 * Selection to be updated. | |
| 254 * @param {function(): boolean} updateFunc Function updating the selection. | |
| 255 * @private | |
| 256 */ | |
| 257 DirectoryModel.prototype.updateSelectionAndPublishEvent_ = | |
| 258 function(selection, updateFunc) { | |
| 259 // Begin change. | |
| 260 selection.beginChange(); | |
| 261 | |
| 262 // If dispatchNeeded is true, we should ensure the change event is | |
| 263 // dispatched. | |
| 264 var dispatchNeeded = updateFunc(); | |
| 265 | |
| 266 // Check if the change event is dispatched in the endChange function | |
| 267 // or not. | |
| 268 var eventDispatched = function() { dispatchNeeded = false; }; | |
| 269 selection.addEventListener('change', eventDispatched); | |
| 270 selection.endChange(); | |
| 271 selection.removeEventListener('change', eventDispatched); | |
| 272 | |
| 273 // If the change event have been already dispatched, dispatchNeeded is false. | |
| 274 if (dispatchNeeded) { | |
| 275 var event = new Event('change'); | |
| 276 // The selection status (selected or not) is not changed because | |
| 277 // this event is caused by the change of selected item. | |
| 278 event.changes = []; | |
| 279 selection.dispatchEvent(event); | |
| 280 } | |
| 281 }; | |
| 282 | |
| 283 /** | |
| 284 * Invoked when a change in the directory is detected by the watcher. | |
| 285 * @private | |
| 286 */ | |
| 287 DirectoryModel.prototype.onWatcherDirectoryChanged_ = function() { | |
| 288 this.rescanSoon(); | |
| 289 }; | |
| 290 | |
| 291 /** | |
| 292 * Invoked when filters are changed. | |
| 293 * @private | |
| 294 */ | |
| 295 DirectoryModel.prototype.onFilterChanged_ = function() { | |
| 296 this.rescanSoon(); | |
| 297 }; | |
| 298 | |
| 299 /** | |
| 300 * Returns the filter. | |
| 301 * @return {FileFilter} The file filter. | |
| 302 */ | |
| 303 DirectoryModel.prototype.getFileFilter = function() { | |
| 304 return this.fileFilter_; | |
| 305 }; | |
| 306 | |
| 307 /** | |
| 308 * @return {DirectoryEntry} Current directory. | |
| 309 */ | |
| 310 DirectoryModel.prototype.getCurrentDirEntry = function() { | |
| 311 return this.currentDirContents_.getDirectoryEntry(); | |
| 312 }; | |
| 313 | |
| 314 /** | |
| 315 * @return {string} URL of the current directory. or null if unavailable. | |
| 316 */ | |
| 317 DirectoryModel.prototype.getCurrentDirectoryURL = function() { | |
| 318 var entry = this.currentDirContents_.getDirectoryEntry(); | |
| 319 if (!entry) | |
| 320 return null; | |
| 321 if (entry === DirectoryModel.fakeDriveOfflineEntry_) | |
| 322 return util.makeFilesystemUrl(entry.fullPath); | |
| 323 return entry.toURL(); | |
| 324 }; | |
| 325 | |
| 326 /** | |
| 327 * @return {string} Path for the current directory, or empty string if the | |
| 328 * current directory is not yet set. | |
| 329 */ | |
| 330 DirectoryModel.prototype.getCurrentDirPath = function() { | |
| 331 var entry = this.currentDirContents_.getDirectoryEntry(); | |
| 332 return entry ? entry.fullPath : ''; | |
| 333 }; | |
| 334 | |
| 335 /** | |
| 336 * @return {Array.<string>} File paths of selected files. | |
| 337 * @private | |
| 338 */ | |
| 339 DirectoryModel.prototype.getSelectedPaths_ = function() { | |
| 340 var indexes = this.fileListSelection_.selectedIndexes; | |
| 341 var fileList = this.getFileList(); | |
| 342 if (fileList) { | |
| 343 return indexes.map(function(i) { | |
| 344 return fileList.item(i).fullPath; | |
| 345 }); | |
| 346 } | |
| 347 return []; | |
| 348 }; | |
| 349 | |
| 350 /** | |
| 351 * @param {Array.<string>} value List of file paths of selected files. | |
| 352 * @private | |
| 353 */ | |
| 354 DirectoryModel.prototype.setSelectedPaths_ = function(value) { | |
| 355 var indexes = []; | |
| 356 var fileList = this.getFileList(); | |
| 357 | |
| 358 var safeKey = function(key) { | |
| 359 // The transformation must: | |
| 360 // 1. Never generate a reserved name ('__proto__') | |
| 361 // 2. Keep different keys different. | |
| 362 return '#' + key; | |
| 363 }; | |
| 364 | |
| 365 var hash = {}; | |
| 366 | |
| 367 for (var i = 0; i < value.length; i++) | |
| 368 hash[safeKey(value[i])] = 1; | |
| 369 | |
| 370 for (var i = 0; i < fileList.length; i++) { | |
| 371 if (hash.hasOwnProperty(safeKey(fileList.item(i).fullPath))) | |
| 372 indexes.push(i); | |
| 373 } | |
| 374 this.fileListSelection_.selectedIndexes = indexes; | |
| 375 }; | |
| 376 | |
| 377 /** | |
| 378 * @return {string} Lead item file path. | |
| 379 * @private | |
| 380 */ | |
| 381 DirectoryModel.prototype.getLeadPath_ = function() { | |
| 382 var index = this.fileListSelection_.leadIndex; | |
| 383 return index >= 0 && this.getFileList().item(index).fullPath; | |
| 384 }; | |
| 385 | |
| 386 /** | |
| 387 * @param {string} value The name of new lead index. | |
| 388 * @private | |
| 389 */ | |
| 390 DirectoryModel.prototype.setLeadPath_ = function(value) { | |
| 391 var fileList = this.getFileList(); | |
| 392 for (var i = 0; i < fileList.length; i++) { | |
| 393 if (fileList.item(i).fullPath === value) { | |
| 394 this.fileListSelection_.leadIndex = i; | |
| 395 return; | |
| 396 } | |
| 397 } | |
| 398 }; | |
| 399 | |
| 400 /** | |
| 401 * Schedule rescan with short delay. | |
| 402 */ | |
| 403 DirectoryModel.prototype.rescanSoon = function() { | |
| 404 this.scheduleRescan(SHORT_RESCAN_INTERVAL); | |
| 405 }; | |
| 406 | |
| 407 /** | |
| 408 * Schedule rescan with delay. Designed to handle directory change | |
| 409 * notification. | |
| 410 */ | |
| 411 DirectoryModel.prototype.rescanLater = function() { | |
| 412 this.scheduleRescan(SIMULTANEOUS_RESCAN_INTERVAL); | |
| 413 }; | |
| 414 | |
| 415 /** | |
| 416 * Schedule rescan with delay. If another rescan has been scheduled does | |
| 417 * nothing. File operation may cause a few notifications what should cause | |
| 418 * a single refresh. | |
| 419 * @param {number} delay Delay in ms after which the rescan will be performed. | |
| 420 */ | |
| 421 DirectoryModel.prototype.scheduleRescan = function(delay) { | |
| 422 if (this.rescanTime_) { | |
| 423 if (this.rescanTime_ <= Date.now() + delay) | |
| 424 return; | |
| 425 clearTimeout(this.rescanTimeoutId_); | |
| 426 } | |
| 427 | |
| 428 this.rescanTime_ = Date.now() + delay; | |
| 429 this.rescanTimeoutId_ = setTimeout(this.rescan.bind(this), delay); | |
| 430 }; | |
| 431 | |
| 432 /** | |
| 433 * Cancel a rescan on timeout if it is scheduled. | |
| 434 * @private | |
| 435 */ | |
| 436 DirectoryModel.prototype.clearRescanTimeout_ = function() { | |
| 437 this.rescanTime_ = null; | |
| 438 if (this.rescanTimeoutId_) { | |
| 439 clearTimeout(this.rescanTimeoutId_); | |
| 440 this.rescanTimeoutId_ = null; | |
| 441 } | |
| 442 }; | |
| 443 | |
| 444 /** | |
| 445 * Rescan current directory. May be called indirectly through rescanLater or | |
| 446 * directly in order to reflect user action. Will first cache all the directory | |
| 447 * contents in an array, then seamlessly substitute the fileList contents, | |
| 448 * preserving the select element etc. | |
| 449 * | |
| 450 * This should be to scan the contents of current directory (or search). | |
| 451 */ | |
| 452 DirectoryModel.prototype.rescan = function() { | |
| 453 this.clearRescanTimeout_(); | |
| 454 if (this.runningScan_) { | |
| 455 this.pendingRescan_ = true; | |
| 456 return; | |
| 457 } | |
| 458 | |
| 459 var dirContents = this.currentDirContents_.clone(); | |
| 460 dirContents.setFileList([]); | |
| 461 | |
| 462 var successCallback = (function() { | |
| 463 this.replaceDirectoryContents_(dirContents); | |
| 464 cr.dispatchSimpleEvent(this, 'rescan-completed'); | |
| 465 }).bind(this); | |
| 466 | |
| 467 this.scan_(dirContents, | |
| 468 successCallback, function() {}, function() {}, function() {}); | |
| 469 }; | |
| 470 | |
| 471 /** | |
| 472 * Run scan on the current DirectoryContents. The active fileList is cleared and | |
| 473 * the entries are added directly. | |
| 474 * | |
| 475 * This should be used when changing directory or initiating a new search. | |
| 476 * | |
| 477 * @param {DirectoryContentes} newDirContents New DirectoryContents instance to | |
| 478 * replace currentDirContents_. | |
| 479 * @param {function()=} opt_callback Called on success. | |
| 480 * @private | |
| 481 */ | |
| 482 DirectoryModel.prototype.clearAndScan_ = function(newDirContents, | |
| 483 opt_callback) { | |
| 484 if (this.currentDirContents_.isScanning()) | |
| 485 this.currentDirContents_.cancelScan(); | |
| 486 this.currentDirContents_ = newDirContents; | |
| 487 this.clearRescanTimeout_(); | |
| 488 | |
| 489 if (this.pendingScan_) | |
| 490 this.pendingScan_ = false; | |
| 491 | |
| 492 if (this.runningScan_) { | |
| 493 if (this.runningScan_.isScanning()) | |
| 494 this.runningScan_.cancelScan(); | |
| 495 this.runningScan_ = null; | |
| 496 } | |
| 497 | |
| 498 var onDone = function() { | |
| 499 cr.dispatchSimpleEvent(this, 'scan-completed'); | |
| 500 if (opt_callback) | |
| 501 opt_callback(); | |
| 502 }.bind(this); | |
| 503 | |
| 504 var onFailed = function() { | |
| 505 cr.dispatchSimpleEvent(this, 'scan-failed'); | |
| 506 }.bind(this); | |
| 507 | |
| 508 var onUpdated = function() { | |
| 509 cr.dispatchSimpleEvent(this, 'scan-updated'); | |
| 510 }.bind(this); | |
| 511 | |
| 512 var onCancelled = function() { | |
| 513 cr.dispatchSimpleEvent(this, 'scan-cancelled'); | |
| 514 }.bind(this); | |
| 515 | |
| 516 // Clear the table, and start scanning. | |
| 517 cr.dispatchSimpleEvent(this, 'scan-started'); | |
| 518 var fileList = this.getFileList(); | |
| 519 fileList.splice(0, fileList.length); | |
| 520 this.scan_(this.currentDirContents_, | |
| 521 onDone, onFailed, onUpdated, onCancelled); | |
| 522 }; | |
| 523 | |
| 524 /** | |
| 525 * Perform a directory contents scan. Should be called only from rescan() and | |
| 526 * clearAndScan_(). | |
| 527 * | |
| 528 * @param {DirectoryContents} dirContents DirectoryContents instance on which | |
| 529 * the scan will be run. | |
| 530 * @param {function()} successCallback Callback on success. | |
| 531 * @param {function()} failureCallback Callback on failure. | |
| 532 * @param {function()} updatedCallback Callback on update. Only on the last | |
| 533 * update, {@code successCallback} is called instead of this. | |
| 534 * @param {function()} cancelledCallback Callback on cancel. | |
| 535 * @private | |
| 536 */ | |
| 537 DirectoryModel.prototype.scan_ = function( | |
| 538 dirContents, | |
| 539 successCallback, failureCallback, updatedCallback, cancelledCallback) { | |
| 540 var self = this; | |
| 541 | |
| 542 /** | |
| 543 * Runs pending scan if there is one. | |
| 544 * | |
| 545 * @return {boolean} Did pending scan exist. | |
| 546 */ | |
| 547 var maybeRunPendingRescan = function() { | |
| 548 if (self.pendingRescan_) { | |
| 549 self.rescanSoon(); | |
| 550 self.pendingRescan_ = false; | |
| 551 return true; | |
| 552 } | |
| 553 return false; | |
| 554 }; | |
| 555 | |
| 556 var onSuccess = function() { | |
| 557 self.runningScan_ = null; | |
| 558 successCallback(); | |
| 559 self.scanFailures_ = 0; | |
| 560 maybeRunPendingRescan(); | |
| 561 }; | |
| 562 | |
| 563 var onFailure = function() { | |
| 564 self.runningScan_ = null; | |
| 565 self.scanFailures_++; | |
| 566 failureCallback(); | |
| 567 | |
| 568 if (maybeRunPendingRescan()) | |
| 569 return; | |
| 570 | |
| 571 if (self.scanFailures_ <= 1) | |
| 572 self.rescanLater(); | |
| 573 }; | |
| 574 | |
| 575 this.runningScan_ = dirContents; | |
| 576 | |
| 577 dirContents.addEventListener('scan-completed', onSuccess); | |
| 578 dirContents.addEventListener('scan-updated', updatedCallback); | |
| 579 dirContents.addEventListener('scan-failed', onFailure); | |
| 580 dirContents.addEventListener('scan-cancelled', cancelledCallback); | |
| 581 dirContents.scan(); | |
| 582 }; | |
| 583 | |
| 584 /** | |
| 585 * @param {DirectoryContents} dirContents DirectoryContents instance. | |
| 586 * @private | |
| 587 */ | |
| 588 DirectoryModel.prototype.replaceDirectoryContents_ = function(dirContents) { | |
| 589 cr.dispatchSimpleEvent(this, 'begin-update-files'); | |
| 590 this.updateSelectionAndPublishEvent_(this.fileListSelection_, function() { | |
| 591 var selectedPaths = this.getSelectedPaths_(); | |
| 592 var selectedIndices = this.fileListSelection_.selectedIndexes; | |
| 593 | |
| 594 // Restore leadIndex in case leadName no longer exists. | |
| 595 var leadIndex = this.fileListSelection_.leadIndex; | |
| 596 var leadPath = this.getLeadPath_(); | |
| 597 | |
| 598 this.currentDirContents_ = dirContents; | |
| 599 dirContents.replaceContextFileList(); | |
| 600 | |
| 601 this.setSelectedPaths_(selectedPaths); | |
| 602 this.fileListSelection_.leadIndex = leadIndex; | |
| 603 this.setLeadPath_(leadPath); | |
| 604 | |
| 605 // If nothing is selected after update, then select file next to the | |
| 606 // latest selection | |
| 607 var forceChangeEvent = false; | |
| 608 if (this.fileListSelection_.selectedIndexes.length == 0 && | |
| 609 selectedIndices.length != 0) { | |
| 610 var maxIdx = Math.max.apply(null, selectedIndices); | |
| 611 this.selectIndex(Math.min(maxIdx - selectedIndices.length + 2, | |
| 612 this.getFileList().length) - 1); | |
| 613 forceChangeEvent = true; | |
| 614 } | |
| 615 return forceChangeEvent; | |
| 616 }.bind(this)); | |
| 617 | |
| 618 cr.dispatchSimpleEvent(this, 'end-update-files'); | |
| 619 }; | |
| 620 | |
| 621 /** | |
| 622 * Callback when an entry is changed. | |
| 623 * @param {util.EntryChangedKind} kind How the entry is changed. | |
| 624 * @param {Entry} entry The changed entry. | |
| 625 */ | |
| 626 DirectoryModel.prototype.onEntryChanged = function(kind, entry) { | |
| 627 // TODO(hidehiko): We should update directory model even the search result | |
| 628 // is shown. | |
| 629 var rootType = this.getCurrentRootType(); | |
| 630 if ((rootType === RootType.DRIVE || | |
| 631 rootType === RootType.DRIVE_SHARED_WITH_ME || | |
| 632 rootType === RootType.DRIVE_RECENT || | |
| 633 rootType === RootType.DRIVE_OFFLINE) && | |
| 634 this.isSearching()) | |
| 635 return; | |
| 636 | |
| 637 if (kind == util.EntryChangedKind.CREATED) { | |
| 638 entry.getParent(function(parentEntry) { | |
| 639 if (this.getCurrentDirEntry().fullPath != parentEntry.fullPath) { | |
| 640 // Do nothing if current directory changed during async operations. | |
| 641 return; | |
| 642 } | |
| 643 this.currentDirContents_.prefetchMetadata([entry], function() { | |
| 644 if (this.getCurrentDirEntry().fullPath != parentEntry.fullPath) { | |
| 645 // Do nothing if current directory changed during async operations. | |
| 646 return; | |
| 647 } | |
| 648 | |
| 649 var index = this.findIndexByEntry_(entry); | |
| 650 if (index >= 0) | |
| 651 this.getFileList().splice(index, 1, entry); | |
| 652 else | |
| 653 this.getFileList().push(entry); | |
| 654 }.bind(this)); | |
| 655 }.bind(this)); | |
| 656 } else { | |
| 657 // This is the delete event. | |
| 658 var index = this.findIndexByEntry_(entry); | |
| 659 if (index >= 0) | |
| 660 this.getFileList().splice(index, 1); | |
| 661 } | |
| 662 }; | |
| 663 | |
| 664 /** | |
| 665 * @param {Entry} entry The entry to be searched. | |
| 666 * @return {number} The index in the fileList, or -1 if not found. | |
| 667 * @private | |
| 668 */ | |
| 669 DirectoryModel.prototype.findIndexByEntry_ = function(entry) { | |
| 670 var fileList = this.getFileList(); | |
| 671 for (var i = 0; i < fileList.length; i++) { | |
| 672 if (util.isSameEntry(fileList.item(i), entry)) | |
| 673 return i; | |
| 674 } | |
| 675 return -1; | |
| 676 }; | |
| 677 | |
| 678 /** | |
| 679 * Called when rename is done successfully. | |
| 680 * Note: conceptually, DirectoryModel should work without this, because entries | |
| 681 * can be renamed by other systems anytime and Files.app should reflect it | |
| 682 * correctly. | |
| 683 * TODO(hidehiko): investigate more background, and remove this if possible. | |
| 684 * | |
| 685 * @param {Entry} oldEntry The old entry. | |
| 686 * @param {Entry} newEntry The new entry. | |
| 687 * @param {function()} opt_callback Called on completion. | |
| 688 */ | |
| 689 DirectoryModel.prototype.onRenameEntry = function( | |
| 690 oldEntry, newEntry, opt_callback) { | |
| 691 this.currentDirContents_.prefetchMetadata([newEntry], function() { | |
| 692 // If the current directory is the old entry, then quietly change to the | |
| 693 // new one. | |
| 694 if (util.isSameEntry(oldEntry, this.getCurrentDirEntry())) | |
| 695 this.changeDirectory(newEntry.fullPath); | |
| 696 | |
| 697 // Look for the old entry. | |
| 698 // If the entry doesn't exist in the list, it has been updated from | |
| 699 // outside (probably by directory rescan). | |
| 700 var index = this.findIndexByEntry_(oldEntry); | |
| 701 if (index >= 0) { | |
| 702 // Update the content list and selection status. | |
| 703 var wasSelected = this.fileListSelection_.getIndexSelected(index); | |
| 704 this.updateSelectionAndPublishEvent_(this.fileListSelection_, function() { | |
| 705 this.fileListSelection_.setIndexSelected(index, false); | |
| 706 this.getFileList().splice(index, 1, newEntry); | |
| 707 if (wasSelected) { | |
| 708 // We re-search the index, because splice may trigger sorting so that | |
| 709 // index may be stale. | |
| 710 this.fileListSelection_.setIndexSelected( | |
| 711 this.findIndexByEntry_(newEntry), true); | |
| 712 } | |
| 713 return true; | |
| 714 }.bind(this)); | |
| 715 } | |
| 716 | |
| 717 // Run callback, finally. | |
| 718 if (opt_callback) | |
| 719 opt_callback(); | |
| 720 }.bind(this)); | |
| 721 }; | |
| 722 | |
| 723 /** | |
| 724 * Creates directory and updates the file list. | |
| 725 * | |
| 726 * @param {string} name Directory name. | |
| 727 * @param {function(DirectoryEntry)} successCallback Callback on success. | |
| 728 * @param {function(FileError)} errorCallback Callback on failure. | |
| 729 */ | |
| 730 DirectoryModel.prototype.createDirectory = function(name, successCallback, | |
| 731 errorCallback) { | |
| 732 var entry = this.getCurrentDirEntry(); | |
| 733 if (!entry) { | |
| 734 errorCallback(util.createFileError(FileError.INVALID_MODIFICATION_ERR)); | |
| 735 return; | |
| 736 } | |
| 737 | |
| 738 var onSuccess = function(newEntry) { | |
| 739 // Do not change anything or call the callback if current | |
| 740 // directory changed. | |
| 741 if (entry.fullPath != this.getCurrentDirPath()) | |
| 742 return; | |
| 743 | |
| 744 var existing = this.getFileList().slice().filter( | |
| 745 function(e) {return e.name == name;}); | |
| 746 | |
| 747 if (existing.length) { | |
| 748 this.selectEntry(name); | |
| 749 successCallback(existing[0]); | |
| 750 } else { | |
| 751 this.fileListSelection_.beginChange(); | |
| 752 this.getFileList().splice(0, 0, newEntry); | |
| 753 this.selectEntry(name); | |
| 754 this.fileListSelection_.endChange(); | |
| 755 successCallback(newEntry); | |
| 756 } | |
| 757 }; | |
| 758 | |
| 759 this.currentDirContents_.createDirectory(name, onSuccess.bind(this), | |
| 760 errorCallback); | |
| 761 }; | |
| 762 | |
| 763 /** | |
| 764 * Changes directory. Causes 'directory-change' event. | |
| 765 * | |
| 766 * @param {string} path New current directory path. | |
| 767 * @param {function(FileError)=} opt_errorCallback Executed if the change | |
| 768 * directory failed. | |
| 769 */ | |
| 770 DirectoryModel.prototype.changeDirectory = function(path, opt_errorCallback) { | |
| 771 if (PathUtil.isSpecialSearchRoot(path)) { | |
| 772 this.specialSearch(path, ''); | |
| 773 return; | |
| 774 } | |
| 775 | |
| 776 this.resolveDirectory(path, function(directoryEntry) { | |
| 777 this.changeDirectoryEntry_(directoryEntry); | |
| 778 }.bind(this), function(error) { | |
| 779 console.error('Error changing directory to ' + path + ': ', error); | |
| 780 if (opt_errorCallback) | |
| 781 opt_errorCallback(error); | |
| 782 }); | |
| 783 }; | |
| 784 | |
| 785 /** | |
| 786 * Resolves absolute directory path. Handles Drive stub. If the drive is | |
| 787 * mounting, callbacks will be called after the mount is completed. | |
| 788 * | |
| 789 * @param {string} path Path to the directory. | |
| 790 * @param {function(DirectoryEntry)} successCallback Success callback. | |
| 791 * @param {function(FileError)} errorCallback Error callback. | |
| 792 */ | |
| 793 DirectoryModel.prototype.resolveDirectory = function( | |
| 794 path, successCallback, errorCallback) { | |
| 795 if (PathUtil.getRootType(path) == RootType.DRIVE) { | |
| 796 if (!this.volumeManager_.getVolumeInfo(RootDirectory.DRIVE)) { | |
| 797 errorCallback(util.createFileError(FileError.NOT_FOUND_ERR)); | |
| 798 return; | |
| 799 } | |
| 800 } | |
| 801 | |
| 802 var onError = function(error) { | |
| 803 // Handle the special case, when in offline mode, and there are no cached | |
| 804 // contents on the C++ side. In such case, let's display the stub. | |
| 805 // The INVALID_STATE_ERR error code is returned from the drive filesystem | |
| 806 // in such situation. | |
| 807 // | |
| 808 // TODO(mtomasz, hashimoto): Consider rewriting this logic. | |
| 809 // crbug.com/253464. | |
| 810 if (PathUtil.getRootType(path) == RootType.DRIVE && | |
| 811 error.code == FileError.INVALID_STATE_ERR) { | |
| 812 successCallback(DirectoryModel.fakeDriveEntry_); | |
| 813 return; | |
| 814 } | |
| 815 errorCallback(error); | |
| 816 }.bind(this); | |
| 817 | |
| 818 this.volumeManager_.resolvePath( | |
| 819 path, | |
| 820 function(entry) { | |
| 821 if (entry.isFile) { | |
| 822 onError(util.createFileError(FileError.TYPE_MISMATCH_ERR)); | |
| 823 return; | |
| 824 } | |
| 825 successCallback(entry); | |
| 826 }, | |
| 827 onError); | |
| 828 }; | |
| 829 | |
| 830 /** | |
| 831 * @param {DirectoryEntry} dirEntry The absolute path to the new directory. | |
| 832 * @param {function()=} opt_callback Executed if the directory loads | |
| 833 * successfully. | |
| 834 * @private | |
| 835 */ | |
| 836 DirectoryModel.prototype.changeDirectoryEntrySilent_ = function(dirEntry, | |
| 837 opt_callback) { | |
| 838 var onScanComplete = function() { | |
| 839 if (opt_callback) | |
| 840 opt_callback(); | |
| 841 // For tests that open the dialog to empty directories, everything | |
| 842 // is loaded at this point. | |
| 843 chrome.test.sendMessage('directory-change-complete'); | |
| 844 }; | |
| 845 this.clearAndScan_( | |
| 846 DirectoryContents.createForDirectory(this.currentFileListContext_, | |
| 847 dirEntry), | |
| 848 onScanComplete.bind(this)); | |
| 849 }; | |
| 850 | |
| 851 /** | |
| 852 * Change the current directory to the directory represented by a | |
| 853 * DirectoryEntry. | |
| 854 * | |
| 855 * Dispatches the 'directory-changed' event when the directory is successfully | |
| 856 * changed. | |
| 857 * | |
| 858 * @param {DirectoryEntry} dirEntry The absolute path to the new directory. | |
| 859 * @param {function()=} opt_callback Executed if the directory loads | |
| 860 * successfully. | |
| 861 * @private | |
| 862 */ | |
| 863 DirectoryModel.prototype.changeDirectoryEntry_ = function( | |
| 864 dirEntry, opt_callback) { | |
| 865 this.fileWatcher_.changeWatchedDirectory(dirEntry, function() { | |
| 866 var previous = this.currentDirContents_.getDirectoryEntry(); | |
| 867 this.clearSearch_(); | |
| 868 this.changeDirectoryEntrySilent_(dirEntry, opt_callback); | |
| 869 | |
| 870 var e = new Event('directory-changed'); | |
| 871 e.previousDirEntry = previous; | |
| 872 e.newDirEntry = dirEntry; | |
| 873 this.dispatchEvent(e); | |
| 874 }.bind(this)); | |
| 875 }; | |
| 876 | |
| 877 /** | |
| 878 * Creates an object which could say whether directory has changed while it has | |
| 879 * been active or not. Designed for long operations that should be cancelled | |
| 880 * if the used change current directory. | |
| 881 * @return {Object} Created object. | |
| 882 */ | |
| 883 DirectoryModel.prototype.createDirectoryChangeTracker = function() { | |
| 884 var tracker = { | |
| 885 dm_: this, | |
| 886 active_: false, | |
| 887 hasChanged: false, | |
| 888 | |
| 889 start: function() { | |
| 890 if (!this.active_) { | |
| 891 this.dm_.addEventListener('directory-changed', | |
| 892 this.onDirectoryChange_); | |
| 893 this.active_ = true; | |
| 894 this.hasChanged = false; | |
| 895 } | |
| 896 }, | |
| 897 | |
| 898 stop: function() { | |
| 899 if (this.active_) { | |
| 900 this.dm_.removeEventListener('directory-changed', | |
| 901 this.onDirectoryChange_); | |
| 902 this.active_ = false; | |
| 903 } | |
| 904 }, | |
| 905 | |
| 906 onDirectoryChange_: function(event) { | |
| 907 tracker.stop(); | |
| 908 tracker.hasChanged = true; | |
| 909 } | |
| 910 }; | |
| 911 return tracker; | |
| 912 }; | |
| 913 | |
| 914 /** | |
| 915 * Change the state of the model to reflect the specified path (either a | |
| 916 * file or directory). | |
| 917 * TODO(hidehiko): This logic should be merged with | |
| 918 * FileManager.setupCurrentDirectory_. | |
| 919 * | |
| 920 * @param {string} path The root path to use. | |
| 921 * @param {function(string, string, boolean)=} opt_pathResolveCallback Invoked | |
| 922 * as soon as the path has been resolved, and called with the base and leaf | |
| 923 * portions of the path name, and a flag indicating if the entry exists. | |
| 924 * Will be called even if another directory change happened while setupPath | |
| 925 * was in progress, but will pass |false| as |exist| parameter. | |
| 926 */ | |
| 927 DirectoryModel.prototype.setupPath = function(path, opt_pathResolveCallback) { | |
| 928 var tracker = this.createDirectoryChangeTracker(); | |
| 929 tracker.start(); | |
| 930 | |
| 931 var self = this; | |
| 932 var resolveCallback = function(directoryPath, fileName, exists) { | |
| 933 tracker.stop(); | |
| 934 if (!opt_pathResolveCallback) | |
| 935 return; | |
| 936 opt_pathResolveCallback(directoryPath, fileName, | |
| 937 exists && !tracker.hasChanged); | |
| 938 }; | |
| 939 | |
| 940 var changeDirectoryEntry = function(directoryEntry, opt_callback) { | |
| 941 tracker.stop(); | |
| 942 if (!tracker.hasChanged) | |
| 943 self.changeDirectoryEntry_(directoryEntry, opt_callback); | |
| 944 }; | |
| 945 | |
| 946 var EXISTS = true; | |
| 947 | |
| 948 var changeToDefault = function(leafName) { | |
| 949 var def = PathUtil.DEFAULT_DIRECTORY; | |
| 950 self.resolveDirectory(def, function(directoryEntry) { | |
| 951 resolveCallback(def, leafName, !EXISTS); | |
| 952 changeDirectoryEntry(directoryEntry); | |
| 953 }, function(error) { | |
| 954 console.error('Failed to resolve default directory: ' + def, error); | |
| 955 resolveCallback('/', leafName, !EXISTS); | |
| 956 }); | |
| 957 }; | |
| 958 | |
| 959 var noParentDirectory = function(leafName, error) { | |
| 960 console.warn('Can\'t resolve parent directory: ' + path, error); | |
| 961 changeToDefault(leafName); | |
| 962 }; | |
| 963 | |
| 964 if (DirectoryModel.isSystemDirectory(path)) { | |
| 965 changeToDefault(''); | |
| 966 return; | |
| 967 } | |
| 968 | |
| 969 this.resolveDirectory(path, function(directoryEntry) { | |
| 970 resolveCallback(directoryEntry.fullPath, '', !EXISTS); | |
| 971 changeDirectoryEntry(directoryEntry); | |
| 972 }, function(error) { | |
| 973 // Usually, leaf does not exist, because it's just a suggested file name. | |
| 974 var fileExists = error.code == FileError.TYPE_MISMATCH_ERR; | |
| 975 var nameDelimiter = path.lastIndexOf('/'); | |
| 976 var parentDirectoryPath = path.substr(0, nameDelimiter); | |
| 977 var leafName = path.substr(nameDelimiter + 1); | |
| 978 if (fileExists || error.code == FileError.NOT_FOUND_ERR) { | |
| 979 if (DirectoryModel.isSystemDirectory(parentDirectoryPath)) { | |
| 980 changeToDefault(leafName); | |
| 981 return; | |
| 982 } | |
| 983 self.resolveDirectory(parentDirectoryPath, | |
| 984 function(parentDirectoryEntry) { | |
| 985 var fileName = path.substr(nameDelimiter + 1); | |
| 986 resolveCallback(parentDirectoryEntry.fullPath, fileName, fileExists); | |
| 987 changeDirectoryEntry(parentDirectoryEntry, | |
| 988 function() { | |
| 989 self.selectEntry(fileName); | |
| 990 }); | |
| 991 }, noParentDirectory.bind(null, leafName)); | |
| 992 } else { | |
| 993 // Unexpected errors. | |
| 994 console.error('Directory resolving error: ', error); | |
| 995 changeToDefault(leafName); | |
| 996 } | |
| 997 }); | |
| 998 }; | |
| 999 | |
| 1000 /** | |
| 1001 * @param {string} name Filename. | |
| 1002 */ | |
| 1003 DirectoryModel.prototype.selectEntry = function(name) { | |
| 1004 var fileList = this.getFileList(); | |
| 1005 for (var i = 0; i < fileList.length; i++) { | |
| 1006 if (fileList.item(i).name == name) { | |
| 1007 this.selectIndex(i); | |
| 1008 return; | |
| 1009 } | |
| 1010 } | |
| 1011 }; | |
| 1012 | |
| 1013 /** | |
| 1014 * @param {Array.<string>} urls Array of URLs. | |
| 1015 */ | |
| 1016 DirectoryModel.prototype.selectUrls = function(urls) { | |
| 1017 var fileList = this.getFileList(); | |
| 1018 this.fileListSelection_.beginChange(); | |
| 1019 this.fileListSelection_.unselectAll(); | |
| 1020 for (var i = 0; i < fileList.length; i++) { | |
| 1021 if (urls.indexOf(fileList.item(i).toURL()) >= 0) | |
| 1022 this.fileListSelection_.setIndexSelected(i, true); | |
| 1023 } | |
| 1024 this.fileListSelection_.endChange(); | |
| 1025 }; | |
| 1026 | |
| 1027 /** | |
| 1028 * @param {number} index Index of file. | |
| 1029 */ | |
| 1030 DirectoryModel.prototype.selectIndex = function(index) { | |
| 1031 // this.focusCurrentList_(); | |
| 1032 if (index >= this.getFileList().length) | |
| 1033 return; | |
| 1034 | |
| 1035 // If a list bound with the model it will do scrollIndexIntoView(index). | |
| 1036 this.fileListSelection_.selectedIndex = index; | |
| 1037 }; | |
| 1038 | |
| 1039 /** | |
| 1040 * Called when VolumeInfoList is updated. | |
| 1041 * | |
| 1042 * @param {Event} event Event of VolumeInfoList's 'splice'. | |
| 1043 * @private | |
| 1044 */ | |
| 1045 DirectoryModel.prototype.onVolumeInfoListUpdated_ = function(event) { | |
| 1046 var driveVolume = this.volumeManager_.getVolumeInfo(RootDirectory.DRIVE); | |
| 1047 if (driveVolume && !driveVolume.error) { | |
| 1048 var currentDirEntry = this.getCurrentDirEntry(); | |
| 1049 if (currentDirEntry) { | |
| 1050 if (currentDirEntry === DirectoryModel.fakeDriveEntry_) { | |
| 1051 // Replace the fake entry by real DirectoryEntry silently. | |
| 1052 this.volumeManager_.resolvePath( | |
| 1053 DirectoryModel.fakeDriveEntry_.fullPath, | |
| 1054 function(entry) { | |
| 1055 // If the current entry is still fake drive entry, replace it. | |
| 1056 if (this.getCurrentDirEntry() === DirectoryModel.fakeDriveEntry_) | |
| 1057 this.changeDirectoryEntrySilent_(entry); | |
| 1058 }, | |
| 1059 function(error) {}); | |
| 1060 } else if (PathUtil.isSpecialSearchRoot(currentDirEntry.fullPath)) { | |
| 1061 for (var i = 0; i < event.added.length; i++) { | |
| 1062 if (event.added[i].volumeType == util.VolumeType.DRIVE) { | |
| 1063 // If the Drive volume is newly mounted, rescan it. | |
| 1064 this.rescan(); | |
| 1065 break; | |
| 1066 } | |
| 1067 } | |
| 1068 } | |
| 1069 } | |
| 1070 } | |
| 1071 | |
| 1072 var rootPath = this.getCurrentRootPath(); | |
| 1073 var rootType = PathUtil.getRootType(rootPath); | |
| 1074 | |
| 1075 // If the path is on drive, reduce to the Drive's mount point. | |
| 1076 if (rootType === RootType.DRIVE) | |
| 1077 rootPath = RootDirectory.DRIVE; | |
| 1078 | |
| 1079 // When the volume where we are is unmounted, fallback to | |
| 1080 // DEFAULT_DIRECTORY. | |
| 1081 // Note: during the initialization, rootType can be undefined. | |
| 1082 if (rootType && !this.volumeManager_.getVolumeInfo(rootPath)) | |
| 1083 this.changeDirectory(PathUtil.DEFAULT_DIRECTORY); | |
| 1084 }; | |
| 1085 | |
| 1086 /** | |
| 1087 * @param {string} path Path. | |
| 1088 * @return {boolean} If current directory is system. | |
| 1089 */ | |
| 1090 DirectoryModel.isSystemDirectory = function(path) { | |
| 1091 path = path.replace(/\/+$/, ''); | |
| 1092 return path === RootDirectory.REMOVABLE || path === RootDirectory.ARCHIVE; | |
| 1093 }; | |
| 1094 | |
| 1095 /** | |
| 1096 * Check if the root of the given path is mountable or not. | |
| 1097 * | |
| 1098 * @param {string} path Path. | |
| 1099 * @return {boolean} Return true, if the given path is under mountable root. | |
| 1100 * Otherwise, return false. | |
| 1101 */ | |
| 1102 DirectoryModel.isMountableRoot = function(path) { | |
| 1103 var rootType = PathUtil.getRootType(path); | |
| 1104 switch (rootType) { | |
| 1105 case RootType.DOWNLOADS: | |
| 1106 return false; | |
| 1107 case RootType.ARCHIVE: | |
| 1108 case RootType.REMOVABLE: | |
| 1109 case RootType.DRIVE: | |
| 1110 return true; | |
| 1111 default: | |
| 1112 throw new Error('Unknown root type!'); | |
| 1113 } | |
| 1114 }; | |
| 1115 | |
| 1116 /** | |
| 1117 * Performs search and displays results. The search type is dependent on the | |
| 1118 * current directory. If we are currently on drive, server side content search | |
| 1119 * over drive mount point. If the current directory is not on the drive, file | |
| 1120 * name search over current directory will be performed. | |
| 1121 * | |
| 1122 * @param {string} query Query that will be searched for. | |
| 1123 * @param {function(Event)} onSearchRescan Function that will be called when the | |
| 1124 * search directory is rescanned (i.e. search results are displayed). | |
| 1125 * @param {function()} onClearSearch Function to be called when search state | |
| 1126 * gets cleared. | |
| 1127 * TODO(olege): Change callbacks to events. | |
| 1128 */ | |
| 1129 DirectoryModel.prototype.search = function(query, | |
| 1130 onSearchRescan, | |
| 1131 onClearSearch) { | |
| 1132 query = query.trimLeft(); | |
| 1133 | |
| 1134 this.clearSearch_(); | |
| 1135 | |
| 1136 var currentDirEntry = this.getCurrentDirEntry(); | |
| 1137 if (!currentDirEntry) { | |
| 1138 // Not yet initialized. Do nothing. | |
| 1139 return; | |
| 1140 } | |
| 1141 | |
| 1142 if (!query) { | |
| 1143 if (this.isSearching()) { | |
| 1144 var newDirContents = DirectoryContents.createForDirectory( | |
| 1145 this.currentFileListContext_, | |
| 1146 this.currentDirContents_.getLastNonSearchDirectoryEntry()); | |
| 1147 this.clearAndScan_(newDirContents); | |
| 1148 } | |
| 1149 return; | |
| 1150 } | |
| 1151 | |
| 1152 this.onSearchCompleted_ = onSearchRescan; | |
| 1153 this.onClearSearch_ = onClearSearch; | |
| 1154 | |
| 1155 this.addEventListener('scan-completed', this.onSearchCompleted_); | |
| 1156 | |
| 1157 // If we are offline, let's fallback to file name search inside dir. | |
| 1158 // A search initiated from directories in Drive or special search results | |
| 1159 // should trigger Drive search. | |
| 1160 var newDirContents; | |
| 1161 if (!this.isDriveOffline() && | |
| 1162 PathUtil.isDriveBasedPath(currentDirEntry.fullPath)) { | |
| 1163 // Drive search is performed over the whole drive, so pass drive root as | |
| 1164 // |directoryEntry|. | |
| 1165 newDirContents = DirectoryContents.createForDriveSearch( | |
| 1166 this.currentFileListContext_, | |
| 1167 currentDirEntry, | |
| 1168 this.currentDirContents_.getLastNonSearchDirectoryEntry(), | |
| 1169 query); | |
| 1170 } else { | |
| 1171 newDirContents = DirectoryContents.createForLocalSearch( | |
| 1172 this.currentFileListContext_, currentDirEntry, query); | |
| 1173 } | |
| 1174 this.clearAndScan_(newDirContents); | |
| 1175 }; | |
| 1176 | |
| 1177 /** | |
| 1178 * Performs special search and displays results. e.g. Drive files available | |
| 1179 * offline, shared-with-me files, recently modified files. | |
| 1180 * @param {string} path Path string representing special search. See fake | |
| 1181 * entries in PathUtil.RootDirectory. | |
| 1182 * @param {string=} opt_query Query string used for the search. | |
| 1183 */ | |
| 1184 DirectoryModel.prototype.specialSearch = function(path, opt_query) { | |
| 1185 var query = opt_query || ''; | |
| 1186 | |
| 1187 this.clearSearch_(); | |
| 1188 | |
| 1189 this.onSearchCompleted_ = null; | |
| 1190 this.onClearSearch_ = null; | |
| 1191 | |
| 1192 var onDriveDirectoryResolved = function(driveRoot) { | |
| 1193 if (!driveRoot || driveRoot == DirectoryModel.fakeDriveEntry_) { | |
| 1194 // Drive root not available or not ready. onVolumeInfoListUpdated_() | |
| 1195 // handles the rescan if necessary. | |
| 1196 driveRoot = null; | |
| 1197 } | |
| 1198 | |
| 1199 var specialSearchType = PathUtil.getRootType(path); | |
| 1200 var searchOption; | |
| 1201 var dirEntry; | |
| 1202 if (specialSearchType == RootType.DRIVE_OFFLINE) { | |
| 1203 dirEntry = DirectoryModel.fakeDriveOfflineEntry_; | |
| 1204 searchOption = | |
| 1205 DriveMetadataSearchContentScanner.SearchType.SEARCH_OFFLINE; | |
| 1206 } else if (specialSearchType == RootType.DRIVE_SHARED_WITH_ME) { | |
| 1207 dirEntry = DirectoryModel.fakeDriveSharedWithMeEntry_; | |
| 1208 searchOption = | |
| 1209 DriveMetadataSearchContentScanner.SearchType.SEARCH_SHARED_WITH_ME; | |
| 1210 } else if (specialSearchType == RootType.DRIVE_RECENT) { | |
| 1211 dirEntry = DirectoryModel.fakeDriveRecentEntry_; | |
| 1212 searchOption = | |
| 1213 DriveMetadataSearchContentScanner.SearchType.SEARCH_RECENT_FILES; | |
| 1214 | |
| 1215 } else { | |
| 1216 // Unknown path. | |
| 1217 this.changeDirectory(PathUtil.DEFAULT_DIRECTORY); | |
| 1218 return; | |
| 1219 } | |
| 1220 | |
| 1221 var newDirContents = DirectoryContents.createForDriveMetadataSearch( | |
| 1222 this.currentFileListContext_, | |
| 1223 dirEntry, driveRoot, query, searchOption); | |
| 1224 var previous = this.currentDirContents_.getDirectoryEntry(); | |
| 1225 this.clearAndScan_(newDirContents); | |
| 1226 | |
| 1227 var e = new Event('directory-changed'); | |
| 1228 e.previousDirEntry = previous; | |
| 1229 e.newDirEntry = dirEntry; | |
| 1230 this.dispatchEvent(e); | |
| 1231 }.bind(this); | |
| 1232 | |
| 1233 this.resolveDirectory(DirectoryModel.fakeDriveEntry_.fullPath, | |
| 1234 onDriveDirectoryResolved /* success */, | |
| 1235 function() {} /* failed */); | |
| 1236 }; | |
| 1237 | |
| 1238 /** | |
| 1239 * In case the search was active, remove listeners and send notifications on | |
| 1240 * its canceling. | |
| 1241 * @private | |
| 1242 */ | |
| 1243 DirectoryModel.prototype.clearSearch_ = function() { | |
| 1244 if (!this.isSearching()) | |
| 1245 return; | |
| 1246 | |
| 1247 if (this.onSearchCompleted_) { | |
| 1248 this.removeEventListener('scan-completed', this.onSearchCompleted_); | |
| 1249 this.onSearchCompleted_ = null; | |
| 1250 } | |
| 1251 | |
| 1252 if (this.onClearSearch_) { | |
| 1253 this.onClearSearch_(); | |
| 1254 this.onClearSearch_ = null; | |
| 1255 } | |
| 1256 }; | |
| OLD | NEW |