blob: 5acb4a3ef679f73c4f91dd386fb4ccfa681f9c7c [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.
/**
* 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_ = {};
/**
* @type {Object.<string, Object>}
* @private
*/
this.mountedVolumes_ = {};
this.initMountPoints_();
this.gDataStatus_ = VolumeManager.GDataStatus.UNMOUNTED;
}
/**
* 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.GDataStatus = {
UNMOUNTED: 'unmounted',
MOUNTING: 'mounting',
ERROR: 'error',
MOUNTED: 'mounted'
};
/**
* 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;
/**
* Delay in milliseconds GDATA changes its state from |UNMOUNTED| to
* |MOUNTING|. Used to display progress in the UI.
*/
VolumeManager.MOUNTING_DELAY = 500;
/**
* @return {VolumeManager} Singleton instance.
*/
VolumeManager.getInstance = function() {
return VolumeManager.instance_ = VolumeManager.instance_ ||
new VolumeManager();
};
/**
* @param {VolumeManager.GDataStatus} newStatus New GDATA status.
* @private
*/
VolumeManager.prototype.setGDataStatus_ = function(newStatus) {
if (this.gDataStatus_ != newStatus) {
this.gDataStatus_ = newStatus;
cr.dispatchSimpleEvent(this, 'gdata-status-changed');
}
};
/**
* @return {VolumeManager.GDataStatus} Current GDATA status.
*/
VolumeManager.prototype.getGDataStatus = function() {
return this.gDataStatus_;
};
/**
* @param {string} mountPath Volume root path.
* @return {boolean} True if mounted.
*/
VolumeManager.prototype.isMounted = function(mountPath) {
this.validateMountPath_(mountPath);
return mountPath in this.mountedVolumes_;
};
/**
* Initialized mount points.
* @private
*/
VolumeManager.prototype.initMountPoints_ = function() {
var mountedVolumes = [];
var self = this;
var index = 0;
this.deferredQueue_ = [];
function step(mountPoints) {
if (index < mountPoints.length) {
var info = mountPoints[index];
if (info.mountType == 'gdata')
console.error('GData is not expected initially mounted');
var error = info.mountCondition ? 'error_' + info.mountCondition : '';
function onVolumeInfo(volume) {
mountedVolumes.push(volume);
index++;
step(mountPoints);
}
self.makeVolumeInfo_('/' + info.mountPath, error, onVolumeInfo);
} else {
for (var i = 0; i < mountedVolumes.length; i++) {
var volume = mountedVolumes[i];
self.mountedVolumes_[volume.mountPath] = volume;
}
// Subscribe to the mount completed event when mount points initialized.
chrome.fileBrowserPrivate.onMountCompleted.addListener(
self.onMountCompleted_.bind(self));
var deferredQueue = self.deferredQueue_;
self.deferredQueue_ = null;
for (var i = 0; i < deferredQueue.length; i++) {
deferredQueue[i]();
}
if (mountedVolumes.length > 0)
cr.dispatchSimpleEvent(self, 'change');
}
}
chrome.fileBrowserPrivate.getMountPoints(step);
};
/**
* 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.mountType, event.sourcePath);
var error = event.status == 'success' ? '' : event.status;
this.makeVolumeInfo_(event.mountPath, error, function(volume) {
this.mountedVolumes_[volume.mountPath] = volume;
this.finishRequest_(requestKey, event.status, event.mountPath);
cr.dispatchSimpleEvent(this, 'change');
}.bind(this));
} else {
console.log('No mount path');
this.finishRequest_(requestKey, event.status);
}
} else if (event.eventType == 'unmount') {
var mountPath = event.mountPath;
this.validateMountPath_(mountPath);
var status = event.status;
if (status == VolumeManager.Error.PATH_UNMOUNTED) {
console.log('Volume already unmounted: ', mountPath);
status = 'success';
}
var requestKey = this.makeRequestKey_('unmount', '', event.mountPath);
var requested = requestKey in this.requests_;
if (event.status == 'success' && !requested &&
mountPath in this.mountedVolumes_) {
console.log('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') {
delete this.mountedVolumes_[mountPath];
cr.dispatchSimpleEvent(this, 'change');
}
}
if (event.mountType == 'gdata') {
if (event.status == 'success') {
if (event.eventType == 'mount') {
this.waitGDataLoaded_(event.mountPath,
this.setGDataStatus_.bind(this, VolumeManager.GDataStatus.MOUNTED));
} else if (event.eventType == 'unmount') {
this.setGDataStatus_(VolumeManager.GDataStatus.UNMOUNTED);
}
}
}
};
/**
* First access to GDrive takes time (to fetch data from the cloud).
* We want to change state to MOUNTED (likely from MOUNTING) when the
* drive ready to operate.
*
* @param {string} mountPath GData mount path.
* @param {function()} callback To be called when waiting finish.
* @private
*/
VolumeManager.prototype.waitGDataLoaded_ = function(mountPath, callback) {
chrome.fileBrowserPrivate.requestLocalFileSystem(function(filesystem) {
filesystem.root.getDirectory(mountPath, {}, function() { callback(); });
});
};
/**
* @param {string} mountPath Path to the volume.
* @param {VolumeManager?} error Mounting error if any.
* @param {function(Object)} callback Result acceptor.
* @private
*/
VolumeManager.prototype.makeVolumeInfo_ = function(
mountPath, error, callback) {
if (error)
this.validateError_(error);
this.validateMountPath_(mountPath);
function onVolumeMetadata(metadata) {
callback({
mountPath: mountPath,
error: error,
deviceType: metadata && metadata.deviceType,
readonly: !!metadata && metadata.isReadOnly
});
}
chrome.fileBrowserPrivate.getVolumeMetadata(
util.makeFilesystemUrl(mountPath), onVolumeMetadata);
};
/**
* Creates string to match mount events with requests.
* @param {string} requestType 'mount' | 'unmount'.
* @param {string} mountType 'device' | 'file' | 'network' | 'gdata'.
* @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,
mountType,
mountOrSourcePath) {
return requestType + ':' + mountType + ':' + mountOrSourcePath;
};
/**
* @param {Function} successCallback Success callback.
* @param {Function} errorCallback Error callback.
*/
VolumeManager.prototype.mountGData = function(successCallback, errorCallback) {
if (this.getGDataStatus() == VolumeManager.GDataStatus.ERROR) {
this.setGDataStatus_(VolumeManager.GDataStatus.UNMOUNTED);
}
var self = this;
var timeout = setTimeout(function() {
if (self.getGDataStatus() == VolumeManager.GDataStatus.UNMOUNTED)
self.setGDataStatus_(VolumeManager.GDataStatus.MOUNTING);
timeout = null;
}, VolumeManager.MOUNTING_DELAY);
this.mount_('', 'gdata', function(mountPath) {
successCallback(mountPath);
}, function(error) {
if (self.getGDataStatus() != VolumeManager.GDataStatus.MOUNTED)
self.setGDataStatus_(VolumeManager.GDataStatus.ERROR);
if (timeout != null)
clearTimeout(timeout);
errorCallback(error);
});
};
/**
* @param {string} fileUrl File url to the archive file.
* @param {Function} successCallback Success callback.
* @param {Function} errorCallback Error callback.
*/
VolumeManager.prototype.mountArchive = function(fileUrl, successCallback,
errorCallback) {
this.mount_(fileUrl, 'file', successCallback, errorCallback);
};
/**
* Unmounts volume.
* @param {string} mountPath Volume mounted path.
* @param {Function} successCallback Success callback.
* @param {Function} errorCallback Error callback.
*/
VolumeManager.prototype.unmount = function(mountPath,
successCallback,
errorCallback) {
this.validateMountPath_(mountPath);
if (this.deferredQueue_) {
this.deferredQueue_.push(this.unmount.bind(this,
mountPath, successCallback, errorCallback));
return;
}
var volumeInfo = this.mountedVolumes_[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);
};
/**
* @param {string} mountPath Volume mounted path.
* @return {VolumeManager.Error?} Returns mount error code
* or undefined if no error.
*/
VolumeManager.prototype.getMountError = function(mountPath) {
return this.getVolumeInfo_(mountPath).error;
};
/**
* @param {string} mountPath Volume mounted path.
* @return {boolean} True if volume at |mountedPath| is mounted but not usable.
*/
VolumeManager.prototype.isUnreadable = function(mountPath) {
var error = this.getMountError(mountPath);
return error == VolumeManager.Error.UNKNOWN_FILESYSTEM ||
error == VolumeManager.Error.UNSUPPORTED_FILESYSTEM;
};
/**
* @param {string} mountPath Volume mounted path.
* @return {string} Device type ('usb'|'sd'|'optical'|'mobile'|'unknown')
* (as defined in chromeos/disks/disk_mount_manager.cc).
*/
VolumeManager.prototype.getDeviceType = function(mountPath) {
return this.getVolumeInfo_(mountPath).deviceType;
};
/**
* @param {string} mountPath Volume mounted path.
* @return {boolean} True if volume at |mountedPath| is read only.
*/
VolumeManager.prototype.isReadOnly = function(mountPath) {
return !!this.getVolumeInfo_(mountPath).readonly;
};
/**
* Helper method.
* @param {string} mountPath Volume mounted path.
* @return {Object} Structure created in |startRequest_|.
* @private
*/
VolumeManager.prototype.getVolumeInfo_ = function(mountPath) {
this.validateMountPath_(mountPath);
return this.mountedVolumes_[mountPath] || {};
};
/**
* @param {string} url URL for for |fileBrowserPrivate.addMount|.
* @param {'gdata'|'file'} mountType Mount type for
* |fileBrowserPrivate.addMount|.
* @param {Function} successCallback Success callback.
* @param {Function} errorCallback Error callback.
* @private
*/
VolumeManager.prototype.mount_ = function(url, mountType,
successCallback, errorCallback) {
if (this.deferredQueue_) {
this.deferredQueue_.push(this.mount_.bind(this,
url, mountType, successCallback, errorCallback));
return;
}
chrome.fileBrowserPrivate.addMount(url, mountType, {},
function(sourcePath) {
console.log('Mount request: url=' + url + '; mountType=' + mountType +
'; sourceUrl=' + sourcePath);
var requestKey = this.makeRequestKey_('mount', mountType, sourcePath);
this.startRequest_(requestKey, successCallback, errorCallback);
}.bind(this));
};
/**
* @param {sting} key Key produced by |makeRequestKey_|.
* @param {Function} successCallback To be called when request finishes
* successfully.
* @param {Function} 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 {sting} key Key produced by |makeRequestKey_|.
* @private
*/
VolumeManager.prototype.onTimeout_ = function(key) {
this.invokeRequestCallbacks_(this.requests_[key],
VolumeManager.Error.TIMEOUT);
delete this.requests_[key];
};
/**
* @param {sting} 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) {
function callEach(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 {
this.validateError_(status);
callEach(request.errorCallbacks, this, [status]);
}
};
/**
* @param {VolumeManager.Error} error Status string iusually received from API.
* @private
*/
VolumeManager.prototype.validateError_ = function(error) {
for (var i in VolumeManager.Error) {
if (error == VolumeManager.Error[i])
return;
}
throw new Error('Invalid mount error: ', error);
};
/**
* @param {string} mountPath Mount path.
* @private
*/
VolumeManager.prototype.validateMountPath_ = function(mountPath) {
if (!/^\/(((archive|removable)\/[^\/]+)|drive|Downloads)$/.test(mountPath))
throw new Error('Invalid mount path: ', mountPath);
};