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.
8 * Represents each volume, such as "drive", "download directory", each "USB
9 * flush storage", or "mounted zip archive" etc.
11 * @param {util.VolumeType} volumeType The type of the volume.
12 * @param {string} mountPath Where the volume is mounted.
13 * @param {DirectoryEntry} root The root directory entry of this volume.
14 * @param {string} error The error if an error is found.
15 * @param {string} deviceType The type of device ('usb'|'sd'|'optical'|'mobile'
16 * |'unknown') (as defined in chromeos/disks/disk_mount_manager.cc).
18 * @param {boolean} isReadOnly True if the volume is read only.
22 volumeType, mountPath, root, error, deviceType, isReadOnly) {
23 this.volumeType = volumeType;
24 // TODO(hidehiko): This should include FileSystem instance.
25 this.mountPath = mountPath;
28 // Note: This represents if the mounting of the volume is successfully done
29 // or not. (If error is empty string, the mount is successfully done).
30 // TODO(hidehiko): Rename to make this more understandable.
32 this.deviceType = deviceType;
33 this.isReadOnly = isReadOnly;
35 // VolumeInfo is immutable.
40 * Utilities for volume manager implementation.
42 var volumeManagerUtil = {};
45 * Throws an Error when the given error is not in util.VolumeError.
46 * @param {util.VolumeError} error Status string usually received from APIs.
48 volumeManagerUtil.validateError = function(error) {
49 for (var key in util.VolumeError) {
50 if (error == util.VolumeError[key])
54 throw new Error('Invalid mount error: ' + error);
58 * The regex pattern which matches valid mount paths.
59 * The valid paths are:
60 * - Either of '/drive', '/drive_shared_with_me', '/drive_offline',
61 * '/drive_recent' or '/Download'
62 * - For archive, drive, removable can have (exactly one) sub directory in the
63 * root path. E.g. '/archive/foo', '/removable/usb1' etc.
68 volumeManagerUtil.validateMountPathRegExp_ = new RegExp(
69 '^/(drive|drive_shared_with_me|drive_offline|drive_recent|Downloads|' +
70 '((archive|drive|removable)\/[^/]+))$');
73 * Throws an Error if the validation fails.
74 * @param {string} mountPath The target path of the validation.
76 volumeManagerUtil.validateMountPath = function(mountPath) {
77 if (!volumeManagerUtil.validateMountPathRegExp_.test(mountPath))
78 throw new Error('Invalid mount path: ' + mountPath);
82 * Returns the root entry of a volume mounted at mountPath.
84 * @param {string} mountPath The mounted path of the volume.
85 * @param {function(DirectoryEntry)} successCallback Called when the root entry
87 * @param {function(FileError)} errorCallback Called when an error is found.
90 volumeManagerUtil.getRootEntry_ = function(
91 mountPath, successCallback, errorCallback) {
92 // We always request FileSystem here, because requestFileSystem() grants
93 // permissions if necessary, especially for Drive File System at first mount
95 // Note that we actually need to request FileSystem after multi file system
96 // support, so this will be more natural code then.
97 chrome.fileBrowserPrivate.requestFileSystem(
99 function(fileSystem) {
100 // TODO(hidehiko): chrome.runtime.lastError should have error reason.
102 errorCallback(util.createFileError(FileError.NOT_FOUND_ERR));
106 fileSystem.root.getDirectory(
107 mountPath.substring(1), // Strip leading '/'.
108 {create: false}, successCallback, errorCallback);
113 * Builds the VolumeInfo data from VolumeMetadata.
114 * @param {VolumeMetadata} volumeMetadata Metadata instance for the volume.
115 * @param {function(VolumeInfo)} callback Called on completion.
117 volumeManagerUtil.createVolumeInfo = function(volumeMetadata, callback) {
118 volumeManagerUtil.getRootEntry_(
119 volumeMetadata.mountPath,
121 if (volumeMetadata.volumeType === util.VolumeType.DRIVE) {
122 // After file system is mounted, we "read" drive grand root
123 // entry at first. This triggers full feed fetch on background.
124 // Note: we don't need to handle errors here, because even if
125 // it fails, accessing to some path later will just become
126 // a fast-fetch and it re-triggers full-feed fetch.
127 entry.createReader().readEntries(
128 function() { /* do nothing */ },
131 'Triggering full feed fetch is failed: ' +
132 util.getFileErrorMnemonic(error.code));
135 callback(new VolumeInfo(
136 volumeMetadata.volumeType,
137 volumeMetadata.mountPath,
139 volumeMetadata.mountCondition,
140 volumeMetadata.deviceType,
141 volumeMetadata.isReadOnly));
143 function(fileError) {
144 console.error('Root entry is not found: ' +
145 volumeMetadata.mountPath + ', ' +
146 util.getFileErrorMnemonic(fileError.code));
147 callback(new VolumeInfo(
148 volumeMetadata.volumeType,
149 volumeMetadata.mountPath,
150 null, // Root entry is not found.
151 volumeMetadata.mountCondition,
152 volumeMetadata.deviceType,
153 volumeMetadata.isReadOnly));
158 * The order of the volume list based on root type.
159 * @type {Array.<string>}
163 volumeManagerUtil.volumeListOrder_ = [
164 RootType.DRIVE, RootType.DOWNLOADS, RootType.ARCHIVE, RootType.REMOVABLE
168 * Compares mount paths to sort the volume list order.
169 * @param {string} mountPath1 The mount path for the first volume.
170 * @param {string} mountPath2 The mount path for the second volume.
171 * @return {number} 0 if mountPath1 and mountPath2 are same, -1 if VolumeInfo
172 * mounted at mountPath1 should be listed before the one mounted at
173 * mountPath2, otherwise 1.
175 volumeManagerUtil.compareMountPath = function(mountPath1, mountPath2) {
176 var order1 = volumeManagerUtil.volumeListOrder_.indexOf(
177 PathUtil.getRootType(mountPath1));
178 var order2 = volumeManagerUtil.volumeListOrder_.indexOf(
179 PathUtil.getRootType(mountPath2));
180 if (order1 != order2)
181 return order1 < order2 ? -1 : 1;
183 if (mountPath1 != mountPath2)
184 return mountPath1 < mountPath2 ? -1 : 1;
191 * The container of the VolumeInfo for each mounted volume.
194 function VolumeInfoList() {
196 * Holds VolumeInfo instances.
197 * @type {cr.ui.ArrayDataModel}
200 this.model_ = new cr.ui.ArrayDataModel([]);
205 VolumeInfoList.prototype = {
206 get length() { return this.model_.length; }
210 * Adds the event listener to listen the change of volume info.
211 * @param {string} type The name of the event.
212 * @param {function(Event)} handler The handler for the event.
214 VolumeInfoList.prototype.addEventListener = function(type, handler) {
215 this.model_.addEventListener(type, handler);
219 * Removes the event listener.
220 * @param {string} type The name of the event.
221 * @param {function(Event)} handler The handler to be removed.
223 VolumeInfoList.prototype.removeEventListener = function(type, handler) {
224 this.model_.removeEventListener(type, handler);
228 * Adds the volumeInfo to the appropriate position. If there already exists,
230 * @param {VolumeInfo} volumeInfo The information of the new volume.
232 VolumeInfoList.prototype.add = function(volumeInfo) {
233 var index = this.findLowerBoundIndex_(volumeInfo.mountPath);
234 if (index < this.length &&
235 this.item(index).mountPath == volumeInfo.mountPath) {
236 // Replace the VolumeInfo.
237 this.model_.splice(index, 1, volumeInfo);
239 // Insert the VolumeInfo.
240 this.model_.splice(index, 0, volumeInfo);
245 * Removes the VolumeInfo of the volume mounted at mountPath.
246 * @param {string} mountPath The path to the location where the volume is
249 VolumeInfoList.prototype.remove = function(mountPath) {
250 var index = this.findLowerBoundIndex_(mountPath);
251 if (index < this.length && this.item(index).mountPath == mountPath)
252 this.model_.splice(index, 1);
256 * Searches the information of the volume mounted at mountPath.
257 * @param {string} mountPath The path to the location where the volume is
259 * @return {VolumeInfo} The volume's information, or null if not found.
261 VolumeInfoList.prototype.find = function(mountPath) {
262 var index = this.findLowerBoundIndex_(mountPath);
263 if (index < this.length && this.item(index).mountPath == mountPath)
264 return this.item(index);
271 * @param {string} mountPath The mount path of searched volume.
272 * @return {number} The index of the volume if found, or the inserting
273 * position of the volume.
276 VolumeInfoList.prototype.findLowerBoundIndex_ = function(mountPath) {
277 // Assuming the number of elements in the array data model is very small
278 // in most cases, use simple linear search, here.
279 for (var i = 0; i < this.length; i++) {
280 if (volumeManagerUtil.compareMountPath(
281 this.item(i).mountPath, mountPath) >= 0)
288 * @param {number} index The index of the volume in the list.
289 * @return {VolumeInfo} The VolumeInfo instance.
291 VolumeInfoList.prototype.item = function(index) {
292 return this.model_.item(index);
296 * VolumeManager is responsible for tracking list of mounted volumes.
299 * @extends {cr.EventTarget}
301 function VolumeManager() {
303 * The list of archives requested to mount. We will show contents once
304 * archive is mounted, but only for mounts from within this filebrowser tab.
305 * @type {Object.<string, Object>}
311 * The list of VolumeInfo instances for each mounted volume.
312 * @type {VolumeInfoList}
314 this.volumeInfoList = new VolumeInfoList();
316 // The status should be merged into VolumeManager.
317 // TODO(hidehiko): Remove them after the migration.
318 this.driveConnectionState_ = {
319 type: util.DriveConnectionType.OFFLINE,
320 reasons: [util.DriveConnectionReason.NO_SERVICE]
323 chrome.fileBrowserPrivate.onDriveConnectionStatusChanged.addListener(
324 this.onDriveConnectionStatusChanged_.bind(this));
325 this.onDriveConnectionStatusChanged_();
329 * Invoked when the drive connection status is changed.
332 VolumeManager.prototype.onDriveConnectionStatusChanged_ = function() {
333 chrome.fileBrowserPrivate.getDriveConnectionState(function(state) {
334 this.driveConnectionState_ = state;
335 cr.dispatchSimpleEvent(this, 'drive-connection-changed');
340 * Returns the drive connection state.
341 * @return {util.DriveConnectionType} Connection type.
343 VolumeManager.prototype.getDriveConnectionState = function() {
344 return this.driveConnectionState_;
348 * VolumeManager extends cr.EventTarget.
350 VolumeManager.prototype.__proto__ = cr.EventTarget.prototype;
353 * Time in milliseconds that we wait a response for. If no response on
354 * mount/unmount received the request supposed failed.
356 VolumeManager.TIMEOUT = 15 * 60 * 1000;
359 * Queue to run getInstance sequentially.
360 * @type {AsyncUtil.Queue}
363 VolumeManager.getInstanceQueue_ = new AsyncUtil.Queue();
366 * The singleton instance of VolumeManager. Initialized by the first invocation
368 * @type {VolumeManager}
371 VolumeManager.instance_ = null;
374 * Returns the VolumeManager instance asynchronously. If it is not created or
375 * under initialization, it will waits for the finish of the initialization.
376 * @param {function(VolumeManager)} callback Called with the VolumeManager
379 VolumeManager.getInstance = function(callback) {
380 VolumeManager.getInstanceQueue_.run(function(continueCallback) {
381 if (VolumeManager.instance_) {
382 callback(VolumeManager.instance_);
387 VolumeManager.instance_ = new VolumeManager();
388 VolumeManager.instance_.initialize_(function() {
389 callback(VolumeManager.instance_);
396 * Initializes mount points.
397 * @param {function()} callback Called upon the completion of the
401 VolumeManager.prototype.initialize_ = function(callback) {
402 chrome.fileBrowserPrivate.getVolumeMetadataList(function(volumeMetadataList) {
403 // Create VolumeInfo for each volume.
404 var group = new AsyncUtil.Group();
405 for (var i = 0; i < volumeMetadataList.length; i++) {
406 group.add(function(volumeMetadata, continueCallback) {
407 volumeManagerUtil.createVolumeInfo(
409 function(volumeInfo) {
410 this.volumeInfoList.add(volumeInfo);
411 if (volumeMetadata.volumeType === util.VolumeType.DRIVE)
412 this.onDriveConnectionStatusChanged_();
415 }.bind(this, volumeMetadataList[i]));
418 // Then, finalize the initialization.
419 group.run(function() {
420 // Subscribe to the mount completed event when mount points initialized.
421 chrome.fileBrowserPrivate.onMountCompleted.addListener(
422 this.onMountCompleted_.bind(this));
429 * Event handler called when some volume was mounted or unmounted.
430 * @param {MountCompletedEvent} event Received event.
433 VolumeManager.prototype.onMountCompleted_ = function(event) {
434 if (event.eventType === 'mount') {
435 if (event.volumeMetadata.mountPath) {
436 var requestKey = this.makeRequestKey_(
438 event.volumeMetadata.sourcePath);
440 var error = event.status === 'success' ? '' : event.status;
442 volumeManagerUtil.createVolumeInfo(
443 event.volumeMetadata,
444 function(volumeInfo) {
445 this.volumeInfoList.add(volumeInfo);
446 this.finishRequest_(requestKey, event.status, volumeInfo.mountPath);
448 if (volumeInfo.volumeType === util.VolumeType.DRIVE) {
449 // Update the network connection status, because until the
450 // drive is initialized, the status is set to not ready.
451 // TODO(hidehiko): The connection status should be migrated into
453 this.onDriveConnectionStatusChanged_();
457 console.warn('No mount path.');
458 this.finishRequest_(requestKey, event.status);
460 } else if (event.eventType === 'unmount') {
461 var mountPath = event.volumeMetadata.mountPath;
462 volumeManagerUtil.validateMountPath(mountPath);
463 var status = event.status;
464 if (status === util.VolumeError.PATH_UNMOUNTED) {
465 console.warn('Volume already unmounted: ', mountPath);
468 var requestKey = this.makeRequestKey_('unmount', mountPath);
469 var requested = requestKey in this.requests_;
470 if (event.status === 'success' && !requested &&
471 this.volumeInfoList.find(mountPath)) {
472 console.warn('Mounted volume without a request: ', mountPath);
473 var e = new Event('externally-unmounted');
474 e.mountPath = mountPath;
475 this.dispatchEvent(e);
477 this.finishRequest_(requestKey, status);
479 if (event.status === 'success')
480 this.volumeInfoList.remove(mountPath);
485 * Creates string to match mount events with requests.
486 * @param {string} requestType 'mount' | 'unmount'. TODO(hidehiko): Replace by
488 * @param {string} path Source path provided by API for mount request, or
489 * mount path for unmount request.
490 * @return {string} Key for |this.requests_|.
493 VolumeManager.prototype.makeRequestKey_ = function(requestType, path) {
494 return requestType + ':' + path;
498 * @param {string} fileUrl File url to the archive file.
499 * @param {function(string)} successCallback Success callback.
500 * @param {function(util.VolumeError)} errorCallback Error callback.
502 VolumeManager.prototype.mountArchive = function(
503 fileUrl, successCallback, errorCallback) {
504 chrome.fileBrowserPrivate.addMount(fileUrl, function(sourcePath) {
506 'Mount request: url=' + fileUrl + '; sourceUrl=' + sourcePath);
507 var requestKey = this.makeRequestKey_('mount', sourcePath);
508 this.startRequest_(requestKey, successCallback, errorCallback);
514 * @param {string} mountPath Volume mounted path.
515 * @param {function(string)} successCallback Success callback.
516 * @param {function(util.VolumeError)} errorCallback Error callback.
518 VolumeManager.prototype.unmount = function(mountPath,
521 volumeManagerUtil.validateMountPath(mountPath);
522 var volumeInfo = this.volumeInfoList.find(mountPath);
524 errorCallback(util.VolumeError.NOT_MOUNTED);
528 chrome.fileBrowserPrivate.removeMount(util.makeFilesystemUrl(mountPath));
529 var requestKey = this.makeRequestKey_('unmount', volumeInfo.mountPath);
530 this.startRequest_(requestKey, successCallback, errorCallback);
534 * Resolve the path to its entry.
535 * @param {string} path The path to be resolved.
536 * @param {function(Entry)} successCallback Called with the resolved entry on
538 * @param {function(FileError)} errorCallback Called on error.
540 VolumeManager.prototype.resolvePath = function(
541 path, successCallback, errorCallback) {
542 // Make sure the path is in the mounted volume.
543 var mountPath = PathUtil.isDriveBasedPath(path) ?
544 RootDirectory.DRIVE : PathUtil.getRootPath(path);
545 var volumeInfo = this.getVolumeInfo(mountPath);
546 if (!volumeInfo || !volumeInfo.root) {
547 errorCallback(util.createFileError(FileError.NOT_FOUND_ERR));
551 webkitResolveLocalFileSystemURL(
552 util.makeFilesystemUrl(path), successCallback, errorCallback);
556 * @param {string} mountPath Volume mounted path.
557 * @return {VolumeInfo} The data about the volume.
559 VolumeManager.prototype.getVolumeInfo = function(mountPath) {
560 volumeManagerUtil.validateMountPath(mountPath);
561 return this.volumeInfoList.find(mountPath);
565 * @param {string} key Key produced by |makeRequestKey_|.
566 * @param {function(string)} successCallback To be called when request finishes
568 * @param {function(util.VolumeError)} errorCallback To be called when
572 VolumeManager.prototype.startRequest_ = function(key,
573 successCallback, errorCallback) {
574 if (key in this.requests_) {
575 var request = this.requests_[key];
576 request.successCallbacks.push(successCallback);
577 request.errorCallbacks.push(errorCallback);
579 this.requests_[key] = {
580 successCallbacks: [successCallback],
581 errorCallbacks: [errorCallback],
583 timeout: setTimeout(this.onTimeout_.bind(this, key),
584 VolumeManager.TIMEOUT)
590 * Called if no response received in |TIMEOUT|.
591 * @param {string} key Key produced by |makeRequestKey_|.
594 VolumeManager.prototype.onTimeout_ = function(key) {
595 this.invokeRequestCallbacks_(this.requests_[key],
596 util.VolumeError.TIMEOUT);
597 delete this.requests_[key];
601 * @param {string} key Key produced by |makeRequestKey_|.
602 * @param {util.VolumeError|'success'} status Status received from the API.
603 * @param {string=} opt_mountPath Mount path.
606 VolumeManager.prototype.finishRequest_ = function(key, status, opt_mountPath) {
607 var request = this.requests_[key];
611 clearTimeout(request.timeout);
612 this.invokeRequestCallbacks_(request, status, opt_mountPath);
613 delete this.requests_[key];
617 * @param {Object} request Structure created in |startRequest_|.
618 * @param {util.VolumeError|string} status If status == 'success'
619 * success callbacks are called.
620 * @param {string=} opt_mountPath Mount path. Required if success.
623 VolumeManager.prototype.invokeRequestCallbacks_ = function(request, status,
625 var callEach = function(callbacks, self, args) {
626 for (var i = 0; i < callbacks.length; i++) {
627 callbacks[i].apply(self, args);
630 if (status == 'success') {
631 callEach(request.successCallbacks, this, [opt_mountPath]);
633 volumeManagerUtil.validateError(status);
634 callEach(request.errorCallbacks, this, [status]);