| // 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. |
| |
| /** |
| * Represents each volume, such as "drive", "download directory", each "USB |
| * flush storage", or "mounted zip archive" etc. |
| * |
| * @param {VolumeManagerCommon.VolumeType} volumeType The type of the volume. |
| * @param {string} volumeId ID of the volume. |
| * @param {FileSystem} fileSystem The file system object for this volume. |
| * @param {(string|undefined)} error The error if an error is found. |
| * @param {(string|undefined)} deviceType The type of device |
| * ('usb'|'sd'|'optical'|'mobile'|'unknown') (as defined in |
| * chromeos/disks/disk_mount_manager.cc). Can be undefined. |
| * @param {(string|undefined)} devicePath Identifier of the device that the |
| * volume belongs to. Can be undefined. |
| * @param {boolean} isReadOnly True if the volume is read only. |
| * @param {!{displayName:string, isCurrentProfile:boolean}} profile Profile |
| * information. |
| * @param {string} label Label of the volume. |
| * @param {(string|undefined)} extensionId Id of the extension providing this |
| * volume. Empty for native volumes. |
| * @constructor |
| */ |
| function VolumeInfo( |
| volumeType, |
| volumeId, |
| fileSystem, |
| error, |
| deviceType, |
| devicePath, |
| isReadOnly, |
| profile, |
| label, |
| extensionId) { |
| this.volumeType_ = volumeType; |
| this.volumeId_ = volumeId; |
| this.fileSystem_ = fileSystem; |
| this.label_ = label; |
| this.displayRoot_ = null; |
| this.fakeEntries_ = {}; |
| this.displayRoot_ = null; |
| this.displayRootPromise_ = null; |
| |
| if (volumeType === VolumeManagerCommon.VolumeType.DRIVE) { |
| // TODO(mtomasz): Convert fake entries to DirectoryProvider. |
| this.fakeEntries_[VolumeManagerCommon.RootType.DRIVE_OFFLINE] = { |
| isDirectory: true, |
| rootType: VolumeManagerCommon.RootType.DRIVE_OFFLINE, |
| toURL: function() { return 'fake-entry://drive_offline'; } |
| }; |
| this.fakeEntries_[VolumeManagerCommon.RootType.DRIVE_SHARED_WITH_ME] = { |
| isDirectory: true, |
| rootType: VolumeManagerCommon.RootType.DRIVE_SHARED_WITH_ME, |
| toURL: function() { return 'fake-entry://drive_shared_with_me'; } |
| }; |
| this.fakeEntries_[VolumeManagerCommon.RootType.DRIVE_RECENT] = { |
| isDirectory: true, |
| rootType: VolumeManagerCommon.RootType.DRIVE_RECENT, |
| toURL: function() { return 'fake-entry://drive_recent'; } |
| }; |
| } |
| |
| // 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.devicePath_ = devicePath; |
| this.isReadOnly_ = isReadOnly; |
| this.profile_ = Object.freeze(profile); |
| this.extensionId_ = extensionId; |
| |
| Object.seal(this); |
| } |
| |
| VolumeInfo.prototype = { |
| /** |
| * @return {VolumeManagerCommon.VolumeType} Volume type. |
| */ |
| get volumeType() { |
| return this.volumeType_; |
| }, |
| /** |
| * @return {string} Volume ID. |
| */ |
| get volumeId() { |
| return this.volumeId_; |
| }, |
| /** |
| * @return {FileSystem} File system object. |
| */ |
| get fileSystem() { |
| return this.fileSystem_; |
| }, |
| /** |
| * @return {DirectoryEntry} Display root path. It is null before finishing to |
| * resolve the entry. |
| */ |
| get displayRoot() { |
| return this.displayRoot_; |
| }, |
| /** |
| * @return {Object.<string, Object>} Fake entries. |
| */ |
| get fakeEntries() { |
| return this.fakeEntries_; |
| }, |
| /** |
| * @return {(string|undefined)} Error identifier. |
| */ |
| get error() { |
| return this.error_; |
| }, |
| /** |
| * @return {(string|undefined)} Device type identifier. |
| */ |
| get deviceType() { |
| return this.deviceType_; |
| }, |
| /** |
| * @return {(string|undefined)} Device identifier. |
| */ |
| get devicePath() { |
| return this.devicePath_; |
| }, |
| /** |
| * @return {boolean} Whether read only or not. |
| */ |
| get isReadOnly() { |
| return this.isReadOnly_; |
| }, |
| /** |
| * @return {!{displayName:string, isCurrentProfile:boolean}} Profile data. |
| */ |
| get profile() { |
| return this.profile_; |
| }, |
| /** |
| * @return {string} Label for the volume. |
| */ |
| get label() { |
| return this.label_; |
| }, |
| /** |
| * @return {(string|undefined)} Id of an extennsion providing this volume. |
| */ |
| get extensionId() { |
| return this.extensionId_; |
| } |
| }; |
| |
| /** |
| * Starts resolving the display root and obtains it. It may take long time for |
| * Drive. Once resolved, it is cached. |
| * |
| * @param {function(DirectoryEntry)=} opt_onSuccess Success callback with the |
| * display root directory as an argument. |
| * @param {function(*)=} opt_onFailure Failure callback. |
| * @return {Promise} |
| */ |
| VolumeInfo.prototype.resolveDisplayRoot = function(opt_onSuccess, |
| opt_onFailure) { |
| if (!this.displayRootPromise_) { |
| // TODO(mtomasz): Do not add VolumeInfo which failed to resolve root, and |
| // remove this if logic. Call opt_onSuccess() always, instead. |
| if (this.volumeType !== VolumeManagerCommon.VolumeType.DRIVE) { |
| if (this.fileSystem_) |
| this.displayRootPromise_ = Promise.resolve(this.fileSystem_.root); |
| else |
| this.displayRootPromise_ = Promise.reject(this.error); |
| } else { |
| // For Drive, we need to resolve. |
| var displayRootURL = this.fileSystem_.root.toURL() + '/root'; |
| this.displayRootPromise_ = new Promise( |
| window.webkitResolveLocalFileSystemURL.bind(null, displayRootURL)); |
| } |
| |
| // Store the obtained displayRoot. |
| this.displayRootPromise_.then(function(displayRoot) { |
| this.displayRoot_ = displayRoot; |
| }.bind(this)); |
| } |
| if (opt_onSuccess) |
| this.displayRootPromise_.then(opt_onSuccess, opt_onFailure); |
| return this.displayRootPromise_; |
| }; |
| |
| /** |
| * Utilities for volume manager implementation. |
| */ |
| var volumeManagerUtil = {}; |
| |
| /** |
| * Throws an Error when the given error is not in |
| * VolumeManagerCommon.VolumeError. |
| * |
| * @param {string} error Status string usually received from APIs. |
| */ |
| volumeManagerUtil.validateError = function(error) { |
| for (var key in VolumeManagerCommon.VolumeError) { |
| if (error === VolumeManagerCommon.VolumeError[key]) |
| return; |
| } |
| |
| throw new Error('Invalid mount error: ' + error); |
| }; |
| |
| /** |
| * 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) { |
| var localizedLabel; |
| switch (volumeMetadata.volumeType) { |
| case VolumeManagerCommon.VolumeType.DOWNLOADS: |
| localizedLabel = str('DOWNLOADS_DIRECTORY_LABEL'); |
| break; |
| case VolumeManagerCommon.VolumeType.DRIVE: |
| localizedLabel = str('DRIVE_DIRECTORY_LABEL'); |
| break; |
| default: |
| // TODO(mtomasz): Calculate volumeLabel for all types of volumes in the |
| // C++ layer. |
| localizedLabel = volumeMetadata.volumeLabel || |
| volumeMetadata.volumeId.split(':', 2)[1]; |
| break; |
| } |
| |
| chrome.fileManagerPrivate.requestFileSystem( |
| volumeMetadata.volumeId, |
| function(fileSystem) { |
| // TODO(mtomasz): chrome.runtime.lastError should have error reason. |
| if (!fileSystem) { |
| console.error('File system not found: ' + volumeMetadata.volumeId); |
| callback(new VolumeInfo( |
| /** @type {VolumeManagerCommon.VolumeType} */ |
| (volumeMetadata.volumeType), |
| volumeMetadata.volumeId, |
| null, // File system is not found. |
| volumeMetadata.mountCondition, |
| volumeMetadata.deviceType, |
| volumeMetadata.devicePath, |
| volumeMetadata.isReadOnly, |
| volumeMetadata.profile, |
| localizedLabel, |
| volumeMetadata.extensionId)); |
| return; |
| } |
| if (volumeMetadata.volumeType == |
| VolumeManagerCommon.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. |
| fileSystem.root.createReader().readEntries( |
| function() { /* do nothing */ }, |
| function(error) { |
| console.error( |
| 'Triggering full feed fetch is failed: ' + error.name); |
| }); |
| } |
| callback(new VolumeInfo( |
| /** @type {VolumeManagerCommon.VolumeType} */ |
| (volumeMetadata.volumeType), |
| volumeMetadata.volumeId, |
| fileSystem, |
| volumeMetadata.mountCondition, |
| volumeMetadata.deviceType, |
| volumeMetadata.devicePath, |
| volumeMetadata.isReadOnly, |
| volumeMetadata.profile, |
| localizedLabel, |
| volumeMetadata.extensionId)); |
| }); |
| }; |
| |
| /** |
| * The order of the volume list based on root type. |
| * @type {Array.<VolumeManagerCommon.VolumeType>} |
| * @const |
| * @private |
| */ |
| volumeManagerUtil.volumeListOrder_ = [ |
| VolumeManagerCommon.VolumeType.DRIVE, |
| VolumeManagerCommon.VolumeType.DOWNLOADS, |
| VolumeManagerCommon.VolumeType.ARCHIVE, |
| VolumeManagerCommon.VolumeType.REMOVABLE, |
| VolumeManagerCommon.VolumeType.MTP, |
| VolumeManagerCommon.VolumeType.PROVIDED, |
| VolumeManagerCommon.VolumeType.CLOUD_DEVICE |
| ]; |
| |
| /** |
| * Orders two volumes by volumeType and volumeId. |
| * |
| * The volumes at first are compared by volume type in the order of |
| * volumeListOrder_. Then they are compared by volume ID. |
| * |
| * @param {!VolumeInfo} volumeInfo1 Volume info to be compared. |
| * @param {!VolumeInfo} volumeInfo2 Volume info to be compared. |
| * @return {number} Returns -1 if volume1 < volume2, returns 1 if volume2 > |
| * volume1, returns 0 if volume1 === volume2. |
| * @private |
| */ |
| volumeManagerUtil.compareVolumeInfo_ = function(volumeInfo1, volumeInfo2) { |
| var typeIndex1 = |
| volumeManagerUtil.volumeListOrder_.indexOf(volumeInfo1.volumeType); |
| var typeIndex2 = |
| volumeManagerUtil.volumeListOrder_.indexOf(volumeInfo2.volumeType); |
| if (typeIndex1 !== typeIndex2) |
| return typeIndex1 < typeIndex2 ? -1 : 1; |
| if (volumeInfo1.volumeId !== volumeInfo2.volumeId) |
| return volumeInfo1.volumeId < volumeInfo2.volumeId ? -1 : 1; |
| return 0; |
| }; |
| |
| /** |
| * The container of the VolumeInfo for each mounted volume. |
| * @constructor |
| */ |
| function VolumeInfoList() { |
| var field = 'volumeType,volumeId'; |
| |
| /** |
| * Holds VolumeInfo instances. |
| * @type {cr.ui.ArrayDataModel} |
| * @private |
| */ |
| this.model_ = new cr.ui.ArrayDataModel([]); |
| this.model_.setCompareFunction(field, |
| /** @type {function(*, *): number} */ |
| (volumeManagerUtil.compareVolumeInfo_)); |
| this.model_.sort(field, 'asc'); |
| |
| 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.findIndex(volumeInfo.volumeId); |
| if (index !== -1) |
| this.model_.splice(index, 1, volumeInfo); |
| else |
| this.model_.push(volumeInfo); |
| }; |
| |
| /** |
| * Removes the VolumeInfo having the given ID. |
| * @param {string} volumeId ID of the volume. |
| */ |
| VolumeInfoList.prototype.remove = function(volumeId) { |
| var index = this.findIndex(volumeId); |
| if (index !== -1) |
| this.model_.splice(index, 1); |
| }; |
| |
| /** |
| * Obtains an index from the volume ID. |
| * @param {string} volumeId Volume ID. |
| * @return {number} Index of the volume. |
| */ |
| VolumeInfoList.prototype.findIndex = function(volumeId) { |
| for (var i = 0; i < this.model_.length; i++) { |
| if (this.model_.item(i).volumeId === volumeId) |
| return i; |
| } |
| return -1; |
| }; |
| |
| /** |
| * Searches the information of the volume that contains the passed entry. |
| * @param {Entry|Object} entry Entry on the volume to be found. |
| * @return {VolumeInfo} The volume's information, or null if not found. |
| */ |
| VolumeInfoList.prototype.findByEntry = function(entry) { |
| for (var i = 0; i < this.length; i++) { |
| var volumeInfo = this.item(i); |
| if (volumeInfo.fileSystem && |
| util.isSameFileSystem(volumeInfo.fileSystem, entry.filesystem)) { |
| return volumeInfo; |
| } |
| // Additionally, check fake entries. |
| for (var key in volumeInfo.fakeEntries_) { |
| var fakeEntry = volumeInfo.fakeEntries_[key]; |
| if (util.isSameEntry(fakeEntry, entry)) |
| return volumeInfo; |
| } |
| } |
| return null; |
| }; |
| |
| /** |
| * @param {number} index The index of the volume in the list. |
| * @return {!VolumeInfo} The VolumeInfo instance. |
| */ |
| VolumeInfoList.prototype.item = function(index) { |
| return /** @type {!VolumeInfo} */ (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(); |
| |
| /** |
| * 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 |
| }; |
| |
| chrome.fileManagerPrivate.onDriveConnectionStatusChanged.addListener( |
| this.onDriveConnectionStatusChanged_.bind(this)); |
| this.onDriveConnectionStatusChanged_(); |
| } |
| |
| /** |
| * Invoked when the drive connection status is changed. |
| * @private |
| */ |
| VolumeManager.prototype.onDriveConnectionStatusChanged_ = function() { |
| chrome.fileManagerPrivate.getDriveConnectionState(function(state) { |
| this.driveConnectionState_ = state; |
| cr.dispatchSimpleEvent(this, 'drive-connection-changed'); |
| }.bind(this)); |
| }; |
| |
| /** |
| * Returns the drive connection state. |
| * @return {VolumeManagerCommon.DriveConnectionState} Connection state. |
| */ |
| 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; |
| |
| /** |
| * The singleton instance of VolumeManager. Initialized by the first invocation |
| * of getInstance(). |
| * @type {VolumeManager} |
| * @private |
| */ |
| VolumeManager.instance_ = null; |
| |
| /** |
| * Returns instance of VolumeManager for debug purpose. |
| * This method returns VolumeManager.instance_ which may not be initialized. |
| * |
| * @return {VolumeManager} Volume manager. |
| */ |
| VolumeManager.getInstanceForDebug = function() { |
| return VolumeManager.instance_; |
| }; |
| |
| /** |
| * @type {Promise} |
| * @private |
| */ |
| VolumeManager.instancePromise_ = 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)=} opt_callback Called with the VolumeManager |
| * instance. TODO(hirono): Remove the callback and use Promise instead. |
| * @return {Promise} Promise to be fulfilled with the volume manager. |
| */ |
| VolumeManager.getInstance = function(opt_callback) { |
| if (!VolumeManager.instancePromise_) { |
| VolumeManager.instance_ = new VolumeManager(); |
| VolumeManager.instancePromise_ = new Promise(function(fulfill) { |
| VolumeManager.instance_.initialize_(function() { |
| return fulfill(VolumeManager.instance_); |
| }); |
| }); |
| } |
| if (opt_callback) |
| VolumeManager.instancePromise_.then(opt_callback); |
| return VolumeManager.instancePromise_; |
| }; |
| |
| /** |
| * Revokes the singleton instance for testing. |
| */ |
| VolumeManager.revokeInstanceForTesting = function() { |
| VolumeManager.instancePromise_ = null; |
| VolumeManager.instance_ = null; |
| } |
| |
| /** |
| * Initializes mount points. |
| * @param {function()} callback Called upon the completion of the |
| * initialization. |
| * @private |
| */ |
| VolumeManager.prototype.initialize_ = function(callback) { |
| chrome.fileManagerPrivate.getVolumeMetadataList(function(volumeMetadataList) { |
| // 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. |
| 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 === |
| VolumeManagerCommon.VolumeType.DRIVE) |
| this.onDriveConnectionStatusChanged_(); |
| continueCallback(); |
| }.bind(this)); |
| }.bind(this, volumeMetadataList[i])); |
| } |
| group.run(function() { |
| // Call the callback of the initialize function. |
| callback(); |
| // Call the callback of AsyncQueue. Maybe it invokes callbacks |
| // registered by mountCompleted events. |
| inCallback(); |
| }); |
| }.bind(this)); |
| |
| chrome.fileManagerPrivate.onMountCompleted.addListener( |
| this.onMountCompleted_.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) { |
| 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) { |
| volumeManagerUtil.createVolumeInfo( |
| event.volumeMetadata, |
| function(volumeInfo) { |
| this.volumeInfoList.add(volumeInfo); |
| this.finishRequest_(requestKey, event.status, volumeInfo); |
| |
| if (volumeInfo.volumeType === |
| VolumeManagerCommon.VolumeType.DRIVE) { |
| // 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 VolumeMetadata. |
| this.onDriveConnectionStatusChanged_(); |
| } |
| callback(); |
| }.bind(this)); |
| } 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; |
| if (status === VolumeManagerCommon.VolumeError.PATH_UNMOUNTED) { |
| console.warn('Volume already unmounted: ', volumeId); |
| status = 'success'; |
| } |
| 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('Mounted 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); |
| 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 |
| */ |
| VolumeManager.prototype.makeRequestKey_ = function(requestType, argument) { |
| return requestType + ':' + argument; |
| }; |
| |
| /** |
| * @param {string} fileUrl File url to the archive file. |
| * @param {function(VolumeInfo)} successCallback Success callback. |
| * @param {function(VolumeManagerCommon.VolumeError)} errorCallback Error |
| * callback. |
| */ |
| VolumeManager.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)); |
| }; |
| |
| /** |
| * Unmounts volume. |
| * @param {!VolumeInfo} volumeInfo Volume to be unmounted. |
| * @param {function()} successCallback Success callback. |
| * @param {function(VolumeManagerCommon.VolumeError)} errorCallback Error |
| * callback. |
| */ |
| VolumeManager.prototype.unmount = function(volumeInfo, |
| successCallback, |
| errorCallback) { |
| chrome.fileManagerPrivate.removeMount(volumeInfo.volumeId); |
| var requestKey = this.makeRequestKey_('unmount', volumeInfo.volumeId); |
| this.startRequest_(requestKey, successCallback, errorCallback); |
| }; |
| |
| /** |
| * Obtains a volume info containing the passed entry. |
| * @param {Entry|Object} entry Entry on the volume to be returned. Can be fake. |
| * @return {VolumeInfo} The VolumeInfo instance or null if not found. |
| */ |
| VolumeManager.prototype.getVolumeInfo = function(entry) { |
| return this.volumeInfoList.findByEntry(entry); |
| }; |
| |
| /** |
| * Obtains volume information of the current profile. |
| * |
| * @param {VolumeManagerCommon.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) { |
| var volumeInfo = this.volumeInfoList.findByEntry(entry); |
| if (!volumeInfo) |
| return null; |
| |
| if (util.isFakeEntry(entry)) { |
| return new EntryLocation( |
| volumeInfo, |
| entry.rootType, |
| true /* the entry points a root directory. */, |
| true /* fake entries are read only. */); |
| } |
| |
| var rootType; |
| var isReadOnly; |
| var isRootEntry; |
| if (volumeInfo.volumeType === VolumeManagerCommon.VolumeType.DRIVE) { |
| // For Drive, the roots are /root 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 == '/other' || |
| entry.fullPath.indexOf('/other/') === 0) { |
| rootType = VolumeManagerCommon.RootType.DRIVE_OTHER; |
| isReadOnly = true; |
| isRootEntry = entry.fullPath === '/other'; |
| } else { |
| // Accessing Drive files outside of /drive/root and /drive/other is not |
| // allowed, but can happen. Therefore returning null. |
| return null; |
| } |
| } else { |
| switch (volumeInfo.volumeType) { |
| case VolumeManagerCommon.VolumeType.DOWNLOADS: |
| rootType = VolumeManagerCommon.RootType.DOWNLOADS; |
| break; |
| case VolumeManagerCommon.VolumeType.REMOVABLE: |
| rootType = VolumeManagerCommon.RootType.REMOVABLE; |
| break; |
| case VolumeManagerCommon.VolumeType.ARCHIVE: |
| rootType = VolumeManagerCommon.RootType.ARCHIVE; |
| break; |
| case VolumeManagerCommon.VolumeType.CLOUD_DEVICE: |
| rootType = VolumeManagerCommon.RootType.CLOUD_DEVICE; |
| break; |
| case VolumeManagerCommon.VolumeType.MTP: |
| rootType = VolumeManagerCommon.RootType.MTP; |
| break; |
| case VolumeManagerCommon.VolumeType.PROVIDED: |
| rootType = VolumeManagerCommon.RootType.PROVIDED; |
| break; |
| default: |
| // Programming error, throw an exception. |
| throw new Error('Invalid volume type: ' + volumeInfo.volumeType); |
| } |
| isReadOnly = volumeInfo.isReadOnly; |
| isRootEntry = util.isSameEntry(entry, volumeInfo.fileSystem.root); |
| } |
| |
| return new EntryLocation(volumeInfo, rootType, isRootEntry, isReadOnly); |
| }; |
| |
| /** |
| * @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 |
| */ |
| 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], |
| 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 |
| */ |
| VolumeManager.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 |
| */ |
| VolumeManager.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]); |
| } |
| }; |
| |
| /** |
| * Returns current state of VolumeManager. |
| * @return {string} Current state of VolumeManager. |
| */ |
| VolumeManager.prototype.toString = function() { |
| return 'VolumeManager\n' + |
| '- MountQueue_:\n' + |
| ' ' + this.mountQueue_.toString().replace(/\n/g, '\n '); |
| }; |
| |
| /** |
| * Location information which shows where the path points in FileManager's |
| * file system. |
| * |
| * @param {!VolumeInfo} volumeInfo Volume information. |
| * @param {VolumeManagerCommon.RootType} rootType Root type. |
| * @param {boolean} isRootEntry Whether the entry is root entry or not. |
| * @param {boolean} isReadOnly Whether the entry is read only or not. |
| * @constructor |
| */ |
| function EntryLocation(volumeInfo, rootType, isRootEntry, isReadOnly) { |
| /** |
| * Volume information. |
| * @type {!VolumeInfo} |
| */ |
| this.volumeInfo = volumeInfo; |
| |
| /** |
| * Root type. |
| * @type {VolumeManagerCommon.RootType} |
| */ |
| this.rootType = rootType; |
| |
| /** |
| * Whether the entry is root entry or not. |
| * @type {boolean} |
| */ |
| this.isRootEntry = isRootEntry; |
| |
| /** |
| * Whether the location obtained from the fake entry correspond to special |
| * searches. |
| * @type {boolean} |
| */ |
| this.isSpecialSearchRoot = |
| this.rootType === VolumeManagerCommon.RootType.DRIVE_OFFLINE || |
| this.rootType === VolumeManagerCommon.RootType.DRIVE_SHARED_WITH_ME || |
| this.rootType === VolumeManagerCommon.RootType.DRIVE_RECENT; |
| |
| /** |
| * Whether the location is under Google Drive or a special search root which |
| * represents a special search from Google Drive. |
| * @type {boolean} |
| */ |
| this.isDriveBased = |
| this.rootType === VolumeManagerCommon.RootType.DRIVE || |
| this.rootType === VolumeManagerCommon.RootType.DRIVE_OTHER || |
| this.rootType === VolumeManagerCommon.RootType.DRIVE_SHARED_WITH_ME || |
| this.rootType === VolumeManagerCommon.RootType.DRIVE_RECENT || |
| this.rootType === VolumeManagerCommon.RootType.DRIVE_OFFLINE; |
| |
| /** |
| * Whether the given path can be a target path of folder shortcut. |
| * @type {boolean} |
| */ |
| this.isEligibleForFolderShortcut = |
| !this.isSpecialSearchRoot && |
| !this.isRootEntry && |
| this.isDriveBased; |
| |
| /** |
| * Whether the entry is read only or not. |
| * @type {boolean} |
| */ |
| this.isReadOnly = isReadOnly; |
| |
| Object.freeze(this); |
| } |