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 {string} volumeId ID of the volume.
14 * @param {DirectoryEntry} root The root directory entry of this volume.
15 * @param {string} error The error if an error is found.
16 * @param {string} deviceType The type of device ('usb'|'sd'|'optical'|'mobile'
17 * |'unknown') (as defined in chromeos/disks/disk_mount_manager.cc).
19 * @param {boolean} isReadOnly True if the volume is read only.
20 * @param {!{displayName:string, isCurrentProfile:boolean}} profile Profile
33 this.volumeType = volumeType;
34 // TODO(hidehiko): This should include FileSystem instance.
35 this.mountPath = mountPath;
36 this.volumeId = volumeId;
39 // Note: This represents if the mounting of the volume is successfully done
40 // or not. (If error is empty string, the mount is successfully done).
41 // TODO(hidehiko): Rename to make this more understandable.
43 this.deviceType = deviceType;
44 this.isReadOnly = isReadOnly;
45 this.profile = Object.freeze(profile);
47 // VolumeInfo is immutable.
52 * Obtains a URL of the display root directory that users can see as a root.
53 * @return {string} URL of root entry.
55 VolumeInfo.prototype.getDisplayRootDirectoryURL = function() {
56 return this.root.toURL() +
57 (this.volumeType === util.VolumeType.DRIVE ? '/root' : '');
61 * Obtains volume label.
62 * @return {string} Label for the volume.
64 VolumeInfo.prototype.getLabel = function() {
65 if (this.volumeType === util.VolumeType.DRIVE)
66 return str('DRIVE_DIRECTORY_LABEL');
68 return PathUtil.getFolderLabel(this.mountPath);
72 * Utilities for volume manager implementation.
74 var volumeManagerUtil = {};
77 * Throws an Error when the given error is not in util.VolumeError.
78 * @param {util.VolumeError} error Status string usually received from APIs.
80 volumeManagerUtil.validateError = function(error) {
81 for (var key in util.VolumeError) {
82 if (error === util.VolumeError[key])
86 throw new Error('Invalid mount error: ' + error);
90 * Returns the root entry of a volume mounted at mountPath.
92 * @param {string} mountPath The mounted path of the volume.
93 * @param {function(DirectoryEntry)} successCallback Called when the root entry
95 * @param {function(FileError)} errorCallback Called when an error is found.
98 volumeManagerUtil.getRootEntry_ = function(
99 mountPath, successCallback, errorCallback) {
100 // We always request FileSystem here, because requestFileSystem() grants
101 // permissions if necessary, especially for Drive File System at first mount
103 // Note that we actually need to request FileSystem after multi file system
104 // support, so this will be more natural code then.
105 chrome.fileBrowserPrivate.requestFileSystem(
107 function(fileSystem) {
108 // TODO(hidehiko): chrome.runtime.lastError should have error reason.
110 errorCallback(util.createFileError(FileError.NOT_FOUND_ERR));
114 fileSystem.root.getDirectory(
115 mountPath.substring(1), // Strip leading '/'.
116 {create: false}, successCallback, errorCallback);
121 * Builds the VolumeInfo data from VolumeMetadata.
122 * @param {VolumeMetadata} volumeMetadata Metadata instance for the volume.
123 * @param {function(VolumeInfo)} callback Called on completion.
125 volumeManagerUtil.createVolumeInfo = function(volumeMetadata, callback) {
126 volumeManagerUtil.getRootEntry_(
127 volumeMetadata.mountPath,
129 if (volumeMetadata.volumeType === util.VolumeType.DRIVE) {
130 // After file system is mounted, we "read" drive grand root
131 // entry at first. This triggers full feed fetch on background.
132 // Note: we don't need to handle errors here, because even if
133 // it fails, accessing to some path later will just become
134 // a fast-fetch and it re-triggers full-feed fetch.
135 entry.createReader().readEntries(
136 function() { /* do nothing */ },
139 'Triggering full feed fetch is failed: ' +
140 util.getFileErrorMnemonic(error.code));
143 callback(new VolumeInfo(
144 volumeMetadata.volumeType,
145 volumeMetadata.mountPath,
146 volumeMetadata.volumeId,
148 volumeMetadata.mountCondition,
149 volumeMetadata.deviceType,
150 volumeMetadata.isReadOnly,
151 volumeMetadata.profile));
153 function(fileError) {
154 console.error('Root entry is not found: ' +
155 volumeMetadata.mountPath + ', ' +
156 util.getFileErrorMnemonic(fileError.code));
157 callback(new VolumeInfo(
158 volumeMetadata.volumeType,
159 volumeMetadata.mountPath,
160 volumeMetadata.volumeId,
161 null, // Root entry is not found.
162 volumeMetadata.mountCondition,
163 volumeMetadata.deviceType,
164 volumeMetadata.isReadOnly,
165 volumeMetadata.profile));
170 * The order of the volume list based on root type.
171 * @type {Array.<string>}
175 volumeManagerUtil.volumeListOrder_ = [
176 RootType.DRIVE, RootType.DOWNLOADS, RootType.ARCHIVE, RootType.REMOVABLE
180 * Compares mount paths to sort the volume list order.
181 * @param {string} mountPath1 The mount path for the first volume.
182 * @param {string} mountPath2 The mount path for the second volume.
183 * @return {number} 0 if mountPath1 and mountPath2 are same, -1 if VolumeInfo
184 * mounted at mountPath1 should be listed before the one mounted at
185 * mountPath2, otherwise 1.
187 volumeManagerUtil.compareMountPath = function(mountPath1, mountPath2) {
188 var order1 = volumeManagerUtil.volumeListOrder_.indexOf(
189 PathUtil.getRootType(mountPath1));
190 var order2 = volumeManagerUtil.volumeListOrder_.indexOf(
191 PathUtil.getRootType(mountPath2));
192 if (order1 !== order2)
193 return order1 < order2 ? -1 : 1;
195 if (mountPath1 !== mountPath2)
196 return mountPath1 < mountPath2 ? -1 : 1;
203 * The container of the VolumeInfo for each mounted volume.
206 function VolumeInfoList() {
208 * Holds VolumeInfo instances.
209 * @type {cr.ui.ArrayDataModel}
212 this.model_ = new cr.ui.ArrayDataModel([]);
217 VolumeInfoList.prototype = {
218 get length() { return this.model_.length; }
222 * Adds the event listener to listen the change of volume info.
223 * @param {string} type The name of the event.
224 * @param {function(Event)} handler The handler for the event.
226 VolumeInfoList.prototype.addEventListener = function(type, handler) {
227 this.model_.addEventListener(type, handler);
231 * Removes the event listener.
232 * @param {string} type The name of the event.
233 * @param {function(Event)} handler The handler to be removed.
235 VolumeInfoList.prototype.removeEventListener = function(type, handler) {
236 this.model_.removeEventListener(type, handler);
240 * Adds the volumeInfo to the appropriate position. If there already exists,
242 * @param {VolumeInfo} volumeInfo The information of the new volume.
244 VolumeInfoList.prototype.add = function(volumeInfo) {
245 var index = this.findLowerBoundIndex_(volumeInfo.mountPath);
246 if (index < this.length &&
247 this.item(index).mountPath === volumeInfo.mountPath) {
248 // Replace the VolumeInfo.
249 this.model_.splice(index, 1, volumeInfo);
251 // Insert the VolumeInfo.
252 this.model_.splice(index, 0, volumeInfo);
257 * Removes the VolumeInfo of the volume mounted at mountPath.
258 * @param {string} mountPath The path to the location where the volume is
261 VolumeInfoList.prototype.remove = function(mountPath) {
262 var index = this.findLowerBoundIndex_(mountPath);
263 if (index < this.length && this.item(index).mountPath === mountPath)
264 this.model_.splice(index, 1);
268 * Searches the information of the volume mounted at mountPath.
269 * @param {string} mountPath The path to the location where the volume is
271 * @return {VolumeInfo} The volume's information, or null if not found.
273 VolumeInfoList.prototype.find = function(mountPath) {
274 var index = this.findLowerBoundIndex_(mountPath);
275 if (index < this.length && this.item(index).mountPath === mountPath)
276 return this.item(index);
283 * Searches the information of the volume that contains an item pointed by the
285 * @param {string} path Path pointing an entry on a volume.
286 * @return {VolumeInfo} The volume's information, or null if not found.
288 VolumeInfoList.prototype.findByPath = function(path) {
289 for (var i = 0; i < this.length; i++) {
290 var mountPath = this.item(i).mountPath;
291 if (path === mountPath || path.indexOf(mountPath + '/') === 0)
298 * @param {string} mountPath The mount path of searched volume.
299 * @return {number} The index of the volume if found, or the inserting
300 * position of the volume.
303 VolumeInfoList.prototype.findLowerBoundIndex_ = function(mountPath) {
304 // Assuming the number of elements in the array data model is very small
305 // in most cases, use simple linear search, here.
306 for (var i = 0; i < this.length; i++) {
307 if (volumeManagerUtil.compareMountPath(
308 this.item(i).mountPath, mountPath) >= 0)
315 * @param {number} index The index of the volume in the list.
316 * @return {VolumeInfo} The VolumeInfo instance.
318 VolumeInfoList.prototype.item = function(index) {
319 return this.model_.item(index);
323 * VolumeManager is responsible for tracking list of mounted volumes.
326 * @extends {cr.EventTarget}
328 function VolumeManager() {
330 * The list of archives requested to mount. We will show contents once
331 * archive is mounted, but only for mounts from within this filebrowser tab.
332 * @type {Object.<string, Object>}
338 * The list of VolumeInfo instances for each mounted volume.
339 * @type {VolumeInfoList}
341 this.volumeInfoList = new VolumeInfoList();
343 // The status should be merged into VolumeManager.
344 // TODO(hidehiko): Remove them after the migration.
345 this.driveConnectionState_ = {
346 type: util.DriveConnectionType.OFFLINE,
347 reason: util.DriveConnectionReason.NO_SERVICE
350 chrome.fileBrowserPrivate.onDriveConnectionStatusChanged.addListener(
351 this.onDriveConnectionStatusChanged_.bind(this));
352 this.onDriveConnectionStatusChanged_();
356 * Invoked when the drive connection status is changed.
359 VolumeManager.prototype.onDriveConnectionStatusChanged_ = function() {
360 chrome.fileBrowserPrivate.getDriveConnectionState(function(state) {
361 this.driveConnectionState_ = state;
362 cr.dispatchSimpleEvent(this, 'drive-connection-changed');
367 * Returns the drive connection state.
368 * @return {util.DriveConnectionType} Connection type.
370 VolumeManager.prototype.getDriveConnectionState = function() {
371 return this.driveConnectionState_;
375 * VolumeManager extends cr.EventTarget.
377 VolumeManager.prototype.__proto__ = cr.EventTarget.prototype;
380 * Time in milliseconds that we wait a response for. If no response on
381 * mount/unmount received the request supposed failed.
383 VolumeManager.TIMEOUT = 15 * 60 * 1000;
386 * Queue to run getInstance sequentially.
387 * @type {AsyncUtil.Queue}
390 VolumeManager.getInstanceQueue_ = new AsyncUtil.Queue();
393 * The singleton instance of VolumeManager. Initialized by the first invocation
395 * @type {VolumeManager}
398 VolumeManager.instance_ = null;
401 * Returns the VolumeManager instance asynchronously. If it is not created or
402 * under initialization, it will waits for the finish of the initialization.
403 * @param {function(VolumeManager)} callback Called with the VolumeManager
406 VolumeManager.getInstance = function(callback) {
407 VolumeManager.getInstanceQueue_.run(function(continueCallback) {
408 if (VolumeManager.instance_) {
409 callback(VolumeManager.instance_);
414 VolumeManager.instance_ = new VolumeManager();
415 VolumeManager.instance_.initialize_(function() {
416 callback(VolumeManager.instance_);
423 * Initializes mount points.
424 * @param {function()} callback Called upon the completion of the
428 VolumeManager.prototype.initialize_ = function(callback) {
429 chrome.fileBrowserPrivate.getVolumeMetadataList(function(volumeMetadataList) {
430 // Create VolumeInfo for each volume.
431 var group = new AsyncUtil.Group();
432 for (var i = 0; i < volumeMetadataList.length; i++) {
433 group.add(function(volumeMetadata, continueCallback) {
434 volumeManagerUtil.createVolumeInfo(
436 function(volumeInfo) {
437 this.volumeInfoList.add(volumeInfo);
438 if (volumeMetadata.volumeType === util.VolumeType.DRIVE)
439 this.onDriveConnectionStatusChanged_();
442 }.bind(this, volumeMetadataList[i]));
445 // Then, finalize the initialization.
446 group.run(function() {
447 // Subscribe to the mount completed event when mount points initialized.
448 chrome.fileBrowserPrivate.onMountCompleted.addListener(
449 this.onMountCompleted_.bind(this));
456 * Event handler called when some volume was mounted or unmounted.
457 * @param {MountCompletedEvent} event Received event.
460 VolumeManager.prototype.onMountCompleted_ = function(event) {
461 if (event.eventType === 'mount') {
462 if (event.volumeMetadata.mountPath) {
463 var requestKey = this.makeRequestKey_(
465 event.volumeMetadata.sourcePath);
467 var error = event.status === 'success' ? '' : event.status;
469 volumeManagerUtil.createVolumeInfo(
470 event.volumeMetadata,
471 function(volumeInfo) {
472 this.volumeInfoList.add(volumeInfo);
473 this.finishRequest_(requestKey, event.status, volumeInfo.mountPath);
475 if (volumeInfo.volumeType === util.VolumeType.DRIVE) {
476 // Update the network connection status, because until the
477 // drive is initialized, the status is set to not ready.
478 // TODO(hidehiko): The connection status should be migrated into
480 this.onDriveConnectionStatusChanged_();
484 console.warn('No mount path.');
485 this.finishRequest_(requestKey, event.status);
487 } else if (event.eventType === 'unmount') {
488 var mountPath = event.volumeMetadata.mountPath;
489 var status = event.status;
490 if (status === util.VolumeError.PATH_UNMOUNTED) {
491 console.warn('Volume already unmounted: ', mountPath);
494 var requestKey = this.makeRequestKey_('unmount', mountPath);
495 var requested = requestKey in this.requests_;
496 var volumeInfo = this.volumeInfoList.find(mountPath);
497 if (event.status === 'success' && !requested && volumeInfo) {
498 console.warn('Mounted volume without a request: ', mountPath);
499 var e = new Event('externally-unmounted');
500 // TODO(mtomasz): The mountPath field is deprecated. Remove it.
501 e.mountPath = mountPath;
502 e.volumeInfo = volumeInfo;
503 this.dispatchEvent(e);
505 this.finishRequest_(requestKey, status);
507 if (event.status === 'success')
508 this.volumeInfoList.remove(mountPath);
513 * Creates string to match mount events with requests.
514 * @param {string} requestType 'mount' | 'unmount'. TODO(hidehiko): Replace by
516 * @param {string} path Source path provided by API for mount request, or
517 * mount path for unmount request.
518 * @return {string} Key for |this.requests_|.
521 VolumeManager.prototype.makeRequestKey_ = function(requestType, path) {
522 return requestType + ':' + path;
526 * @param {string} fileUrl File url to the archive file.
527 * @param {function(string)} successCallback Success callback.
528 * @param {function(util.VolumeError)} errorCallback Error callback.
530 VolumeManager.prototype.mountArchive = function(
531 fileUrl, successCallback, errorCallback) {
532 chrome.fileBrowserPrivate.addMount(fileUrl, function(sourcePath) {
534 'Mount request: url=' + fileUrl + '; sourceUrl=' + sourcePath);
535 var requestKey = this.makeRequestKey_('mount', sourcePath);
536 this.startRequest_(requestKey, successCallback, errorCallback);
542 * @param {string} mountPath Volume mounted path.
543 * @param {function(string)} successCallback Success callback.
544 * @param {function(util.VolumeError)} errorCallback Error callback.
546 VolumeManager.prototype.unmount = function(mountPath,
549 var volumeInfo = this.volumeInfoList.find(mountPath);
551 errorCallback(util.VolumeError.NOT_MOUNTED);
555 chrome.fileBrowserPrivate.removeMount(util.makeFilesystemUrl(mountPath));
556 var requestKey = this.makeRequestKey_('unmount', volumeInfo.mountPath);
557 this.startRequest_(requestKey, successCallback, errorCallback);
561 * Resolves the absolute path to its entry. Shouldn't be used outside of the
562 * Files app's initialization.
563 * @param {string} path The path to be resolved.
564 * @param {function(Entry)} successCallback Called with the resolved entry on
566 * @param {function(FileError)} errorCallback Called on error.
568 VolumeManager.prototype.resolveAbsolutePath = function(
569 path, successCallback, errorCallback) {
570 // Make sure the path is in the mounted volume.
571 var volumeInfo = this.getVolumeInfo(path);
572 if (!volumeInfo || !volumeInfo.root) {
573 errorCallback(util.createFileError(FileError.NOT_FOUND_ERR));
577 webkitResolveLocalFileSystemURL(
578 util.makeFilesystemUrl(path), successCallback, errorCallback);
582 * Obtains the information of the volume that containing an entry pointed by the
584 * TODO(hirono): Stop to use path to get a volume info.
586 * @param {string|Entry} target Path or Entry pointing anywhere on a volume.
587 * @return {VolumeInfo} The data about the volume.
589 VolumeManager.prototype.getVolumeInfo = function(target) {
590 if (typeof target === 'string')
591 return this.volumeInfoList.findByPath(target);
592 else if (util.isFakeEntry(target))
593 return this.getCurrentProfileVolumeInfo(util.VolumeType.DRIVE);
595 return this.volumeInfoList.findByPath(target.fullPath);
599 * Obtains a volume information from a file entry URL.
600 * TODO(hirono): Check a file system to find a volume.
602 * @param {string} url URL of entry.
603 * @return {VolumeInfo} Volume info.
605 VolumeManager.prototype.getVolumeInfoByURL = function(url) {
606 return this.getVolumeInfo(util.extractFilePath(url));
610 * Obtains a volume infomration of the current profile.
612 * @param {util.VolumeType} volumeType Volume type.
613 * @return {VolumeInfo} Volume info.
615 VolumeManager.prototype.getCurrentProfileVolumeInfo = function(volumeType) {
616 for (var i = 0; i < this.volumeInfoList.length; i++) {
617 var volumeInfo = this.volumeInfoList.item(i);
618 if (volumeInfo.profile.isCurrentProfile &&
619 volumeInfo.volumeType === volumeType)
626 * Obtains location information from an entry.
628 * @param {Entry|Object} entry File or directory entry. It can be a fake entry.
629 * @return {EntryLocation} Location information.
631 VolumeManager.prototype.getLocationInfo = function(entry) {
632 if (util.isFakeEntry(entry)) {
633 return new EntryLocation(
634 // TODO(hirono): Specify currect volume.
635 this.getCurrentProfileVolumeInfo(RootType.DRIVE),
637 true /* the entry points a root directory. */);
639 return this.getLocationInfoByPath(entry.fullPath);
644 * Obtains location information from a path.
645 * TODO(hirono): Remove the method before introducing separate file system.
647 * @param {string} path Path.
648 * @return {EntryLocation} Location information.
650 VolumeManager.prototype.getLocationInfoByPath = function(path) {
651 var volumeInfo = this.volumeInfoList.findByPath(path);
652 return volumeInfo && PathUtil.getLocationInfo(volumeInfo, path);
656 * @param {string} key Key produced by |makeRequestKey_|.
657 * @param {function(string)} successCallback To be called when request finishes
659 * @param {function(util.VolumeError)} errorCallback To be called when
663 VolumeManager.prototype.startRequest_ = function(key,
664 successCallback, errorCallback) {
665 if (key in this.requests_) {
666 var request = this.requests_[key];
667 request.successCallbacks.push(successCallback);
668 request.errorCallbacks.push(errorCallback);
670 this.requests_[key] = {
671 successCallbacks: [successCallback],
672 errorCallbacks: [errorCallback],
674 timeout: setTimeout(this.onTimeout_.bind(this, key),
675 VolumeManager.TIMEOUT)
681 * Called if no response received in |TIMEOUT|.
682 * @param {string} key Key produced by |makeRequestKey_|.
685 VolumeManager.prototype.onTimeout_ = function(key) {
686 this.invokeRequestCallbacks_(this.requests_[key],
687 util.VolumeError.TIMEOUT);
688 delete this.requests_[key];
692 * @param {string} key Key produced by |makeRequestKey_|.
693 * @param {util.VolumeError|'success'} status Status received from the API.
694 * @param {string=} opt_mountPath Mount path.
697 VolumeManager.prototype.finishRequest_ = function(key, status, opt_mountPath) {
698 var request = this.requests_[key];
702 clearTimeout(request.timeout);
703 this.invokeRequestCallbacks_(request, status, opt_mountPath);
704 delete this.requests_[key];
708 * @param {Object} request Structure created in |startRequest_|.
709 * @param {util.VolumeError|string} status If status === 'success'
710 * success callbacks are called.
711 * @param {string=} opt_mountPath Mount path. Required if success.
714 VolumeManager.prototype.invokeRequestCallbacks_ = function(request, status,
716 var callEach = function(callbacks, self, args) {
717 for (var i = 0; i < callbacks.length; i++) {
718 callbacks[i].apply(self, args);
721 if (status === 'success') {
722 callEach(request.successCallbacks, this, [opt_mountPath]);
724 volumeManagerUtil.validateError(status);
725 callEach(request.errorCallbacks, this, [status]);