OLD | NEW |
(Empty) | |
| 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 /** |
| 6 * VolumeManager is responsible for tracking list of mounted volumes. |
| 7 * |
| 8 * @constructor |
| 9 * @extends {cr.EventTarget} |
| 10 */ |
| 11 function VolumeManager() { |
| 12 /** |
| 13 * The list of archives requested to mount. We will show contents once |
| 14 * archive is mounted, but only for mounts from within this filebrowser tab. |
| 15 * @type {Object.<string, Object>} |
| 16 * @private |
| 17 */ |
| 18 this.requests_ = {}; |
| 19 |
| 20 /** |
| 21 * @type {Object.<string, Object>} |
| 22 * @private |
| 23 */ |
| 24 this.mountedVolumes_ = {}; |
| 25 |
| 26 this.initMountPoints_(); |
| 27 chrome.fileBrowserPrivate.onMountCompleted.addListener( |
| 28 this.onMountCompleted_.bind(this)); |
| 29 this.gDataStatus_ = VolumeManager.GDataStatus.UNMOUNTED; |
| 30 } |
| 31 |
| 32 /** |
| 33 * VolumeManager extends cr.EventTarget. |
| 34 */ |
| 35 VolumeManager.prototype.__proto__ = cr.EventTarget.prototype; |
| 36 |
| 37 /** |
| 38 * @enum |
| 39 */ |
| 40 VolumeManager.Error = { |
| 41 /* Internal errors */ |
| 42 NOT_MOUNTED: 'not_mounted', |
| 43 TIMEOUT: 'timeout', |
| 44 |
| 45 /* System events */ |
| 46 UNKNOWN: 'error_unknown', |
| 47 INTERNAL: 'error_internal', |
| 48 UNKNOWN_FILESYSTEM: 'error_unknown_filesystem', |
| 49 UNSUPPORTED_FILESYSTEM: 'error_unsuported_filesystem', |
| 50 INVALID_ARCHIVE: 'error_invalid_archive', |
| 51 LIBCROS_MISSING: 'error_libcros_missing', |
| 52 AUTHENTICATION: 'error_authentication', |
| 53 PATH_UNMOUNTED: 'error_path_unmounted' |
| 54 }; |
| 55 |
| 56 /** |
| 57 * @enum |
| 58 */ |
| 59 VolumeManager.GDataStatus = { |
| 60 UNMOUNTED: 'unmounted', |
| 61 MOUNTING: 'mounting', |
| 62 ERROR: 'error', |
| 63 MOUNTED: 'mounted' |
| 64 }; |
| 65 |
| 66 /** |
| 67 * Time in milliseconds that we wait a respone for. If no response on |
| 68 * mount/unmount received the request supposed failed. |
| 69 */ |
| 70 VolumeManager.TIMEOUT = 15 * 60 * 1000; |
| 71 |
| 72 /** |
| 73 * Delay in milliseconds GDATA changes its state from |UNMOUNTED| to |
| 74 * |MOUNTING|. Used to display progress in the UI. |
| 75 */ |
| 76 VolumeManager.MOUNTING_DELAY = 500; |
| 77 |
| 78 /** |
| 79 * @return {VolumeManager} Singleton instance. |
| 80 */ |
| 81 VolumeManager.getInstance = function() { |
| 82 return VolumeManager.instance_ = VolumeManager.instance_ || |
| 83 new VolumeManager(); |
| 84 }; |
| 85 |
| 86 /** |
| 87 * @param {VolumeManager.GDataStatus} newStatus New GDATA status. |
| 88 * @private |
| 89 */ |
| 90 VolumeManager.prototype.setGDataStatus_ = function(newStatus) { |
| 91 if (this.gDataStatus_ != newStatus) { |
| 92 this.gDataStatus_ = newStatus; |
| 93 cr.dispatchSimpleEvent(this, 'gdata-status-changed'); |
| 94 } |
| 95 }; |
| 96 |
| 97 /** |
| 98 * @return {VolumeManager.GDataStatus} Current GDATA status. |
| 99 */ |
| 100 VolumeManager.prototype.getGDataStatus = function() { |
| 101 return this.gDataStatus_; |
| 102 }; |
| 103 |
| 104 /** |
| 105 * @param {string} mountPath Volume root path. |
| 106 * @return {boolean} True if mounted. |
| 107 */ |
| 108 VolumeManager.prototype.isMounted = function(mountPath) { |
| 109 this.validateMountPath_(mountPath); |
| 110 return mountPath in this.mountedVolumes_; |
| 111 }; |
| 112 |
| 113 /** |
| 114 * Initialized mount points. |
| 115 * @private |
| 116 */ |
| 117 VolumeManager.prototype.initMountPoints_ = function() { |
| 118 var mountedVolumes = []; |
| 119 var self = this; |
| 120 var index = 0; |
| 121 function step(mountPoints) { |
| 122 if (index < mountPoints.length) { |
| 123 var info = mountPoints[index]; |
| 124 if (info.mountType == 'gdata') |
| 125 console.error('GData is not expected initially mounted'); |
| 126 var error = info.mountCondition ? 'error_' + info.mountCondition : ''; |
| 127 function onVolumeInfo(volume) { |
| 128 mountedVolumes.push(volume); |
| 129 index++; |
| 130 step(mountPoints); |
| 131 } |
| 132 self.makeVolumeInfo_('/' + info.mountPath, error, onVolumeInfo); |
| 133 } else { |
| 134 for (var i = 0; i < mountedVolumes.length; i++) { |
| 135 var volume = mountedVolumes[i]; |
| 136 self.mountedVolumes_[volume.mountPath] = volume; |
| 137 } |
| 138 if (mountedVolumes.length > 0) |
| 139 cr.dispatchSimpleEvent(self, 'change'); |
| 140 } |
| 141 } |
| 142 |
| 143 chrome.fileBrowserPrivate.getMountPoints(step); |
| 144 }; |
| 145 |
| 146 /** |
| 147 * Event handler called when some volume was mounted or unmouted. |
| 148 * @param {MountCompletedEvent} event Received event. |
| 149 * @private |
| 150 */ |
| 151 VolumeManager.prototype.onMountCompleted_ = function(event) { |
| 152 if (event.eventType == 'mount') { |
| 153 if (event.mountPath) { |
| 154 var requestKey = this.makeRequestKey_( |
| 155 'mount', event.mountType, event.sourcePath); |
| 156 var error = event.status == 'success' ? '' : event.status; |
| 157 this.makeVolumeInfo_(event.mountPath, error, function(volume) { |
| 158 this.mountedVolumes_[volume.mountPath] = volume; |
| 159 this.finishRequest_(requestKey, event.status, event.mountPath); |
| 160 cr.dispatchSimpleEvent(this, 'change'); |
| 161 }.bind(this)); |
| 162 } else { |
| 163 console.log('No mount path'); |
| 164 this.finishRequest_(requestKey, event.status); |
| 165 } |
| 166 } else if (event.eventType == 'unmount') { |
| 167 var mountPath = event.mountPath; |
| 168 this.validateMountPath_(mountPath); |
| 169 var status = event.status; |
| 170 if (status == VolumeManager.Error.PATH_UNMOUNTED) { |
| 171 console.log('Volume already unmounted: ', mountPath); |
| 172 status = 'success'; |
| 173 } |
| 174 var requestKey = this.makeRequestKey_('unmount', '', event.mountPath); |
| 175 var requested = requestKey in this.requests_; |
| 176 if (event.status == 'success' && !requested && |
| 177 mountPath in this.mountedVolumes_) { |
| 178 console.log('Mounted volume without a request: ', mountPath); |
| 179 var e = new cr.Event('externally-unmounted'); |
| 180 e.mountPath = mountPath; |
| 181 this.dispatchEvent(e); |
| 182 } |
| 183 this.finishRequest_(requestKey, status); |
| 184 |
| 185 if (event.status == 'success') { |
| 186 delete this.mountedVolumes_[mountPath]; |
| 187 cr.dispatchSimpleEvent(this, 'change'); |
| 188 } |
| 189 } |
| 190 |
| 191 if (event.mountType == 'gdata') { |
| 192 if (event.status == 'success') { |
| 193 if (event.eventType == 'mount') |
| 194 this.setGDataStatus_(VolumeManager.GDataStatus.MOUNTED); |
| 195 else if (event.eventType == 'unmount') |
| 196 this.setGDataStatus_(VolumeManager.GDataStatus.UMOUNTED); |
| 197 } |
| 198 } |
| 199 }; |
| 200 |
| 201 /** |
| 202 * @param {string} mountPath Path to the volume. |
| 203 * @param {VolumeManager?} error Mounting error if any. |
| 204 * @param {function(Object)} callback Result acceptor. |
| 205 * @private |
| 206 */ |
| 207 VolumeManager.prototype.makeVolumeInfo_ = function( |
| 208 mountPath, error, callback) { |
| 209 if (error) |
| 210 this.validateError_(error); |
| 211 this.validateMountPath_(mountPath); |
| 212 function onVolumeMetadata(metadata) { |
| 213 callback({ |
| 214 mountPath: mountPath, |
| 215 error: error, |
| 216 readonly: !!metadata && metadata.isReadOnly |
| 217 }); |
| 218 } |
| 219 chrome.fileBrowserPrivate.getVolumeMetadata( |
| 220 util.makeFilesystemUrl(mountPath), onVolumeMetadata); |
| 221 }; |
| 222 |
| 223 /** |
| 224 * Creates string to match mount events with requests. |
| 225 * @param {string} requestType 'mount' | 'unmount'. |
| 226 * @param {string} mountType 'device' | 'file' | 'network' | 'gdata'. |
| 227 * @param {string} mountOrSourcePath Source path provided by API after |
| 228 * resolving mount request or mountPath for unmount request. |
| 229 * @return {string} Key for |this.requests_|. |
| 230 * @private |
| 231 */ |
| 232 VolumeManager.prototype.makeRequestKey_ = function(requestType, |
| 233 mountType, |
| 234 mountOrSourcePath) { |
| 235 return requestType + ':' + mountType + ':' + mountOrSourcePath; |
| 236 }; |
| 237 |
| 238 |
| 239 /** |
| 240 * @param {Function} successCallback Success callback. |
| 241 * @param {Function} errorCallback Error callback. |
| 242 */ |
| 243 VolumeManager.prototype.mountGData = function(successCallback, errorCallback) { |
| 244 if (this.getGDataStatus() == VolumeManager.GDataStatus.ERROR) { |
| 245 this.setGDataStatus_(VolumeManager.GDataStatus.UNMOUNTED); |
| 246 } |
| 247 var self = this; |
| 248 var timeout = setTimeout(function() { |
| 249 if (self.getGDataStatus() == VolumeManager.GDataStatus.UNMOUNTED) |
| 250 self.setGDataStatus_(VolumeManager.GDataStatus.MOUNTING); |
| 251 timeout = null; |
| 252 }, VolumeManager.MOUNTING_DELAY); |
| 253 this.mount_('', 'gdata', function(mountPath) { |
| 254 if (timeout !== null) |
| 255 clearTimeout(timeout); |
| 256 successCallback(mountPath); |
| 257 }, function(error) { |
| 258 if (self.getGDataStatus() != VolumeManager.GDataStatus.MOUNTED) |
| 259 self.setGDataStatus_(VolumeManager.GDataStatus.ERROR); |
| 260 if (timeout != null) |
| 261 clearTimeout(timeout); |
| 262 errorCallback(error); |
| 263 }); |
| 264 }; |
| 265 |
| 266 /** |
| 267 * @param {string} fullPath Path to the archive file. |
| 268 * @param {Function} successCallback Success callback. |
| 269 * @param {Function} errorCallback Error callback. |
| 270 */ |
| 271 VolumeManager.prototype.mountArchive = function(fullPath, successCallback, |
| 272 errorCallback) { |
| 273 this.mount_(util.makeFilesystemUrl(fullPath), |
| 274 'file', successCallback, errorCallback); |
| 275 }; |
| 276 |
| 277 /** |
| 278 * Unmounts volume. |
| 279 * @param {string} mountPath Volume mounted path. |
| 280 * @param {Function} successCallback Success callback. |
| 281 * @param {Function} errorCallback Error callback. |
| 282 */ |
| 283 VolumeManager.prototype.unmount = function(mountPath, |
| 284 successCallback, |
| 285 errorCallback) { |
| 286 this.validateMountPath_(mountPath); |
| 287 var volumeInfo = this.mountedVolumes_[mountPath]; |
| 288 if (!volumeInfo) { |
| 289 errorCallback(VolumeManager.Error.NOT_MOUNTED); |
| 290 return; |
| 291 } |
| 292 |
| 293 chrome.fileBrowserPrivate.removeMount(util.makeFilesystemUrl(mountPath)); |
| 294 var requestKey = this.makeRequestKey_('unmount', '', volumeInfo.mountPath); |
| 295 this.startRequest_(requestKey, successCallback, errorCallback); |
| 296 }; |
| 297 |
| 298 /** |
| 299 * @param {string} mountPath Volume mounted path. |
| 300 * @return {VolumeManager.Error?} Returns mount error code |
| 301 * or undefined if no error. |
| 302 */ |
| 303 VolumeManager.prototype.getMountError = function(mountPath) { |
| 304 return this.getVolumeInfo_(mountPath).error; |
| 305 }; |
| 306 |
| 307 /** |
| 308 * @param {string} mountPath Volume mounted path. |
| 309 * @return {boolean} True if volume at |mountedPath| is mounted but not usable. |
| 310 */ |
| 311 VolumeManager.prototype.isUnreadable = function(mountPath) { |
| 312 var error = this.getMountError(mountPath); |
| 313 return error == VolumeManager.Error.UNKNOWN_FILESYSTEM || |
| 314 error == VolumeManager.Error.UNSUPPORTED_FILESYSTEM; |
| 315 }; |
| 316 |
| 317 /** |
| 318 * @param {string} mountPath Volume mounted path. |
| 319 * @return {boolean} True if volume at |mountedPath| is read only. |
| 320 */ |
| 321 VolumeManager.prototype.isReadOnly = function(mountPath) { |
| 322 return !!this.getVolumeInfo_(mountPath).readonly; |
| 323 }; |
| 324 |
| 325 /** |
| 326 * Helper method. |
| 327 * @param {string} mountPath Volume mounted path. |
| 328 * @return {Object} Structure created in |startRequest_|. |
| 329 * @private |
| 330 */ |
| 331 VolumeManager.prototype.getVolumeInfo_ = function(mountPath) { |
| 332 this.validateMountPath_(mountPath); |
| 333 return this.mountedVolumes_[mountPath] || {}; |
| 334 }; |
| 335 |
| 336 /** |
| 337 * @param {string} url URL for for |fileBrowserPrivate.addMount|. |
| 338 * @param {'gdata'|'file'} mountType Mount type for |
| 339 * |fileBrowserPrivate.addMount|. |
| 340 * @param {Function} successCallback Success callback. |
| 341 * @param {Function} errorCallback Error callback. |
| 342 * @private |
| 343 */ |
| 344 VolumeManager.prototype.mount_ = function(url, mountType, |
| 345 successCallback, errorCallback) { |
| 346 chrome.fileBrowserPrivate.addMount(url, mountType, {}, |
| 347 function(sourcePath) { |
| 348 console.log('Mount request: url=' + url + '; mountType=' + mountType + |
| 349 '; sourceUrl=' + sourcePath); |
| 350 var requestKey = this.makeRequestKey_('mount', mountType, sourcePath); |
| 351 this.startRequest_(requestKey, successCallback, errorCallback); |
| 352 }.bind(this)); |
| 353 }; |
| 354 |
| 355 /** |
| 356 * @param {sting} key Key produced by |makeRequestKey_|. |
| 357 * @param {Function} successCallback To be called when request finishes |
| 358 * successfully. |
| 359 * @param {Function} errorCallback To be called when request fails. |
| 360 * @private |
| 361 */ |
| 362 VolumeManager.prototype.startRequest_ = function(key, |
| 363 successCallback, errorCallback) { |
| 364 if (key in this.requests_) { |
| 365 var request = this.requests_[key]; |
| 366 request.successCallbacks.push(successCallback); |
| 367 request.errorCallbacks.push(errorCallback); |
| 368 } else { |
| 369 this.requests_[key] = { |
| 370 successCallbacks: [successCallback], |
| 371 errorCallbacks: [errorCallback], |
| 372 |
| 373 timeout: setTimeout(this.onTimeout_.bind(this, key), |
| 374 VolumeManager.TIMEOUT) |
| 375 }; |
| 376 } |
| 377 }; |
| 378 |
| 379 /** |
| 380 * Called if no response received in |TIMEOUT|. |
| 381 * @param {sting} key Key produced by |makeRequestKey_|. |
| 382 * @private |
| 383 */ |
| 384 VolumeManager.prototype.onTimeout_ = function(key) { |
| 385 this.invokeRequestCallbacks_(this.requests_[key], |
| 386 VolumeManager.Error.TIMEOUT); |
| 387 delete this.requests_[key]; |
| 388 }; |
| 389 |
| 390 /** |
| 391 * @param {sting} key Key produced by |makeRequestKey_|. |
| 392 * @param {VolumeManager.Error|'success'} status Status received from the API. |
| 393 * @param {string} opt_mountPath Mount path. |
| 394 * @private |
| 395 */ |
| 396 VolumeManager.prototype.finishRequest_ = function(key, status, opt_mountPath) { |
| 397 var request = this.requests_[key]; |
| 398 if (!request) |
| 399 return; |
| 400 |
| 401 clearTimeout(request.timeout); |
| 402 this.invokeRequestCallbacks_(request, status, opt_mountPath); |
| 403 delete this.requests_[key]; |
| 404 }; |
| 405 |
| 406 /** |
| 407 * @param {object} request Structure created in |startRequest_|. |
| 408 * @param {VolumeManager.Error|string} status If status == 'success' |
| 409 * success callbacks are called. |
| 410 * @param {string} opt_mountPath Mount path. Required if success. |
| 411 * @private |
| 412 */ |
| 413 VolumeManager.prototype.invokeRequestCallbacks_ = function(request, status, |
| 414 opt_mountPath) { |
| 415 function callEach(callbacks, self, args) { |
| 416 for (var i = 0; i < callbacks.length; i++) { |
| 417 callbacks[i].apply(self, args); |
| 418 } |
| 419 } |
| 420 if (status == 'success') { |
| 421 callEach(request.successCallbacks, this, [opt_mountPath]); |
| 422 } else { |
| 423 this.validateError_(status); |
| 424 callEach(request.errorCallbacks, this, [status]); |
| 425 } |
| 426 }; |
| 427 |
| 428 /** |
| 429 * @param {VolumeManager.Error} error Status string iusually received from API. |
| 430 * @private |
| 431 */ |
| 432 VolumeManager.prototype.validateError_ = function(error) { |
| 433 for (var i in VolumeManager.Error) { |
| 434 if (error == VolumeManager.Error[i]) |
| 435 return; |
| 436 } |
| 437 throw new Error('Invalid mount error: ', error); |
| 438 }; |
| 439 |
| 440 /** |
| 441 * @param {string} mountPath Mount path. |
| 442 * @private |
| 443 */ |
| 444 VolumeManager.prototype.validateMountPath_ = function(mountPath) { |
| 445 if (!/^\/(((archive|removable)\/[^\/]+)|drive|Downloads)$/.test(mountPath)) |
| 446 throw new Error('Invalid mount path: ', mountPath); |
| 447 }; |
OLD | NEW |