Home | History | Annotate | Download | only in js
      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  * VolumeManager is responsible for tracking list of mounted volumes.
      9  *
     10  * @constructor
     11  * @extends {cr.EventTarget}
     12  */
     13 function VolumeManager() {
     14   /**
     15    * The list of archives requested to mount. We will show contents once
     16    * archive is mounted, but only for mounts from within this filebrowser tab.
     17    * @type {Object.<string, Object>}
     18    * @private
     19    */
     20   this.requests_ = {};
     21 
     22   /**
     23    * @type {Object.<string, Object>}
     24    * @private
     25    */
     26   this.mountedVolumes_ = {};
     27 
     28   /**
     29    * True, if mount points have been initialized.
     30    * @type {boolean}
     31    * @private
     32    */
     33   this.ready_ = false;
     34 
     35   this.initMountPoints_();
     36   this.driveStatus_ = VolumeManager.DriveStatus.UNMOUNTED;
     37 
     38   this.driveConnectionState_ = {
     39       type: VolumeManager.DriveConnectionType.OFFLINE,
     40       reasons: VolumeManager.DriveConnectionType.NO_SERVICE
     41   };
     42 
     43   chrome.fileBrowserPrivate.onDriveConnectionStatusChanged.addListener(
     44       this.onDriveConnectionStatusChanged_.bind(this));
     45   this.onDriveConnectionStatusChanged_();
     46 
     47 }
     48 
     49 /**
     50  * Invoked when the drive connection status is changed.
     51  * @private_
     52  */
     53 VolumeManager.prototype.onDriveConnectionStatusChanged_ = function() {
     54   chrome.fileBrowserPrivate.getDriveConnectionState(function(state) {
     55     this.driveConnectionState_ = state;
     56     cr.dispatchSimpleEvent(this, 'drive-connection-changed');
     57   }.bind(this));
     58 };
     59 
     60 /**
     61  * Returns the drive connection state.
     62  * @return {VolumeManager.DriveConnectionType} Connection type.
     63  */
     64 VolumeManager.prototype.getDriveConnectionState = function() {
     65   return this.driveConnectionState_;
     66 };
     67 
     68 /**
     69  * VolumeManager extends cr.EventTarget.
     70  */
     71 VolumeManager.prototype.__proto__ = cr.EventTarget.prototype;
     72 
     73 /**
     74  * @enum
     75  */
     76 VolumeManager.Error = {
     77   /* Internal errors */
     78   NOT_MOUNTED: 'not_mounted',
     79   TIMEOUT: 'timeout',
     80 
     81   /* System events */
     82   UNKNOWN: 'error_unknown',
     83   INTERNAL: 'error_internal',
     84   UNKNOWN_FILESYSTEM: 'error_unknown_filesystem',
     85   UNSUPPORTED_FILESYSTEM: 'error_unsupported_filesystem',
     86   INVALID_ARCHIVE: 'error_invalid_archive',
     87   AUTHENTICATION: 'error_authentication',
     88   PATH_UNMOUNTED: 'error_path_unmounted'
     89 };
     90 
     91 /**
     92  * @enum
     93  */
     94 VolumeManager.DriveStatus = {
     95   UNMOUNTED: 'unmounted',
     96   MOUNTING: 'mounting',
     97   ERROR: 'error',
     98   MOUNTED: 'mounted'
     99 };
    100 
    101 /**
    102  * List of connection types of drive.
    103  *
    104  * Keep this in sync with the kDriveConnectionType* constants in
    105  * file_browser_private_api.cc.
    106  *
    107  * @enum {string}
    108  */
    109 VolumeManager.DriveConnectionType = {
    110   OFFLINE: 'offline',  // Connection is offline or drive is unavailable.
    111   METERED: 'metered',  // Connection is metered. Should limit traffic.
    112   ONLINE: 'online'     // Connection is online.
    113 };
    114 
    115 /**
    116  * List of reasons of DriveConnectionType.
    117  *
    118  * Keep this in sync with the kDriveConnectionReason constants in
    119  * file_browser_private_api.cc.
    120  *
    121  * @enum {string}
    122  */
    123 VolumeManager.DriveConnectionReason = {
    124   NOT_READY: 'not_ready',    // Drive is not ready or authentication is failed.
    125   NO_NETWORK: 'no_network',  // Network connection is unavailable.
    126   NO_SERVICE: 'no_service'   // Drive service is unavailable.
    127 };
    128 
    129 /**
    130  * Time in milliseconds that we wait a respone for. If no response on
    131  * mount/unmount received the request supposed failed.
    132  */
    133 VolumeManager.TIMEOUT = 15 * 60 * 1000;
    134 
    135 /**
    136  * Delay in milliseconds DRIVE changes its state from |UNMOUNTED| to
    137  * |MOUNTING|. Used to display progress in the UI.
    138  */
    139 VolumeManager.MOUNTING_DELAY = 500;
    140 
    141 /**
    142  * @return {VolumeManager} Singleton instance.
    143  */
    144 VolumeManager.getInstance = function() {
    145   return VolumeManager.instance_ = VolumeManager.instance_ ||
    146                                    new VolumeManager();
    147 };
    148 
    149 /**
    150  * @param {VolumeManager.DriveStatus} newStatus New DRIVE status.
    151  * @private
    152  */
    153 VolumeManager.prototype.setDriveStatus_ = function(newStatus) {
    154   if (this.driveStatus_ != newStatus) {
    155     this.driveStatus_ = newStatus;
    156     cr.dispatchSimpleEvent(this, 'drive-status-changed');
    157   }
    158 };
    159 
    160 /**
    161  * @return {VolumeManager.DriveStatus} Current DRIVE status.
    162  */
    163 VolumeManager.prototype.getDriveStatus = function() {
    164   return this.driveStatus_;
    165 };
    166 
    167 /**
    168  * @param {string} mountPath Volume root path.
    169  * @return {boolean} True if mounted.
    170  */
    171 VolumeManager.prototype.isMounted = function(mountPath) {
    172   this.validateMountPath_(mountPath);
    173   return mountPath in this.mountedVolumes_;
    174 };
    175 
    176 /**
    177  * @return {boolean} True if already initialized.
    178  */
    179 VolumeManager.prototype.isReady = function() {
    180   return this.ready_;
    181 };
    182 
    183 /**
    184  * Initialized mount points.
    185  * @private
    186  */
    187 VolumeManager.prototype.initMountPoints_ = function() {
    188   var mountedVolumes = [];
    189   var self = this;
    190   var index = 0;
    191   this.deferredQueue_ = [];
    192   var step = function(mountPoints) {
    193     if (index < mountPoints.length) {
    194       var info = mountPoints[index];
    195       if (info.mountType == 'drive')
    196         console.error('Drive is not expected initially mounted');
    197       var error = info.mountCondition ? 'error_' + info.mountCondition : '';
    198       var onVolumeInfo = function(volume) {
    199         mountedVolumes.push(volume);
    200         index++;
    201         step(mountPoints);
    202       };
    203       self.makeVolumeInfo_('/' + info.mountPath, error, onVolumeInfo);
    204     } else {
    205       for (var i = 0; i < mountedVolumes.length; i++) {
    206         var volume = mountedVolumes[i];
    207         self.mountedVolumes_[volume.mountPath] = volume;
    208       }
    209 
    210       // Subscribe to the mount completed event when mount points initialized.
    211       chrome.fileBrowserPrivate.onMountCompleted.addListener(
    212           self.onMountCompleted_.bind(self));
    213 
    214       var deferredQueue = self.deferredQueue_;
    215       self.deferredQueue_ = null;
    216       for (var i = 0; i < deferredQueue.length; i++) {
    217         deferredQueue[i]();
    218       }
    219 
    220       cr.dispatchSimpleEvent(self, 'ready');
    221       self.ready_ = true;
    222       if (mountedVolumes.length > 0)
    223         cr.dispatchSimpleEvent(self, 'change');
    224     }
    225   };
    226 
    227   chrome.fileBrowserPrivate.getMountPoints(step);
    228 };
    229 
    230 /**
    231  * Event handler called when some volume was mounted or unmouted.
    232  * @param {MountCompletedEvent} event Received event.
    233  * @private
    234  */
    235 VolumeManager.prototype.onMountCompleted_ = function(event) {
    236   if (event.eventType == 'mount') {
    237     if (event.mountPath) {
    238       var requestKey = this.makeRequestKey_(
    239           'mount', event.mountType, event.sourcePath);
    240       var error = event.status == 'success' ? '' : event.status;
    241       this.makeVolumeInfo_(event.mountPath, error, function(volume) {
    242         this.mountedVolumes_[volume.mountPath] = volume;
    243         this.finishRequest_(requestKey, event.status, event.mountPath);
    244         cr.dispatchSimpleEvent(this, 'change');
    245       }.bind(this));
    246     } else {
    247       console.warn('No mount path.');
    248       this.finishRequest_(requestKey, event.status);
    249     }
    250   } else if (event.eventType == 'unmount') {
    251     var mountPath = event.mountPath;
    252     this.validateMountPath_(mountPath);
    253     var status = event.status;
    254     if (status == VolumeManager.Error.PATH_UNMOUNTED) {
    255       console.warn('Volume already unmounted: ', mountPath);
    256       status = 'success';
    257     }
    258     var requestKey = this.makeRequestKey_('unmount', '', event.mountPath);
    259     var requested = requestKey in this.requests_;
    260     if (event.status == 'success' && !requested &&
    261         mountPath in this.mountedVolumes_) {
    262       console.warn('Mounted volume without a request: ', mountPath);
    263       var e = new cr.Event('externally-unmounted');
    264       e.mountPath = mountPath;
    265       this.dispatchEvent(e);
    266     }
    267     this.finishRequest_(requestKey, status);
    268 
    269     if (event.status == 'success') {
    270       delete this.mountedVolumes_[mountPath];
    271       cr.dispatchSimpleEvent(this, 'change');
    272     }
    273   }
    274 
    275   if (event.mountType == 'drive') {
    276     if (event.status == 'success') {
    277       if (event.eventType == 'mount') {
    278         // If the mount is not requested, the mount status will not be changed
    279         // at mountDrive(). Sets it here in such a case.
    280         var self = this;
    281         var timeout = setTimeout(function() {
    282           if (self.getDriveStatus() == VolumeManager.DriveStatus.UNMOUNTED)
    283             self.setDriveStatus_(VolumeManager.DriveStatus.MOUNTING);
    284           timeout = null;
    285         }, VolumeManager.MOUNTING_DELAY);
    286 
    287         this.waitDriveLoaded_(event.mountPath, function(success) {
    288           if (timeout != null)
    289             clearTimeout(timeout);
    290           this.setDriveStatus_(success ? VolumeManager.DriveStatus.MOUNTED :
    291                                          VolumeManager.DriveStatus.ERROR);
    292         }.bind(this));
    293       } else if (event.eventType == 'unmount') {
    294         this.setDriveStatus_(VolumeManager.DriveStatus.UNMOUNTED);
    295       }
    296     } else {
    297       this.setDriveStatus_(VolumeManager.DriveStatus.ERROR);
    298     }
    299   }
    300 };
    301 
    302 /**
    303  * First access to Drive takes time (to fetch data from the cloud).
    304  * We want to change state to MOUNTED (likely from MOUNTING) when the
    305  * drive ready to operate.
    306  *
    307  * @param {string} mountPath Drive mount path.
    308  * @param {function(boolean, FileError=)} callback To be called when waiting
    309  *     finishes. If the case of error, there may be a FileError parameter.
    310  * @private
    311  */
    312 VolumeManager.prototype.waitDriveLoaded_ = function(mountPath, callback) {
    313   chrome.fileBrowserPrivate.requestFileSystem(function(filesystem) {
    314     filesystem.root.getDirectory(mountPath, {},
    315         function(entry) {
    316           // After file system is mounted, we need to "read" drive grand root
    317           // entry at first. It loads mydrive root entry as a part of
    318           // 'fast-fetch' quickly, and starts full feed fetch in parallel.
    319           // Without this read, accessing mydrive root will be 'full-fetch'
    320           // rather than 'fast-fetch' on the current architecture.
    321           // Just "getting" the grand root entry doesn't trigger it. Rather,
    322           // it starts when the entry is "read".
    323           entry.createReader().readEntries(
    324               callback.bind(null, true),
    325               callback.bind(null, false));
    326         },
    327         callback.bind(null, false));
    328   });
    329 };
    330 
    331 /**
    332  * @param {string} mountPath Path to the volume.
    333  * @param {VolumeManager?} error Mounting error if any.
    334  * @param {function(Object)} callback Result acceptor.
    335  * @private
    336  */
    337 VolumeManager.prototype.makeVolumeInfo_ = function(
    338     mountPath, error, callback) {
    339   if (error)
    340     this.validateError_(error);
    341   this.validateMountPath_(mountPath);
    342   var onVolumeMetadata = function(metadata) {
    343    callback({
    344      mountPath: mountPath,
    345      error: error,
    346      deviceType: metadata && metadata.deviceType,
    347      readonly: !!metadata && metadata.isReadOnly
    348    });
    349   };
    350   chrome.fileBrowserPrivate.getVolumeMetadata(
    351       util.makeFilesystemUrl(mountPath), onVolumeMetadata);
    352 };
    353 
    354 /**
    355  * Creates string to match mount events with requests.
    356  * @param {string} requestType 'mount' | 'unmount'.
    357  * @param {string} mountType 'device' | 'file' | 'network' | 'drive'.
    358  * @param {string} mountOrSourcePath Source path provided by API after
    359  *     resolving mount request or mountPath for unmount request.
    360  * @return {string} Key for |this.requests_|.
    361  * @private
    362  */
    363 VolumeManager.prototype.makeRequestKey_ = function(requestType,
    364                                                    mountType,
    365                                                    mountOrSourcePath) {
    366   return requestType + ':' + mountType + ':' + mountOrSourcePath;
    367 };
    368 
    369 
    370 /**
    371  * @param {function(string)} successCallback Success callback.
    372  * @param {function(VolumeManager.Error)} errorCallback Error callback.
    373  */
    374 VolumeManager.prototype.mountDrive = function(successCallback, errorCallback) {
    375   if (this.getDriveStatus() == VolumeManager.DriveStatus.ERROR) {
    376     this.setDriveStatus_(VolumeManager.DriveStatus.UNMOUNTED);
    377   }
    378   this.setDriveStatus_(VolumeManager.DriveStatus.MOUNTING);
    379   var self = this;
    380   this.mount_('', 'drive', function(mountPath) {
    381     this.waitDriveLoaded_(mountPath, function(success, error) {
    382       if (success) {
    383         successCallback(mountPath);
    384       } else {
    385         errorCallback(error);
    386       }
    387     });
    388   }, function(error) {
    389     if (self.getDriveStatus() != VolumeManager.DriveStatus.MOUNTED)
    390       self.setDriveStatus_(VolumeManager.DriveStatus.ERROR);
    391     errorCallback(error);
    392   });
    393 };
    394 
    395 /**
    396  * @param {string} fileUrl File url to the archive file.
    397  * @param {function(string)} successCallback Success callback.
    398  * @param {function(VolumeManager.Error)} errorCallback Error callback.
    399  */
    400 VolumeManager.prototype.mountArchive = function(fileUrl, successCallback,
    401                                                 errorCallback) {
    402   this.mount_(fileUrl, 'file', successCallback, errorCallback);
    403 };
    404 
    405 /**
    406  * Unmounts volume.
    407  * @param {string} mountPath Volume mounted path.
    408  * @param {function(string)} successCallback Success callback.
    409  * @param {function(VolumeManager.Error)} errorCallback Error callback.
    410  */
    411 VolumeManager.prototype.unmount = function(mountPath,
    412                                            successCallback,
    413                                            errorCallback) {
    414   this.validateMountPath_(mountPath);
    415   if (this.deferredQueue_) {
    416     this.deferredQueue_.push(this.unmount.bind(this,
    417         mountPath, successCallback, errorCallback));
    418     return;
    419   }
    420 
    421   var volumeInfo = this.mountedVolumes_[mountPath];
    422   if (!volumeInfo) {
    423     errorCallback(VolumeManager.Error.NOT_MOUNTED);
    424     return;
    425   }
    426 
    427   chrome.fileBrowserPrivate.removeMount(util.makeFilesystemUrl(mountPath));
    428   var requestKey = this.makeRequestKey_('unmount', '', volumeInfo.mountPath);
    429   this.startRequest_(requestKey, successCallback, errorCallback);
    430 };
    431 
    432 /**
    433  * @param {string} mountPath Volume mounted path.
    434  * @return {VolumeManager.Error?} Returns mount error code
    435  *                                or undefined if no error.
    436  */
    437 VolumeManager.prototype.getMountError = function(mountPath) {
    438   return this.getVolumeInfo_(mountPath).error;
    439 };
    440 
    441 /**
    442  * @param {string} mountPath Volume mounted path.
    443  * @return {boolean} True if volume at |mountedPath| is mounted but not usable.
    444  */
    445 VolumeManager.prototype.isUnreadable = function(mountPath) {
    446   var error = this.getMountError(mountPath);
    447   return error == VolumeManager.Error.UNKNOWN_FILESYSTEM ||
    448          error == VolumeManager.Error.UNSUPPORTED_FILESYSTEM;
    449 };
    450 
    451 /**
    452  * @param {string} mountPath Volume mounted path.
    453  * @return {string} Device type ('usb'|'sd'|'optical'|'mobile'|'unknown')
    454  *   (as defined in chromeos/disks/disk_mount_manager.cc).
    455  */
    456 VolumeManager.prototype.getDeviceType = function(mountPath) {
    457   return this.getVolumeInfo_(mountPath).deviceType;
    458 };
    459 
    460 /**
    461  * @param {string} mountPath Volume mounted path.
    462  * @return {boolean} True if volume at |mountedPath| is read only.
    463  */
    464 VolumeManager.prototype.isReadOnly = function(mountPath) {
    465   return !!this.getVolumeInfo_(mountPath).readonly;
    466 };
    467 
    468 /**
    469  * Helper method.
    470  * @param {string} mountPath Volume mounted path.
    471  * @return {Object} Structure created in |startRequest_|.
    472  * @private
    473  */
    474 VolumeManager.prototype.getVolumeInfo_ = function(mountPath) {
    475   this.validateMountPath_(mountPath);
    476   return this.mountedVolumes_[mountPath] || {};
    477 };
    478 
    479 /**
    480  * @param {string} url URL for for |fileBrowserPrivate.addMount|.
    481  * @param {'drive'|'file'} mountType Mount type for
    482  *     |fileBrowserPrivate.addMount|.
    483  * @param {function(string)} successCallback Success callback.
    484  * @param {function(VolumeManager.Error)} errorCallback Error callback.
    485  * @private
    486  */
    487 VolumeManager.prototype.mount_ = function(url, mountType,
    488                                           successCallback, errorCallback) {
    489   if (this.deferredQueue_) {
    490     this.deferredQueue_.push(this.mount_.bind(this,
    491         url, mountType, successCallback, errorCallback));
    492     return;
    493   }
    494 
    495   chrome.fileBrowserPrivate.addMount(url, mountType, {},
    496                                      function(sourcePath) {
    497     console.info('Mount request: url=' + url + '; mountType=' + mountType +
    498                  '; sourceUrl=' + sourcePath);
    499     var requestKey = this.makeRequestKey_('mount', mountType, sourcePath);
    500     this.startRequest_(requestKey, successCallback, errorCallback);
    501   }.bind(this));
    502 };
    503 
    504 /**
    505  * @param {string} key Key produced by |makeRequestKey_|.
    506  * @param {function(string)} successCallback To be called when request finishes
    507  *     successfully.
    508  * @param {function(VolumeManager.Error)} errorCallback To be called when
    509  *     request fails.
    510  * @private
    511  */
    512 VolumeManager.prototype.startRequest_ = function(key,
    513     successCallback, errorCallback) {
    514   if (key in this.requests_) {
    515     var request = this.requests_[key];
    516     request.successCallbacks.push(successCallback);
    517     request.errorCallbacks.push(errorCallback);
    518   } else {
    519     this.requests_[key] = {
    520       successCallbacks: [successCallback],
    521       errorCallbacks: [errorCallback],
    522 
    523       timeout: setTimeout(this.onTimeout_.bind(this, key),
    524                           VolumeManager.TIMEOUT)
    525     };
    526   }
    527 };
    528 
    529 /**
    530  * Called if no response received in |TIMEOUT|.
    531  * @param {string} key Key produced by |makeRequestKey_|.
    532  * @private
    533  */
    534 VolumeManager.prototype.onTimeout_ = function(key) {
    535   this.invokeRequestCallbacks_(this.requests_[key],
    536                                VolumeManager.Error.TIMEOUT);
    537   delete this.requests_[key];
    538 };
    539 
    540 /**
    541  * @param {string} key Key produced by |makeRequestKey_|.
    542  * @param {VolumeManager.Error|'success'} status Status received from the API.
    543  * @param {string=} opt_mountPath Mount path.
    544  * @private
    545  */
    546 VolumeManager.prototype.finishRequest_ = function(key, status, opt_mountPath) {
    547   var request = this.requests_[key];
    548   if (!request)
    549     return;
    550 
    551   clearTimeout(request.timeout);
    552   this.invokeRequestCallbacks_(request, status, opt_mountPath);
    553   delete this.requests_[key];
    554 };
    555 
    556 /**
    557  * @param {Object} request Structure created in |startRequest_|.
    558  * @param {VolumeManager.Error|string} status If status == 'success'
    559  *     success callbacks are called.
    560  * @param {string=} opt_mountPath Mount path. Required if success.
    561  * @private
    562  */
    563 VolumeManager.prototype.invokeRequestCallbacks_ = function(request, status,
    564                                                            opt_mountPath) {
    565   var callEach = function(callbacks, self, args) {
    566     for (var i = 0; i < callbacks.length; i++) {
    567       callbacks[i].apply(self, args);
    568     }
    569   };
    570   if (status == 'success') {
    571     callEach(request.successCallbacks, this, [opt_mountPath]);
    572   } else {
    573     this.validateError_(status);
    574     callEach(request.errorCallbacks, this, [status]);
    575   }
    576 };
    577 
    578 /**
    579  * @param {VolumeManager.Error} error Status string iusually received from API.
    580  * @private
    581  */
    582 VolumeManager.prototype.validateError_ = function(error) {
    583   for (var i in VolumeManager.Error) {
    584     if (error == VolumeManager.Error[i])
    585       return;
    586   }
    587   throw new Error('Invalid mount error: ', error);
    588 };
    589 
    590 /**
    591  * @param {string} mountPath Mount path.
    592  * @private
    593  */
    594 VolumeManager.prototype.validateMountPath_ = function(mountPath) {
    595   if (!/^\/(drive|drive_shared_with_me|drive_offline|drive_recent|Downloads)$/
    596        .test(mountPath) &&
    597       !/^\/((archive|removable|drive)\/[^\/]+)$/.test(mountPath))
    598     throw new Error('Invalid mount path: ', mountPath);
    599 };
    600