blob: 68eda2c6b714a9b0d1c7ed804bed188eb758935e [file] [log] [blame]
// Copyright 2016 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
* @implements {VolumeManager}
* @extends {cr.EventTarget}
*/
function VolumeManagerImpl() {
/** @override */
this.volumeInfoList = new VolumeInfoListImpl();
/**
* 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<Object>}
* @private
*/
this.requests_ = {};
/**
* Queue for mounting.
* @type {AsyncUtil.Queue}
* @private
*/
this.mountQueue_ = new AsyncUtil.Queue();
// The status should be merged into VolumeManager.
// TODO(hidehiko): Remove them after the migration.
/**
* Connection state of the Drive.
* @type {VolumeManagerCommon.DriveConnectionState}
* @private
*/
this.driveConnectionState_ = {
type: VolumeManagerCommon.DriveConnectionType.OFFLINE,
reason: VolumeManagerCommon.DriveConnectionReason.NO_SERVICE,
hasCellularNetworkAccess: false
};
chrome.fileManagerPrivate.onDriveConnectionStatusChanged.addListener(
this.onDriveConnectionStatusChanged_.bind(this));
this.onDriveConnectionStatusChanged_();
}
/**
* Invoked when the drive connection status is changed.
* @private
*/
VolumeManagerImpl.prototype.onDriveConnectionStatusChanged_ = function() {
chrome.fileManagerPrivate.getDriveConnectionState(function(state) {
this.driveConnectionState_ = state;
cr.dispatchSimpleEvent(this, 'drive-connection-changed');
}.bind(this));
};
/** @override */
VolumeManagerImpl.prototype.getDriveConnectionState = function() {
return this.driveConnectionState_;
};
/**
* VolumeManager extends cr.EventTarget.
*/
VolumeManagerImpl.prototype.__proto__ = cr.EventTarget.prototype;
/**
* Adds new volume info from the given volumeMetadata. If the corresponding
* volume info has already been added, the volumeMetadata is ignored.
* @param {!chrome.fileManagerPrivate.VolumeMetadata} volumeMetadata
* @return {!Promise<!VolumeInfo>}
* @private
*/
VolumeManagerImpl.prototype.addVolumeMetadata_ = function(volumeMetadata) {
return volumeManagerUtil.createVolumeInfo(volumeMetadata).then(
(/**
* @param {!VolumeInfo} volumeInfo
* @return {!VolumeInfo}
*/
function(volumeInfo) {
// We don't show Downloads and Drive on volume list if they have mount
// error, since users can do nothing in this situation.
// We show Removable and Provided volumes regardless of mount error so
// that users can unmount or format the volume.
// TODO(fukino): Once the Files app gets ready, show erroneous Drive
// volume so that users can see auth warning banner on the volume.
// crbug.com/517772.
var shouldShow = true;
switch (volumeInfo.volumeType) {
case VolumeManagerCommon.VolumeType.DOWNLOADS:
case VolumeManagerCommon.VolumeType.DRIVE:
shouldShow = !!volumeInfo.fileSystem;
break;
}
if (!shouldShow)
return volumeInfo;
if (this.volumeInfoList.findIndex(volumeInfo.volumeId) === -1) {
this.volumeInfoList.add(volumeInfo);
// Update the network connection status, because until the drive is
// initialized, the status is set to not ready.
// TODO(mtomasz): The connection status should be migrated into
// chrome.fileManagerPrivate.VolumeMetadata.
if (volumeMetadata.volumeType ===
VolumeManagerCommon.VolumeType.DRIVE) {
this.onDriveConnectionStatusChanged_();
}
} else if (volumeMetadata.volumeType ===
VolumeManagerCommon.VolumeType.REMOVABLE) {
// Update for remounted USB external storage, because they were
// remounted to switch read-only policy.
this.volumeInfoList.add(volumeInfo);
}
return volumeInfo;
}).bind(this));
};
/**
* Initializes mount points.
* @param {function()} callback Called upon the completion of the
* initialization.
* @private
*/
VolumeManagerImpl.prototype.initialize_ = function(callback) {
chrome.fileManagerPrivate.onMountCompleted.addListener(
this.onMountCompleted_.bind(this));
console.debug('Requesting volume list.');
chrome.fileManagerPrivate.getVolumeMetadataList(function(volumeMetadataList) {
console.debug(
'Volume list fetched with: ' + volumeMetadataList.length + ' items.');
// We must subscribe to the mount completed event in the callback of
// getVolumeMetadataList. crbug.com/330061.
// But volumes reported by onMountCompleted events must be added after the
// volumes in the volumeMetadataList are mounted. crbug.com/135477.
this.mountQueue_.run(function(inCallback) {
// Create VolumeInfo for each volume.
Promise.all(
volumeMetadataList.map(function(volumeMetadata) {
console.debug(
'Initializing volume: ' + volumeMetadata.volumeId);
return this.addVolumeMetadata_(volumeMetadata).then(
function(volumeInfo) {
console.debug('Initialized volume: ' + volumeInfo.volumeId);
});
}.bind(this)))
.then(function() {
console.debug('Initialized all volumes.');
// Call the callback of the initialize function.
callback();
// Call the callback of AsyncQueue. Maybe it invokes callbacks
// registered by mountCompleted events.
inCallback();
});
}.bind(this));
}.bind(this));
};
/**
* Event handler called when some volume was mounted or unmounted.
* @param {chrome.fileManagerPrivate.MountCompletedEvent} event Received event.
* @private
*/
VolumeManagerImpl.prototype.onMountCompleted_ = function(event) {
this.mountQueue_.run(function(callback) {
switch (event.eventType) {
case 'mount':
var requestKey = this.makeRequestKey_(
'mount',
event.volumeMetadata.sourcePath || '');
if (event.status === 'success' ||
event.status ===
VolumeManagerCommon.VolumeError.UNKNOWN_FILESYSTEM ||
event.status ===
VolumeManagerCommon.VolumeError.UNSUPPORTED_FILESYSTEM) {
this.addVolumeMetadata_(event.volumeMetadata).then(
function(volumeInfo) {
this.finishRequest_(requestKey, event.status, volumeInfo);
callback();
}.bind(this));
} else if (event.status ===
VolumeManagerCommon.VolumeError.ALREADY_MOUNTED) {
var navigationEvent =
new Event(VolumeManagerCommon.VOLUME_ALREADY_MOUNTED);
navigationEvent.volumeId = event.volumeMetadata.volumeId;
this.dispatchEvent(navigationEvent);
this.finishRequest_(requestKey, event.status, volumeInfo);
callback();
} else {
console.warn('Failed to mount a volume: ' + event.status);
this.finishRequest_(requestKey, event.status);
callback();
}
break;
case 'unmount':
var volumeId = event.volumeMetadata.volumeId;
var status = event.status;
var requestKey = this.makeRequestKey_('unmount', volumeId);
var requested = requestKey in this.requests_;
var volumeInfoIndex =
this.volumeInfoList.findIndex(volumeId);
var volumeInfo = volumeInfoIndex !== -1 ?
this.volumeInfoList.item(volumeInfoIndex) : null;
if (event.status === 'success' && !requested && volumeInfo) {
console.warn('Unmounted volume without a request: ' + volumeId);
var e = new Event('externally-unmounted');
e.volumeInfo = volumeInfo;
this.dispatchEvent(e);
}
this.finishRequest_(requestKey, status);
if (event.status === 'success')
this.volumeInfoList.remove(event.volumeMetadata.volumeId);
console.debug('unmounted volume: ' + volumeId);
callback();
break;
}
}.bind(this));
};
/**
* Creates string to match mount events with requests.
* @param {string} requestType 'mount' | 'unmount'. TODO(hidehiko): Replace by
* enum.
* @param {string} argument Argument describing the request, eg. source file
* path of the archive to be mounted, or a volumeId for unmounting.
* @return {string} Key for |this.requests_|.
* @private
*/
VolumeManagerImpl.prototype.makeRequestKey_ = function(requestType, argument) {
return requestType + ':' + argument;
};
/** @override */
VolumeManagerImpl.prototype.mountArchive = function(
fileUrl, successCallback, errorCallback) {
chrome.fileManagerPrivate.addMount(fileUrl, function(sourcePath) {
console.info(
'Mount request: url=' + fileUrl + '; sourcePath=' + sourcePath);
var requestKey = this.makeRequestKey_('mount', sourcePath);
this.startRequest_(requestKey, successCallback, errorCallback);
}.bind(this));
};
/** @override */
VolumeManagerImpl.prototype.unmount = function(volumeInfo,
successCallback,
errorCallback) {
chrome.fileManagerPrivate.removeMount(volumeInfo.volumeId);
var requestKey = this.makeRequestKey_('unmount', volumeInfo.volumeId);
this.startRequest_(requestKey, successCallback, errorCallback);
};
/** @override */
VolumeManagerImpl.prototype.configure = function(volumeInfo) {
return new Promise(function(fulfill, reject) {
chrome.fileManagerPrivate.configureVolume(
volumeInfo.volumeId,
function() {
if (chrome.runtime.lastError)
reject(chrome.runtime.lastError.message);
else
fulfill();
});
});
};
/** @override */
VolumeManagerImpl.prototype.getVolumeInfo = function(entry) {
for (let i = 0; i < this.volumeInfoList.length; i++) {
const volumeInfo = this.volumeInfoList.item(i);
if (volumeInfo.fileSystem &&
util.isSameFileSystem(volumeInfo.fileSystem, entry.filesystem)) {
return volumeInfo;
}
// Additionally, check fake entries.
for (let key in volumeInfo.fakeEntries_) {
const fakeEntry = volumeInfo.fakeEntries_[key];
if (util.isSameEntry(fakeEntry, entry))
return volumeInfo;
}
}
return null;
};
/** @override */
VolumeManagerImpl.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;
};
/** @override */
VolumeManagerImpl.prototype.getLocationInfo = function(entry) {
var volumeInfo = this.getVolumeInfo(entry);
if (util.isFakeEntry(entry)) {
return new EntryLocationImpl(
volumeInfo, assert(entry.rootType),
true /* the entry points a root directory. */,
true /* fake entries are read only. */);
}
if (!volumeInfo)
return null;
var rootType;
var isReadOnly;
var isRootEntry;
if (volumeInfo.volumeType === VolumeManagerCommon.VolumeType.DRIVE) {
// For Drive, the roots are /root, /team_drives, /Computers and /other,
// instead of /. Root URLs contain trailing slashes.
if (entry.fullPath == '/root' || entry.fullPath.indexOf('/root/') === 0) {
rootType = VolumeManagerCommon.RootType.DRIVE;
isReadOnly = volumeInfo.isReadOnly;
isRootEntry = entry.fullPath === '/root';
} else if (
entry.fullPath == VolumeManagerCommon.TEAM_DRIVES_DIRECTORY_PATH ||
entry.fullPath.indexOf(
VolumeManagerCommon.TEAM_DRIVES_DIRECTORY_PATH + '/') === 0) {
if (entry.fullPath == VolumeManagerCommon.TEAM_DRIVES_DIRECTORY_PATH) {
rootType = VolumeManagerCommon.RootType.TEAM_DRIVES_GRAND_ROOT;
isReadOnly = true;
isRootEntry = true;
} else {
rootType = VolumeManagerCommon.RootType.TEAM_DRIVE;
if (util.isTeamDriveRoot(entry)) {
isReadOnly = false;
isRootEntry = true;
} else {
// Regular files/directories under Team Drives.
isRootEntry = false;
isReadOnly = volumeInfo.isReadOnly;
}
}
} else if (
entry.fullPath == VolumeManagerCommon.COMPUTERS_DIRECTORY_PATH ||
entry.fullPath.indexOf(
VolumeManagerCommon.COMPUTERS_DIRECTORY_PATH + '/') === 0) {
if (entry.fullPath == VolumeManagerCommon.COMPUTERS_DIRECTORY_PATH) {
rootType = VolumeManagerCommon.RootType.COMPUTERS_GRAND_ROOT;
isReadOnly = true;
isRootEntry = true;
} else {
rootType = VolumeManagerCommon.RootType.COMPUTER;
if (util.isComputersRoot(entry)) {
isReadOnly = false;
isRootEntry = true;
} else {
// Regular files/directories under a Computer entry.
isRootEntry = false;
isReadOnly = volumeInfo.isReadOnly;
}
}
} else if (entry.fullPath == '/other' ||
entry.fullPath.indexOf('/other/') === 0) {
rootType = VolumeManagerCommon.RootType.DRIVE_OTHER;
isReadOnly = true;
isRootEntry = entry.fullPath === '/other';
} else if (
entry.fullPath === '/.files-by-id' ||
entry.fullPath.indexOf('/.files-by-id/') === 0) {
rootType = VolumeManagerCommon.RootType.DRIVE_OTHER;
// /.files-by-id/<id> is read-only, but /.files-by-id/<id>/foo is
// read-write.
isReadOnly = entry.fullPath.split('/').length < 4;
isRootEntry = entry.fullPath === '/.files-by-id';
} else {
// Accessing Drive files outside of /drive/root and /drive/other is not
// allowed, but can happen. Therefore returning null.
return null;
}
} else {
rootType =
VolumeManagerCommon.getRootTypeFromVolumeType(volumeInfo.volumeType);
isRootEntry = util.isSameEntry(entry, volumeInfo.fileSystem.root);
// Although "Play files" root directory is writable in file system level,
// we prohibit write operations on it in the UI level to avoid confusion.
// Users can still have write access in sub directories like
// /Play files/Pictures, /Play files/DCIM, etc...
if (volumeInfo.volumeType == VolumeManagerCommon.VolumeType.ANDROID_FILES &&
isRootEntry) {
isReadOnly = true;
} else {
isReadOnly = volumeInfo.isReadOnly;
}
}
return new EntryLocationImpl(volumeInfo, rootType, isRootEntry, isReadOnly);
};
/** @override */
VolumeManagerImpl.prototype.findByDevicePath = function(devicePath) {
for (let i = 0; i < this.volumeInfoList.length; i++) {
const volumeInfo = this.volumeInfoList.item(i);
if (volumeInfo.devicePath && volumeInfo.devicePath === devicePath)
return volumeInfo;
}
return null;
};
/** @override */
VolumeManagerImpl.prototype.whenVolumeInfoReady = function(volumeId) {
return new Promise((fulfill) => {
const handler = () => {
const index = this.volumeInfoList.findIndex(volumeId);
if (index !== -1) {
fulfill(this.volumeInfoList.item(index));
this.volumeInfoList.removeEventListener('splice', handler);
}
};
this.volumeInfoList.addEventListener('splice', handler);
handler();
});
};
/**
* @param {string} key Key produced by |makeRequestKey_|.
* @param {function(VolumeInfo)} successCallback To be called when the request
* finishes successfully.
* @param {function(VolumeManagerCommon.VolumeError)} errorCallback To be called
* when the request fails.
* @private
*/
VolumeManagerImpl.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),
volumeManagerUtil.TIMEOUT)
};
}
};
/**
* Called if no response received in |TIMEOUT|.
* @param {string} key Key produced by |makeRequestKey_|.
* @private
*/
VolumeManagerImpl.prototype.onTimeout_ = function(key) {
this.invokeRequestCallbacks_(this.requests_[key],
VolumeManagerCommon.VolumeError.TIMEOUT);
delete this.requests_[key];
};
/**
* @param {string} key Key produced by |makeRequestKey_|.
* @param {VolumeManagerCommon.VolumeError|string} status Status received
* from the API.
* @param {VolumeInfo=} opt_volumeInfo Volume info of the mounted volume.
* @private
*/
VolumeManagerImpl.prototype.finishRequest_ =
function(key, status, opt_volumeInfo) {
var request = this.requests_[key];
if (!request)
return;
clearTimeout(request.timeout);
this.invokeRequestCallbacks_(request, status, opt_volumeInfo);
delete this.requests_[key];
};
/**
* @param {Object} request Structure created in |startRequest_|.
* @param {VolumeManagerCommon.VolumeError|string} status If status ===
* 'success' success callbacks are called.
* @param {VolumeInfo=} opt_volumeInfo Volume info of the mounted volume.
* @private
*/
VolumeManagerImpl.prototype.invokeRequestCallbacks_ = function(
request, status, opt_volumeInfo) {
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_volumeInfo]);
} else {
volumeManagerUtil.validateError(status);
callEach(request.errorCallbacks, this, [status]);
}
};