| 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 |