blob: 2d98d6bb1c3f8c59bff09e6b0e448d60e0aff1f2 [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.
'use strict';
/**
* Represents each volume, such as "drive", "download directory", each "USB
* flush storage", or "mounted zip archive" etc.
*
* @param {util.VolumeType} volumeType The type of the volume.
* @param {string} mountPath Where the volume is mounted.
* @param {string} volumeId ID of the volume.
* @param {DirectoryEntry} root The root directory entry of this volume.
* @param {string} error The error if an error is found.
* @param {string} deviceType The type of device ('usb'|'sd'|'optical'|'mobile'
* |'unknown') (as defined in chromeos/disks/disk_mount_manager.cc).
* Can be null.
* @param {boolean} isReadOnly True if the volume is read only.
* @param {!{displayName:string, isCurrentProfile:boolean}} profile Profile
* information.
* @constructor
*/
function VolumeInfo(
volumeType,
mountPath,
volumeId,
root,
error,
deviceType,
isReadOnly,
profile) {
this.volumeType = volumeType;
// TODO(hidehiko): This should include FileSystem instance.
this.mountPath = mountPath;
this.volumeId = volumeId;
this.root = root;
// Note: This represents if the mounting of the volume is successfully done
// or not. (If error is empty string, the mount is successfully done).
// TODO(hidehiko): Rename to make this more understandable.
this.error = error;
this.deviceType = deviceType;
this.isReadOnly = isReadOnly;
this.profile = Object.freeze(profile);
// VolumeInfo is immutable.
Object.freeze(this);
}
/**
* Obtains a URL of the display root directory that users can see as a root.
* @return {string} URL of root entry.
*/
VolumeInfo.prototype.getDisplayRootDirectoryURL = function() {
return this.root.toURL() +
(this.volumeType === util.VolumeType.DRIVE ? '/root' : '');
};
/**
* Utilities for volume manager implementation.
*/
var volumeManagerUtil = {};
/**
* Throws an Error when the given error is not in util.VolumeError.
* @param {util.VolumeError} error Status string usually received from APIs.
*/
volumeManagerUtil.validateError = function(error) {
for (var key in util.VolumeError) {
if (error === util.VolumeError[key])
return;
}
throw new Error('Invalid mount error: ' + error);
};
/**
* Returns the root entry of a volume mounted at mountPath.
*
* @param {string} mountPath The mounted path of the volume.
* @param {function(DirectoryEntry)} successCallback Called when the root entry
* is found.
* @param {function(FileError)} errorCallback Called when an error is found.
* @private
*/
volumeManagerUtil.getRootEntry_ = function(
mountPath, successCallback, errorCallback) {
// We always request FileSystem here, because requestFileSystem() grants
// permissions if necessary, especially for Drive File System at first mount
// time.
// Note that we actually need to request FileSystem after multi file system
// support, so this will be more natural code then.
chrome.fileBrowserPrivate.requestFileSystem(
'compatible',
function(fileSystem) {
// TODO(hidehiko): chrome.runtime.lastError should have error reason.
if (!fileSystem) {
errorCallback(util.createFileError(FileError.NOT_FOUND_ERR));
return;
}
fileSystem.root.getDirectory(
mountPath.substring(1), // Strip leading '/'.
{create: false}, successCallback, errorCallback);
});
};
/**
* Builds the VolumeInfo data from VolumeMetadata.
* @param {VolumeMetadata} volumeMetadata Metadata instance for the volume.
* @param {function(VolumeInfo)} callback Called on completion.
*/
volumeManagerUtil.createVolumeInfo = function(volumeMetadata, callback) {
volumeManagerUtil.getRootEntry_(
volumeMetadata.mountPath,
function(entry) {
if (volumeMetadata.volumeType === util.VolumeType.DRIVE) {
// After file system is mounted, we "read" drive grand root
// entry at first. This triggers full feed fetch on background.
// Note: we don't need to handle errors here, because even if
// it fails, accessing to some path later will just become
// a fast-fetch and it re-triggers full-feed fetch.
entry.createReader().readEntries(
function() { /* do nothing */ },
function(error) {
console.error(
'Triggering full feed fetch is failed: ' +
util.getFileErrorMnemonic(error.code));
});
}
callback(new VolumeInfo(
volumeMetadata.volumeType,
volumeMetadata.mountPath,
volumeMetadata.volumeId,
entry,
volumeMetadata.mountCondition,
volumeMetadata.deviceType,
volumeMetadata.isReadOnly,
volumeMetadata.profile));
},
function(fileError) {
console.error('Root entry is not found: ' +
volumeMetadata.mountPath + ', ' +
util.getFileErrorMnemonic(fileError.code));
callback(new VolumeInfo(
volumeMetadata.volumeType,
volumeMetadata.mountPath,
volumeMetadata.volumeId,
null, // Root entry is not found.
volumeMetadata.mountCondition,
volumeMetadata.deviceType,
volumeMetadata.isReadOnly,
volumeMetadata.profile));
});
};
/**
* The order of the volume list based on root type.
* @type {Array.<string>}
* @const
* @private
*/
volumeManagerUtil.volumeListOrder_ = [
RootType.DRIVE, RootType.DOWNLOADS, RootType.ARCHIVE, RootType.REMOVABLE
];
/**
* Compares mount paths to sort the volume list order.
* @param {string} mountPath1 The mount path for the first volume.
* @param {string} mountPath2 The mount path for the second volume.
* @return {number} 0 if mountPath1 and mountPath2 are same, -1 if VolumeInfo
* mounted at mountPath1 should be listed before the one mounted at
* mountPath2, otherwise 1.
*/
volumeManagerUtil.compareMountPath = function(mountPath1, mountPath2) {
var order1 = volumeManagerUtil.volumeListOrder_.indexOf(
PathUtil.getRootType(mountPath1));
var order2 = volumeManagerUtil.volumeListOrder_.indexOf(
PathUtil.getRootType(mountPath2));
if (order1 !== order2)
return order1 < order2 ? -1 : 1;
if (mountPath1 !== mountPath2)
return mountPath1 < mountPath2 ? -1 : 1;
// The path is same.
return 0;
};
/**
* The container of the VolumeInfo for each mounted volume.
* @constructor
*/
function VolumeInfoList() {
/**
* Holds VolumeInfo instances.
* @type {cr.ui.ArrayDataModel}
* @private
*/
this.model_ = new cr.ui.ArrayDataModel([]);
Object.freeze(this);
}
VolumeInfoList.prototype = {
get length() { return this.model_.length; }
};
/**
* Adds the event listener to listen the change of volume info.
* @param {string} type The name of the event.
* @param {function(Event)} handler The handler for the event.
*/
VolumeInfoList.prototype.addEventListener = function(type, handler) {
this.model_.addEventListener(type, handler);
};
/**
* Removes the event listener.
* @param {string} type The name of the event.
* @param {function(Event)} handler The handler to be removed.
*/
VolumeInfoList.prototype.removeEventListener = function(type, handler) {
this.model_.removeEventListener(type, handler);
};
/**
* Adds the volumeInfo to the appropriate position. If there already exists,
* just replaces it.
* @param {VolumeInfo} volumeInfo The information of the new volume.
*/
VolumeInfoList.prototype.add = function(volumeInfo) {
var index = this.findLowerBoundIndex_(volumeInfo.mountPath);
if (index < this.length &&
this.item(index).mountPath === volumeInfo.mountPath) {
// Replace the VolumeInfo.
this.model_.splice(index, 1, volumeInfo);
} else {
// Insert the VolumeInfo.
this.model_.splice(index, 0, volumeInfo);
}
};
/**
* Removes the VolumeInfo of the volume mounted at mountPath.
* @param {string} mountPath The path to the location where the volume is
* mounted.
*/
VolumeInfoList.prototype.remove = function(mountPath) {
var index = this.findLowerBoundIndex_(mountPath);
if (index < this.length && this.item(index).mountPath === mountPath)
this.model_.splice(index, 1);
};
/**
* Searches the information of the volume mounted at mountPath.
* @param {string} mountPath The path to the location where the volume is
* mounted.
* @return {VolumeInfo} The volume's information, or null if not found.
*/
VolumeInfoList.prototype.find = function(mountPath) {
var index = this.findLowerBoundIndex_(mountPath);
if (index < this.length && this.item(index).mountPath === mountPath)
return this.item(index);
// Not found.
return null;
};
/**
* Searches the information of the volume that contains an item pointed by the
* path.
* @param {string} path Path pointing an entry on a volume.
* @return {VolumeInfo} The volume's information, or null if not found.
*/
VolumeInfoList.prototype.findByPath = function(path) {
for (var i = 0; i < this.length; i++) {
var mountPath = this.item(i).mountPath;
if (path === mountPath || path.indexOf(mountPath + '/') === 0)
return this.item(i);
}
return null;
};
/**
* @param {string} mountPath The mount path of searched volume.
* @return {number} The index of the volume if found, or the inserting
* position of the volume.
* @private
*/
VolumeInfoList.prototype.findLowerBoundIndex_ = function(mountPath) {
// Assuming the number of elements in the array data model is very small
// in most cases, use simple linear search, here.
for (var i = 0; i < this.length; i++) {
if (volumeManagerUtil.compareMountPath(
this.item(i).mountPath, mountPath) >= 0)
return i;
}
return this.length;
};
/**
* @param {number} index The index of the volume in the list.
* @return {VolumeInfo} The VolumeInfo instance.
*/
VolumeInfoList.prototype.item = function(index) {
return this.model_.item(index);
};
/**
* 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_ = {};
/**
* The list of VolumeInfo instances for each mounted volume.
* @type {VolumeInfoList}
*/
this.volumeInfoList = new VolumeInfoList();
// The status should be merged into VolumeManager.
// TODO(hidehiko): Remove them after the migration.
this.driveConnectionState_ = {
type: util.DriveConnectionType.OFFLINE,
reason: util.DriveConnectionReason.NO_SERVICE
};
chrome.fileBrowserPrivate.onDriveConnectionStatusChanged.addListener(
this.onDriveConnectionStatusChanged_.bind(this));
this.onDriveConnectionStatusChanged_();
}
/**
* Invoked when the drive connection status is changed.
* @private_
*/
VolumeManager.prototype.onDriveConnectionStatusChanged_ = function() {
chrome.fileBrowserPrivate.getDriveConnectionState(function(state) {
this.driveConnectionState_ = state;
cr.dispatchSimpleEvent(this, 'drive-connection-changed');
}.bind(this));
};
/**
* Returns the drive connection state.
* @return {util.DriveConnectionType} Connection type.
*/
VolumeManager.prototype.getDriveConnectionState = function() {
return this.driveConnectionState_;
};
/**
* VolumeManager extends cr.EventTarget.
*/
VolumeManager.prototype.__proto__ = cr.EventTarget.prototype;
/**
* Time in milliseconds that we wait a response for. If no response on
* mount/unmount received the request supposed failed.
*/
VolumeManager.TIMEOUT = 15 * 60 * 1000;
/**
* Queue to run getInstance sequentially.
* @type {AsyncUtil.Queue}
* @private
*/
VolumeManager.getInstanceQueue_ = new AsyncUtil.Queue();
/**
* The singleton instance of VolumeManager. Initialized by the first invocation
* of getInstance().
* @type {VolumeManager}
* @private
*/
VolumeManager.instance_ = null;
/**
* Returns the VolumeManager instance asynchronously. If it is not created or
* under initialization, it will waits for the finish of the initialization.
* @param {function(VolumeManager)} callback Called with the VolumeManager
* instance.
*/
VolumeManager.getInstance = function(callback) {
VolumeManager.getInstanceQueue_.run(function(continueCallback) {
if (VolumeManager.instance_) {
callback(VolumeManager.instance_);
continueCallback();
return;
}
VolumeManager.instance_ = new VolumeManager();
VolumeManager.instance_.initialize_(function() {
callback(VolumeManager.instance_);
continueCallback();
});
});
};
/**
* Initializes mount points.
* @param {function()} callback Called upon the completion of the
* initialization.
* @private
*/
VolumeManager.prototype.initialize_ = function(callback) {
chrome.fileBrowserPrivate.getVolumeMetadataList(function(volumeMetadataList) {
// Create VolumeInfo for each volume.
var group = new AsyncUtil.Group();
for (var i = 0; i < volumeMetadataList.length; i++) {
group.add(function(volumeMetadata, continueCallback) {
volumeManagerUtil.createVolumeInfo(
volumeMetadata,
function(volumeInfo) {
this.volumeInfoList.add(volumeInfo);
if (volumeMetadata.volumeType === util.VolumeType.DRIVE)
this.onDriveConnectionStatusChanged_();
continueCallback();
}.bind(this));
}.bind(this, volumeMetadataList[i]));
}
// Then, finalize the initialization.
group.run(function() {
// Subscribe to the mount completed event when mount points initialized.
chrome.fileBrowserPrivate.onMountCompleted.addListener(
this.onMountCompleted_.bind(this));
callback();
}.bind(this));
}.bind(this));
};
/**
* Event handler called when some volume was mounted or unmounted.
* @param {MountCompletedEvent} event Received event.
* @private
*/
VolumeManager.prototype.onMountCompleted_ = function(event) {
if (event.eventType === 'mount') {
if (event.volumeMetadata.mountPath) {
var requestKey = this.makeRequestKey_(
'mount',
event.volumeMetadata.sourcePath);
var error = event.status === 'success' ? '' : event.status;
volumeManagerUtil.createVolumeInfo(
event.volumeMetadata,
function(volumeInfo) {
this.volumeInfoList.add(volumeInfo);
this.finishRequest_(requestKey, event.status, volumeInfo.mountPath);
if (volumeInfo.volumeType === util.VolumeType.DRIVE) {
// Update the network connection status, because until the
// drive is initialized, the status is set to not ready.
// TODO(hidehiko): The connection status should be migrated into
// VolumeMetadata.
this.onDriveConnectionStatusChanged_();
}
}.bind(this));
} else {
console.warn('No mount path.');
this.finishRequest_(requestKey, event.status);
}
} else if (event.eventType === 'unmount') {
var mountPath = event.volumeMetadata.mountPath;
var status = event.status;
if (status === util.VolumeError.PATH_UNMOUNTED) {
console.warn('Volume already unmounted: ', mountPath);
status = 'success';
}
var requestKey = this.makeRequestKey_('unmount', mountPath);
var requested = requestKey in this.requests_;
var volumeInfo = this.volumeInfoList.find(mountPath);
if (event.status === 'success' && !requested && volumeInfo) {
console.warn('Mounted volume without a request: ', mountPath);
var e = new Event('externally-unmounted');
// TODO(mtomasz): The mountPath field is deprecated. Remove it.
e.mountPath = mountPath;
e.volumeInfo = volumeInfo;
this.dispatchEvent(e);
}
this.finishRequest_(requestKey, status);
if (event.status === 'success')
this.volumeInfoList.remove(mountPath);
}
};
/**
* Creates string to match mount events with requests.
* @param {string} requestType 'mount' | 'unmount'. TODO(hidehiko): Replace by
* enum.
* @param {string} path Source path provided by API for mount request, or
* mount path for unmount request.
* @return {string} Key for |this.requests_|.
* @private
*/
VolumeManager.prototype.makeRequestKey_ = function(requestType, path) {
return requestType + ':' + path;
};
/**
* @param {string} fileUrl File url to the archive file.
* @param {function(string)} successCallback Success callback.
* @param {function(util.VolumeError)} errorCallback Error callback.
*/
VolumeManager.prototype.mountArchive = function(
fileUrl, successCallback, errorCallback) {
chrome.fileBrowserPrivate.addMount(fileUrl, function(sourcePath) {
console.info(
'Mount request: url=' + fileUrl + '; sourceUrl=' + sourcePath);
var requestKey = this.makeRequestKey_('mount', sourcePath);
this.startRequest_(requestKey, successCallback, errorCallback);
}.bind(this));
};
/**
* Unmounts volume.
* @param {string} mountPath Volume mounted path.
* @param {function(string)} successCallback Success callback.
* @param {function(util.VolumeError)} errorCallback Error callback.
*/
VolumeManager.prototype.unmount = function(mountPath,
successCallback,
errorCallback) {
var volumeInfo = this.volumeInfoList.find(mountPath);
if (!volumeInfo) {
errorCallback(util.VolumeError.NOT_MOUNTED);
return;
}
chrome.fileBrowserPrivate.removeMount(util.makeFilesystemUrl(mountPath));
var requestKey = this.makeRequestKey_('unmount', volumeInfo.mountPath);
this.startRequest_(requestKey, successCallback, errorCallback);
};
/**
* Resolves the absolute path to its entry. Shouldn't be used outside of the
* Files app's initialization.
* @param {string} path The path to be resolved.
* @param {function(Entry)} successCallback Called with the resolved entry on
* success.
* @param {function(FileError)} errorCallback Called on error.
*/
VolumeManager.prototype.resolveAbsolutePath = function(
path, successCallback, errorCallback) {
// Make sure the path is in the mounted volume.
var volumeInfo = this.getVolumeInfo(path);
if (!volumeInfo || !volumeInfo.root) {
errorCallback(util.createFileError(FileError.NOT_FOUND_ERR));
return;
}
webkitResolveLocalFileSystemURL(
util.makeFilesystemUrl(path), successCallback, errorCallback);
};
/**
* Obtains the information of the volume that containing an entry pointed by the
* specified path.
* TODO(hirono): Stop to use path to get a volume info.
*
* @param {string|Entry} target Path or Entry pointing anywhere on a volume.
* @return {VolumeInfo} The data about the volume.
*/
VolumeManager.prototype.getVolumeInfo = function(target) {
if (typeof target === 'string')
return this.volumeInfoList.findByPath(target);
else if (util.isFakeEntry(target))
return this.getCurrentProfileVolumeInfo(util.VolumeType.DRIVE);
else
return this.volumeInfoList.findByPath(target.fullPath);
};
/**
* Obtains a volume information from a file entry URL.
* TODO(hirono): Check a file system to find a volume.
*
* @param {string} url URL of entry.
* @return {VolumeInfo} Volume info.
*/
VolumeManager.prototype.getVolumeInfoByURL = function(url) {
return this.getVolumeInfo(util.extractFilePath(url));
};
/**
* Obtains a volume infomration of the current profile.
*
* @param {util.VolumeType} volumeType Volume type.
* @return {VolumeInfo} Volume info.
*/
VolumeManager.prototype.getCurrentProfileVolumeInfo = function(volumeType) {
for (var i = 0; i < this.volumeInfoList.length; i++) {
var volumeInfo = this.volumeInfoList.item(i);
if (volumeInfo.profile.isCurrentProfile &&
volumeInfo.volumeType === volumeType)
return volumeInfo;
}
return null;
};
/**
* Obtains location information from an entry.
*
* @param {Entry|Object} entry File or directory entry. It can be a fake entry.
* @return {EntryLocation} Location information.
*/
VolumeManager.prototype.getLocationInfo = function(entry) {
if (util.isFakeEntry(entry)) {
return new EntryLocation(
// TODO(hirono): Specify currect volume.
this.getCurrentProfileVolumeInfo(RootType.DRIVE),
entry.rootType,
true /* the entry points a root directory. */);
} else {
return this.getLocationInfoByPath(entry.fullPath);
}
};
/**
* Obtains location information from a path.
* TODO(hirono): Remove the method before introducing separate file system.
*
* @param {string} path Path.
* @return {EntryLocation} Location information.
*/
VolumeManager.prototype.getLocationInfoByPath = function(path) {
var volumeInfo = this.volumeInfoList.findByPath(path);
return volumeInfo && PathUtil.getLocationInfo(volumeInfo, path);
};
/**
* @param {string} key Key produced by |makeRequestKey_|.
* @param {function(string)} successCallback To be called when request finishes
* successfully.
* @param {function(util.VolumeError)} 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 {string} key Key produced by |makeRequestKey_|.
* @private
*/
VolumeManager.prototype.onTimeout_ = function(key) {
this.invokeRequestCallbacks_(this.requests_[key],
util.VolumeError.TIMEOUT);
delete this.requests_[key];
};
/**
* @param {string} key Key produced by |makeRequestKey_|.
* @param {util.VolumeError|'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 {util.VolumeError|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) {
var callEach = function(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 {
volumeManagerUtil.validateError(status);
callEach(request.errorCallbacks, this, [status]);
}
};