OLD | NEW |
---|---|
(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 /** | |
6 * VolumeManager is responsible for tracking list of mounted volumes. | |
7 * | |
8 * @constructor | |
9 * @extends {cr.EventTarget} | |
10 */ | |
11 function VolumeManager() { | |
12 /** | |
13 * The list of archives requested to mount. We will show contents once | |
14 * archive is mounted, but only for mounts from within this filebrowser tab. | |
15 * @type {Object.<string, Object>} | |
16 * @private | |
17 */ | |
18 this.requests_ = {}; | |
19 | |
20 /** | |
21 * @type {Object.<string, Object>} | |
22 * @private | |
23 */ | |
24 this.mountedVolumes_ = {}; | |
25 | |
26 this.initMountPoints_(); | |
27 chrome.fileBrowserPrivate.onMountCompleted.addListener( | |
28 this.onMountCompleted_.bind(this)); | |
29 this.gDataStatus_ = VolumeManager.GDataStatus.UNMOUNTED; | |
30 } | |
31 | |
32 /** | |
33 * VolumeManager extends cr.EventTarget. | |
34 */ | |
35 VolumeManager.prototype.__proto__ = cr.EventTarget.prototype; | |
36 | |
37 /** | |
38 * @enum | |
39 */ | |
40 VolumeManager.Error = { | |
41 /* Internal errors */ | |
42 NOT_MOUNTED: 'not_mounted', | |
43 TIMEOUT: 'timeout', | |
44 | |
45 /* System events */ | |
46 UNKNOWN: 'error_unknown', | |
47 INTERNAL: 'error_internal', | |
48 UNKNOWN_FILESYSTEM: 'error_unknown_filesystem', | |
49 UNSUPPORTED_FILESYSTEM: 'error_unsuported_filesystem', | |
50 INVALID_ARCHIVE: 'error_invalid_archive', | |
51 LIBCROS_MISSING: 'error_libcros_missing', | |
52 AUTHENTICATION: 'error_authentication', | |
53 PATH_UNMOUNTED: 'error_path_unmounted' | |
54 }; | |
55 | |
56 /** | |
57 * @enum | |
58 */ | |
59 VolumeManager.GDataStatus = { | |
60 UNMOUNTED: 'unmounted', | |
61 MOUNTING: 'mounting', | |
62 ERROR: 'error', | |
63 MOUNTED: 'mounted' | |
64 }; | |
65 | |
66 /** | |
67 * Time in milliseconds that we wait a respone for. If no response on | |
68 * mount/unmount received the request supposed failed. | |
69 */ | |
70 VolumeManager.TIMEOUT = 15 * 60 * 1000; | |
71 | |
72 /** | |
73 * Delay in milliseconds GDATA changes its state from |UNMOUNTED| to | |
74 * |MOUNTING|. Used to display progress in the UI. | |
75 */ | |
76 VolumeManager.MOUNTING_DELAY = 500; | |
77 | |
78 /** | |
79 * @return {VolumeManager} Singleton instance. | |
80 */ | |
81 VolumeManager.getInstance = function() { | |
82 return VolumeManager.instance_ = VolumeManager.instance_ || | |
83 new VolumeManager(); | |
84 }; | |
85 | |
86 /** | |
87 * @param {VolumeManager.GDataStatus} newStatus New GDATA status. | |
88 * @private | |
89 */ | |
90 VolumeManager.prototype.setGDataStatus_ = function(newStatus) { | |
91 if (this.gDataStatus_ != newStatus) { | |
92 this.gDataStatus_ = newStatus; | |
93 cr.dispatchSimpleEvent(this, 'gdata-status-changed'); | |
94 } | |
95 }; | |
96 | |
97 /** | |
98 * @return {VolumeManager.GDataStatus} Current GDATA status. | |
99 */ | |
100 VolumeManager.prototype.getGDataStatus = function() { | |
101 return this.gDataStatus_; | |
102 }; | |
103 | |
104 /** | |
105 * @param {string} mountPath Volume root path. | |
106 * @return {boolean} True if mounted. | |
107 */ | |
108 VolumeManager.prototype.isMounted = function(mountPath) { | |
109 this.validateMountPath_(mountPath); | |
110 return mountPath in this.mountedVolumes_; | |
111 }; | |
112 | |
113 /** | |
114 * Initialized mount points. | |
115 * @private | |
116 */ | |
117 VolumeManager.prototype.initMountPoints_ = function() { | |
118 var mountedVolumes = []; | |
119 var self = this; | |
120 var index = 0; | |
121 function step(mountPoints) { | |
122 if (index < mountPoints.length) { | |
123 var info = mountPoints[index]; | |
124 if (info.mountType == 'gdata') | |
125 console.error('GData is not expected initially mounted'); | |
126 var error = info.mountCondition ? 'error_' + info.mountCondition : ''; | |
127 function onVolumeInfo(volume) { | |
128 mountedVolumes.push(volume); | |
129 index++; | |
130 step(mountPoints); | |
131 } | |
132 self.makeVolumeInfo_('/' + info.mountPath, error, onVolumeInfo); | |
133 } else { | |
134 for (var i = 0; i < mountedVolumes.length; i++) { | |
135 var volume = mountedVolumes[i]; | |
136 self.mountedVolumes_[volume.mountPath] = volume; | |
137 } | |
138 if (mountedVolumes.length > 0) | |
139 cr.dispatchSimpleEvent(self, 'change'); | |
140 } | |
141 } | |
142 | |
143 chrome.fileBrowserPrivate.getMountPoints(step); | |
144 }; | |
145 | |
146 /** | |
147 * Event handler called when some volume was mounted or unmouted. | |
148 * @param {MountCompletedEvent} event Received event. | |
149 * @private | |
150 */ | |
151 VolumeManager.prototype.onMountCompleted_ = function(event) { | |
152 if (event.eventType == 'mount') { | |
153 if (event.mountPath) { | |
154 var requestKey = this.makeRequestKey_( | |
155 'mount', event.mountType, event.sourcePath); | |
156 var error = event.status == 'success' ? '' : event.status; | |
157 this.makeVolumeInfo_(event.mountPath, error, function(volume) { | |
158 this.mountedVolumes_[volume.mountPath] = volume; | |
159 this.finishRequest_(requestKey, event.status, event.mountPath); | |
160 cr.dispatchSimpleEvent(this, 'change'); | |
161 }.bind(this)); | |
162 } else { | |
163 console.log('No mount path'); | |
164 this.finishRequest_(requestKey, event.status); | |
165 } | |
166 } else if (event.eventType == 'unmount') { | |
167 var mountPath = '/' + event.mountPath; | |
168 this.validateMountPath_(mountPath); | |
169 var status = event.status; | |
170 if (status == VolumeManager.Error.PATH_UNMOUNTED) { | |
171 console.log('Volume already unmounted: ', mountPath); | |
172 status = 'success'; | |
173 } | |
174 var requestKey = this.makeRequestKey_('unmount', '', event.sourcePath); | |
175 var expectedly = requestKey in this.requests_; | |
dgozman
2012/05/21 13:32:16
expectedly -> externally
SeRya
2012/05/22 09:10:50
Changed to 'requested'.
| |
176 if (event.status == 'success' && !expectedly && | |
177 mountPath in this.mountedVolumes_) { | |
178 console.log('Mounted volume without a request: ', mountPath); | |
179 var e = new cr.Event('externally-unmounted'); | |
180 e.mountPath = mountPath; | |
181 this.dispatchEvent(e); | |
182 } | |
183 this.finishRequest_(requestKey, status); | |
184 | |
185 if (event.status == 'success') { | |
186 delete this.mountedVolumes_[mountPath]; | |
187 cr.dispatchSimpleEvent(this, 'change'); | |
188 } | |
189 } | |
190 | |
191 if (event.mountType == 'gdata') { | |
192 if (event.status == 'success') { | |
193 if (event.eventType == 'mount') | |
194 this.setGDataStatus_(VolumeManager.GDataStatus.MOUNTED); | |
195 else if (event.eventType == 'unmount') | |
196 this.setGDataStatus_(VolumeManager.GDataStatus.UMOUNTED); | |
197 } | |
198 } | |
199 }; | |
200 | |
201 /** | |
202 * @param {string} mountPath Path to the volume. | |
203 * @param {VolumeManager?} error Mounting error if any. | |
204 * @param {function(Object)} callback Result acceptor. | |
205 * @private | |
206 */ | |
207 VolumeManager.prototype.makeVolumeInfo_ = function( | |
208 mountPath, error, callback) { | |
209 if (error) | |
210 this.validateError_(error); | |
211 this.validateMountPath_(mountPath); | |
212 function onVolumeMetadata(metadata) { | |
213 callback({ | |
214 mountPath: mountPath, | |
215 error: error, | |
216 readonly: !!metadata && metadata.isReadOnly | |
217 }); | |
218 } | |
219 chrome.fileBrowserPrivate.getVolumeMetadata( | |
220 util.makeFilesystemUrl(mountPath), onVolumeMetadata); | |
221 }; | |
222 | |
223 /** | |
224 * Creates string to match mount events with requests. | |
225 * @param {string} requestType 'mount' | 'unmount'. | |
226 * @param {string} mountType 'device' | 'file' | 'network' | 'gdata'. | |
227 * @param {string} sourcePath Source path provided by API after | |
228 * resolving mount request. | |
229 * @return {string} Key for |this.requests_|. | |
230 * @private | |
231 */ | |
232 VolumeManager.prototype.makeRequestKey_ = function(requestType, | |
233 mountType, | |
234 sourcePath) { | |
235 return requestType + ':' + mountType + ':' + sourcePath; | |
236 }; | |
237 | |
238 | |
239 /** | |
240 * @param {Function} successCallback Success callback. | |
241 * @param {Function} errorCallback Error callback. | |
242 */ | |
243 VolumeManager.prototype.mountGData = function(successCallback, errorCallback) { | |
244 if (this.getGDataStatus() == VolumeManager.GDataStatus.ERROR) { | |
245 this.setGDataStatus_(VolumeManager.GDataStatus.UNMOUNTED); | |
246 } | |
247 var self = this; | |
248 var timeout = setTimeout(function() { | |
249 if (self.getGDataStatus() == VolumeManager.GDataStatus.UNMOUNTED) | |
250 self.setGDataStatus_(VolumeManager.GDataStatus.MOUNTING); | |
251 timeout = null; | |
252 }, VolumeManager.MOUNTING_DELAY); | |
253 this.mount_('', 'gdata', function(mountPath) { | |
254 if (timeout !== null) | |
255 clearTimeout(timeout); | |
256 successCallback(mountPath); | |
257 }, function(error) { | |
258 if (self.getGDataStatus() != VolumeManager.GDataStatus.MOUNTED) | |
259 self.setGDataStatus_(VolumeManager.GDataStatus.ERROR); | |
260 if (timeout != null) | |
261 clearTimeout(timeout); | |
262 errorCallback(error); | |
263 }); | |
264 }; | |
265 | |
266 /** | |
267 * @param {string} fullPath Path to the archive file. | |
268 * @param {Function} successCallback Success callback. | |
269 * @param {Function} errorCallback Error callback. | |
270 */ | |
271 VolumeManager.prototype.mountArchive = function(fullPath, successCallback, | |
272 errorCallback) { | |
273 this.mount_(util.makeFilesystemUrl(fullPath), | |
274 'file', successCallback, errorCallback); | |
275 }; | |
276 | |
277 /** | |
278 * Unmounts volume. | |
279 * @param {string} mountPath Volume mounted path. | |
280 * @param {Function} successCallback Success callback. | |
281 * @param {Function} errorCallback Error callback. | |
282 */ | |
283 VolumeManager.prototype.unmount = function(mountPath, | |
284 successCallback, | |
285 errorCallback) { | |
286 this.validateMountPath_(mountPath); | |
287 var volumeInfo = this.mountedVolumes_[mountPath]; | |
288 if (!volumeInfo) { | |
289 errorCallback(VolumeManager.Error.NOT_MOUNTED); | |
290 return; | |
291 } | |
292 | |
293 chrome.fileBrowserPrivate.removeMount(util.makeFilesystemUrl(mountPath)); | |
294 this.startRequest_(this.makeRequestKey_('unmount', '', | |
295 volumeInfo.sourcePath)); | |
296 }; | |
297 | |
298 /** | |
299 * @param {string} mountPath Volume mounted path. | |
300 * @return {VolumeManager.Error?} Returns mount error code | |
301 * or undefined if no error. | |
302 */ | |
303 VolumeManager.prototype.getMountError = function(mountPath) { | |
304 return this.getVolumeInfo_(mountPath).error; | |
305 }; | |
306 | |
307 /** | |
308 * @param {string} mountPath Volume mounted path. | |
309 * @return {boolean} True if volume at |mountedPath| is mounted but not usable. | |
310 */ | |
311 VolumeManager.prototype.isUnreadable = function(mountPath) { | |
312 var error = this.getMountError(mountPath); | |
313 return error == VolumeManager.Error.UNKNOWN_FILESYSTEM || | |
314 error == VolumeManager.Error.UNSUPPORTED_FILESYSTEM; | |
315 }; | |
316 | |
317 /** | |
318 * @param {string} mountPath Volume mounted path. | |
319 * @return {boolean} True if volume at |mountedPath| is read only. | |
320 */ | |
321 VolumeManager.prototype.isReadOnly = function(mountPath) { | |
322 return !!this.getVolumeInfo_(mountPath).readonly; | |
323 }; | |
324 | |
325 /** | |
326 * Helper method. | |
327 * @param {string} mountPath Volume mounted path. | |
328 * @return {Object} Structure created in |startRequest_|. | |
329 * @private | |
330 */ | |
331 VolumeManager.prototype.getVolumeInfo_ = function(mountPath) { | |
332 this.validateMountPath_(mountPath); | |
333 return this.mountedVolumes_[mountPath] || {}; | |
334 }; | |
335 | |
336 /** | |
337 * @param {string} url URL for for |fileBrowserPrivate.addMount|. | |
338 * @param {'gdata'|'file'} mountType Mount type for | |
339 * |fileBrowserPrivate.addMount|. | |
340 * @param {Function} successCallback Success callback. | |
341 * @param {Function} errorCallback Error callback. | |
342 * @private | |
343 */ | |
344 VolumeManager.prototype.mount_ = function(url, mountType, | |
345 successCallback, errorCallback) { | |
346 chrome.fileBrowserPrivate.addMount(url, mountType, {}, | |
347 function(sourcePath) { | |
348 console.log('Mount request: url=' + url + '; mountType=' + mountType + | |
349 '; sourceUrl=' + sourcePath); | |
350 var requestKey = this.makeRequestKey_('mount', mountType, sourcePath); | |
351 this.startRequest_(requestKey, successCallback, errorCallback); | |
352 }.bind(this)); | |
353 }; | |
354 | |
355 /** | |
356 * @param {sting} key Key produced by |makeRequestKey_|. | |
357 * @param {Function} successCallback To be called when request finishes | |
358 * successfully. | |
359 * @param {Function} errorCallback To be called when request fails. | |
360 * @private | |
361 */ | |
362 VolumeManager.prototype.startRequest_ = function(key, | |
363 successCallback, errorCallback) { | |
364 if (key in this.requests_) { | |
365 var request = this.requests_[key]; | |
366 request.successCallbacks.push(successCallback); | |
367 request.errorCallbacks.push(errorCallback); | |
368 } else { | |
369 this.requests_[key] = { | |
370 successCallbacks: [successCallback], | |
371 errorCallbacks: [errorCallback], | |
372 | |
373 timeout: setTimeout(this.onTimeout_.bind(this, key), | |
374 VolumeManager.TIMEOUT) | |
375 }; | |
376 } | |
377 }; | |
378 | |
379 /** | |
380 * Called if no response received in |TIMEOUT|. | |
381 * @param {sting} key Key produced by |makeRequestKey_|. | |
382 * @private | |
383 */ | |
384 VolumeManager.prototype.onTimeout_ = function(key) { | |
385 this.invokeRequestCallbacks_(this.requests_[key], | |
386 VolumeManager.Error.TIMEOUT); | |
387 delete this.requests_[key]; | |
388 }; | |
389 | |
390 /** | |
391 * @param {sting} key Key produced by |makeRequestKey_|. | |
392 * @param {VolumeManager.Error|'success'} status Status received from the API. | |
393 * @param {string} opt_mountPath Mount path. | |
394 * @private | |
395 */ | |
396 VolumeManager.prototype.finishRequest_ = function(key, status, opt_mountPath) { | |
397 var request = this.requests_[key]; | |
398 if (!request) | |
399 return; | |
400 | |
401 clearTimeout(request.timeout); | |
402 this.invokeRequestCallbacks_(request, status, opt_mountPath); | |
403 delete this.requests_[key]; | |
404 }; | |
405 | |
406 /** | |
407 * @param {object} request Structure created in |startRequest_|. | |
408 * @param {VolumeManager.Error|string} status If status == 'success' | |
409 * success callbacks are called. | |
410 * @param {string} opt_mountPath Mount path. Required if success. | |
411 * @private | |
412 */ | |
413 VolumeManager.prototype.invokeRequestCallbacks_ = function(request, status, | |
414 opt_mountPath) { | |
415 function callEach(callbacks, self, args) { | |
416 for (var i = 0; i < callbacks.length; i++) { | |
417 callbacks[i].apply(self, args); | |
418 } | |
419 } | |
420 if (status == 'success') { | |
421 callEach(request.successCallbacks, this, [opt_mountPath]); | |
422 } else { | |
423 this.validateError_(status); | |
424 callEach(request.errorCallbacks, this, [status]); | |
425 } | |
426 }; | |
427 | |
428 /** | |
429 * @param {VolumeManager.Error} error Status string iusually received from API. | |
430 * @private | |
431 */ | |
432 VolumeManager.prototype.validateError_ = function(error) { | |
433 for (var i in VolumeManager.Error) { | |
434 if (error == VolumeManager.Error[i]) | |
435 return; | |
436 } | |
437 throw new Error('Invalid mount error: ', error); | |
438 }; | |
439 | |
440 /** | |
441 * @param {string} mountPath Mount path. | |
442 * @private | |
443 */ | |
444 VolumeManager.prototype.validateMountPath_ = function(mountPath) { | |
445 if (!/^\/(((archive|removable)\/[^\/]+)|drive|Downloads)$/.test(mountPath)) | |
446 throw new Error('Invalid mount path: ', mountPath); | |
447 }; | |
OLD | NEW |