Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(284)

Side by Side Diff: chrome/browser/resources/file_manager/js/volume_manager.js

Issue 39123003: [Files.app] Split the JavaScript files into subdirectories: common, background, and foreground (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: fixed test failure. Created 7 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
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 mountPath + ', ' + util.getFileErrorMnemonic(fileError.code));
146 callback(new VolumeInfo(
147 volumeMetadata.volumeType,
148 volumeMetadata.mountPath,
149 null, // Root entry is not found.
150 volumeMetadata.mountCondition,
151 volumeMetadata.deviceType,
152 volumeMetadata.isReadOnly));
153 });
154 };
155
156 /**
157 * The order of the volume list based on root type.
158 * @type {Array.<string>}
159 * @const
160 * @private
161 */
162 volumeManagerUtil.volumeListOrder_ = [
163 RootType.DRIVE, RootType.DOWNLOADS, RootType.ARCHIVE, RootType.REMOVABLE
164 ];
165
166 /**
167 * Compares mount paths to sort the volume list order.
168 * @param {string} mountPath1 The mount path for the first volume.
169 * @param {string} mountPath2 The mount path for the second volume.
170 * @return {number} 0 if mountPath1 and mountPath2 are same, -1 if VolumeInfo
171 * mounted at mountPath1 should be listed before the one mounted at
172 * mountPath2, otherwise 1.
173 */
174 volumeManagerUtil.compareMountPath = function(mountPath1, mountPath2) {
175 var order1 = volumeManagerUtil.volumeListOrder_.indexOf(
176 PathUtil.getRootType(mountPath1));
177 var order2 = volumeManagerUtil.volumeListOrder_.indexOf(
178 PathUtil.getRootType(mountPath2));
179 if (order1 != order2)
180 return order1 < order2 ? -1 : 1;
181
182 if (mountPath1 != mountPath2)
183 return mountPath1 < mountPath2 ? -1 : 1;
184
185 // The path is same.
186 return 0;
187 };
188
189 /**
190 * The container of the VolumeInfo for each mounted volume.
191 * @constructor
192 */
193 function VolumeInfoList() {
194 /**
195 * Holds VolumeInfo instances.
196 * @type {cr.ui.ArrayDataModel}
197 * @private
198 */
199 this.model_ = new cr.ui.ArrayDataModel([]);
200
201 Object.freeze(this);
202 }
203
204 VolumeInfoList.prototype = {
205 get length() { return this.model_.length; }
206 };
207
208 /**
209 * Adds the event listener to listen the change of volume info.
210 * @param {string} type The name of the event.
211 * @param {function(Event)} handler The handler for the event.
212 */
213 VolumeInfoList.prototype.addEventListener = function(type, handler) {
214 this.model_.addEventListener(type, handler);
215 };
216
217 /**
218 * Removes the event listener.
219 * @param {string} type The name of the event.
220 * @param {function(Event)} handler The handler to be removed.
221 */
222 VolumeInfoList.prototype.removeEventListener = function(type, handler) {
223 this.model_.removeEventListener(type, handler);
224 };
225
226 /**
227 * Adds the volumeInfo to the appropriate position. If there already exists,
228 * just replaces it.
229 * @param {VolumeInfo} volumeInfo The information of the new volume.
230 */
231 VolumeInfoList.prototype.add = function(volumeInfo) {
232 var index = this.findLowerBoundIndex_(volumeInfo.mountPath);
233 if (index < this.length &&
234 this.item(index).mountPath == volumeInfo.mountPath) {
235 // Replace the VolumeInfo.
236 this.model_.splice(index, 1, volumeInfo);
237 } else {
238 // Insert the VolumeInfo.
239 this.model_.splice(index, 0, volumeInfo);
240 }
241 };
242
243 /**
244 * Removes the VolumeInfo of the volume mounted at mountPath.
245 * @param {string} mountPath The path to the location where the volume is
246 * mounted.
247 */
248 VolumeInfoList.prototype.remove = function(mountPath) {
249 var index = this.findLowerBoundIndex_(mountPath);
250 if (index < this.length && this.item(index).mountPath == mountPath)
251 this.model_.splice(index, 1);
252 };
253
254 /**
255 * Searches the information of the volume mounted at mountPath.
256 * @param {string} mountPath The path to the location where the volume is
257 * mounted.
258 * @return {VolumeInfo} The volume's information, or null if not found.
259 */
260 VolumeInfoList.prototype.find = function(mountPath) {
261 var index = this.findLowerBoundIndex_(mountPath);
262 if (index < this.length && this.item(index).mountPath == mountPath)
263 return this.item(index);
264
265 // Not found.
266 return null;
267 };
268
269 /**
270 * @param {string} mountPath The mount path of searched volume.
271 * @return {number} The index of the volume if found, or the inserting
272 * position of the volume.
273 * @private
274 */
275 VolumeInfoList.prototype.findLowerBoundIndex_ = function(mountPath) {
276 // Assuming the number of elements in the array data model is very small
277 // in most cases, use simple linear search, here.
278 for (var i = 0; i < this.length; i++) {
279 if (volumeManagerUtil.compareMountPath(
280 this.item(i).mountPath, mountPath) >= 0)
281 return i;
282 }
283 return this.length;
284 };
285
286 /**
287 * @param {number} index The index of the volume in the list.
288 * @return {VolumeInfo} The VolumeInfo instance.
289 */
290 VolumeInfoList.prototype.item = function(index) {
291 return this.model_.item(index);
292 };
293
294 /**
295 * VolumeManager is responsible for tracking list of mounted volumes.
296 *
297 * @constructor
298 * @extends {cr.EventTarget}
299 */
300 function VolumeManager() {
301 /**
302 * The list of archives requested to mount. We will show contents once
303 * archive is mounted, but only for mounts from within this filebrowser tab.
304 * @type {Object.<string, Object>}
305 * @private
306 */
307 this.requests_ = {};
308
309 /**
310 * The list of VolumeInfo instances for each mounted volume.
311 * @type {VolumeInfoList}
312 */
313 this.volumeInfoList = new VolumeInfoList();
314
315 // The status should be merged into VolumeManager.
316 // TODO(hidehiko): Remove them after the migration.
317 this.driveConnectionState_ = {
318 type: util.DriveConnectionType.OFFLINE,
319 reasons: [util.DriveConnectionReason.NO_SERVICE]
320 };
321
322 chrome.fileBrowserPrivate.onDriveConnectionStatusChanged.addListener(
323 this.onDriveConnectionStatusChanged_.bind(this));
324 this.onDriveConnectionStatusChanged_();
325 }
326
327 /**
328 * Invoked when the drive connection status is changed.
329 * @private_
330 */
331 VolumeManager.prototype.onDriveConnectionStatusChanged_ = function() {
332 chrome.fileBrowserPrivate.getDriveConnectionState(function(state) {
333 this.driveConnectionState_ = state;
334 cr.dispatchSimpleEvent(this, 'drive-connection-changed');
335 }.bind(this));
336 };
337
338 /**
339 * Returns the drive connection state.
340 * @return {util.DriveConnectionType} Connection type.
341 */
342 VolumeManager.prototype.getDriveConnectionState = function() {
343 return this.driveConnectionState_;
344 };
345
346 /**
347 * VolumeManager extends cr.EventTarget.
348 */
349 VolumeManager.prototype.__proto__ = cr.EventTarget.prototype;
350
351 /**
352 * Time in milliseconds that we wait a response for. If no response on
353 * mount/unmount received the request supposed failed.
354 */
355 VolumeManager.TIMEOUT = 15 * 60 * 1000;
356
357 /**
358 * Queue to run getInstance sequentially.
359 * @type {AsyncUtil.Queue}
360 * @private
361 */
362 VolumeManager.getInstanceQueue_ = new AsyncUtil.Queue();
363
364 /**
365 * The singleton instance of VolumeManager. Initialized by the first invocation
366 * of getInstance().
367 * @type {VolumeManager}
368 * @private
369 */
370 VolumeManager.instance_ = null;
371
372 /**
373 * Returns the VolumeManager instance asynchronously. If it is not created or
374 * under initialization, it will waits for the finish of the initialization.
375 * @param {function(VolumeManager)} callback Called with the VolumeManager
376 * instance.
377 */
378 VolumeManager.getInstance = function(callback) {
379 VolumeManager.getInstanceQueue_.run(function(continueCallback) {
380 if (VolumeManager.instance_) {
381 callback(VolumeManager.instance_);
382 continueCallback();
383 return;
384 }
385
386 VolumeManager.instance_ = new VolumeManager();
387 VolumeManager.instance_.initialize_(function() {
388 callback(VolumeManager.instance_);
389 continueCallback();
390 });
391 });
392 };
393
394 /**
395 * Initializes mount points.
396 * @param {function()} callback Called upon the completion of the
397 * initialization.
398 * @private
399 */
400 VolumeManager.prototype.initialize_ = function(callback) {
401 chrome.fileBrowserPrivate.getVolumeMetadataList(function(volumeMetadataList) {
402 // Create VolumeInfo for each volume.
403 var group = new AsyncUtil.Group();
404 for (var i = 0; i < volumeMetadataList.length; i++) {
405 group.add(function(volumeMetadata, continueCallback) {
406 volumeManagerUtil.createVolumeInfo(
407 volumeMetadata,
408 function(volumeInfo) {
409 this.volumeInfoList.add(volumeInfo);
410 if (volumeMetadata.volumeType === util.VolumeType.DRIVE)
411 this.onDriveConnectionStatusChanged_();
412 continueCallback();
413 }.bind(this));
414 }.bind(this, volumeMetadataList[i]));
415 }
416
417 // Then, finalize the initialization.
418 group.run(function() {
419 // Subscribe to the mount completed event when mount points initialized.
420 chrome.fileBrowserPrivate.onMountCompleted.addListener(
421 this.onMountCompleted_.bind(this));
422 callback();
423 }.bind(this));
424 }.bind(this));
425 };
426
427 /**
428 * Event handler called when some volume was mounted or unmounted.
429 * @param {MountCompletedEvent} event Received event.
430 * @private
431 */
432 VolumeManager.prototype.onMountCompleted_ = function(event) {
433 if (event.eventType === 'mount') {
434 if (event.volumeMetadata.mountPath) {
435 var requestKey = this.makeRequestKey_(
436 'mount',
437 event.volumeMetadata.sourcePath);
438
439 var error = event.status === 'success' ? '' : event.status;
440
441 volumeManagerUtil.createVolumeInfo(
442 event.volumeMetadata,
443 function(volumeInfo) {
444 this.volumeInfoList.add(volumeInfo);
445 this.finishRequest_(requestKey, event.status, volumeInfo.mountPath);
446
447 if (volumeInfo.volumeType === util.VolumeType.DRIVE) {
448 // Update the network connection status, because until the
449 // drive is initialized, the status is set to not ready.
450 // TODO(hidehiko): The connection status should be migrated into
451 // VolumeMetadata.
452 this.onDriveConnectionStatusChanged_();
453 }
454 }.bind(this));
455 } else {
456 console.warn('No mount path.');
457 this.finishRequest_(requestKey, event.status);
458 }
459 } else if (event.eventType === 'unmount') {
460 var mountPath = event.volumeMetadata.mountPath;
461 volumeManagerUtil.validateMountPath(mountPath);
462 var status = event.status;
463 if (status === util.VolumeError.PATH_UNMOUNTED) {
464 console.warn('Volume already unmounted: ', mountPath);
465 status = 'success';
466 }
467 var requestKey = this.makeRequestKey_('unmount', mountPath);
468 var requested = requestKey in this.requests_;
469 if (event.status === 'success' && !requested &&
470 this.volumeInfoList.find(mountPath)) {
471 console.warn('Mounted volume without a request: ', mountPath);
472 var e = new Event('externally-unmounted');
473 e.mountPath = mountPath;
474 this.dispatchEvent(e);
475 }
476 this.finishRequest_(requestKey, status);
477
478 if (event.status === 'success')
479 this.volumeInfoList.remove(mountPath);
480 }
481 };
482
483 /**
484 * Creates string to match mount events with requests.
485 * @param {string} requestType 'mount' | 'unmount'. TODO(hidehiko): Replace by
486 * enum.
487 * @param {string} path Source path provided by API for mount request, or
488 * mount path for unmount request.
489 * @return {string} Key for |this.requests_|.
490 * @private
491 */
492 VolumeManager.prototype.makeRequestKey_ = function(requestType, path) {
493 return requestType + ':' + path;
494 };
495
496 /**
497 * @param {string} fileUrl File url to the archive file.
498 * @param {function(string)} successCallback Success callback.
499 * @param {function(util.VolumeError)} errorCallback Error callback.
500 */
501 VolumeManager.prototype.mountArchive = function(
502 fileUrl, successCallback, errorCallback) {
503 chrome.fileBrowserPrivate.addMount(fileUrl, function(sourcePath) {
504 console.info(
505 'Mount request: url=' + fileUrl + '; sourceUrl=' + sourcePath);
506 var requestKey = this.makeRequestKey_('mount', sourcePath);
507 this.startRequest_(requestKey, successCallback, errorCallback);
508 }.bind(this));
509 };
510
511 /**
512 * Unmounts volume.
513 * @param {string} mountPath Volume mounted path.
514 * @param {function(string)} successCallback Success callback.
515 * @param {function(util.VolumeError)} errorCallback Error callback.
516 */
517 VolumeManager.prototype.unmount = function(mountPath,
518 successCallback,
519 errorCallback) {
520 volumeManagerUtil.validateMountPath(mountPath);
521 var volumeInfo = this.volumeInfoList.find(mountPath);
522 if (!volumeInfo) {
523 errorCallback(util.VolumeError.NOT_MOUNTED);
524 return;
525 }
526
527 chrome.fileBrowserPrivate.removeMount(util.makeFilesystemUrl(mountPath));
528 var requestKey = this.makeRequestKey_('unmount', volumeInfo.mountPath);
529 this.startRequest_(requestKey, successCallback, errorCallback);
530 };
531
532 /**
533 * Resolve the path to its entry.
534 * @param {string} path The path to be resolved.
535 * @param {function(Entry)} successCallback Called with the resolved entry on
536 * success.
537 * @param {function(FileError)} errorCallback Called on error.
538 */
539 VolumeManager.prototype.resolvePath = function(
540 path, successCallback, errorCallback) {
541 // Make sure the path is in the mounted volume.
542 var mountPath = PathUtil.isDriveBasedPath(path) ?
543 RootDirectory.DRIVE : PathUtil.getRootPath(path);
544 var volumeInfo = this.getVolumeInfo(mountPath);
545 if (!volumeInfo || !volumeInfo.root) {
546 errorCallback(util.createFileError(FileError.NOT_FOUND_ERR));
547 return;
548 }
549
550 webkitResolveLocalFileSystemURL(
551 util.makeFilesystemUrl(path), successCallback, errorCallback);
552 };
553
554 /**
555 * @param {string} mountPath Volume mounted path.
556 * @return {VolumeInfo} The data about the volume.
557 */
558 VolumeManager.prototype.getVolumeInfo = function(mountPath) {
559 volumeManagerUtil.validateMountPath(mountPath);
560 return this.volumeInfoList.find(mountPath);
561 };
562
563 /**
564 * @param {string} key Key produced by |makeRequestKey_|.
565 * @param {function(string)} successCallback To be called when request finishes
566 * successfully.
567 * @param {function(util.VolumeError)} errorCallback To be called when
568 * request fails.
569 * @private
570 */
571 VolumeManager.prototype.startRequest_ = function(key,
572 successCallback, errorCallback) {
573 if (key in this.requests_) {
574 var request = this.requests_[key];
575 request.successCallbacks.push(successCallback);
576 request.errorCallbacks.push(errorCallback);
577 } else {
578 this.requests_[key] = {
579 successCallbacks: [successCallback],
580 errorCallbacks: [errorCallback],
581
582 timeout: setTimeout(this.onTimeout_.bind(this, key),
583 VolumeManager.TIMEOUT)
584 };
585 }
586 };
587
588 /**
589 * Called if no response received in |TIMEOUT|.
590 * @param {string} key Key produced by |makeRequestKey_|.
591 * @private
592 */
593 VolumeManager.prototype.onTimeout_ = function(key) {
594 this.invokeRequestCallbacks_(this.requests_[key],
595 util.VolumeError.TIMEOUT);
596 delete this.requests_[key];
597 };
598
599 /**
600 * @param {string} key Key produced by |makeRequestKey_|.
601 * @param {util.VolumeError|'success'} status Status received from the API.
602 * @param {string=} opt_mountPath Mount path.
603 * @private
604 */
605 VolumeManager.prototype.finishRequest_ = function(key, status, opt_mountPath) {
606 var request = this.requests_[key];
607 if (!request)
608 return;
609
610 clearTimeout(request.timeout);
611 this.invokeRequestCallbacks_(request, status, opt_mountPath);
612 delete this.requests_[key];
613 };
614
615 /**
616 * @param {Object} request Structure created in |startRequest_|.
617 * @param {util.VolumeError|string} status If status == 'success'
618 * success callbacks are called.
619 * @param {string=} opt_mountPath Mount path. Required if success.
620 * @private
621 */
622 VolumeManager.prototype.invokeRequestCallbacks_ = function(request, status,
623 opt_mountPath) {
624 var callEach = function(callbacks, self, args) {
625 for (var i = 0; i < callbacks.length; i++) {
626 callbacks[i].apply(self, args);
627 }
628 };
629 if (status == 'success') {
630 callEach(request.successCallbacks, this, [opt_mountPath]);
631 } else {
632 volumeManagerUtil.validateError(status);
633 callEach(request.errorCallbacks, this, [status]);
634 }
635 };
OLDNEW
« no previous file with comments | « chrome/browser/resources/file_manager/js/util.js ('k') | chrome/browser/resources/file_manager/js/volume_manager_wrapper.js » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698