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