- add sources.
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / file_manager / background / js / volume_manager.js
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.
4
5 'use strict';
6
7 /**
8  * Represents each volume, such as "drive", "download directory", each "USB
9  * flush storage", or "mounted zip archive" etc.
10  *
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).
17  *     Can be null.
18  * @param {boolean} isReadOnly True if the volume is read only.
19  * @constructor
20  */
21 function VolumeInfo(
22     volumeType, mountPath, root, error, deviceType, isReadOnly) {
23   this.volumeType = volumeType;
24   // TODO(hidehiko): This should include FileSystem instance.
25   this.mountPath = mountPath;
26   this.root = root;
27
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.
31   this.error = error;
32   this.deviceType = deviceType;
33   this.isReadOnly = isReadOnly;
34
35   // VolumeInfo is immutable.
36   Object.freeze(this);
37 }
38
39 /**
40  * Utilities for volume manager implementation.
41  */
42 var volumeManagerUtil = {};
43
44 /**
45  * Throws an Error when the given error is not in util.VolumeError.
46  * @param {util.VolumeError} error Status string usually received from APIs.
47  */
48 volumeManagerUtil.validateError = function(error) {
49   for (var key in util.VolumeError) {
50     if (error == util.VolumeError[key])
51       return;
52   }
53
54   throw new Error('Invalid mount error: ' + error);
55 };
56
57 /**
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.
64  *
65  * @type {RegExp}
66  * @private
67  */
68 volumeManagerUtil.validateMountPathRegExp_ = new RegExp(
69     '^/(drive|drive_shared_with_me|drive_offline|drive_recent|Downloads|' +
70     '((archive|drive|removable)\/[^/]+))$');
71
72 /**
73  * Throws an Error if the validation fails.
74  * @param {string} mountPath The target path of the validation.
75  */
76 volumeManagerUtil.validateMountPath = function(mountPath) {
77   if (!volumeManagerUtil.validateMountPathRegExp_.test(mountPath))
78     throw new Error('Invalid mount path: ' + mountPath);
79 };
80
81 /**
82  * Returns the root entry of a volume mounted at mountPath.
83  *
84  * @param {string} mountPath The mounted path of the volume.
85  * @param {function(DirectoryEntry)} successCallback Called when the root entry
86  *     is found.
87  * @param {function(FileError)} errorCallback Called when an error is found.
88  * @private
89  */
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
94   // time.
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(
98       'compatible',
99       function(fileSystem) {
100         // TODO(hidehiko): chrome.runtime.lastError should have error reason.
101         if (!fileSystem) {
102           errorCallback(util.createFileError(FileError.NOT_FOUND_ERR));
103           return;
104         }
105
106         fileSystem.root.getDirectory(
107             mountPath.substring(1),  // Strip leading '/'.
108             {create: false}, successCallback, errorCallback);
109       });
110 };
111
112 /**
113  * Builds the VolumeInfo data from VolumeMetadata.
114  * @param {VolumeMetadata} volumeMetadata Metadata instance for the volume.
115  * @param {function(VolumeInfo)} callback Called on completion.
116  */
117 volumeManagerUtil.createVolumeInfo = function(volumeMetadata, callback) {
118   volumeManagerUtil.getRootEntry_(
119       volumeMetadata.mountPath,
120       function(entry) {
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 */ },
129               function(error) {
130                 console.error(
131                     'Triggering full feed fetch is failed: ' +
132                         util.getFileErrorMnemonic(error.code));
133               });
134         }
135         callback(new VolumeInfo(
136             volumeMetadata.volumeType,
137             volumeMetadata.mountPath,
138             entry,
139             volumeMetadata.mountCondition,
140             volumeMetadata.deviceType,
141             volumeMetadata.isReadOnly));
142       },
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));
154       });
155 };
156
157 /**
158  * The order of the volume list based on root type.
159  * @type {Array.<string>}
160  * @const
161  * @private
162  */
163 volumeManagerUtil.volumeListOrder_ = [
164   RootType.DRIVE, RootType.DOWNLOADS, RootType.ARCHIVE, RootType.REMOVABLE
165 ];
166
167 /**
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.
174  */
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;
182
183   if (mountPath1 != mountPath2)
184     return mountPath1 < mountPath2 ? -1 : 1;
185
186   // The path is same.
187   return 0;
188 };
189
190 /**
191  * The container of the VolumeInfo for each mounted volume.
192  * @constructor
193  */
194 function VolumeInfoList() {
195   /**
196    * Holds VolumeInfo instances.
197    * @type {cr.ui.ArrayDataModel}
198    * @private
199    */
200   this.model_ = new cr.ui.ArrayDataModel([]);
201
202   Object.freeze(this);
203 }
204
205 VolumeInfoList.prototype = {
206   get length() { return this.model_.length; }
207 };
208
209 /**
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.
213  */
214 VolumeInfoList.prototype.addEventListener = function(type, handler) {
215   this.model_.addEventListener(type, handler);
216 };
217
218 /**
219  * Removes the event listener.
220  * @param {string} type The name of the event.
221  * @param {function(Event)} handler The handler to be removed.
222  */
223 VolumeInfoList.prototype.removeEventListener = function(type, handler) {
224   this.model_.removeEventListener(type, handler);
225 };
226
227 /**
228  * Adds the volumeInfo to the appropriate position. If there already exists,
229  * just replaces it.
230  * @param {VolumeInfo} volumeInfo The information of the new volume.
231  */
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);
238   } else {
239     // Insert the VolumeInfo.
240     this.model_.splice(index, 0, volumeInfo);
241   }
242 };
243
244 /**
245  * Removes the VolumeInfo of the volume mounted at mountPath.
246  * @param {string} mountPath The path to the location where the volume is
247  *     mounted.
248  */
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);
253 };
254
255 /**
256  * Searches the information of the volume mounted at mountPath.
257  * @param {string} mountPath The path to the location where the volume is
258  *     mounted.
259  * @return {VolumeInfo} The volume's information, or null if not found.
260  */
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);
265
266   // Not found.
267   return null;
268 };
269
270 /**
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.
274  * @private
275  */
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)
282       return i;
283   }
284   return this.length;
285 };
286
287 /**
288  * @param {number} index The index of the volume in the list.
289  * @return {VolumeInfo} The VolumeInfo instance.
290  */
291 VolumeInfoList.prototype.item = function(index) {
292   return this.model_.item(index);
293 };
294
295 /**
296  * VolumeManager is responsible for tracking list of mounted volumes.
297  *
298  * @constructor
299  * @extends {cr.EventTarget}
300  */
301 function VolumeManager() {
302   /**
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>}
306    * @private
307    */
308   this.requests_ = {};
309
310   /**
311    * The list of VolumeInfo instances for each mounted volume.
312    * @type {VolumeInfoList}
313    */
314   this.volumeInfoList = new VolumeInfoList();
315
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]
321   };
322
323   chrome.fileBrowserPrivate.onDriveConnectionStatusChanged.addListener(
324       this.onDriveConnectionStatusChanged_.bind(this));
325   this.onDriveConnectionStatusChanged_();
326 }
327
328 /**
329  * Invoked when the drive connection status is changed.
330  * @private_
331  */
332 VolumeManager.prototype.onDriveConnectionStatusChanged_ = function() {
333   chrome.fileBrowserPrivate.getDriveConnectionState(function(state) {
334     this.driveConnectionState_ = state;
335     cr.dispatchSimpleEvent(this, 'drive-connection-changed');
336   }.bind(this));
337 };
338
339 /**
340  * Returns the drive connection state.
341  * @return {util.DriveConnectionType} Connection type.
342  */
343 VolumeManager.prototype.getDriveConnectionState = function() {
344   return this.driveConnectionState_;
345 };
346
347 /**
348  * VolumeManager extends cr.EventTarget.
349  */
350 VolumeManager.prototype.__proto__ = cr.EventTarget.prototype;
351
352 /**
353  * Time in milliseconds that we wait a response for. If no response on
354  * mount/unmount received the request supposed failed.
355  */
356 VolumeManager.TIMEOUT = 15 * 60 * 1000;
357
358 /**
359  * Queue to run getInstance sequentially.
360  * @type {AsyncUtil.Queue}
361  * @private
362  */
363 VolumeManager.getInstanceQueue_ = new AsyncUtil.Queue();
364
365 /**
366  * The singleton instance of VolumeManager. Initialized by the first invocation
367  * of getInstance().
368  * @type {VolumeManager}
369  * @private
370  */
371 VolumeManager.instance_ = null;
372
373 /**
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
377  *     instance.
378  */
379 VolumeManager.getInstance = function(callback) {
380   VolumeManager.getInstanceQueue_.run(function(continueCallback) {
381     if (VolumeManager.instance_) {
382       callback(VolumeManager.instance_);
383       continueCallback();
384       return;
385     }
386
387     VolumeManager.instance_ = new VolumeManager();
388     VolumeManager.instance_.initialize_(function() {
389       callback(VolumeManager.instance_);
390       continueCallback();
391     });
392   });
393 };
394
395 /**
396  * Initializes mount points.
397  * @param {function()} callback Called upon the completion of the
398  *     initialization.
399  * @private
400  */
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(
408             volumeMetadata,
409             function(volumeInfo) {
410               this.volumeInfoList.add(volumeInfo);
411               if (volumeMetadata.volumeType === util.VolumeType.DRIVE)
412                 this.onDriveConnectionStatusChanged_();
413               continueCallback();
414             }.bind(this));
415       }.bind(this, volumeMetadataList[i]));
416     }
417
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));
423       callback();
424     }.bind(this));
425   }.bind(this));
426 };
427
428 /**
429  * Event handler called when some volume was mounted or unmounted.
430  * @param {MountCompletedEvent} event Received event.
431  * @private
432  */
433 VolumeManager.prototype.onMountCompleted_ = function(event) {
434   if (event.eventType === 'mount') {
435     if (event.volumeMetadata.mountPath) {
436       var requestKey = this.makeRequestKey_(
437           'mount',
438           event.volumeMetadata.sourcePath);
439
440       var error = event.status === 'success' ? '' : event.status;
441
442       volumeManagerUtil.createVolumeInfo(
443           event.volumeMetadata,
444           function(volumeInfo) {
445             this.volumeInfoList.add(volumeInfo);
446             this.finishRequest_(requestKey, event.status, volumeInfo.mountPath);
447
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
452               // VolumeMetadata.
453               this.onDriveConnectionStatusChanged_();
454             }
455           }.bind(this));
456     } else {
457       console.warn('No mount path.');
458       this.finishRequest_(requestKey, event.status);
459     }
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);
466       status = 'success';
467     }
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);
476     }
477     this.finishRequest_(requestKey, status);
478
479     if (event.status === 'success')
480       this.volumeInfoList.remove(mountPath);
481   }
482 };
483
484 /**
485  * Creates string to match mount events with requests.
486  * @param {string} requestType 'mount' | 'unmount'. TODO(hidehiko): Replace by
487  *     enum.
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_|.
491  * @private
492  */
493 VolumeManager.prototype.makeRequestKey_ = function(requestType, path) {
494   return requestType + ':' + path;
495 };
496
497 /**
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.
501  */
502 VolumeManager.prototype.mountArchive = function(
503     fileUrl, successCallback, errorCallback) {
504   chrome.fileBrowserPrivate.addMount(fileUrl, function(sourcePath) {
505     console.info(
506         'Mount request: url=' + fileUrl + '; sourceUrl=' + sourcePath);
507     var requestKey = this.makeRequestKey_('mount', sourcePath);
508     this.startRequest_(requestKey, successCallback, errorCallback);
509   }.bind(this));
510 };
511
512 /**
513  * Unmounts volume.
514  * @param {string} mountPath Volume mounted path.
515  * @param {function(string)} successCallback Success callback.
516  * @param {function(util.VolumeError)} errorCallback Error callback.
517  */
518 VolumeManager.prototype.unmount = function(mountPath,
519                                            successCallback,
520                                            errorCallback) {
521   volumeManagerUtil.validateMountPath(mountPath);
522   var volumeInfo = this.volumeInfoList.find(mountPath);
523   if (!volumeInfo) {
524     errorCallback(util.VolumeError.NOT_MOUNTED);
525     return;
526   }
527
528   chrome.fileBrowserPrivate.removeMount(util.makeFilesystemUrl(mountPath));
529   var requestKey = this.makeRequestKey_('unmount', volumeInfo.mountPath);
530   this.startRequest_(requestKey, successCallback, errorCallback);
531 };
532
533 /**
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
537  *     success.
538  * @param {function(FileError)} errorCallback Called on error.
539  */
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));
548     return;
549   }
550
551   webkitResolveLocalFileSystemURL(
552       util.makeFilesystemUrl(path), successCallback, errorCallback);
553 };
554
555 /**
556  * @param {string} mountPath Volume mounted path.
557  * @return {VolumeInfo} The data about the volume.
558  */
559 VolumeManager.prototype.getVolumeInfo = function(mountPath) {
560   volumeManagerUtil.validateMountPath(mountPath);
561   return this.volumeInfoList.find(mountPath);
562 };
563
564 /**
565  * @param {string} key Key produced by |makeRequestKey_|.
566  * @param {function(string)} successCallback To be called when request finishes
567  *     successfully.
568  * @param {function(util.VolumeError)} errorCallback To be called when
569  *     request fails.
570  * @private
571  */
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);
578   } else {
579     this.requests_[key] = {
580       successCallbacks: [successCallback],
581       errorCallbacks: [errorCallback],
582
583       timeout: setTimeout(this.onTimeout_.bind(this, key),
584                           VolumeManager.TIMEOUT)
585     };
586   }
587 };
588
589 /**
590  * Called if no response received in |TIMEOUT|.
591  * @param {string} key Key produced by |makeRequestKey_|.
592  * @private
593  */
594 VolumeManager.prototype.onTimeout_ = function(key) {
595   this.invokeRequestCallbacks_(this.requests_[key],
596                                util.VolumeError.TIMEOUT);
597   delete this.requests_[key];
598 };
599
600 /**
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.
604  * @private
605  */
606 VolumeManager.prototype.finishRequest_ = function(key, status, opt_mountPath) {
607   var request = this.requests_[key];
608   if (!request)
609     return;
610
611   clearTimeout(request.timeout);
612   this.invokeRequestCallbacks_(request, status, opt_mountPath);
613   delete this.requests_[key];
614 };
615
616 /**
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.
621  * @private
622  */
623 VolumeManager.prototype.invokeRequestCallbacks_ = function(request, status,
624                                                            opt_mountPath) {
625   var callEach = function(callbacks, self, args) {
626     for (var i = 0; i < callbacks.length; i++) {
627       callbacks[i].apply(self, args);
628     }
629   };
630   if (status == 'success') {
631     callEach(request.successCallbacks, this, [opt_mountPath]);
632   } else {
633     volumeManagerUtil.validateError(status);
634     callEach(request.errorCallbacks, this, [status]);
635   }
636 };