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 {util.VolumeType} volumeType The type of the volume. 12 * @param {string} mountPath Where the volume is mounted. 13 * @param {string} volumeId ID of the volume. 14 * @param {DirectoryEntry} root The root directory entry of this volume. 15 * @param {string} error The error if an error is found. 16 * @param {string} deviceType The type of device ('usb'|'sd'|'optical'|'mobile' 17 * |'unknown') (as defined in chromeos/disks/disk_mount_manager.cc). 18 * Can be null. 19 * @param {boolean} isReadOnly True if the volume is read only. 20 * @param {!{displayName:string, isCurrentProfile:boolean}} profile Profile 21 * information. 22 * @constructor 23 */ 24function VolumeInfo( 25 volumeType, 26 mountPath, 27 volumeId, 28 root, 29 error, 30 deviceType, 31 isReadOnly, 32 profile) { 33 this.volumeType = volumeType; 34 // TODO(hidehiko): This should include FileSystem instance. 35 this.mountPath = mountPath; 36 this.volumeId = volumeId; 37 this.root = root; 38 39 // Note: This represents if the mounting of the volume is successfully done 40 // or not. (If error is empty string, the mount is successfully done). 41 // TODO(hidehiko): Rename to make this more understandable. 42 this.error = error; 43 this.deviceType = deviceType; 44 this.isReadOnly = isReadOnly; 45 this.profile = Object.freeze(profile); 46 47 // VolumeInfo is immutable. 48 Object.freeze(this); 49} 50 51/** 52 * Obtains a URL of the display root directory that users can see as a root. 53 * @return {string} URL of root entry. 54 */ 55VolumeInfo.prototype.getDisplayRootDirectoryURL = function() { 56 return this.root.toURL() + 57 (this.volumeType === util.VolumeType.DRIVE ? '/root' : ''); 58}; 59 60/** 61 * Obtains volume label. 62 * @return {string} Label for the volume. 63 */ 64VolumeInfo.prototype.getLabel = function() { 65 if (this.volumeType === util.VolumeType.DRIVE) 66 return str('DRIVE_DIRECTORY_LABEL'); 67 else 68 return PathUtil.getFolderLabel(this.mountPath); 69}; 70 71/** 72 * Utilities for volume manager implementation. 73 */ 74var volumeManagerUtil = {}; 75 76/** 77 * Throws an Error when the given error is not in util.VolumeError. 78 * @param {util.VolumeError} error Status string usually received from APIs. 79 */ 80volumeManagerUtil.validateError = function(error) { 81 for (var key in util.VolumeError) { 82 if (error === util.VolumeError[key]) 83 return; 84 } 85 86 throw new Error('Invalid mount error: ' + error); 87}; 88 89/** 90 * Returns the root entry of a volume mounted at mountPath. 91 * 92 * @param {string} mountPath The mounted path of the volume. 93 * @param {function(DirectoryEntry)} successCallback Called when the root entry 94 * is found. 95 * @param {function(FileError)} errorCallback Called when an error is found. 96 * @private 97 */ 98volumeManagerUtil.getRootEntry_ = function( 99 mountPath, successCallback, errorCallback) { 100 // We always request FileSystem here, because requestFileSystem() grants 101 // permissions if necessary, especially for Drive File System at first mount 102 // time. 103 // Note that we actually need to request FileSystem after multi file system 104 // support, so this will be more natural code then. 105 chrome.fileBrowserPrivate.requestFileSystem( 106 'compatible', 107 function(fileSystem) { 108 // TODO(hidehiko): chrome.runtime.lastError should have error reason. 109 if (!fileSystem) { 110 errorCallback(util.createFileError(FileError.NOT_FOUND_ERR)); 111 return; 112 } 113 114 fileSystem.root.getDirectory( 115 mountPath.substring(1), // Strip leading '/'. 116 {create: false}, successCallback, errorCallback); 117 }); 118}; 119 120/** 121 * Builds the VolumeInfo data from VolumeMetadata. 122 * @param {VolumeMetadata} volumeMetadata Metadata instance for the volume. 123 * @param {function(VolumeInfo)} callback Called on completion. 124 */ 125volumeManagerUtil.createVolumeInfo = function(volumeMetadata, callback) { 126 volumeManagerUtil.getRootEntry_( 127 volumeMetadata.mountPath, 128 function(entry) { 129 if (volumeMetadata.volumeType === util.VolumeType.DRIVE) { 130 // After file system is mounted, we "read" drive grand root 131 // entry at first. This triggers full feed fetch on background. 132 // Note: we don't need to handle errors here, because even if 133 // it fails, accessing to some path later will just become 134 // a fast-fetch and it re-triggers full-feed fetch. 135 entry.createReader().readEntries( 136 function() { /* do nothing */ }, 137 function(error) { 138 console.error( 139 'Triggering full feed fetch is failed: ' + 140 util.getFileErrorMnemonic(error.code)); 141 }); 142 } 143 callback(new VolumeInfo( 144 volumeMetadata.volumeType, 145 volumeMetadata.mountPath, 146 volumeMetadata.volumeId, 147 entry, 148 volumeMetadata.mountCondition, 149 volumeMetadata.deviceType, 150 volumeMetadata.isReadOnly, 151 volumeMetadata.profile)); 152 }, 153 function(fileError) { 154 console.error('Root entry is not found: ' + 155 volumeMetadata.mountPath + ', ' + 156 util.getFileErrorMnemonic(fileError.code)); 157 callback(new VolumeInfo( 158 volumeMetadata.volumeType, 159 volumeMetadata.mountPath, 160 volumeMetadata.volumeId, 161 null, // Root entry is not found. 162 volumeMetadata.mountCondition, 163 volumeMetadata.deviceType, 164 volumeMetadata.isReadOnly, 165 volumeMetadata.profile)); 166 }); 167}; 168 169/** 170 * The order of the volume list based on root type. 171 * @type {Array.<string>} 172 * @const 173 * @private 174 */ 175volumeManagerUtil.volumeListOrder_ = [ 176 RootType.DRIVE, RootType.DOWNLOADS, RootType.ARCHIVE, RootType.REMOVABLE 177]; 178 179/** 180 * Compares mount paths to sort the volume list order. 181 * @param {string} mountPath1 The mount path for the first volume. 182 * @param {string} mountPath2 The mount path for the second volume. 183 * @return {number} 0 if mountPath1 and mountPath2 are same, -1 if VolumeInfo 184 * mounted at mountPath1 should be listed before the one mounted at 185 * mountPath2, otherwise 1. 186 */ 187volumeManagerUtil.compareMountPath = function(mountPath1, mountPath2) { 188 var order1 = volumeManagerUtil.volumeListOrder_.indexOf( 189 PathUtil.getRootType(mountPath1)); 190 var order2 = volumeManagerUtil.volumeListOrder_.indexOf( 191 PathUtil.getRootType(mountPath2)); 192 if (order1 !== order2) 193 return order1 < order2 ? -1 : 1; 194 195 if (mountPath1 !== mountPath2) 196 return mountPath1 < mountPath2 ? -1 : 1; 197 198 // The path is same. 199 return 0; 200}; 201 202/** 203 * The container of the VolumeInfo for each mounted volume. 204 * @constructor 205 */ 206function VolumeInfoList() { 207 /** 208 * Holds VolumeInfo instances. 209 * @type {cr.ui.ArrayDataModel} 210 * @private 211 */ 212 this.model_ = new cr.ui.ArrayDataModel([]); 213 214 Object.freeze(this); 215} 216 217VolumeInfoList.prototype = { 218 get length() { return this.model_.length; } 219}; 220 221/** 222 * Adds the event listener to listen the change of volume info. 223 * @param {string} type The name of the event. 224 * @param {function(Event)} handler The handler for the event. 225 */ 226VolumeInfoList.prototype.addEventListener = function(type, handler) { 227 this.model_.addEventListener(type, handler); 228}; 229 230/** 231 * Removes the event listener. 232 * @param {string} type The name of the event. 233 * @param {function(Event)} handler The handler to be removed. 234 */ 235VolumeInfoList.prototype.removeEventListener = function(type, handler) { 236 this.model_.removeEventListener(type, handler); 237}; 238 239/** 240 * Adds the volumeInfo to the appropriate position. If there already exists, 241 * just replaces it. 242 * @param {VolumeInfo} volumeInfo The information of the new volume. 243 */ 244VolumeInfoList.prototype.add = function(volumeInfo) { 245 var index = this.findLowerBoundIndex_(volumeInfo.mountPath); 246 if (index < this.length && 247 this.item(index).mountPath === volumeInfo.mountPath) { 248 // Replace the VolumeInfo. 249 this.model_.splice(index, 1, volumeInfo); 250 } else { 251 // Insert the VolumeInfo. 252 this.model_.splice(index, 0, volumeInfo); 253 } 254}; 255 256/** 257 * Removes the VolumeInfo of the volume mounted at mountPath. 258 * @param {string} mountPath The path to the location where the volume is 259 * mounted. 260 */ 261VolumeInfoList.prototype.remove = function(mountPath) { 262 var index = this.findLowerBoundIndex_(mountPath); 263 if (index < this.length && this.item(index).mountPath === mountPath) 264 this.model_.splice(index, 1); 265}; 266 267/** 268 * Searches the information of the volume mounted at mountPath. 269 * @param {string} mountPath The path to the location where the volume is 270 * mounted. 271 * @return {VolumeInfo} The volume's information, or null if not found. 272 */ 273VolumeInfoList.prototype.find = function(mountPath) { 274 var index = this.findLowerBoundIndex_(mountPath); 275 if (index < this.length && this.item(index).mountPath === mountPath) 276 return this.item(index); 277 278 // Not found. 279 return null; 280}; 281 282/** 283 * Searches the information of the volume that contains an item pointed by the 284 * path. 285 * @param {string} path Path pointing an entry on a volume. 286 * @return {VolumeInfo} The volume's information, or null if not found. 287 */ 288VolumeInfoList.prototype.findByPath = function(path) { 289 for (var i = 0; i < this.length; i++) { 290 var mountPath = this.item(i).mountPath; 291 if (path === mountPath || path.indexOf(mountPath + '/') === 0) 292 return this.item(i); 293 } 294 return null; 295}; 296 297/** 298 * @param {string} mountPath The mount path of searched volume. 299 * @return {number} The index of the volume if found, or the inserting 300 * position of the volume. 301 * @private 302 */ 303VolumeInfoList.prototype.findLowerBoundIndex_ = function(mountPath) { 304 // Assuming the number of elements in the array data model is very small 305 // in most cases, use simple linear search, here. 306 for (var i = 0; i < this.length; i++) { 307 if (volumeManagerUtil.compareMountPath( 308 this.item(i).mountPath, mountPath) >= 0) 309 return i; 310 } 311 return this.length; 312}; 313 314/** 315 * @param {number} index The index of the volume in the list. 316 * @return {VolumeInfo} The VolumeInfo instance. 317 */ 318VolumeInfoList.prototype.item = function(index) { 319 return this.model_.item(index); 320}; 321 322/** 323 * VolumeManager is responsible for tracking list of mounted volumes. 324 * 325 * @constructor 326 * @extends {cr.EventTarget} 327 */ 328function VolumeManager() { 329 /** 330 * The list of archives requested to mount. We will show contents once 331 * archive is mounted, but only for mounts from within this filebrowser tab. 332 * @type {Object.<string, Object>} 333 * @private 334 */ 335 this.requests_ = {}; 336 337 /** 338 * The list of VolumeInfo instances for each mounted volume. 339 * @type {VolumeInfoList} 340 */ 341 this.volumeInfoList = new VolumeInfoList(); 342 343 // The status should be merged into VolumeManager. 344 // TODO(hidehiko): Remove them after the migration. 345 this.driveConnectionState_ = { 346 type: util.DriveConnectionType.OFFLINE, 347 reason: util.DriveConnectionReason.NO_SERVICE 348 }; 349 350 chrome.fileBrowserPrivate.onDriveConnectionStatusChanged.addListener( 351 this.onDriveConnectionStatusChanged_.bind(this)); 352 this.onDriveConnectionStatusChanged_(); 353} 354 355/** 356 * Invoked when the drive connection status is changed. 357 * @private_ 358 */ 359VolumeManager.prototype.onDriveConnectionStatusChanged_ = function() { 360 chrome.fileBrowserPrivate.getDriveConnectionState(function(state) { 361 this.driveConnectionState_ = state; 362 cr.dispatchSimpleEvent(this, 'drive-connection-changed'); 363 }.bind(this)); 364}; 365 366/** 367 * Returns the drive connection state. 368 * @return {util.DriveConnectionType} Connection type. 369 */ 370VolumeManager.prototype.getDriveConnectionState = function() { 371 return this.driveConnectionState_; 372}; 373 374/** 375 * VolumeManager extends cr.EventTarget. 376 */ 377VolumeManager.prototype.__proto__ = cr.EventTarget.prototype; 378 379/** 380 * Time in milliseconds that we wait a response for. If no response on 381 * mount/unmount received the request supposed failed. 382 */ 383VolumeManager.TIMEOUT = 15 * 60 * 1000; 384 385/** 386 * Queue to run getInstance sequentially. 387 * @type {AsyncUtil.Queue} 388 * @private 389 */ 390VolumeManager.getInstanceQueue_ = new AsyncUtil.Queue(); 391 392/** 393 * The singleton instance of VolumeManager. Initialized by the first invocation 394 * of getInstance(). 395 * @type {VolumeManager} 396 * @private 397 */ 398VolumeManager.instance_ = null; 399 400/** 401 * Returns the VolumeManager instance asynchronously. If it is not created or 402 * under initialization, it will waits for the finish of the initialization. 403 * @param {function(VolumeManager)} callback Called with the VolumeManager 404 * instance. 405 */ 406VolumeManager.getInstance = function(callback) { 407 VolumeManager.getInstanceQueue_.run(function(continueCallback) { 408 if (VolumeManager.instance_) { 409 callback(VolumeManager.instance_); 410 continueCallback(); 411 return; 412 } 413 414 VolumeManager.instance_ = new VolumeManager(); 415 VolumeManager.instance_.initialize_(function() { 416 callback(VolumeManager.instance_); 417 continueCallback(); 418 }); 419 }); 420}; 421 422/** 423 * Initializes mount points. 424 * @param {function()} callback Called upon the completion of the 425 * initialization. 426 * @private 427 */ 428VolumeManager.prototype.initialize_ = function(callback) { 429 chrome.fileBrowserPrivate.getVolumeMetadataList(function(volumeMetadataList) { 430 // Create VolumeInfo for each volume. 431 var group = new AsyncUtil.Group(); 432 for (var i = 0; i < volumeMetadataList.length; i++) { 433 group.add(function(volumeMetadata, continueCallback) { 434 volumeManagerUtil.createVolumeInfo( 435 volumeMetadata, 436 function(volumeInfo) { 437 this.volumeInfoList.add(volumeInfo); 438 if (volumeMetadata.volumeType === util.VolumeType.DRIVE) 439 this.onDriveConnectionStatusChanged_(); 440 continueCallback(); 441 }.bind(this)); 442 }.bind(this, volumeMetadataList[i])); 443 } 444 445 // Then, finalize the initialization. 446 group.run(function() { 447 // Subscribe to the mount completed event when mount points initialized. 448 chrome.fileBrowserPrivate.onMountCompleted.addListener( 449 this.onMountCompleted_.bind(this)); 450 callback(); 451 }.bind(this)); 452 }.bind(this)); 453}; 454 455/** 456 * Event handler called when some volume was mounted or unmounted. 457 * @param {MountCompletedEvent} event Received event. 458 * @private 459 */ 460VolumeManager.prototype.onMountCompleted_ = function(event) { 461 if (event.eventType === 'mount') { 462 if (event.volumeMetadata.mountPath) { 463 var requestKey = this.makeRequestKey_( 464 'mount', 465 event.volumeMetadata.sourcePath); 466 467 var error = event.status === 'success' ? '' : event.status; 468 469 volumeManagerUtil.createVolumeInfo( 470 event.volumeMetadata, 471 function(volumeInfo) { 472 this.volumeInfoList.add(volumeInfo); 473 this.finishRequest_(requestKey, event.status, volumeInfo.mountPath); 474 475 if (volumeInfo.volumeType === util.VolumeType.DRIVE) { 476 // Update the network connection status, because until the 477 // drive is initialized, the status is set to not ready. 478 // TODO(hidehiko): The connection status should be migrated into 479 // VolumeMetadata. 480 this.onDriveConnectionStatusChanged_(); 481 } 482 }.bind(this)); 483 } else { 484 console.warn('No mount path.'); 485 this.finishRequest_(requestKey, event.status); 486 } 487 } else if (event.eventType === 'unmount') { 488 var mountPath = event.volumeMetadata.mountPath; 489 var status = event.status; 490 if (status === util.VolumeError.PATH_UNMOUNTED) { 491 console.warn('Volume already unmounted: ', mountPath); 492 status = 'success'; 493 } 494 var requestKey = this.makeRequestKey_('unmount', mountPath); 495 var requested = requestKey in this.requests_; 496 var volumeInfo = this.volumeInfoList.find(mountPath); 497 if (event.status === 'success' && !requested && volumeInfo) { 498 console.warn('Mounted volume without a request: ', mountPath); 499 var e = new Event('externally-unmounted'); 500 // TODO(mtomasz): The mountPath field is deprecated. Remove it. 501 e.mountPath = mountPath; 502 e.volumeInfo = volumeInfo; 503 this.dispatchEvent(e); 504 } 505 this.finishRequest_(requestKey, status); 506 507 if (event.status === 'success') 508 this.volumeInfoList.remove(mountPath); 509 } 510}; 511 512/** 513 * Creates string to match mount events with requests. 514 * @param {string} requestType 'mount' | 'unmount'. TODO(hidehiko): Replace by 515 * enum. 516 * @param {string} path Source path provided by API for mount request, or 517 * mount path for unmount request. 518 * @return {string} Key for |this.requests_|. 519 * @private 520 */ 521VolumeManager.prototype.makeRequestKey_ = function(requestType, path) { 522 return requestType + ':' + path; 523}; 524 525/** 526 * @param {string} fileUrl File url to the archive file. 527 * @param {function(string)} successCallback Success callback. 528 * @param {function(util.VolumeError)} errorCallback Error callback. 529 */ 530VolumeManager.prototype.mountArchive = function( 531 fileUrl, successCallback, errorCallback) { 532 chrome.fileBrowserPrivate.addMount(fileUrl, function(sourcePath) { 533 console.info( 534 'Mount request: url=' + fileUrl + '; sourceUrl=' + sourcePath); 535 var requestKey = this.makeRequestKey_('mount', sourcePath); 536 this.startRequest_(requestKey, successCallback, errorCallback); 537 }.bind(this)); 538}; 539 540/** 541 * Unmounts volume. 542 * @param {string} mountPath Volume mounted path. 543 * @param {function(string)} successCallback Success callback. 544 * @param {function(util.VolumeError)} errorCallback Error callback. 545 */ 546VolumeManager.prototype.unmount = function(mountPath, 547 successCallback, 548 errorCallback) { 549 var volumeInfo = this.volumeInfoList.find(mountPath); 550 if (!volumeInfo) { 551 errorCallback(util.VolumeError.NOT_MOUNTED); 552 return; 553 } 554 555 chrome.fileBrowserPrivate.removeMount(util.makeFilesystemUrl(mountPath)); 556 var requestKey = this.makeRequestKey_('unmount', volumeInfo.mountPath); 557 this.startRequest_(requestKey, successCallback, errorCallback); 558}; 559 560/** 561 * Resolves the absolute path to its entry. Shouldn't be used outside of the 562 * Files app's initialization. 563 * @param {string} path The path to be resolved. 564 * @param {function(Entry)} successCallback Called with the resolved entry on 565 * success. 566 * @param {function(FileError)} errorCallback Called on error. 567 */ 568VolumeManager.prototype.resolveAbsolutePath = function( 569 path, successCallback, errorCallback) { 570 // Make sure the path is in the mounted volume. 571 var volumeInfo = this.getVolumeInfo(path); 572 if (!volumeInfo || !volumeInfo.root) { 573 errorCallback(util.createFileError(FileError.NOT_FOUND_ERR)); 574 return; 575 } 576 577 webkitResolveLocalFileSystemURL( 578 util.makeFilesystemUrl(path), successCallback, errorCallback); 579}; 580 581/** 582 * Obtains the information of the volume that containing an entry pointed by the 583 * specified path. 584 * TODO(hirono): Stop to use path to get a volume info. 585 * 586 * @param {string|Entry} target Path or Entry pointing anywhere on a volume. 587 * @return {VolumeInfo} The data about the volume. 588 */ 589VolumeManager.prototype.getVolumeInfo = function(target) { 590 if (typeof target === 'string') 591 return this.volumeInfoList.findByPath(target); 592 else if (util.isFakeEntry(target)) 593 return this.getCurrentProfileVolumeInfo(util.VolumeType.DRIVE); 594 else 595 return this.volumeInfoList.findByPath(target.fullPath); 596}; 597 598/** 599 * Obtains a volume information from a file entry URL. 600 * TODO(hirono): Check a file system to find a volume. 601 * 602 * @param {string} url URL of entry. 603 * @return {VolumeInfo} Volume info. 604 */ 605VolumeManager.prototype.getVolumeInfoByURL = function(url) { 606 return this.getVolumeInfo(util.extractFilePath(url)); 607}; 608 609/** 610 * Obtains a volume infomration of the current profile. 611 * 612 * @param {util.VolumeType} volumeType Volume type. 613 * @return {VolumeInfo} Volume info. 614 */ 615VolumeManager.prototype.getCurrentProfileVolumeInfo = function(volumeType) { 616 for (var i = 0; i < this.volumeInfoList.length; i++) { 617 var volumeInfo = this.volumeInfoList.item(i); 618 if (volumeInfo.profile.isCurrentProfile && 619 volumeInfo.volumeType === volumeType) 620 return volumeInfo; 621 } 622 return null; 623}; 624 625/** 626 * Obtains location information from an entry. 627 * 628 * @param {Entry|Object} entry File or directory entry. It can be a fake entry. 629 * @return {EntryLocation} Location information. 630 */ 631VolumeManager.prototype.getLocationInfo = function(entry) { 632 if (util.isFakeEntry(entry)) { 633 return new EntryLocation( 634 // TODO(hirono): Specify currect volume. 635 this.getCurrentProfileVolumeInfo(RootType.DRIVE), 636 entry.rootType, 637 true /* the entry points a root directory. */); 638 } else { 639 return this.getLocationInfoByPath(entry.fullPath); 640 } 641}; 642 643/** 644 * Obtains location information from a path. 645 * TODO(hirono): Remove the method before introducing separate file system. 646 * 647 * @param {string} path Path. 648 * @return {EntryLocation} Location information. 649 */ 650VolumeManager.prototype.getLocationInfoByPath = function(path) { 651 var volumeInfo = this.volumeInfoList.findByPath(path); 652 return volumeInfo && PathUtil.getLocationInfo(volumeInfo, path); 653}; 654 655/** 656 * @param {string} key Key produced by |makeRequestKey_|. 657 * @param {function(string)} successCallback To be called when request finishes 658 * successfully. 659 * @param {function(util.VolumeError)} errorCallback To be called when 660 * request fails. 661 * @private 662 */ 663VolumeManager.prototype.startRequest_ = function(key, 664 successCallback, errorCallback) { 665 if (key in this.requests_) { 666 var request = this.requests_[key]; 667 request.successCallbacks.push(successCallback); 668 request.errorCallbacks.push(errorCallback); 669 } else { 670 this.requests_[key] = { 671 successCallbacks: [successCallback], 672 errorCallbacks: [errorCallback], 673 674 timeout: setTimeout(this.onTimeout_.bind(this, key), 675 VolumeManager.TIMEOUT) 676 }; 677 } 678}; 679 680/** 681 * Called if no response received in |TIMEOUT|. 682 * @param {string} key Key produced by |makeRequestKey_|. 683 * @private 684 */ 685VolumeManager.prototype.onTimeout_ = function(key) { 686 this.invokeRequestCallbacks_(this.requests_[key], 687 util.VolumeError.TIMEOUT); 688 delete this.requests_[key]; 689}; 690 691/** 692 * @param {string} key Key produced by |makeRequestKey_|. 693 * @param {util.VolumeError|'success'} status Status received from the API. 694 * @param {string=} opt_mountPath Mount path. 695 * @private 696 */ 697VolumeManager.prototype.finishRequest_ = function(key, status, opt_mountPath) { 698 var request = this.requests_[key]; 699 if (!request) 700 return; 701 702 clearTimeout(request.timeout); 703 this.invokeRequestCallbacks_(request, status, opt_mountPath); 704 delete this.requests_[key]; 705}; 706 707/** 708 * @param {Object} request Structure created in |startRequest_|. 709 * @param {util.VolumeError|string} status If status === 'success' 710 * success callbacks are called. 711 * @param {string=} opt_mountPath Mount path. Required if success. 712 * @private 713 */ 714VolumeManager.prototype.invokeRequestCallbacks_ = function(request, status, 715 opt_mountPath) { 716 var callEach = function(callbacks, self, args) { 717 for (var i = 0; i < callbacks.length; i++) { 718 callbacks[i].apply(self, args); 719 } 720 }; 721 if (status === 'success') { 722 callEach(request.successCallbacks, this, [opt_mountPath]); 723 } else { 724 volumeManagerUtil.validateError(status); 725 callEach(request.errorCallbacks, this, [status]); 726 } 727}; 728