OLD | NEW |
| (Empty) |
1 // Copyright 2014 The Chromium OS 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 * The main namespace for the extension. | |
9 * @namespace | |
10 */ | |
11 unpacker.app = { | |
12 /** | |
13 * The key used by chrome.storage.local to save and restore the volumes state. | |
14 * @const {string} | |
15 */ | |
16 STORAGE_KEY: 'state', | |
17 | |
18 /** | |
19 * The default id for the NaCl module. | |
20 * @const {string} | |
21 */ | |
22 DEFAULT_MODULE_ID: 'nacl_module', | |
23 | |
24 /** | |
25 * Time in milliseconds before the notification about mounting is shown. | |
26 * @const {number} | |
27 */ | |
28 MOUNTING_NOTIFICATION_DELAY: 1000, | |
29 | |
30 /** | |
31 * The default filename for .nmf file. | |
32 * This value must not be const because it is overwritten in tests. | |
33 * @type {string} | |
34 */ | |
35 DEFAULT_MODULE_NMF: 'module.nmf', | |
36 | |
37 /** | |
38 * The default MIME type for .nmf file. | |
39 * @const {string} | |
40 */ | |
41 DEFAULT_MODULE_TYPE: 'application/x-pnacl', | |
42 | |
43 /** | |
44 * Multiple volumes can be opened at the same time. | |
45 * @type {!Object<!unpacker.types.FileSystemId, !unpacker.Volume>} | |
46 */ | |
47 volumes: {}, | |
48 | |
49 /** | |
50 * Each "Pack with" request from Files app creates a new compressor. | |
51 * Thus, multiple compressors can exist at the same time. | |
52 * @type {!Object<!unpacker.types.CompressorId, !unpacker.Compressor>} | |
53 */ | |
54 compressors: {}, | |
55 | |
56 /** | |
57 * A map with promises of loading a volume's metadata from NaCl. | |
58 * Any call from fileSystemProvider API should work only on valid metadata. | |
59 * These promises ensure that the fileSystemProvider API calls wait for the | |
60 * metatada load. | |
61 * @type {!Object<!unpacker.types.FileSystemId, !Promise>} | |
62 */ | |
63 volumeLoadedPromises: {}, | |
64 | |
65 /** | |
66 * A Promise used to postpone all calls to fileSystemProvider API after | |
67 * the NaCl module loads. | |
68 * @type {?Promise} | |
69 */ | |
70 moduleLoadedPromise: null, | |
71 | |
72 /** | |
73 * The NaCl module containing the logic for decompressing archives. | |
74 * @type {?Object} | |
75 */ | |
76 naclModule: null, | |
77 | |
78 /** | |
79 * The number of mount process in progress. NaCL module is unloaded if | |
80 * app.unpacker.volumes is empty and this counter is 0. This is incremented | |
81 * when a mounting process starts and decremented when it ends. | |
82 * @type {number} | |
83 */ | |
84 mountProcessCounter: 0, | |
85 | |
86 /** | |
87 * Function called on receiving a message from NaCl module. Registered by | |
88 * common.js. | |
89 * Process pack message by getting compressor and passing the message to it. | |
90 * @param {!Object} message The message received from NaCl module. | |
91 * @param {!unpacker.request.Operation} operation | |
92 * @private | |
93 */ | |
94 handlePackMessage_: function(message, operation) { | |
95 var compressorId = message.data[unpacker.request.Key.COMPRESSOR_ID]; | |
96 console.assert(compressorId, 'No NaCl compressor id.'); | |
97 | |
98 var compressor = unpacker.app.compressors[compressorId]; | |
99 if (!compressor) { | |
100 console.error('No compressor for compressor id: ' + compressorId + '.'); | |
101 return; | |
102 } | |
103 compressor.processMessage(message.data, operation); | |
104 }, | |
105 | |
106 /** | |
107 * Process unpack message by getting volume and passing the message to it. | |
108 * @param {!Object} message The message received from NaCl module. | |
109 * @param {!unpacker.request.Operation} operation | |
110 * @private | |
111 */ | |
112 handleUnpackMessage_: function(message, operation) { | |
113 var fileSystemId = message.data[unpacker.request.Key.FILE_SYSTEM_ID]; | |
114 console.assert(fileSystemId, 'No NaCl file system id.'); | |
115 | |
116 var requestId = message.data[unpacker.request.Key.REQUEST_ID]; | |
117 console.assert(!!requestId, 'No NaCl request id.'); | |
118 | |
119 var volume = unpacker.app.volumes[fileSystemId]; | |
120 if (!volume) { | |
121 // The volume is gone, which can happen. | |
122 console.info('No volume for: ' + fileSystemId + '.'); | |
123 return; | |
124 } | |
125 | |
126 volume.decompressor.processMessage(message.data, operation, | |
127 Number(requestId)); | |
128 }, | |
129 | |
130 /** | |
131 * Function called on receiving a message from NaCl module. Registered by | |
132 * common.js. | |
133 * @param {!Object} message The message received from NaCl module. | |
134 * @private | |
135 */ | |
136 handleMessage_: function(message) { | |
137 // Get mandatory fields in a message. | |
138 var operation = message.data[unpacker.request.Key.OPERATION]; | |
139 console.assert(operation != undefined, // Operation can be 0. | |
140 'No NaCl operation: ' + operation + '.'); | |
141 | |
142 // Assign the message to either module. | |
143 if (unpacker.request.isPackRequest(operation)) | |
144 unpacker.app.handlePackMessage_(message, operation); | |
145 else | |
146 unpacker.app.handleUnpackMessage_(message, operation); | |
147 }, | |
148 | |
149 /** | |
150 * Saves state in case of restarts, event page suspend, crashes, etc. | |
151 * @param {!Array<!unpacker.types.FileSystemId>} fileSystemIdsArray | |
152 * @private | |
153 */ | |
154 saveState_: function(fileSystemIdsArray) { | |
155 chrome.storage.local.get([unpacker.app.STORAGE_KEY], function(result) { | |
156 if (!result[unpacker.app.STORAGE_KEY]) // First save state call. | |
157 result[unpacker.app.STORAGE_KEY] = {}; | |
158 | |
159 // Overwrite state only for the volumes that have their file system id | |
160 // present in the input array. Leave the rest of the volumes state | |
161 // untouched. | |
162 fileSystemIdsArray.forEach(function(fileSystemId) { | |
163 var entryId = chrome.fileSystem.retainEntry( | |
164 unpacker.app.volumes[fileSystemId].entry); | |
165 result[unpacker.app.STORAGE_KEY][fileSystemId] = { | |
166 entryId: entryId, | |
167 passphrase: unpacker.app.volumes[fileSystemId] | |
168 .decompressor.passphraseManager.rememberedPassphrase | |
169 }; | |
170 }); | |
171 | |
172 chrome.storage.local.set(result); | |
173 }); | |
174 }, | |
175 | |
176 /** | |
177 * Removes state from local storage for a single volume. | |
178 * @param {!unpacker.types.FileSystemId} fileSystemId | |
179 */ | |
180 removeState_: function(fileSystemId) { | |
181 chrome.storage.local.get([unpacker.app.STORAGE_KEY], function(result) { | |
182 console.assert(result[unpacker.app.STORAGE_KEY] && | |
183 result[unpacker.app.STORAGE_KEY][fileSystemId], | |
184 'Should call removeState_ only for file systems that ', | |
185 'have previously called saveState_.'); | |
186 | |
187 delete result[unpacker.app.STORAGE_KEY][fileSystemId]; | |
188 chrome.storage.local.set(result); | |
189 }); | |
190 }, | |
191 | |
192 /** | |
193 * Restores archive's entry and opened files for the passed file system id. | |
194 * @param {!unpacker.types.FileSystemId} fileSystemId | |
195 * @return {!Promise<!Object>} Promise fulfilled with the entry and list of | |
196 * opened files. | |
197 * @private | |
198 */ | |
199 restoreVolumeState_: function(fileSystemId) { | |
200 return new Promise(function(fulfill, reject) { | |
201 chrome.storage.local.get([unpacker.app.STORAGE_KEY], function(result) { | |
202 if (!result[unpacker.app.STORAGE_KEY]) { | |
203 reject('FAILED'); | |
204 return; | |
205 } | |
206 | |
207 var volumeState = result[unpacker.app.STORAGE_KEY][fileSystemId]; | |
208 if (!volumeState) { | |
209 console.error('No state for: ' + fileSystemId + '.'); | |
210 reject('FAILED'); | |
211 return; | |
212 } | |
213 | |
214 chrome.fileSystem.restoreEntry(volumeState.entryId, function(entry) { | |
215 if (chrome.runtime.lastError) { | |
216 console.error('Restore entry error for <', fileSystemId, '>: ' + | |
217 chrome.runtime.lastError.message); | |
218 reject('FAILED'); | |
219 return; | |
220 } | |
221 fulfill({ | |
222 entry: entry, | |
223 passphrase: volumeState.passphrase | |
224 }); | |
225 }); | |
226 }); | |
227 }); | |
228 }, | |
229 | |
230 /** | |
231 * Creates a volume and loads its metadata from NaCl. | |
232 * @param {!unpacker.types.FileSystemId} fileSystemId | |
233 * @param {!Entry} entry The volume's archive entry. | |
234 * @param {!Object<!unpacker.types.RequestId, | |
235 * !unpacker.types.OpenFileRequestedOptions>} | |
236 * openedFiles Previously opened files before a suspend. | |
237 * @param {string} passphrase Previously used passphrase before a suspend. | |
238 * @return {!Promise} Promise fulfilled on success and rejected on failure. | |
239 * @private | |
240 */ | |
241 loadVolume_: function(fileSystemId, entry, openedFiles, passphrase) { | |
242 return new Promise(function(fulfill, reject) { | |
243 entry.file(function(file) { | |
244 // File is a Blob object, so it's ok to construct the Decompressor | |
245 // directly with it. | |
246 var passphraseManager = new unpacker.PassphraseManager(passphrase); | |
247 console.assert(unpacker.app.naclModule, | |
248 'The NaCL module should have already been defined.'); | |
249 var decompressor = new unpacker.Decompressor( | |
250 /** @type {!Object} */ (unpacker.app.naclModule), | |
251 fileSystemId, file, passphraseManager); | |
252 var volume = new unpacker.Volume(decompressor, entry); | |
253 | |
254 var onLoadVolumeSuccess = function() { | |
255 if (Object.keys(openedFiles).length == 0) { | |
256 fulfill(); | |
257 return; | |
258 } | |
259 | |
260 // Restore opened files on NaCl side. | |
261 var openFilePromises = []; | |
262 for (var key in openedFiles) { | |
263 // 'key' is always a number but JS compiler complains that it is | |
264 // a string. | |
265 var openRequestId = Number(key); | |
266 var options = | |
267 /** @type {!unpacker.types.OpenFileRequestedOptions} */ | |
268 (openedFiles[openRequestId]); | |
269 openFilePromises.push(new Promise(function(resolve, reject) { | |
270 volume.onOpenFileRequested(options, resolve, reject); | |
271 })); | |
272 } | |
273 | |
274 Promise.all(openFilePromises).then(fulfill, reject); | |
275 }; | |
276 | |
277 unpacker.app.volumes[fileSystemId] = volume; | |
278 volume.initialize(onLoadVolumeSuccess, reject); | |
279 }, function(error) { | |
280 reject('FAILED'); | |
281 }); | |
282 }); | |
283 }, | |
284 | |
285 /** | |
286 * Restores a volume mounted previously to a suspend / restart. In case of | |
287 * failure of the load promise for fileSystemId, the corresponding volume is | |
288 * forcely unmounted. | |
289 * @param {!unpacker.types.FileSystemId} fileSystemId | |
290 * @return {!Promise} A promise that restores state and loads volume. | |
291 * @private | |
292 */ | |
293 restoreSingleVolume_: function(fileSystemId) { | |
294 // Load volume after restart / suspend page event. | |
295 return unpacker.app.restoreVolumeState_(fileSystemId) | |
296 .then(function(state) { | |
297 return new Promise(function(fulfill, reject) { | |
298 // Check if the file system is compatible with this version of the | |
299 // ZIP unpacker. | |
300 // TODO(mtomasz): Implement remounting instead of unmounting. | |
301 chrome.fileSystemProvider.get(fileSystemId, function(fileSystem) { | |
302 if (chrome.runtime.lastError) { | |
303 console.error(chrome.runtime.lastError.name); | |
304 reject('FAILED'); | |
305 return; | |
306 } | |
307 if (!fileSystem || fileSystem.openedFilesLimit != 1) { | |
308 console.error('No compatible mounted file system found.'); | |
309 reject('FAILED'); | |
310 return; | |
311 } | |
312 fulfill({state: state, fileSystem: fileSystem}); | |
313 }); | |
314 }); | |
315 }) | |
316 .then(function(stateWithFileSystem) { | |
317 var openedFilesOptions = {}; | |
318 stateWithFileSystem.fileSystem.openedFiles.forEach(function( | |
319 openedFile) { | |
320 openedFilesOptions[openedFile.openRequestId] = { | |
321 fileSystemId: fileSystemId, | |
322 requestId: openedFile.openRequestId, | |
323 mode: openedFile.mode, | |
324 filePath: openedFile.filePath | |
325 }; | |
326 }); | |
327 return unpacker.app.loadVolume_( | |
328 fileSystemId, stateWithFileSystem.state.entry, openedFilesOptions, | |
329 stateWithFileSystem.state.passphrase); | |
330 }) | |
331 .catch(function(error) { | |
332 console.error(error.stack || error); | |
333 // Force unmount in case restore failed. All resources related to the | |
334 // volume will be cleanup from both memory and local storage. | |
335 // TODO(523195): Show a notification that the source file is gone. | |
336 return unpacker.app.unmountVolume(fileSystemId, true) | |
337 .then(function() { return Promise.reject('FAILED'); }); | |
338 }); | |
339 }, | |
340 | |
341 /** | |
342 * Ensures a volume is loaded by returning its corresponding loaded promise | |
343 * from unpacker.app.volumeLoadedPromises. In case there is no such promise, | |
344 * then this is a call after suspend / restart and a new volume loaded promise | |
345 * that restores state is returned. | |
346 * @param {!unpacker.types.FileSystemId} fileSystemId | |
347 * @return {!Promise} The loading volume promise. | |
348 * @private | |
349 */ | |
350 ensureVolumeLoaded_: function(fileSystemId) { | |
351 // Increment the counter so that the NaCl module won't be unloaded until | |
352 // the mounting process ends. | |
353 unpacker.app.mountProcessCounter++; | |
354 // Create a promise to load the NaCL module. | |
355 if (!unpacker.app.moduleLoadedPromise) | |
356 unpacker.app.loadNaclModule(unpacker.app.DEFAULT_MODULE_NMF, | |
357 unpacker.app.DEFAULT_MODULE_TYPE); | |
358 | |
359 return unpacker.app.moduleLoadedPromise.then(function() { | |
360 // In case there is no volume promise for fileSystemId then we | |
361 // received a call after restart / suspend as load promises are | |
362 // created on launched. In this case we will restore volume state | |
363 // from local storage and create a new load promise. | |
364 if (!unpacker.app.volumeLoadedPromises[fileSystemId]) { | |
365 unpacker.app.volumeLoadedPromises[fileSystemId] = | |
366 unpacker.app.restoreSingleVolume_(fileSystemId); | |
367 } | |
368 | |
369 // Decrement the counter when the mounting process ends. | |
370 unpacker.app.volumeLoadedPromises[fileSystemId].then(function() { | |
371 unpacker.app.mountProcessCounter--; | |
372 }).catch(function(error) { | |
373 unpacker.app.mountProcessCounter--; | |
374 }); | |
375 | |
376 return unpacker.app.volumeLoadedPromises[fileSystemId]; | |
377 }); | |
378 }, | |
379 | |
380 /** | |
381 * @return {boolean} True if NaCl module is loaded. | |
382 */ | |
383 naclModuleIsLoaded: function() { return !!unpacker.app.naclModule; }, | |
384 | |
385 /** | |
386 * Loads the NaCl module. | |
387 * @param {string} pathToConfigureFile Path to the module's configuration | |
388 * file, which should be a .nmf file. | |
389 * @param {string} mimeType The mime type for the NaCl executable. | |
390 * @param {string=} opt_moduleId The NaCl module id. Necessary for testing | |
391 * purposes. | |
392 */ | |
393 loadNaclModule: function(pathToConfigureFile, mimeType, opt_moduleId) { | |
394 unpacker.app.moduleLoadedPromise = new Promise(function(fulfill) { | |
395 var moduleId = | |
396 opt_moduleId ? opt_moduleId : unpacker.app.DEFAULT_MODULE_ID; | |
397 var elementDiv = document.createElement('div'); | |
398 | |
399 // Promise fulfills only after NaCl module has been loaded. | |
400 elementDiv.addEventListener('load', function() { | |
401 // Since the first load of the NaCL module is slow, the module is loaded | |
402 // once in background.js in advance. If there is no mounted volume and | |
403 // ongoing mounting process, the module is just unloaded. This is the | |
404 // workaround for crbug.com/699930. | |
405 if (Object.keys(unpacker.app.volumes).length === 0 && | |
406 unpacker.app.mountProcessCounter === 0) { | |
407 elementDiv.parentNode.removeChild(elementDiv); | |
408 // This is necessary for tests. | |
409 fulfill(); | |
410 unpacker.app.moduleLoadedPromise = null; | |
411 return; | |
412 } | |
413 unpacker.app.naclModule = document.getElementById(moduleId); | |
414 fulfill(); | |
415 }, true); | |
416 | |
417 elementDiv.addEventListener('message', unpacker.app.handleMessage_, true); | |
418 | |
419 var elementEmbed = document.createElement('embed'); | |
420 elementEmbed.id = moduleId; | |
421 elementEmbed.style.width = 0; | |
422 elementEmbed.style.height = 0; | |
423 elementEmbed.src = pathToConfigureFile; | |
424 elementEmbed.type = mimeType; | |
425 elementDiv.appendChild(elementEmbed); | |
426 | |
427 document.body.appendChild(elementDiv); | |
428 // Request the offsetTop property to force a relayout. As of Apr 10, 2014 | |
429 // this is needed if the module is being loaded on a Chrome App's | |
430 // background page (see crbug.com/350445). | |
431 /** @suppress {suspiciousCode} */ elementEmbed.offsetTop; | |
432 }); | |
433 }, | |
434 | |
435 /** | |
436 * Unloads the NaCl module. | |
437 */ | |
438 unloadNaclModule: function() { | |
439 var naclModuleParentNode = unpacker.app.naclModule.parentNode; | |
440 naclModuleParentNode.parentNode.removeChild(naclModuleParentNode); | |
441 unpacker.app.naclModule = null; | |
442 unpacker.app.moduleLoadedPromise = null; | |
443 }, | |
444 | |
445 /** | |
446 * Cleans up the resources for a volume, except for the local storage. If | |
447 * necessary that can be done using unpacker.app.removeState_. | |
448 * @param {!unpacker.types.FileSystemId} fileSystemId | |
449 */ | |
450 cleanupVolume: function(fileSystemId) { | |
451 delete unpacker.app.volumes[fileSystemId]; | |
452 // Allow mount after clean. | |
453 delete unpacker.app.volumeLoadedPromises[fileSystemId]; | |
454 | |
455 if (Object.keys(unpacker.app.volumes).length === 0 && | |
456 unpacker.app.mountProcessCounter === 0) { | |
457 unpacker.app.unloadNaclModule(); | |
458 } else { | |
459 unpacker.app.naclModule.postMessage( | |
460 unpacker.request.createCloseVolumeRequest(fileSystemId)); | |
461 } | |
462 }, | |
463 | |
464 /** | |
465 * Cleans up the resources for a compressor. | |
466 * @param {!unpacker.types.CompressorId} compressorId | |
467 * @param {boolean} hasError | |
468 */ | |
469 cleanupCompressor: function(compressorId, hasError) { | |
470 var compressor = unpacker.app.compressors[compressorId]; | |
471 if (!compressor) { | |
472 console.error('No compressor for: compressor id' + compressorId + '.'); | |
473 return; | |
474 } | |
475 | |
476 unpacker.app.mountProcessCounter--; | |
477 if (Object.keys(unpacker.app.volumes).length === 0 && | |
478 unpacker.app.mountProcessCounter === 0) { | |
479 unpacker.app.unloadNaclModule(); | |
480 } else { | |
481 // Request libarchive to abort any ongoing process and release resources. | |
482 // The argument indicates whether an error occurred or not. | |
483 if (hasError) | |
484 compressor.sendCloseArchiveRequest(hasError); | |
485 } | |
486 | |
487 // Delete the archive file if it exists. | |
488 if (compressor.archiveFileEntry) | |
489 compressor.archiveFileEntry.remove(); | |
490 | |
491 delete unpacker.app.compressors[compressorId]; | |
492 }, | |
493 | |
494 /** | |
495 * Unmounts a volume and removes any resources related to the volume from both | |
496 * the extension and the local storage state. | |
497 * @param {!unpacker.types.FileSystemId} fileSystemId | |
498 * @param {boolean=} opt_forceUnmount True if unmount should be forced even if | |
499 * volume might be in use, or is not restored yet. | |
500 * @return {!Promise} A promise that fulfills if volume is unmounted or | |
501 * rejects with ProviderError in case of any errors. | |
502 */ | |
503 unmountVolume: function(fileSystemId, opt_forceUnmount) { | |
504 return new Promise(function(fulfill, reject) { | |
505 var volume = unpacker.app.volumes[fileSystemId]; | |
506 console.assert(volume || opt_forceUnmount, | |
507 'Unmount that is not forced must not be called for ', | |
508 'volumes that are not restored.'); | |
509 | |
510 if (!opt_forceUnmount && volume.inUse()) { | |
511 reject('IN_USE'); | |
512 return; | |
513 } | |
514 | |
515 var options = { | |
516 fileSystemId: fileSystemId | |
517 }; | |
518 chrome.fileSystemProvider.unmount(options, function() { | |
519 if (chrome.runtime.lastError) { | |
520 console.error('Unmount error: ' + chrome.runtime.lastError.message + | |
521 '.'); | |
522 reject('FAILED'); | |
523 return; | |
524 } | |
525 | |
526 // In case of forced unmount volume can be undefined due to not being | |
527 // restored. An unmount that is not forced will be called only after | |
528 // restoring state. In the case of forced unmount when volume is not | |
529 // restored, we will not do a normal cleanup, but just remove the load | |
530 // volume promise to allow further mounts. | |
531 if (opt_forceUnmount) | |
532 delete unpacker.app.volumeLoadedPromises[fileSystemId]; | |
533 else | |
534 unpacker.app.cleanupVolume(fileSystemId); | |
535 | |
536 // Remove volume from local storage. | |
537 unpacker.app.removeState_(fileSystemId); | |
538 fulfill(); | |
539 }); | |
540 }); | |
541 }, | |
542 | |
543 /** | |
544 * Handles an unmount request received from File System Provider API. | |
545 * @param {!unpacker.types.UnmountRequestedOptions} options | |
546 * @param {function()} onSuccess Callback to execute on success. | |
547 * @param {function(!ProviderError)} onError Callback to execute on error. | |
548 */ | |
549 onUnmountRequested: function(options, onSuccess, onError) { | |
550 unpacker.app.ensureVolumeLoaded_(options.fileSystemId) | |
551 .then(function() { | |
552 return unpacker.app.unmountVolume(options.fileSystemId); | |
553 }) | |
554 .then(onSuccess) | |
555 .catch(/** @type {function(*)} */ (onError)); | |
556 }, | |
557 | |
558 /** | |
559 * Obtains metadata about a file system entry. | |
560 * @param {!unpacker.types.GetMetadataRequestedOptions} options | |
561 * @param {function(!EntryMetadata)} onSuccess Callback to execute on success. | |
562 * The parameter is the EntryMetadata obtained by this function. | |
563 * @param {function(!ProviderError)} onError Callback to execute on error. | |
564 */ | |
565 onGetMetadataRequested: function(options, onSuccess, onError) { | |
566 unpacker.app.ensureVolumeLoaded_(options.fileSystemId) | |
567 .then(function() { | |
568 unpacker.app.volumes[options.fileSystemId].onGetMetadataRequested( | |
569 options, onSuccess, onError); | |
570 }) | |
571 .catch(/** @type {function(*)} */ (onError)); | |
572 }, | |
573 | |
574 /** | |
575 * Reads a directory entries. | |
576 * @param {!unpacker.types.ReadDirectoryRequestedOptions} options | |
577 * @param {function(!Array<!EntryMetadata>, boolean)} onSuccess Callback to | |
578 * execute on success. The first parameter is an array with directory | |
579 * entries. The second parameter is 'hasMore', and if it's set to true, | |
580 * then onSuccess must be called again with the next directory entries. | |
581 * @param {function(!ProviderError)} onError Callback to execute on error. | |
582 */ | |
583 onReadDirectoryRequested: function(options, onSuccess, onError) { | |
584 unpacker.app.ensureVolumeLoaded_(options.fileSystemId) | |
585 .then(function() { | |
586 unpacker.app.volumes[options.fileSystemId].onReadDirectoryRequested( | |
587 options, onSuccess, onError); | |
588 }) | |
589 .catch(/** @type {function(*)} */ (onError)); | |
590 }, | |
591 | |
592 /** | |
593 * Opens a file for read or write. | |
594 * @param {!unpacker.types.OpenFileRequestedOptions} options | |
595 * @param {function()} onSuccess Callback to execute on success. | |
596 * @param {function(!ProviderError)} onError Callback to execute on error. | |
597 */ | |
598 onOpenFileRequested: function(options, onSuccess, onError) { | |
599 unpacker.app.ensureVolumeLoaded_(options.fileSystemId) | |
600 .then(function() { | |
601 unpacker.app.volumes[options.fileSystemId].onOpenFileRequested( | |
602 options, onSuccess, onError); | |
603 }) | |
604 .catch(/** @type {function(*)} */ (onError)); | |
605 }, | |
606 | |
607 /** | |
608 * Closes a file identified by options.openRequestId. | |
609 * @param {!unpacker.types.CloseFileRequestedOptions} options | |
610 * @param {function()} onSuccess Callback to execute on success. | |
611 * @param {function(!ProviderError)} onError Callback to execute on error. | |
612 */ | |
613 onCloseFileRequested: function(options, onSuccess, onError) { | |
614 unpacker.app.ensureVolumeLoaded_(options.fileSystemId) | |
615 .then(function() { | |
616 unpacker.app.volumes[options.fileSystemId].onCloseFileRequested( | |
617 options, onSuccess, onError); | |
618 }) | |
619 .catch(/** @type {function(*)} */ (onError)); | |
620 }, | |
621 | |
622 /** | |
623 * Reads the contents of a file identified by options.openRequestId. | |
624 * @param {!unpacker.types.ReadFileRequestedOptions} options | |
625 * @param {function(!ArrayBuffer, boolean)} onSuccess Callback to execute on | |
626 * success. The first parameter is the read data and the second parameter | |
627 * is 'hasMore'. If it's set to true, then onSuccess must be called again | |
628 * with the next data to read. | |
629 * @param {function(!ProviderError)} onError Callback to execute on error. | |
630 */ | |
631 onReadFileRequested: function(options, onSuccess, onError) { | |
632 unpacker.app.ensureVolumeLoaded_(options.fileSystemId) | |
633 .then(function() { | |
634 unpacker.app.volumes[options.fileSystemId].onReadFileRequested( | |
635 options, onSuccess, onError); | |
636 }) | |
637 .catch(/** @type {function(*)} */ (onError)); | |
638 }, | |
639 | |
640 /** | |
641 * Creates a new compressor and compresses entries. | |
642 * @param {!Object} launchData | |
643 */ | |
644 onLaunchedWithPack: function(launchData) { | |
645 unpacker.app.mountProcessCounter++; | |
646 | |
647 // Create a promise to load the NaCL module. | |
648 if (!unpacker.app.moduleLoadedPromise) { | |
649 unpacker.app.loadNaclModule(unpacker.app.DEFAULT_MODULE_NMF, | |
650 unpacker.app.DEFAULT_MODULE_TYPE); | |
651 } | |
652 | |
653 unpacker.app.moduleLoadedPromise | |
654 .then(function() { | |
655 var compressor = new unpacker.Compressor( | |
656 /** @type {!Object} */ (unpacker.app.naclModule), | |
657 launchData.items); | |
658 | |
659 var compressorId = compressor.getCompressorId(); | |
660 | |
661 unpacker.app.compressors[compressorId] = compressor; | |
662 | |
663 // TODO(takise): Error messages have not been prepared yet for timer | |
664 // and error processing. | |
665 | |
666 // If packing takes significant amount of time, then show a | |
667 // notification about packing in progress. | |
668 // var deferredNotificationTimer = setTimeout(function() { | |
669 // chrome.notifications.create(compressorId.toString(), { | |
670 // type: 'basic', | |
671 // iconUrl: chrome.runtime.getManifest().icons[128], | |
672 // title: entry.name, | |
673 // message: chrome.i18n.getMessage('packingMessage'), | |
674 // }, function() {}); | |
675 // }, unpacker.app.PACKING_NOTIFICATION_DELAY); | |
676 | |
677 var onError = function(compressorId) { | |
678 // clearTimeout(deferredNotificationTimer); | |
679 // console.error('Packing error: ' + error.message + '.'); | |
680 // chrome.notifications.create(compressorId.toString(), { | |
681 // type: 'basic', | |
682 // iconUrl: chrome.runtime.getManifest().icons[128], | |
683 // title: entry.name, | |
684 // message: chrome.i18n.getMessage('packingErrorMessage') | |
685 // }, function() {}); | |
686 unpacker.app.cleanupCompressor(compressorId, true /* hasError */); | |
687 }; | |
688 | |
689 var onSuccess = function(compressorId) { | |
690 // clearTimeout(deferredNotificationTimer); | |
691 // chrome.notifications.clear(compressorId.toString(), | |
692 // function() {}); | |
693 unpacker.app.cleanupCompressor(compressorId, false /* hasError */); | |
694 }; | |
695 | |
696 compressor.compress(onSuccess, onError); | |
697 }); | |
698 }, | |
699 | |
700 /** | |
701 * Creates a volume for every opened file with the extension or mime type | |
702 * declared in the manifest file. | |
703 * @param {!Object} launchData | |
704 * @param {function(string)=} opt_onSuccess Callback to execute in case a | |
705 * volume was loaded successfully. Has one parameter, which is the file | |
706 * system id of the loaded volume. Can be called multiple times, depending | |
707 * on how many volumes must be loaded. | |
708 * @param {function(string)=} opt_onError Callback to execute in case of | |
709 * failure when loading a volume. Has one parameter, which is the file | |
710 * system id of the volume that failed to load. Can be called multiple | |
711 * times, depending on how many volumes must be loaded. | |
712 */ | |
713 onLaunchedWithUnpack: function(launchData, opt_onSuccess, opt_onError) { | |
714 // Increment the counter that indicates the number of ongoing mouot process. | |
715 unpacker.app.mountProcessCounter++; | |
716 | |
717 // Create a promise to load the NaCL module. | |
718 if (!unpacker.app.moduleLoadedPromise) { | |
719 unpacker.app.loadNaclModule(unpacker.app.DEFAULT_MODULE_NMF, | |
720 unpacker.app.DEFAULT_MODULE_TYPE); | |
721 } | |
722 | |
723 unpacker.app.moduleLoadedPromise | |
724 .then(function() { | |
725 unpacker.app.mountProcessCounter--; | |
726 launchData.items.forEach(function(item) { | |
727 unpacker.app.mountProcessCounter++; | |
728 chrome.fileSystem.getDisplayPath(item.entry, function( | |
729 entry, | |
730 fileSystemId) { | |
731 // If loading takes significant amount of time, then show a | |
732 // notification about scanning in progress. | |
733 var deferredNotificationTimer = setTimeout(function() { | |
734 chrome.notifications.create(fileSystemId, { | |
735 type: 'basic', | |
736 iconUrl: chrome.runtime.getManifest().icons[128], | |
737 title: entry.name, | |
738 message: chrome.i18n.getMessage('mountingMessage'), | |
739 }, function() {}); | |
740 }, unpacker.app.MOUNTING_NOTIFICATION_DELAY); | |
741 | |
742 var onError = function(error, fileSystemId) { | |
743 clearTimeout(deferredNotificationTimer); | |
744 console.error('Mount error: ' + error.message + '.'); | |
745 // Decrement the counter that indicates the number of ongoing | |
746 // mount process. | |
747 unpacker.app.mountProcessCounter--; | |
748 if (error.message === 'EXISTS') { | |
749 if (opt_onError) | |
750 opt_onError(fileSystemId); | |
751 return; | |
752 } | |
753 chrome.notifications.create(fileSystemId, { | |
754 type: 'basic', | |
755 iconUrl: chrome.runtime.getManifest().icons[128], | |
756 title: entry.name, | |
757 message: chrome.i18n.getMessage('otherErrorMessage') | |
758 }, function() {}); | |
759 if (opt_onError) | |
760 opt_onError(fileSystemId); | |
761 // Cleanup volume resources in order to allow future attempts | |
762 // to mount the volume. The volume can't be cleaned up in | |
763 // case of 'EXIST' because we should not clean the other | |
764 // already mounted volume. | |
765 unpacker.app.cleanupVolume(fileSystemId); | |
766 }; | |
767 | |
768 var onSuccess = function(fileSystemId) { | |
769 clearTimeout(deferredNotificationTimer); | |
770 chrome.notifications.clear(fileSystemId, function() {}); | |
771 // Decrement the counter that indicates the number of ongoing | |
772 // mount process. | |
773 unpacker.app.mountProcessCounter--; | |
774 if (opt_onSuccess) | |
775 opt_onSuccess(fileSystemId); | |
776 }; | |
777 | |
778 var loadPromise = unpacker.app.loadVolume_( | |
779 fileSystemId, entry, {}, '' /* passphrase */); | |
780 loadPromise.then(function() { | |
781 // Mount the volume and save its information in local storage | |
782 // in order to be able to recover the metadata in case of | |
783 // restarts, system crashes, etc. | |
784 chrome.fileSystemProvider.mount({ | |
785 fileSystemId: fileSystemId, | |
786 displayName: entry.name, | |
787 openedFilesLimit: 1 | |
788 }, | |
789 function() { | |
790 if (chrome.runtime.lastError) { | |
791 onError(chrome.runtime.lastError, fileSystemId); | |
792 return; | |
793 } | |
794 // Save state so in case of restarts we are able to correctly | |
795 // get the archive's metadata. | |
796 unpacker.app.saveState_([fileSystemId]); | |
797 onSuccess(fileSystemId); | |
798 }); | |
799 }).catch(function(error) { | |
800 onError(error.stack || error, fileSystemId); | |
801 return Promise.reject(error); | |
802 }); | |
803 | |
804 unpacker.app.volumeLoadedPromises[fileSystemId] = loadPromise; | |
805 }.bind(null, item.entry)); | |
806 }); | |
807 }) | |
808 .catch(function(error) { console.error(error.stack || error); }); | |
809 }, | |
810 | |
811 /** | |
812 * Fired when this extension is launched. | |
813 * Calls a module designated by launchData.id. | |
814 * Currently, Verbs API does not support "unpack" option. Thus, any launchData | |
815 * that does not have "pack" as id is regarded as unpack for now. | |
816 * @param {!Object} launchData | |
817 * @param {function(string)=} opt_onSuccess | |
818 * @param {function(string)=} opt_onError | |
819 */ | |
820 onLaunched: function(launchData, opt_onSuccess, opt_onError) { | |
821 if (launchData.items == null) { | |
822 // The user tried to launch us directly. | |
823 console.log('Ignoring launch request w/out items field', {launchData}); | |
824 return; | |
825 } | |
826 | |
827 if (launchData.id === "pack") | |
828 unpacker.app.onLaunchedWithPack(launchData); | |
829 else | |
830 unpacker.app.onLaunchedWithUnpack(launchData, opt_onSuccess, opt_onError); | |
831 }, | |
832 | |
833 /** | |
834 * Saves the state before suspending the event page, so we can resume it | |
835 * once new events arrive. | |
836 */ | |
837 onSuspend: function() { | |
838 unpacker.app.saveState_(Object.keys(unpacker.app.volumes)); | |
839 } | |
840 }; | |
OLD | NEW |