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 * Represents each volume, such as "drive", "download directory", each "USB 9 * flush storage", or "mounted zip archive" etc. 10 * 11 * @param {VolumeManagerCommon.VolumeType} volumeType The type of the volume. 12 * @param {string} volumeId ID of the volume. 13 * @param {DOMFileSystem} fileSystem The file system object for this volume. 14 * @param {string} error The error if an error is found. 15 * @param {string} deviceType The type of device ('usb'|'sd'|'optical'|'mobile' 16 * |'unknown') (as defined in chromeos/disks/disk_mount_manager.cc). 17 * Can be null. 18 * @param {boolean} isReadOnly True if the volume is read only. 19 * @param {!{displayName:string, isCurrentProfile:boolean}} profile Profile 20 * information. 21 * @param {string} label Label of the volume. 22 * @param {string} extensionId Id of the extension providing this volume. Empty 23 * for native volumes. 24 * @constructor 25 */ 26function VolumeInfo( 27 volumeType, 28 volumeId, 29 fileSystem, 30 error, 31 deviceType, 32 isReadOnly, 33 profile, 34 label, 35 extensionId) { 36 this.volumeType_ = volumeType; 37 this.volumeId_ = volumeId; 38 this.fileSystem_ = fileSystem; 39 this.label_ = label; 40 this.displayRoot_ = null; 41 this.fakeEntries_ = {}; 42 this.displayRoot_ = null; 43 this.displayRootPromise_ = null; 44 45 if (volumeType === VolumeManagerCommon.VolumeType.DRIVE) { 46 // TODO(mtomasz): Convert fake entries to DirectoryProvider. 47 this.fakeEntries_[VolumeManagerCommon.RootType.DRIVE_OFFLINE] = { 48 isDirectory: true, 49 rootType: VolumeManagerCommon.RootType.DRIVE_OFFLINE, 50 toURL: function() { return 'fake-entry://drive_offline'; } 51 }; 52 this.fakeEntries_[VolumeManagerCommon.RootType.DRIVE_SHARED_WITH_ME] = { 53 isDirectory: true, 54 rootType: VolumeManagerCommon.RootType.DRIVE_SHARED_WITH_ME, 55 toURL: function() { return 'fake-entry://drive_shared_with_me'; } 56 }; 57 this.fakeEntries_[VolumeManagerCommon.RootType.DRIVE_RECENT] = { 58 isDirectory: true, 59 rootType: VolumeManagerCommon.RootType.DRIVE_RECENT, 60 toURL: function() { return 'fake-entry://drive_recent'; } 61 }; 62 } 63 64 // Note: This represents if the mounting of the volume is successfully done 65 // or not. (If error is empty string, the mount is successfully done). 66 // TODO(hidehiko): Rename to make this more understandable. 67 this.error_ = error; 68 this.deviceType_ = deviceType; 69 this.isReadOnly_ = isReadOnly; 70 this.profile_ = Object.freeze(profile); 71 this.extensionId_ = extensionId; 72 73 Object.seal(this); 74} 75 76VolumeInfo.prototype = { 77 /** 78 * @return {VolumeManagerCommon.VolumeType} Volume type. 79 */ 80 get volumeType() { 81 return this.volumeType_; 82 }, 83 /** 84 * @return {string} Volume ID. 85 */ 86 get volumeId() { 87 return this.volumeId_; 88 }, 89 /** 90 * @return {DOMFileSystem} File system object. 91 */ 92 get fileSystem() { 93 return this.fileSystem_; 94 }, 95 /** 96 * @return {DirectoryEntry} Display root path. It is null before finishing to 97 * resolve the entry. 98 */ 99 get displayRoot() { 100 return this.displayRoot_; 101 }, 102 /** 103 * @return {Object.<string, Object>} Fake entries. 104 */ 105 get fakeEntries() { 106 return this.fakeEntries_; 107 }, 108 /** 109 * @return {string} Error identifier. 110 */ 111 get error() { 112 return this.error_; 113 }, 114 /** 115 * @return {string} Device type identifier. 116 */ 117 get deviceType() { 118 return this.deviceType_; 119 }, 120 /** 121 * @return {boolean} Whether read only or not. 122 */ 123 get isReadOnly() { 124 return this.isReadOnly_; 125 }, 126 /** 127 * @return {!{displayName:string, isCurrentProfile:boolean}} Profile data. 128 */ 129 get profile() { 130 return this.profile_; 131 }, 132 /** 133 * @return {string} Label for the volume. 134 */ 135 get label() { 136 return this.label_; 137 }, 138 /** 139 * @return {string} Id of an extennsion providing this volume. 140 */ 141 get extensionId() { 142 return this.extensionId_; 143 } 144}; 145 146/** 147 * Starts resolving the display root and obtains it. It may take long time for 148 * Drive. Once resolved, it is cached. 149 * 150 * @param {function(DirectoryEntry)} onSuccess Success callback with the 151 * display root directory as an argument. 152 * @param {function(FileError)} onFailure Failure callback. 153 */ 154VolumeInfo.prototype.resolveDisplayRoot = function(onSuccess, onFailure) { 155 if (!this.displayRootPromise_) { 156 // TODO(mtomasz): Do not add VolumeInfo which failed to resolve root, and 157 // remove this if logic. Call onSuccess() always, instead. 158 if (this.volumeType !== VolumeManagerCommon.VolumeType.DRIVE) { 159 if (this.fileSystem_) 160 this.displayRootPromise_ = Promise.resolve(this.fileSystem_.root); 161 else 162 this.displayRootPromise_ = Promise.reject(this.error); 163 } else { 164 // For Drive, we need to resolve. 165 var displayRootURL = this.fileSystem_.root.toURL() + '/root'; 166 this.displayRootPromise_ = new Promise( 167 webkitResolveLocalFileSystemURL.bind(null, displayRootURL)); 168 } 169 170 // Store the obtained displayRoot. 171 this.displayRootPromise_.then(function(displayRoot) { 172 this.displayRoot_ = displayRoot; 173 }.bind(this)); 174 } 175 this.displayRootPromise_.then(onSuccess, onFailure); 176}; 177 178/** 179 * Utilities for volume manager implementation. 180 */ 181var volumeManagerUtil = {}; 182 183/** 184 * Throws an Error when the given error is not in 185 * VolumeManagerCommon.VolumeError. 186 * 187 * @param {VolumeManagerCommon.VolumeError} error Status string usually received 188 * from APIs. 189 */ 190volumeManagerUtil.validateError = function(error) { 191 for (var key in VolumeManagerCommon.VolumeError) { 192 if (error === VolumeManagerCommon.VolumeError[key]) 193 return; 194 } 195 196 throw new Error('Invalid mount error: ' + error); 197}; 198 199/** 200 * Builds the VolumeInfo data from VolumeMetadata. 201 * @param {VolumeMetadata} volumeMetadata Metadata instance for the volume. 202 * @param {function(VolumeInfo)} callback Called on completion. 203 */ 204volumeManagerUtil.createVolumeInfo = function(volumeMetadata, callback) { 205 var localizedLabel; 206 switch (volumeMetadata.volumeType) { 207 case VolumeManagerCommon.VolumeType.DOWNLOADS: 208 localizedLabel = str('DOWNLOADS_DIRECTORY_LABEL'); 209 break; 210 case VolumeManagerCommon.VolumeType.DRIVE: 211 localizedLabel = str('DRIVE_DIRECTORY_LABEL'); 212 break; 213 default: 214 // TODO(mtomasz): Calculate volumeLabel for all types of volumes in the 215 // C++ layer. 216 localizedLabel = volumeMetadata.volumeLabel || 217 volumeMetadata.volumeId.split(':', 2)[1]; 218 break; 219 } 220 221 chrome.fileBrowserPrivate.requestFileSystem( 222 volumeMetadata.volumeId, 223 function(fileSystem) { 224 // TODO(mtomasz): chrome.runtime.lastError should have error reason. 225 if (!fileSystem) { 226 console.error('File system not found: ' + volumeMetadata.volumeId); 227 callback(new VolumeInfo( 228 volumeMetadata.volumeType, 229 volumeMetadata.volumeId, 230 null, // File system is not found. 231 volumeMetadata.mountCondition, 232 volumeMetadata.deviceType, 233 volumeMetadata.isReadOnly, 234 volumeMetadata.profile, 235 localizedLabel, 236 volumeMetadata.extensionId)); 237 return; 238 } 239 if (volumeMetadata.volumeType == 240 VolumeManagerCommon.VolumeType.DRIVE) { 241 // After file system is mounted, we "read" drive grand root 242 // entry at first. This triggers full feed fetch on background. 243 // Note: we don't need to handle errors here, because even if 244 // it fails, accessing to some path later will just become 245 // a fast-fetch and it re-triggers full-feed fetch. 246 fileSystem.root.createReader().readEntries( 247 function() { /* do nothing */ }, 248 function(error) { 249 console.error( 250 'Triggering full feed fetch is failed: ' + error.name); 251 }); 252 } 253 callback(new VolumeInfo( 254 volumeMetadata.volumeType, 255 volumeMetadata.volumeId, 256 fileSystem, 257 volumeMetadata.mountCondition, 258 volumeMetadata.deviceType, 259 volumeMetadata.isReadOnly, 260 volumeMetadata.profile, 261 localizedLabel, 262 volumeMetadata.extensionId)); 263 }); 264}; 265 266/** 267 * The order of the volume list based on root type. 268 * @type {Array.<VolumeManagerCommon.VolumeType>} 269 * @const 270 * @private 271 */ 272volumeManagerUtil.volumeListOrder_ = [ 273 VolumeManagerCommon.VolumeType.DRIVE, 274 VolumeManagerCommon.VolumeType.DOWNLOADS, 275 VolumeManagerCommon.VolumeType.ARCHIVE, 276 VolumeManagerCommon.VolumeType.REMOVABLE, 277 VolumeManagerCommon.VolumeType.MTP, 278 VolumeManagerCommon.VolumeType.PROVIDED, 279 VolumeManagerCommon.VolumeType.CLOUD_DEVICE 280]; 281 282/** 283 * Orders two volumes by volumeType and volumeId. 284 * 285 * The volumes at first are compared by volume type in the order of 286 * volumeListOrder_. Then they are compared by volume ID. 287 * 288 * @param {VolumeInfo} volumeInfo1 Volume info to be compared. 289 * @param {VolumeInfo} volumeInfo2 Volume info to be compared. 290 * @return {number} Returns -1 if volume1 < volume2, returns 1 if volume2 > 291 * volume1, returns 0 if volume1 === volume2. 292 * @private 293 */ 294volumeManagerUtil.compareVolumeInfo_ = function(volumeInfo1, volumeInfo2) { 295 var typeIndex1 = 296 volumeManagerUtil.volumeListOrder_.indexOf(volumeInfo1.volumeType); 297 var typeIndex2 = 298 volumeManagerUtil.volumeListOrder_.indexOf(volumeInfo2.volumeType); 299 if (typeIndex1 !== typeIndex2) 300 return typeIndex1 < typeIndex2 ? -1 : 1; 301 if (volumeInfo1.volumeId !== volumeInfo2.volumeId) 302 return volumeInfo1.volumeId < volumeInfo2.volumeId ? -1 : 1; 303 return 0; 304}; 305 306/** 307 * The container of the VolumeInfo for each mounted volume. 308 * @constructor 309 */ 310function VolumeInfoList() { 311 var field = 'volumeType,volumeId'; 312 313 /** 314 * Holds VolumeInfo instances. 315 * @type {cr.ui.ArrayDataModel} 316 * @private 317 */ 318 this.model_ = new cr.ui.ArrayDataModel([]); 319 this.model_.setCompareFunction(field, volumeManagerUtil.compareVolumeInfo_); 320 this.model_.sort(field, 'asc'); 321 322 Object.freeze(this); 323} 324 325VolumeInfoList.prototype = { 326 get length() { return this.model_.length; } 327}; 328 329/** 330 * Adds the event listener to listen the change of volume info. 331 * @param {string} type The name of the event. 332 * @param {function(Event)} handler The handler for the event. 333 */ 334VolumeInfoList.prototype.addEventListener = function(type, handler) { 335 this.model_.addEventListener(type, handler); 336}; 337 338/** 339 * Removes the event listener. 340 * @param {string} type The name of the event. 341 * @param {function(Event)} handler The handler to be removed. 342 */ 343VolumeInfoList.prototype.removeEventListener = function(type, handler) { 344 this.model_.removeEventListener(type, handler); 345}; 346 347/** 348 * Adds the volumeInfo to the appropriate position. If there already exists, 349 * just replaces it. 350 * @param {VolumeInfo} volumeInfo The information of the new volume. 351 */ 352VolumeInfoList.prototype.add = function(volumeInfo) { 353 var index = this.findIndex(volumeInfo.volumeId); 354 if (index !== -1) 355 this.model_.splice(index, 1, volumeInfo); 356 else 357 this.model_.push(volumeInfo); 358}; 359 360/** 361 * Removes the VolumeInfo having the given ID. 362 * @param {string} volumeId ID of the volume. 363 */ 364VolumeInfoList.prototype.remove = function(volumeId) { 365 var index = this.findIndex(volumeId); 366 if (index !== -1) 367 this.model_.splice(index, 1); 368}; 369 370/** 371 * Obtains an index from the volume ID. 372 * @param {string} volumeId Volume ID. 373 * @return {number} Index of the volume. 374 */ 375VolumeInfoList.prototype.findIndex = function(volumeId) { 376 for (var i = 0; i < this.model_.length; i++) { 377 if (this.model_.item(i).volumeId === volumeId) 378 return i; 379 } 380 return -1; 381}; 382 383/** 384 * Searches the information of the volume that contains the passed entry. 385 * @param {Entry|Object} entry Entry on the volume to be found. 386 * @return {VolumeInfo} The volume's information, or null if not found. 387 */ 388VolumeInfoList.prototype.findByEntry = function(entry) { 389 for (var i = 0; i < this.length; i++) { 390 var volumeInfo = this.item(i); 391 if (volumeInfo.fileSystem && 392 util.isSameFileSystem(volumeInfo.fileSystem, entry.filesystem)) { 393 return volumeInfo; 394 } 395 // Additionally, check fake entries. 396 for (var key in volumeInfo.fakeEntries_) { 397 var fakeEntry = volumeInfo.fakeEntries_[key]; 398 if (util.isSameEntry(fakeEntry, entry)) 399 return volumeInfo; 400 } 401 } 402 return null; 403}; 404 405/** 406 * @param {number} index The index of the volume in the list. 407 * @return {VolumeInfo} The VolumeInfo instance. 408 */ 409VolumeInfoList.prototype.item = function(index) { 410 return this.model_.item(index); 411}; 412 413/** 414 * VolumeManager is responsible for tracking list of mounted volumes. 415 * 416 * @constructor 417 * @extends {cr.EventTarget} 418 */ 419function VolumeManager() { 420 /** 421 * The list of archives requested to mount. We will show contents once 422 * archive is mounted, but only for mounts from within this filebrowser tab. 423 * @type {Object.<string, Object>} 424 * @private 425 */ 426 this.requests_ = {}; 427 428 /** 429 * The list of VolumeInfo instances for each mounted volume. 430 * @type {VolumeInfoList} 431 */ 432 this.volumeInfoList = new VolumeInfoList(); 433 434 /** 435 * Queue for mounting. 436 * @type {AsyncUtil.Queue} 437 * @private 438 */ 439 this.mountQueue_ = new AsyncUtil.Queue(); 440 441 // The status should be merged into VolumeManager. 442 // TODO(hidehiko): Remove them after the migration. 443 this.driveConnectionState_ = { 444 type: VolumeManagerCommon.DriveConnectionType.OFFLINE, 445 reason: VolumeManagerCommon.DriveConnectionReason.NO_SERVICE 446 }; 447 448 chrome.fileBrowserPrivate.onDriveConnectionStatusChanged.addListener( 449 this.onDriveConnectionStatusChanged_.bind(this)); 450 this.onDriveConnectionStatusChanged_(); 451} 452 453/** 454 * Invoked when the drive connection status is changed. 455 * @private_ 456 */ 457VolumeManager.prototype.onDriveConnectionStatusChanged_ = function() { 458 chrome.fileBrowserPrivate.getDriveConnectionState(function(state) { 459 this.driveConnectionState_ = state; 460 cr.dispatchSimpleEvent(this, 'drive-connection-changed'); 461 }.bind(this)); 462}; 463 464/** 465 * Returns the drive connection state. 466 * @return {VolumeManagerCommon.DriveConnectionType} Connection type. 467 */ 468VolumeManager.prototype.getDriveConnectionState = function() { 469 return this.driveConnectionState_; 470}; 471 472/** 473 * VolumeManager extends cr.EventTarget. 474 */ 475VolumeManager.prototype.__proto__ = cr.EventTarget.prototype; 476 477/** 478 * Time in milliseconds that we wait a response for. If no response on 479 * mount/unmount received the request supposed failed. 480 */ 481VolumeManager.TIMEOUT = 15 * 60 * 1000; 482 483/** 484 * Queue to run getInstance sequentially. 485 * @type {AsyncUtil.Queue} 486 * @private 487 */ 488VolumeManager.getInstanceQueue_ = new AsyncUtil.Queue(); 489 490/** 491 * The singleton instance of VolumeManager. Initialized by the first invocation 492 * of getInstance(). 493 * @type {VolumeManager} 494 * @private 495 */ 496VolumeManager.instance_ = null; 497 498/** 499 * Returns the VolumeManager instance asynchronously. If it is not created or 500 * under initialization, it will waits for the finish of the initialization. 501 * @param {function(VolumeManager)} callback Called with the VolumeManager 502 * instance. 503 */ 504VolumeManager.getInstance = function(callback) { 505 VolumeManager.getInstanceQueue_.run(function(continueCallback) { 506 if (VolumeManager.instance_) { 507 callback(VolumeManager.instance_); 508 continueCallback(); 509 return; 510 } 511 512 VolumeManager.instance_ = new VolumeManager(); 513 VolumeManager.instance_.initialize_(function() { 514 callback(VolumeManager.instance_); 515 continueCallback(); 516 }); 517 }); 518}; 519 520/** 521 * Initializes mount points. 522 * @param {function()} callback Called upon the completion of the 523 * initialization. 524 * @private 525 */ 526VolumeManager.prototype.initialize_ = function(callback) { 527 chrome.fileBrowserPrivate.getVolumeMetadataList(function(volumeMetadataList) { 528 // We must subscribe to the mount completed event in the callback of 529 // getVolumeMetadataList. crbug.com/330061. 530 // But volumes reported by onMountCompleted events must be added after the 531 // volumes in the volumeMetadataList are mounted. crbug.com/135477. 532 this.mountQueue_.run(function(inCallback) { 533 // Create VolumeInfo for each volume. 534 var group = new AsyncUtil.Group(); 535 for (var i = 0; i < volumeMetadataList.length; i++) { 536 group.add(function(volumeMetadata, continueCallback) { 537 volumeManagerUtil.createVolumeInfo( 538 volumeMetadata, 539 function(volumeInfo) { 540 this.volumeInfoList.add(volumeInfo); 541 if (volumeMetadata.volumeType === 542 VolumeManagerCommon.VolumeType.DRIVE) 543 this.onDriveConnectionStatusChanged_(); 544 continueCallback(); 545 }.bind(this)); 546 }.bind(this, volumeMetadataList[i])); 547 } 548 group.run(function() { 549 // Call the callback of the initialize function. 550 callback(); 551 // Call the callback of AsyncQueue. Maybe it invokes callbacks 552 // registered by mountCompleted events. 553 inCallback(); 554 }); 555 }.bind(this)); 556 557 chrome.fileBrowserPrivate.onMountCompleted.addListener( 558 this.onMountCompleted_.bind(this)); 559 }.bind(this)); 560}; 561 562/** 563 * Event handler called when some volume was mounted or unmounted. 564 * @param {MountCompletedEvent} event Received event. 565 * @private 566 */ 567VolumeManager.prototype.onMountCompleted_ = function(event) { 568 this.mountQueue_.run(function(callback) { 569 switch (event.eventType) { 570 case 'mount': 571 var requestKey = this.makeRequestKey_( 572 'mount', 573 event.volumeMetadata.sourcePath); 574 575 var error = event.status === 'success' ? '' : event.status; 576 if (!error || event.status === 'error_unknown_filesystem') { 577 volumeManagerUtil.createVolumeInfo( 578 event.volumeMetadata, 579 function(volumeInfo) { 580 this.volumeInfoList.add(volumeInfo); 581 this.finishRequest_(requestKey, event.status, volumeInfo); 582 583 if (volumeInfo.volumeType === 584 VolumeManagerCommon.VolumeType.DRIVE) { 585 // Update the network connection status, because until the 586 // drive is initialized, the status is set to not ready. 587 // TODO(mtomasz): The connection status should be migrated 588 // into VolumeMetadata. 589 this.onDriveConnectionStatusChanged_(); 590 } 591 callback(); 592 }.bind(this)); 593 } else { 594 console.warn('Failed to mount a volume: ' + event.status); 595 this.finishRequest_(requestKey, event.status); 596 callback(); 597 } 598 break; 599 600 case 'unmount': 601 var volumeId = event.volumeMetadata.volumeId; 602 var status = event.status; 603 if (status === VolumeManagerCommon.VolumeError.PATH_UNMOUNTED) { 604 console.warn('Volume already unmounted: ', volumeId); 605 status = 'success'; 606 } 607 var requestKey = this.makeRequestKey_('unmount', volumeId); 608 var requested = requestKey in this.requests_; 609 var volumeInfoIndex = 610 this.volumeInfoList.findIndex(volumeId); 611 var volumeInfo = volumeInfoIndex !== -1 ? 612 this.volumeInfoList.item(volumeInfoIndex) : null; 613 if (event.status === 'success' && !requested && volumeInfo) { 614 console.warn('Mounted volume without a request: ' + volumeId); 615 var e = new Event('externally-unmounted'); 616 e.volumeInfo = volumeInfo; 617 this.dispatchEvent(e); 618 } 619 620 this.finishRequest_(requestKey, status); 621 if (event.status === 'success') 622 this.volumeInfoList.remove(event.volumeMetadata.volumeId); 623 callback(); 624 break; 625 } 626 }.bind(this)); 627}; 628 629/** 630 * Creates string to match mount events with requests. 631 * @param {string} requestType 'mount' | 'unmount'. TODO(hidehiko): Replace by 632 * enum. 633 * @param {string} argument Argument describing the request, eg. source file 634 * path of the archive to be mounted, or a volumeId for unmounting. 635 * @return {string} Key for |this.requests_|. 636 * @private 637 */ 638VolumeManager.prototype.makeRequestKey_ = function(requestType, argument) { 639 return requestType + ':' + argument; 640}; 641 642/** 643 * @param {string} fileUrl File url to the archive file. 644 * @param {function(VolumeInfo)} successCallback Success callback. 645 * @param {function(VolumeManagerCommon.VolumeError)} errorCallback Error 646 * callback. 647 */ 648VolumeManager.prototype.mountArchive = function( 649 fileUrl, successCallback, errorCallback) { 650 chrome.fileBrowserPrivate.addMount(fileUrl, function(sourcePath) { 651 console.info( 652 'Mount request: url=' + fileUrl + '; sourcePath=' + sourcePath); 653 var requestKey = this.makeRequestKey_('mount', sourcePath); 654 this.startRequest_(requestKey, successCallback, errorCallback); 655 }.bind(this)); 656}; 657 658/** 659 * Unmounts volume. 660 * @param {!VolumeInfo} volumeInfo Volume to be unmounted. 661 * @param {function()} successCallback Success callback. 662 * @param {function(VolumeManagerCommon.VolumeError)} errorCallback Error 663 * callback. 664 */ 665VolumeManager.prototype.unmount = function(volumeInfo, 666 successCallback, 667 errorCallback) { 668 chrome.fileBrowserPrivate.removeMount(volumeInfo.volumeId); 669 var requestKey = this.makeRequestKey_('unmount', volumeInfo.volumeId); 670 this.startRequest_(requestKey, successCallback, errorCallback); 671}; 672 673/** 674 * Obtains a volume info containing the passed entry. 675 * @param {Entry|Object} entry Entry on the volume to be returned. Can be fake. 676 * @return {VolumeInfo} The VolumeInfo instance or null if not found. 677 */ 678VolumeManager.prototype.getVolumeInfo = function(entry) { 679 return this.volumeInfoList.findByEntry(entry); 680}; 681 682/** 683 * Obtains volume information of the current profile. 684 * 685 * @param {VolumeManagerCommon.VolumeType} volumeType Volume type. 686 * @return {VolumeInfo} Volume info. 687 */ 688VolumeManager.prototype.getCurrentProfileVolumeInfo = function(volumeType) { 689 for (var i = 0; i < this.volumeInfoList.length; i++) { 690 var volumeInfo = this.volumeInfoList.item(i); 691 if (volumeInfo.profile.isCurrentProfile && 692 volumeInfo.volumeType === volumeType) 693 return volumeInfo; 694 } 695 return null; 696}; 697 698/** 699 * Obtains location information from an entry. 700 * 701 * @param {Entry|Object} entry File or directory entry. It can be a fake entry. 702 * @return {EntryLocation} Location information. 703 */ 704VolumeManager.prototype.getLocationInfo = function(entry) { 705 var volumeInfo = this.volumeInfoList.findByEntry(entry); 706 if (!volumeInfo) 707 return null; 708 709 if (util.isFakeEntry(entry)) { 710 return new EntryLocation( 711 volumeInfo, 712 entry.rootType, 713 true /* the entry points a root directory. */, 714 true /* fake entries are read only. */); 715 } 716 717 var rootType; 718 var isReadOnly; 719 var isRootEntry; 720 if (volumeInfo.volumeType === VolumeManagerCommon.VolumeType.DRIVE) { 721 // For Drive, the roots are /root and /other, instead of /. Root URLs 722 // contain trailing slashes. 723 if (entry.fullPath == '/root' || entry.fullPath.indexOf('/root/') === 0) { 724 rootType = VolumeManagerCommon.RootType.DRIVE; 725 isReadOnly = volumeInfo.isReadOnly; 726 isRootEntry = entry.fullPath === '/root'; 727 } else if (entry.fullPath == '/other' || 728 entry.fullPath.indexOf('/other/') === 0) { 729 rootType = VolumeManagerCommon.RootType.DRIVE_OTHER; 730 isReadOnly = true; 731 isRootEntry = entry.fullPath === '/other'; 732 } else { 733 // Accessing Drive files outside of /drive/root and /drive/other is not 734 // allowed, but can happen. Therefore returning null. 735 return null; 736 } 737 } else { 738 switch (volumeInfo.volumeType) { 739 case VolumeManagerCommon.VolumeType.DOWNLOADS: 740 rootType = VolumeManagerCommon.RootType.DOWNLOADS; 741 break; 742 case VolumeManagerCommon.VolumeType.REMOVABLE: 743 rootType = VolumeManagerCommon.RootType.REMOVABLE; 744 break; 745 case VolumeManagerCommon.VolumeType.ARCHIVE: 746 rootType = VolumeManagerCommon.RootType.ARCHIVE; 747 break; 748 case VolumeManagerCommon.VolumeType.CLOUD_DEVICE: 749 rootType = VolumeManagerCommon.RootType.CLOUD_DEVICE; 750 break; 751 case VolumeManagerCommon.VolumeType.MTP: 752 rootType = VolumeManagerCommon.RootType.MTP; 753 break; 754 case VolumeManagerCommon.VolumeType.PROVIDED: 755 rootType = VolumeManagerCommon.RootType.PROVIDED; 756 break; 757 default: 758 // Programming error, throw an exception. 759 throw new Error('Invalid volume type: ' + volumeInfo.volumeType); 760 } 761 isReadOnly = volumeInfo.isReadOnly; 762 isRootEntry = util.isSameEntry(entry, volumeInfo.fileSystem.root); 763 } 764 765 return new EntryLocation(volumeInfo, rootType, isRootEntry, isReadOnly); 766}; 767 768/** 769 * @param {string} key Key produced by |makeRequestKey_|. 770 * @param {function(VolumeInfo)} successCallback To be called when the request 771 * finishes successfully. 772 * @param {function(VolumeManagerCommon.VolumeError)} errorCallback To be called 773 * when the request fails. 774 * @private 775 */ 776VolumeManager.prototype.startRequest_ = function(key, 777 successCallback, errorCallback) { 778 if (key in this.requests_) { 779 var request = this.requests_[key]; 780 request.successCallbacks.push(successCallback); 781 request.errorCallbacks.push(errorCallback); 782 } else { 783 this.requests_[key] = { 784 successCallbacks: [successCallback], 785 errorCallbacks: [errorCallback], 786 787 timeout: setTimeout(this.onTimeout_.bind(this, key), 788 VolumeManager.TIMEOUT) 789 }; 790 } 791}; 792 793/** 794 * Called if no response received in |TIMEOUT|. 795 * @param {string} key Key produced by |makeRequestKey_|. 796 * @private 797 */ 798VolumeManager.prototype.onTimeout_ = function(key) { 799 this.invokeRequestCallbacks_(this.requests_[key], 800 VolumeManagerCommon.VolumeError.TIMEOUT); 801 delete this.requests_[key]; 802}; 803 804/** 805 * @param {string} key Key produced by |makeRequestKey_|. 806 * @param {VolumeManagerCommon.VolumeError|'success'} status Status received 807 * from the API. 808 * @param {VolumeInfo=} opt_volumeInfo Volume info of the mounted volume. 809 * @private 810 */ 811VolumeManager.prototype.finishRequest_ = function(key, status, opt_volumeInfo) { 812 var request = this.requests_[key]; 813 if (!request) 814 return; 815 816 clearTimeout(request.timeout); 817 this.invokeRequestCallbacks_(request, status, opt_volumeInfo); 818 delete this.requests_[key]; 819}; 820 821/** 822 * @param {Object} request Structure created in |startRequest_|. 823 * @param {VolumeManagerCommon.VolumeError|string} status If status === 824 * 'success' success callbacks are called. 825 * @param {VolumeInfo=} opt_volumeInfo Volume info of the mounted volume. 826 * @private 827 */ 828VolumeManager.prototype.invokeRequestCallbacks_ = function( 829 request, status, opt_volumeInfo) { 830 var callEach = function(callbacks, self, args) { 831 for (var i = 0; i < callbacks.length; i++) { 832 callbacks[i].apply(self, args); 833 } 834 }; 835 if (status === 'success') { 836 callEach(request.successCallbacks, this, [opt_volumeInfo]); 837 } else { 838 volumeManagerUtil.validateError(status); 839 callEach(request.errorCallbacks, this, [status]); 840 } 841}; 842 843/** 844 * Location information which shows where the path points in FileManager's 845 * file system. 846 * 847 * @param {!VolumeInfo} volumeInfo Volume information. 848 * @param {VolumeManagerCommon.RootType} rootType Root type. 849 * @param {boolean} isRootEntry Whether the entry is root entry or not. 850 * @param {boolean} isReadOnly Whether the entry is read only or not. 851 * @constructor 852 */ 853function EntryLocation(volumeInfo, rootType, isRootEntry, isReadOnly) { 854 /** 855 * Volume information. 856 * @type {!VolumeInfo} 857 */ 858 this.volumeInfo = volumeInfo; 859 860 /** 861 * Root type. 862 * @type {VolumeManagerCommon.RootType} 863 */ 864 this.rootType = rootType; 865 866 /** 867 * Whether the entry is root entry or not. 868 * @type {boolean} 869 */ 870 this.isRootEntry = isRootEntry; 871 872 /** 873 * Whether the location obtained from the fake entry correspond to special 874 * searches. 875 * @type {boolean} 876 */ 877 this.isSpecialSearchRoot = 878 this.rootType === VolumeManagerCommon.RootType.DRIVE_OFFLINE || 879 this.rootType === VolumeManagerCommon.RootType.DRIVE_SHARED_WITH_ME || 880 this.rootType === VolumeManagerCommon.RootType.DRIVE_RECENT; 881 882 /** 883 * Whether the location is under Google Drive or a special search root which 884 * represents a special search from Google Drive. 885 * @type {boolean} 886 */ 887 this.isDriveBased = 888 this.rootType === VolumeManagerCommon.RootType.DRIVE || 889 this.rootType === VolumeManagerCommon.RootType.DRIVE_OTHER || 890 this.rootType === VolumeManagerCommon.RootType.DRIVE_SHARED_WITH_ME || 891 this.rootType === VolumeManagerCommon.RootType.DRIVE_RECENT || 892 this.rootType === VolumeManagerCommon.RootType.DRIVE_OFFLINE; 893 894 /** 895 * Whether the given path can be a target path of folder shortcut. 896 * @type {boolean} 897 */ 898 this.isEligibleForFolderShortcut = 899 !this.isSpecialSearchRoot && 900 !this.isRootEntry && 901 this.isDriveBased; 902 903 /** 904 * Whether the entry is read only or not. 905 * @type {boolean} 906 */ 907 this.isReadOnly = isReadOnly; 908 909 Object.freeze(this); 910} 911