blob: 8f8419a007614ba171cc6c2909e7f769e86295cd [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';
/**
* Represents each volume, such as "drive", "download directory", each "USB
* flush storage", or "mounted zip archive" etc.
*
* @param {string} mountPath Where the volume is mounted.
* @param {DirectoryEntry} root The root directory entry of this volume.
* @param {string} error The error if an error is found.
* @param {string} deviceType The type of device ('usb'|'sd'|'optical'|'mobile'
* |'unknown') (as defined in chromeos/disks/disk_mount_manager.cc).
* Can be null.
* @param {boolean} isReadOnly True if the volume is read only.
* @constructor
*/
function VolumeInfo(mountPath, root, error, deviceType, isReadOnly) {
// TODO(hidehiko): This should include FileSystem instance.
this.mountPath = mountPath;
this.root = root;
// Note: This represents if the mounting of the volume is successfully done
// or not. (If error is empty string, the mount is successfully done).
// TODO(hidehiko): Rename to make this more understandable.
this.error = error;
this.deviceType = deviceType;
this.isReadOnly = isReadOnly;
// VolumeInfo is immutable.
Object.freeze(this);
}
/**
* Utilities for volume manager implementation.
*/
var volumeManagerUtil = {};
/**
* Throws an Error when the given error is not in VolumeManager.Error.
* @param {VolumeManager.Error} error Status string usually received from APIs.
*/
volumeManagerUtil.validateError = function(error) {
for (var key in VolumeManager.Error) {
if (error == VolumeManager.Error[key])
return;
}
throw new Error('Invalid mount error: ' + error);
};
/**
* The regex pattern which matches valid mount paths.
* The valid paths are:
* - Either of '/drive', '/drive_shared_with_me', '/drive_offline',
* '/drive_recent' or '/Download'
* - For archive, drive, removable can have (exactly one) sub directory in the
* root path. E.g. '/arhive/foo', '/removable/usb1' etc.
*
* @type {RegExp}
* @private
*/
volumeManagerUtil.validateMountPathRegExp_ = new RegExp(
'^/(drive|drive_shared_with_me|drive_offline|drive_recent|Downloads|' +
'((archive|drive|removable)\/[^/]+))$');
/**
* Throws an Error if the validation fails.
* @param {string} mountPath The target path of the validation.
*/
volumeManagerUtil.validateMountPath = function(mountPath) {
if (!volumeManagerUtil.validateMountPathRegExp_.test(mountPath))
throw new Error('Invalid mount path: ' + mountPath);
};
/**
* Returns the root entry of a volume mounted at mountPath.
*
* @param {string} mountPath The mounted path of the volume.
* @param {function(DirectoryEntry)} successCallback Called when the root entry
* is found.
* @param {function(FileError)} errorCallback Called when an error is found.
* @private
*/
volumeManagerUtil.getRootEntry_ = function(
mountPath, successCallback, errorCallback) {
// We always request FileSystem here, because requestFileSystem() grants
// permissions if necessary, especially for Drive File System at first mount
// time.
// Note that we actually need to request FileSystem after multi file system
// support, so this will be more natural code then.
chrome.fileBrowserPrivate.requestFileSystem(
'compatible',
function(fileSystem) {
// TODO(hidehiko): chrome.runtime.lastError should have error reason.
if (!fileSystem) {
errorCallback(util.createFileError(FileError.NOT_FOUND_ERR));
return;
}
fileSystem.root.getDirectory(
mountPath.substring(1), // Strip leading '/'.
{create: false}, successCallback, errorCallback);
});
};
/**
* Builds the VolumeInfo data for mountPath.
* @param {string} mountPath Path to the volume.
* @param {VolumeManager.Error} error The error string if available.
* @param {function(Object)} callback Called on completion.
* TODO(hidehiko): Replace the type from Object to its original type.
*/
volumeManagerUtil.createVolumeInfo = function(mountPath, error, callback) {
// Validation of the input.
volumeManagerUtil.validateMountPath(mountPath);
if (error)
volumeManagerUtil.validateError(error);
// TODO(hidehiko): Do we really need to create a volume info for error
// case?
chrome.fileBrowserPrivate.getVolumeMetadata(
util.makeFilesystemUrl(mountPath),
function(metadata) {
if (chrome.runtime.lastError && !error)
error = VolumeManager.Error.UNKNOWN;
// TODO(hidehiko): These values should be merged into the result of
// onMountCompleted's event object. crbug.com/284975.
var deviceType = null;
var isReadOnly = false;
if (metadata) {
deviceType = metadata.deviceType;
isReadOnly = metadata.isReadOnly;
}
volumeManagerUtil.getRootEntry_(
mountPath,
function(entry) {
if (mountPath == RootDirectory.DRIVE) {
// After file system is mounted, we "read" drive grand root
// entry at first. This triggers full feed fetch on background.
// Note: we don't need to handle errors here, because even if
// it fails, accessing to some path later will just become
// a fast-fetch and it re-triggers full-feed fetch.
entry.createReader().readEntries(
function() { /* do nothing */ },
function(error) {
console.error(
'Triggering full feed fetch is failed: ' +
util.getFileErrorMnemonic(error.code));
});
}
callback(new VolumeInfo(
mountPath, entry, error, deviceType, isReadOnly));
},
function(fileError) {
console.error('Root entry is not found: ' +
mountPath + ', ' + util.getFileErrorMnemonic(fileError.code));
if (!error)
error = VolumeManager.Error.UNKNOWN;
callback(new VolumeInfo(
mountPath, null, error, deviceType, isReadOnly));
});
});
};
/**
* The order of the volume list based on root type.
* @type {Array.<string>}
* @const
* @private
*/
volumeManagerUtil.volumeListOrder_ = [
RootType.DRIVE, RootType.DOWNLOADS, RootType.ARCHIVE, RootType.REMOVABLE
];
/**
* Compares mount paths to sort the volume list order.
* @param {string} mountPath1 The mount path for the first volume.
* @param {string} mountPath2 The mount path for the second volume.
* @return {number} 0 if mountPath1 and mountPath2 are same, -1 if VolumeInfo
* mounted at mountPath1 should be listed before the one mounted at
* mountPath2, otherwise 1.
*/
volumeManagerUtil.compareMountPath = function(mountPath1, mountPath2) {
var order1 = volumeManagerUtil.volumeListOrder_.indexOf(
PathUtil.getRootType(mountPath1));
var order2 = volumeManagerUtil.volumeListOrder_.indexOf(
PathUtil.getRootType(mountPath2));
if (order1 != order2)
return order1 < order2 ? -1 : 1;
if (mountPath1 != mountPath2)
return mountPath1 < mountPath2 ? -1 : 1;
// The path is same.
return 0;
};
/**
* The container of the VolumeInfo for each mounted volume.
* @constructor
*/
function VolumeInfoList() {
/**
* Holds VolumeInfo instances.
* @type {cr.ui.ArrayDataModel}
* @private
*/
this.model_ = new cr.ui.ArrayDataModel([]);
Object.freeze(this);
}
VolumeInfoList.prototype = {
get length() { return this.model_.length; }
};
/**
* Adds the event listener to listen the change of volume info.
* @param {string} type The name of the event.
* @param {function(cr.Event)} handler The handler for the event.
*/
VolumeInfoList.prototype.addEventListener = function(type, handler) {
this.model_.addEventListener(type, handler);
};
/**
* Removes the event listener.
* @param {string} type The name of the event.
* @param {function(cr.Event)} handler The handler to be removed.
*/
VolumeInfoList.prototype.removeEventListener = function(type, handler) {
this.model_.removeEventListener(type, handler);
};
/**
* Adds the volumeInfo to the appropriate position. If there already exists,
* just replaces it.
* @param {VolumeInfo} volumeInfo The information of the new volume.
*/
VolumeInfoList.prototype.add = function(volumeInfo) {
var index = this.findLowerBoundIndex_(volumeInfo.mountPath);
if (index < this.length &&
this.item(index).mountPath == volumeInfo.mountPath) {
// Replace the VolumeInfo.
this.model_.splice(index, 1, volumeInfo);
} else {
// Insert the VolumeInfo.
this.model_.splice(index, 0, volumeInfo);
}
};
/**
* Removes the VolumeInfo of the volume mounted at mountPath.
* @param {string} mountPath The path to the location where the volume is
* mounted.
*/
VolumeInfoList.prototype.remove = function(mountPath) {
var index = this.findLowerBoundIndex_(mountPath);
if (index < this.length && this.item(index).mountPath == mountPath)
this.model_.splice(index, 1);
};
/**
* Searches the information of the volume mounted at mountPath.
* @param {string} mountPath The path to the location where the volume is
* mounted.
* @return {VolumeInfo} The volume's information, or null if not found.
*/
VolumeInfoList.prototype.find = function(mountPath) {
var index = this.findLowerBoundIndex_(mountPath);
if (index < this.length && this.item(index).mountPath == mountPath)
return this.item(index);
// Not found.
return null;
};
/**
* @param {string} mountPath The mount path of searched volume.
* @return {number} The index of the volumee if found, or the inserting
* position of the volume.
* @private
*/
VolumeInfoList.prototype.findLowerBoundIndex_ = function(mountPath) {
// Assuming the number of elements in the array data model is very small
// in most cases, use simple linear search, here.
for (var i = 0; i < this.length; i++) {
if (volumeManagerUtil.compareMountPath(
this.item(i).mountPath, mountPath) >= 0)
return i;
}
return this.length;
};
/**
* @param {number} index The index of the volume in the list.
* @return {VolumeInfo} The VolumeInfo instance.
*/
VolumeInfoList.prototype.item = function(index) {
return this.model_.item(index);
};
/**
* VolumeManager is responsible for tracking list of mounted volumes.
*
* @constructor
* @extends {cr.EventTarget}
*/
function VolumeManager() {
/**
* The list of archives requested to mount. We will show contents once
* archive is mounted, but only for mounts from within this filebrowser tab.
* @type {Object.<string, Object>}
* @private
*/
this.requests_ = {};
/**
* The list of VolumeInfo instances for each mounted volume.
* @type {VolumeInfoList}
*/
this.volumeInfoList = new VolumeInfoList();
/**
* True, if mount points have been initialized.
* TODO(hidehiko): Remove this by returning the VolumeManager instance
* after the initialization is done.
* @type {boolean}
* @private
*/
this.ready_ = false;
/**
* True if Drive file system is enabled.
* TODO(hidehiko): This should be migrated into the multi-file system
* support. If drive file system is disabled, Files.app should receive
* unmounted event and UI should hide Drive.
* @type {boolean}
*/
this.driveEnabled = false;
this.initMountPoints_();
// These status should be merged into VolumeManager.
// TODO(hidehiko): Remove them after the migration.
this.driveStatus_ = VolumeManager.DriveStatus.UNMOUNTED;
this.driveConnectionState_ = {
type: VolumeManager.DriveConnectionType.OFFLINE,
reasons: VolumeManager.DriveConnectionType.NO_SERVICE
};
chrome.fileBrowserPrivate.onDriveConnectionStatusChanged.addListener(
this.onDriveConnectionStatusChanged_.bind(this));
this.onDriveConnectionStatusChanged_();
}
/**
* Invoked when the drive connection status is changed.
* @private_
*/
VolumeManager.prototype.onDriveConnectionStatusChanged_ = function() {
chrome.fileBrowserPrivate.getDriveConnectionState(function(state) {
this.driveConnectionState_ = state;
cr.dispatchSimpleEvent(this, 'drive-connection-changed');
}.bind(this));
};
/**
* Returns the drive connection state.
* @return {VolumeManager.DriveConnectionType} Connection type.
*/
VolumeManager.prototype.getDriveConnectionState = function() {
return this.driveConnectionState_;
};
/**
* VolumeManager extends cr.EventTarget.
*/
VolumeManager.prototype.__proto__ = cr.EventTarget.prototype;
/**
* @enum
*/
VolumeManager.Error = {
/* Internal errors */
NOT_MOUNTED: 'not_mounted',
TIMEOUT: 'timeout',
/* System events */
UNKNOWN: 'error_unknown',
INTERNAL: 'error_internal',
UNKNOWN_FILESYSTEM: 'error_unknown_filesystem',
UNSUPPORTED_FILESYSTEM: 'error_unsupported_filesystem',
INVALID_ARCHIVE: 'error_invalid_archive',
AUTHENTICATION: 'error_authentication',
PATH_UNMOUNTED: 'error_path_unmounted'
};
/**
* @enum
*/
VolumeManager.DriveStatus = {
UNMOUNTED: 'unmounted',
ERROR: 'error',
MOUNTED: 'mounted'
};
/**
* List of connection types of drive.
*
* Keep this in sync with the kDriveConnectionType* constants in
* file_browser_private_api.cc.
*
* @enum {string}
*/
VolumeManager.DriveConnectionType = {
OFFLINE: 'offline', // Connection is offline or drive is unavailable.
METERED: 'metered', // Connection is metered. Should limit traffic.
ONLINE: 'online' // Connection is online.
};
/**
* List of reasons of DriveConnectionType.
*
* Keep this in sync with the kDriveConnectionReason constants in
* file_browser_private_api.cc.
*
* @enum {string}
*/
VolumeManager.DriveConnectionReason = {
NOT_READY: 'not_ready', // Drive is not ready or authentication is failed.
NO_NETWORK: 'no_network', // Network connection is unavailable.
NO_SERVICE: 'no_service' // Drive service is unavailable.
};
/**
* Time in milliseconds that we wait a respone for. If no response on
* mount/unmount received the request supposed failed.
*/
VolumeManager.TIMEOUT = 15 * 60 * 1000;
/**
* The singleton instance of VolumeManager. Initialized by the first invocation
* of getInstance().
* @type {VolumeManager}
* @private
*/
VolumeManager.instance_ = null;
/**
* @param {function(VolumeManager)} callback Callback to obtain VolumeManager
* instance.
*/
VolumeManager.getInstance = function(callback) {
if (!VolumeManager.instance_)
VolumeManager.instance_ = new VolumeManager();
callback(VolumeManager.instance_);
};
/**
* Enables/disables Drive file system. Dispatches
* 'drive-enabled-status-changed' event, when the enabled state is actually
* changed.
* TODO(hidehiko): Enable/Disable of drive based on preference is managed by
* backend (C++) layer. Remove this.
* @param {boolean} enabled True if Drive file system is enabled.
*/
VolumeManager.prototype.setDriveEnabled = function(enabled) {
if (this.driveEnabled == enabled)
return;
this.driveEnabled = enabled;
};
/**
* @param {VolumeManager.DriveStatus} newStatus New DRIVE status.
* @private
*/
VolumeManager.prototype.setDriveStatus_ = function(newStatus) {
if (this.driveStatus_ != newStatus) {
this.driveStatus_ = newStatus;
cr.dispatchSimpleEvent(this, 'drive-status-changed');
}
};
/**
* @return {boolean} True if already initialized.
*/
VolumeManager.prototype.isReady = function() {
return this.ready_;
};
/**
* Initialized mount points.
* @private
*/
VolumeManager.prototype.initMountPoints_ = function() {
this.deferredQueue_ = [];
chrome.fileBrowserPrivate.getMountPoints(function(mountPointList) {
// According to the C++ implementation, getMountPoints only looks at
// the connected devices (such as USB memory), and doesn't return anything
// about Drive, Downloads nor archives.
// TODO(hidehiko): Figure out the historical intention of the method,
// and clean them up.
// Create VolumeInfo for each mount point.
var group = new AsyncUtil.Group();
for (var i = 0; i < mountPointList.length; i++) {
group.add(function(mountPoint, callback) {
var error = mountPoint.mountCondition ?
'error_' + mountPoint.mountCondition : '';
volumeManagerUtil.createVolumeInfo(
'/' + mountPoint.mountPath, error,
function(volumeInfo) {
this.volumeInfoList.add(volumeInfo);
if (mountPoint.volumeType == 'drive') {
// Set Drive status here.
this.setDriveStatus_(
volumeInfo.error ? VolumeManager.DriveStatus.ERROR :
VolumeManager.DriveStatus.MOUNTED);
this.onDriveConnectionStatusChanged_();
}
callback();
}.bind(this));
}.bind(this, mountPointList[i]));
}
// Then, finalize the initialization.
group.run(function() {
// Subscribe to the mount completed event when mount points initialized.
chrome.fileBrowserPrivate.onMountCompleted.addListener(
this.onMountCompleted_.bind(this));
// Run pending tasks.
var deferredQueue = this.deferredQueue_;
this.deferredQueue_ = null;
for (var i = 0; i < deferredQueue.length; i++) {
deferredQueue[i]();
}
// Now, the initialization is completed. Set the state to the ready.
this.ready_ = true;
// Notify event listeners that the initialization is done.
cr.dispatchSimpleEvent(this, 'ready');
if (mountPointList.length > 0)
cr.dispatchSimpleEvent(self, 'change');
}.bind(this));
}.bind(this));
};
/**
* Event handler called when some volume was mounted or unmouted.
* @param {MountCompletedEvent} event Received event.
* @private
*/
VolumeManager.prototype.onMountCompleted_ = function(event) {
if (event.eventType == 'mount') {
if (event.mountPath) {
var requestKey = this.makeRequestKey_(
'mount', event.volumeType, event.sourcePath);
var error = event.status == 'success' ? '' : event.status;
volumeManagerUtil.createVolumeInfo(
event.mountPath, error, function(volume) {
this.volumeInfoList.add(volume);
this.finishRequest_(requestKey, event.status, event.mountPath);
cr.dispatchSimpleEvent(this, 'change');
// For mounting Drive File System, we need to update some
// VolumeManager's state.
if (event.volumeType == 'drive') {
// Set Drive status here.
this.setDriveStatus_(
volume.error ? VolumeManager.DriveStatus.ERROR :
VolumeManager.DriveStatus.MOUNTED);
// Also update the network connection status, because until the
// drive is initialized, the status is set to not ready.
// TODO(hidehiko): The connection status should be migrated into
// VolumeMetadata.
this.onDriveConnectionStatusChanged_();
}
}.bind(this));
} else {
console.warn('No mount path.');
this.finishRequest_(requestKey, event.status);
if (event.volumeType == 'drive')
this.setDriveStatus_(VolumeManager.DriveStatus.ERROR);
}
} else if (event.eventType == 'unmount') {
var mountPath = event.mountPath;
volumeManagerUtil.validateMountPath(mountPath);
var status = event.status;
if (status == VolumeManager.Error.PATH_UNMOUNTED) {
console.warn('Volume already unmounted: ', mountPath);
status = 'success';
}
var requestKey = this.makeRequestKey_('unmount', '', event.mountPath);
var requested = requestKey in this.requests_;
if (event.status == 'success' && !requested &&
this.volumeInfoList.find(mountPath)) {
console.warn('Mounted volume without a request: ', mountPath);
var e = new cr.Event('externally-unmounted');
e.mountPath = mountPath;
this.dispatchEvent(e);
}
this.finishRequest_(requestKey, status);
if (event.status == 'success') {
this.volumeInfoList.remove(mountPath);
cr.dispatchSimpleEvent(this, 'change');
if (event.volumeType == 'drive')
this.setDriveStatus_(VolumeManager.DriveStatus.UNMOUNTED);
} else {
if (event.volumeType == 'drive')
this.setDriveStatus_(VolumeManager.DriveStatus.ERROR);
}
}
};
/**
* Creates string to match mount events with requests.
* @param {string} requestType 'mount' | 'unmount'.
* @param {string} volumeType 'drive' | 'downloads' | 'removable' | 'archive'.
* @param {string} mountOrSourcePath Source path provided by API after
* resolving mount request or mountPath for unmount request.
* @return {string} Key for |this.requests_|.
* @private
*/
VolumeManager.prototype.makeRequestKey_ = function(requestType,
volumeType,
mountOrSourcePath) {
return requestType + ':' + volumeType + ':' + mountOrSourcePath;
};
/**
* @param {string} fileUrl File url to the archive file.
* @param {function(string)} successCallback Success callback.
* @param {function(VolumeManager.Error)} errorCallback Error callback.
*/
VolumeManager.prototype.mountArchive = function(fileUrl, successCallback,
errorCallback) {
this.mount_(fileUrl, 'archive', successCallback, errorCallback);
};
/**
* Unmounts volume.
* @param {string} mountPath Volume mounted path.
* @param {function(string)} successCallback Success callback.
* @param {function(VolumeManager.Error)} errorCallback Error callback.
*/
VolumeManager.prototype.unmount = function(mountPath,
successCallback,
errorCallback) {
volumeManagerUtil.validateMountPath(mountPath);
if (this.deferredQueue_) {
this.deferredQueue_.push(this.unmount.bind(this,
mountPath, successCallback, errorCallback));
return;
}
var volumeInfo = this.volumeInfoList.find(mountPath);
if (!volumeInfo) {
errorCallback(VolumeManager.Error.NOT_MOUNTED);
return;
}
chrome.fileBrowserPrivate.removeMount(util.makeFilesystemUrl(mountPath));
var requestKey = this.makeRequestKey_('unmount', '', volumeInfo.mountPath);
this.startRequest_(requestKey, successCallback, errorCallback);
};
/**
* Resolve the path to its entry.
* @param {string} path The path to be resolved.
* @param {function(Entry)} successCallback Called with the resolved entry on
* success.
* @param {function(FileError)} errorCallback Called on error.
*/
VolumeManager.prototype.resolvePath = function(
path, successCallback, errorCallback) {
// Make sure the path is in the mounted volume.
var mountPath = PathUtil.isDriveBasedPath(path) ?
RootDirectory.DRIVE : PathUtil.getRootPath(path);
var volumeInfo = this.getVolumeInfo(mountPath);
if (!volumeInfo || !volumeInfo.root) {
errorCallback(util.createFileError(FileError.NOT_FOUND_ERR));
return;
}
webkitResolveLocalFileSystemURL(
util.makeFilesystemUrl(path), successCallback, errorCallback);
};
/**
* @param {string} mountPath Volume mounted path.
* @return {VolumeInfo} The data about the volume.
*/
VolumeManager.prototype.getVolumeInfo = function(mountPath) {
volumeManagerUtil.validateMountPath(mountPath);
return this.volumeInfoList.find(mountPath);
};
/**
* @param {string} url URL for for |fileBrowserPrivate.addMount|.
* @param {'drive'|'archive'} volumeType Volume type for
* |fileBrowserPrivate.addMount|.
* @param {function(string)} successCallback Success callback.
* @param {function(VolumeManager.Error)} errorCallback Error callback.
* @private
*/
VolumeManager.prototype.mount_ = function(url, volumeType,
successCallback, errorCallback) {
if (this.deferredQueue_) {
this.deferredQueue_.push(this.mount_.bind(this,
url, volumeType, successCallback, errorCallback));
return;
}
chrome.fileBrowserPrivate.addMount(url, volumeType, {},
function(sourcePath) {
console.info('Mount request: url=' + url + '; volumeType=' + volumeType +
'; sourceUrl=' + sourcePath);
var requestKey = this.makeRequestKey_('mount', volumeType, sourcePath);
this.startRequest_(requestKey, successCallback, errorCallback);
}.bind(this));
};
/**
* @param {string} key Key produced by |makeRequestKey_|.
* @param {function(string)} successCallback To be called when request finishes
* successfully.
* @param {function(VolumeManager.Error)} errorCallback To be called when
* request fails.
* @private
*/
VolumeManager.prototype.startRequest_ = function(key,
successCallback, errorCallback) {
if (key in this.requests_) {
var request = this.requests_[key];
request.successCallbacks.push(successCallback);
request.errorCallbacks.push(errorCallback);
} else {
this.requests_[key] = {
successCallbacks: [successCallback],
errorCallbacks: [errorCallback],
timeout: setTimeout(this.onTimeout_.bind(this, key),
VolumeManager.TIMEOUT)
};
}
};
/**
* Called if no response received in |TIMEOUT|.
* @param {string} key Key produced by |makeRequestKey_|.
* @private
*/
VolumeManager.prototype.onTimeout_ = function(key) {
this.invokeRequestCallbacks_(this.requests_[key],
VolumeManager.Error.TIMEOUT);
delete this.requests_[key];
};
/**
* @param {string} key Key produced by |makeRequestKey_|.
* @param {VolumeManager.Error|'success'} status Status received from the API.
* @param {string=} opt_mountPath Mount path.
* @private
*/
VolumeManager.prototype.finishRequest_ = function(key, status, opt_mountPath) {
var request = this.requests_[key];
if (!request)
return;
clearTimeout(request.timeout);
this.invokeRequestCallbacks_(request, status, opt_mountPath);
delete this.requests_[key];
};
/**
* @param {Object} request Structure created in |startRequest_|.
* @param {VolumeManager.Error|string} status If status == 'success'
* success callbacks are called.
* @param {string=} opt_mountPath Mount path. Required if success.
* @private
*/
VolumeManager.prototype.invokeRequestCallbacks_ = function(request, status,
opt_mountPath) {
var callEach = function(callbacks, self, args) {
for (var i = 0; i < callbacks.length; i++) {
callbacks[i].apply(self, args);
}
};
if (status == 'success') {
callEach(request.successCallbacks, this, [opt_mountPath]);
} else {
volumeManagerUtil.validateError(status);
callEach(request.errorCallbacks, this, [status]);
}
};