• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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