| // Copyright 2014 The Chromium OS Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| 'use strict'; |
| |
| /** |
| * The main namespace for the extension. |
| * @namespace |
| */ |
| unpacker.app = { |
| /** |
| * The key used by chrome.storage.local to save and restore the volumes state. |
| * @const {string} |
| */ |
| STORAGE_KEY: 'state', |
| |
| /** |
| * The default id for the NaCl module. |
| * @const {string} |
| */ |
| DEFAULT_MODULE_ID: 'nacl_module', |
| |
| /** |
| * Time in milliseconds before the notification about mounting is shown. |
| * @const {number} |
| */ |
| MOUNTING_NOTIFICATION_DELAY: 1000, |
| |
| /** |
| * The default filename for .nmf file. |
| * This value must not be const because it is overwritten in tests. |
| * @type {string} |
| */ |
| DEFAULT_MODULE_NMF: 'module.nmf', |
| |
| /** |
| * The default MIME type for .nmf file. |
| * @const {string} |
| */ |
| DEFAULT_MODULE_TYPE: 'application/x-pnacl', |
| |
| /** |
| * Multiple volumes can be opened at the same time. |
| * @type {!Object<!unpacker.types.FileSystemId, !unpacker.Volume>} |
| */ |
| volumes: {}, |
| |
| /** |
| * Each "Pack with" request from Files app creates a new compressor. |
| * Thus, multiple compressors can exist at the same time. |
| * @type {!Object<!unpacker.types.CompressorId, !unpacker.Compressor>} |
| */ |
| compressors: {}, |
| |
| /** |
| * A map with promises of loading a volume's metadata from NaCl. |
| * Any call from fileSystemProvider API should work only on valid metadata. |
| * These promises ensure that the fileSystemProvider API calls wait for the |
| * metatada load. |
| * @type {!Object<!unpacker.types.FileSystemId, !Promise>} |
| */ |
| volumeLoadedPromises: {}, |
| |
| /** |
| * A Promise used to postpone all calls to fileSystemProvider API after |
| * the NaCl module loads. |
| * @type {?Promise} |
| */ |
| moduleLoadedPromise: null, |
| |
| /** |
| * The NaCl module containing the logic for decompressing archives. |
| * @type {?Object} |
| */ |
| naclModule: null, |
| |
| /** |
| * The number of mount process in progress. NaCL module is unloaded if |
| * app.unpacker.volumes is empty and this counter is 0. This is incremented |
| * when a mounting process starts and decremented when it ends. |
| * @type {number} |
| */ |
| mountProcessCounter: 0, |
| |
| /** |
| * Function called on receiving a message from NaCl module. Registered by |
| * common.js. |
| * Process pack message by getting compressor and passing the message to it. |
| * @param {!Object} message The message received from NaCl module. |
| * @param {!unpacker.request.Operation} operation |
| * @private |
| */ |
| handlePackMessage_: function(message, operation) { |
| var compressorId = message.data[unpacker.request.Key.COMPRESSOR_ID]; |
| console.assert(compressorId, 'No NaCl compressor id.'); |
| |
| var compressor = unpacker.app.compressors[compressorId]; |
| if (!compressor) { |
| console.error('No compressor for compressor id: ' + compressorId + '.'); |
| return; |
| } |
| compressor.processMessage(message.data, operation); |
| }, |
| |
| /** |
| * Process unpack message by getting volume and passing the message to it. |
| * @param {!Object} message The message received from NaCl module. |
| * @param {!unpacker.request.Operation} operation |
| * @private |
| */ |
| handleUnpackMessage_: function(message, operation) { |
| var fileSystemId = message.data[unpacker.request.Key.FILE_SYSTEM_ID]; |
| console.assert(fileSystemId, 'No NaCl file system id.'); |
| |
| var requestId = message.data[unpacker.request.Key.REQUEST_ID]; |
| console.assert(!!requestId, 'No NaCl request id.'); |
| |
| var volume = unpacker.app.volumes[fileSystemId]; |
| if (!volume) { |
| // The volume is gone, which can happen. |
| console.info('No volume for: ' + fileSystemId + '.'); |
| return; |
| } |
| |
| volume.decompressor.processMessage(message.data, operation, |
| Number(requestId)); |
| }, |
| |
| /** |
| * Function called on receiving a message from NaCl module. Registered by |
| * common.js. |
| * @param {!Object} message The message received from NaCl module. |
| * @private |
| */ |
| handleMessage_: function(message) { |
| // Get mandatory fields in a message. |
| var operation = message.data[unpacker.request.Key.OPERATION]; |
| console.assert(operation != undefined, // Operation can be 0. |
| 'No NaCl operation: ' + operation + '.'); |
| |
| // Assign the message to either module. |
| if (unpacker.request.isPackRequest(operation)) |
| unpacker.app.handlePackMessage_(message, operation); |
| else |
| unpacker.app.handleUnpackMessage_(message, operation); |
| }, |
| |
| /** |
| * Saves state in case of restarts, event page suspend, crashes, etc. |
| * @param {!Array<!unpacker.types.FileSystemId>} fileSystemIdsArray |
| * @private |
| */ |
| saveState_: function(fileSystemIdsArray) { |
| chrome.storage.local.get([unpacker.app.STORAGE_KEY], function(result) { |
| if (!result[unpacker.app.STORAGE_KEY]) // First save state call. |
| result[unpacker.app.STORAGE_KEY] = {}; |
| |
| // Overwrite state only for the volumes that have their file system id |
| // present in the input array. Leave the rest of the volumes state |
| // untouched. |
| fileSystemIdsArray.forEach(function(fileSystemId) { |
| var entryId = chrome.fileSystem.retainEntry( |
| unpacker.app.volumes[fileSystemId].entry); |
| result[unpacker.app.STORAGE_KEY][fileSystemId] = { |
| entryId: entryId, |
| passphrase: unpacker.app.volumes[fileSystemId] |
| .decompressor.passphraseManager.rememberedPassphrase |
| }; |
| }); |
| |
| chrome.storage.local.set(result); |
| }); |
| }, |
| |
| /** |
| * Removes state from local storage for a single volume. |
| * @param {!unpacker.types.FileSystemId} fileSystemId |
| */ |
| removeState_: function(fileSystemId) { |
| chrome.storage.local.get([unpacker.app.STORAGE_KEY], function(result) { |
| console.assert(result[unpacker.app.STORAGE_KEY] && |
| result[unpacker.app.STORAGE_KEY][fileSystemId], |
| 'Should call removeState_ only for file systems that ', |
| 'have previously called saveState_.'); |
| |
| delete result[unpacker.app.STORAGE_KEY][fileSystemId]; |
| chrome.storage.local.set(result); |
| }); |
| }, |
| |
| /** |
| * Restores archive's entry and opened files for the passed file system id. |
| * @param {!unpacker.types.FileSystemId} fileSystemId |
| * @return {!Promise<!Object>} Promise fulfilled with the entry and list of |
| * opened files. |
| * @private |
| */ |
| restoreVolumeState_: function(fileSystemId) { |
| return new Promise(function(fulfill, reject) { |
| chrome.storage.local.get([unpacker.app.STORAGE_KEY], function(result) { |
| if (!result[unpacker.app.STORAGE_KEY]) { |
| reject('FAILED'); |
| return; |
| } |
| |
| var volumeState = result[unpacker.app.STORAGE_KEY][fileSystemId]; |
| if (!volumeState) { |
| console.error('No state for: ' + fileSystemId + '.'); |
| reject('FAILED'); |
| return; |
| } |
| |
| chrome.fileSystem.restoreEntry(volumeState.entryId, function(entry) { |
| if (chrome.runtime.lastError) { |
| console.error('Restore entry error for <', fileSystemId, '>: ' + |
| chrome.runtime.lastError.message); |
| reject('FAILED'); |
| return; |
| } |
| fulfill({ |
| entry: entry, |
| passphrase: volumeState.passphrase |
| }); |
| }); |
| }); |
| }); |
| }, |
| |
| /** |
| * Creates a volume and loads its metadata from NaCl. |
| * @param {!unpacker.types.FileSystemId} fileSystemId |
| * @param {!Entry} entry The volume's archive entry. |
| * @param {!Object<!unpacker.types.RequestId, |
| * !unpacker.types.OpenFileRequestedOptions>} |
| * openedFiles Previously opened files before a suspend. |
| * @param {string} passphrase Previously used passphrase before a suspend. |
| * @return {!Promise} Promise fulfilled on success and rejected on failure. |
| * @private |
| */ |
| loadVolume_: function(fileSystemId, entry, openedFiles, passphrase) { |
| return new Promise(function(fulfill, reject) { |
| entry.file(function(file) { |
| // File is a Blob object, so it's ok to construct the Decompressor |
| // directly with it. |
| var passphraseManager = new unpacker.PassphraseManager(passphrase); |
| console.assert(unpacker.app.naclModule, |
| 'The NaCL module should have already been defined.'); |
| var decompressor = new unpacker.Decompressor( |
| /** @type {!Object} */ (unpacker.app.naclModule), |
| fileSystemId, file, passphraseManager); |
| var volume = new unpacker.Volume(decompressor, entry); |
| |
| var onLoadVolumeSuccess = function() { |
| if (Object.keys(openedFiles).length == 0) { |
| fulfill(); |
| return; |
| } |
| |
| // Restore opened files on NaCl side. |
| var openFilePromises = []; |
| for (var key in openedFiles) { |
| // 'key' is always a number but JS compiler complains that it is |
| // a string. |
| var openRequestId = Number(key); |
| var options = |
| /** @type {!unpacker.types.OpenFileRequestedOptions} */ |
| (openedFiles[openRequestId]); |
| openFilePromises.push(new Promise(function(resolve, reject) { |
| volume.onOpenFileRequested(options, resolve, reject); |
| })); |
| } |
| |
| Promise.all(openFilePromises).then(fulfill, reject); |
| }; |
| |
| unpacker.app.volumes[fileSystemId] = volume; |
| volume.initialize(onLoadVolumeSuccess, reject); |
| }, function(error) { |
| reject('FAILED'); |
| }); |
| }); |
| }, |
| |
| /** |
| * Restores a volume mounted previously to a suspend / restart. In case of |
| * failure of the load promise for fileSystemId, the corresponding volume is |
| * forcely unmounted. |
| * @param {!unpacker.types.FileSystemId} fileSystemId |
| * @return {!Promise} A promise that restores state and loads volume. |
| * @private |
| */ |
| restoreSingleVolume_: function(fileSystemId) { |
| // Load volume after restart / suspend page event. |
| return unpacker.app.restoreVolumeState_(fileSystemId) |
| .then(function(state) { |
| return new Promise(function(fulfill, reject) { |
| // Check if the file system is compatible with this version of the |
| // ZIP unpacker. |
| // TODO(mtomasz): Implement remounting instead of unmounting. |
| chrome.fileSystemProvider.get(fileSystemId, function(fileSystem) { |
| if (chrome.runtime.lastError) { |
| console.error(chrome.runtime.lastError.name); |
| reject('FAILED'); |
| return; |
| } |
| if (!fileSystem || fileSystem.openedFilesLimit != 1) { |
| console.error('No compatible mounted file system found.'); |
| reject('FAILED'); |
| return; |
| } |
| fulfill({state: state, fileSystem: fileSystem}); |
| }); |
| }); |
| }) |
| .then(function(stateWithFileSystem) { |
| var openedFilesOptions = {}; |
| stateWithFileSystem.fileSystem.openedFiles.forEach(function( |
| openedFile) { |
| openedFilesOptions[openedFile.openRequestId] = { |
| fileSystemId: fileSystemId, |
| requestId: openedFile.openRequestId, |
| mode: openedFile.mode, |
| filePath: openedFile.filePath |
| }; |
| }); |
| return unpacker.app.loadVolume_( |
| fileSystemId, stateWithFileSystem.state.entry, openedFilesOptions, |
| stateWithFileSystem.state.passphrase); |
| }) |
| .catch(function(error) { |
| console.error(error.stack || error); |
| // Force unmount in case restore failed. All resources related to the |
| // volume will be cleanup from both memory and local storage. |
| // TODO(523195): Show a notification that the source file is gone. |
| return unpacker.app.unmountVolume(fileSystemId, true) |
| .then(function() { return Promise.reject('FAILED'); }); |
| }); |
| }, |
| |
| /** |
| * Ensures a volume is loaded by returning its corresponding loaded promise |
| * from unpacker.app.volumeLoadedPromises. In case there is no such promise, |
| * then this is a call after suspend / restart and a new volume loaded promise |
| * that restores state is returned. |
| * @param {!unpacker.types.FileSystemId} fileSystemId |
| * @return {!Promise} The loading volume promise. |
| * @private |
| */ |
| ensureVolumeLoaded_: function(fileSystemId) { |
| // Increment the counter so that the NaCl module won't be unloaded until |
| // the mounting process ends. |
| unpacker.app.mountProcessCounter++; |
| // Create a promise to load the NaCL module. |
| if (!unpacker.app.moduleLoadedPromise) |
| unpacker.app.loadNaclModule(unpacker.app.DEFAULT_MODULE_NMF, |
| unpacker.app.DEFAULT_MODULE_TYPE); |
| |
| return unpacker.app.moduleLoadedPromise.then(function() { |
| // In case there is no volume promise for fileSystemId then we |
| // received a call after restart / suspend as load promises are |
| // created on launched. In this case we will restore volume state |
| // from local storage and create a new load promise. |
| if (!unpacker.app.volumeLoadedPromises[fileSystemId]) { |
| unpacker.app.volumeLoadedPromises[fileSystemId] = |
| unpacker.app.restoreSingleVolume_(fileSystemId); |
| } |
| |
| // Decrement the counter when the mounting process ends. |
| unpacker.app.volumeLoadedPromises[fileSystemId].then(function() { |
| unpacker.app.mountProcessCounter--; |
| }).catch(function(error) { |
| unpacker.app.mountProcessCounter--; |
| }); |
| |
| return unpacker.app.volumeLoadedPromises[fileSystemId]; |
| }); |
| }, |
| |
| /** |
| * @return {boolean} True if NaCl module is loaded. |
| */ |
| naclModuleIsLoaded: function() { return !!unpacker.app.naclModule; }, |
| |
| /** |
| * Loads the NaCl module. |
| * @param {string} pathToConfigureFile Path to the module's configuration |
| * file, which should be a .nmf file. |
| * @param {string} mimeType The mime type for the NaCl executable. |
| * @param {string=} opt_moduleId The NaCl module id. Necessary for testing |
| * purposes. |
| */ |
| loadNaclModule: function(pathToConfigureFile, mimeType, opt_moduleId) { |
| unpacker.app.moduleLoadedPromise = new Promise(function(fulfill) { |
| var moduleId = |
| opt_moduleId ? opt_moduleId : unpacker.app.DEFAULT_MODULE_ID; |
| var elementDiv = document.createElement('div'); |
| |
| // Promise fulfills only after NaCl module has been loaded. |
| elementDiv.addEventListener('load', function() { |
| // Since the first load of the NaCL module is slow, the module is loaded |
| // once in background.js in advance. If there is no mounted volume and |
| // ongoing mounting process, the module is just unloaded. This is the |
| // workaround for crbug.com/699930. |
| if (Object.keys(unpacker.app.volumes).length === 0 && |
| unpacker.app.mountProcessCounter === 0) { |
| elementDiv.parentNode.removeChild(elementDiv); |
| // This is necessary for tests. |
| fulfill(); |
| unpacker.app.moduleLoadedPromise = null; |
| return; |
| } |
| unpacker.app.naclModule = document.getElementById(moduleId); |
| fulfill(); |
| }, true); |
| |
| elementDiv.addEventListener('message', unpacker.app.handleMessage_, true); |
| |
| var elementEmbed = document.createElement('embed'); |
| elementEmbed.id = moduleId; |
| elementEmbed.style.width = 0; |
| elementEmbed.style.height = 0; |
| elementEmbed.src = pathToConfigureFile; |
| elementEmbed.type = mimeType; |
| elementDiv.appendChild(elementEmbed); |
| |
| document.body.appendChild(elementDiv); |
| // Request the offsetTop property to force a relayout. As of Apr 10, 2014 |
| // this is needed if the module is being loaded on a Chrome App's |
| // background page (see crbug.com/350445). |
| /** @suppress {suspiciousCode} */ elementEmbed.offsetTop; |
| }); |
| }, |
| |
| /** |
| * Unloads the NaCl module. |
| */ |
| unloadNaclModule: function() { |
| var naclModuleParentNode = unpacker.app.naclModule.parentNode; |
| naclModuleParentNode.parentNode.removeChild(naclModuleParentNode); |
| unpacker.app.naclModule = null; |
| unpacker.app.moduleLoadedPromise = null; |
| }, |
| |
| /** |
| * Cleans up the resources for a volume, except for the local storage. If |
| * necessary that can be done using unpacker.app.removeState_. |
| * @param {!unpacker.types.FileSystemId} fileSystemId |
| */ |
| cleanupVolume: function(fileSystemId) { |
| delete unpacker.app.volumes[fileSystemId]; |
| // Allow mount after clean. |
| delete unpacker.app.volumeLoadedPromises[fileSystemId]; |
| |
| if (Object.keys(unpacker.app.volumes).length === 0 && |
| unpacker.app.mountProcessCounter === 0) { |
| unpacker.app.unloadNaclModule(); |
| } else { |
| unpacker.app.naclModule.postMessage( |
| unpacker.request.createCloseVolumeRequest(fileSystemId)); |
| } |
| }, |
| |
| /** |
| * Cleans up the resources for a compressor. |
| * @param {!unpacker.types.CompressorId} compressorId |
| * @param {boolean} hasError |
| */ |
| cleanupCompressor: function(compressorId, hasError) { |
| var compressor = unpacker.app.compressors[compressorId]; |
| if (!compressor) { |
| console.error('No compressor for: compressor id' + compressorId + '.'); |
| return; |
| } |
| |
| unpacker.app.mountProcessCounter--; |
| if (Object.keys(unpacker.app.volumes).length === 0 && |
| unpacker.app.mountProcessCounter === 0) { |
| unpacker.app.unloadNaclModule(); |
| } else { |
| // Request libarchive to abort any ongoing process and release resources. |
| // The argument indicates whether an error occurred or not. |
| if (hasError) |
| compressor.sendCloseArchiveRequest(hasError); |
| } |
| |
| // Delete the archive file if it exists. |
| if (compressor.archiveFileEntry) |
| compressor.archiveFileEntry.remove(); |
| |
| delete unpacker.app.compressors[compressorId]; |
| }, |
| |
| /** |
| * Unmounts a volume and removes any resources related to the volume from both |
| * the extension and the local storage state. |
| * @param {!unpacker.types.FileSystemId} fileSystemId |
| * @param {boolean=} opt_forceUnmount True if unmount should be forced even if |
| * volume might be in use, or is not restored yet. |
| * @return {!Promise} A promise that fulfills if volume is unmounted or |
| * rejects with ProviderError in case of any errors. |
| */ |
| unmountVolume: function(fileSystemId, opt_forceUnmount) { |
| return new Promise(function(fulfill, reject) { |
| var volume = unpacker.app.volumes[fileSystemId]; |
| console.assert(volume || opt_forceUnmount, |
| 'Unmount that is not forced must not be called for ', |
| 'volumes that are not restored.'); |
| |
| if (!opt_forceUnmount && volume.inUse()) { |
| reject('IN_USE'); |
| return; |
| } |
| |
| var options = { |
| fileSystemId: fileSystemId |
| }; |
| chrome.fileSystemProvider.unmount(options, function() { |
| if (chrome.runtime.lastError) { |
| console.error('Unmount error: ' + chrome.runtime.lastError.message + |
| '.'); |
| reject('FAILED'); |
| return; |
| } |
| |
| // In case of forced unmount volume can be undefined due to not being |
| // restored. An unmount that is not forced will be called only after |
| // restoring state. In the case of forced unmount when volume is not |
| // restored, we will not do a normal cleanup, but just remove the load |
| // volume promise to allow further mounts. |
| if (opt_forceUnmount) |
| delete unpacker.app.volumeLoadedPromises[fileSystemId]; |
| else |
| unpacker.app.cleanupVolume(fileSystemId); |
| |
| // Remove volume from local storage. |
| unpacker.app.removeState_(fileSystemId); |
| fulfill(); |
| }); |
| }); |
| }, |
| |
| /** |
| * Handles an unmount request received from File System Provider API. |
| * @param {!unpacker.types.UnmountRequestedOptions} options |
| * @param {function()} onSuccess Callback to execute on success. |
| * @param {function(!ProviderError)} onError Callback to execute on error. |
| */ |
| onUnmountRequested: function(options, onSuccess, onError) { |
| unpacker.app.ensureVolumeLoaded_(options.fileSystemId) |
| .then(function() { |
| return unpacker.app.unmountVolume(options.fileSystemId); |
| }) |
| .then(onSuccess) |
| .catch(/** @type {function(*)} */ (onError)); |
| }, |
| |
| /** |
| * Obtains metadata about a file system entry. |
| * @param {!unpacker.types.GetMetadataRequestedOptions} options |
| * @param {function(!EntryMetadata)} onSuccess Callback to execute on success. |
| * The parameter is the EntryMetadata obtained by this function. |
| * @param {function(!ProviderError)} onError Callback to execute on error. |
| */ |
| onGetMetadataRequested: function(options, onSuccess, onError) { |
| unpacker.app.ensureVolumeLoaded_(options.fileSystemId) |
| .then(function() { |
| unpacker.app.volumes[options.fileSystemId].onGetMetadataRequested( |
| options, onSuccess, onError); |
| }) |
| .catch(/** @type {function(*)} */ (onError)); |
| }, |
| |
| /** |
| * Reads a directory entries. |
| * @param {!unpacker.types.ReadDirectoryRequestedOptions} options |
| * @param {function(!Array<!EntryMetadata>, boolean)} onSuccess Callback to |
| * execute on success. The first parameter is an array with directory |
| * entries. The second parameter is 'hasMore', and if it's set to true, |
| * then onSuccess must be called again with the next directory entries. |
| * @param {function(!ProviderError)} onError Callback to execute on error. |
| */ |
| onReadDirectoryRequested: function(options, onSuccess, onError) { |
| unpacker.app.ensureVolumeLoaded_(options.fileSystemId) |
| .then(function() { |
| unpacker.app.volumes[options.fileSystemId].onReadDirectoryRequested( |
| options, onSuccess, onError); |
| }) |
| .catch(/** @type {function(*)} */ (onError)); |
| }, |
| |
| /** |
| * Opens a file for read or write. |
| * @param {!unpacker.types.OpenFileRequestedOptions} options |
| * @param {function()} onSuccess Callback to execute on success. |
| * @param {function(!ProviderError)} onError Callback to execute on error. |
| */ |
| onOpenFileRequested: function(options, onSuccess, onError) { |
| unpacker.app.ensureVolumeLoaded_(options.fileSystemId) |
| .then(function() { |
| unpacker.app.volumes[options.fileSystemId].onOpenFileRequested( |
| options, onSuccess, onError); |
| }) |
| .catch(/** @type {function(*)} */ (onError)); |
| }, |
| |
| /** |
| * Closes a file identified by options.openRequestId. |
| * @param {!unpacker.types.CloseFileRequestedOptions} options |
| * @param {function()} onSuccess Callback to execute on success. |
| * @param {function(!ProviderError)} onError Callback to execute on error. |
| */ |
| onCloseFileRequested: function(options, onSuccess, onError) { |
| unpacker.app.ensureVolumeLoaded_(options.fileSystemId) |
| .then(function() { |
| unpacker.app.volumes[options.fileSystemId].onCloseFileRequested( |
| options, onSuccess, onError); |
| }) |
| .catch(/** @type {function(*)} */ (onError)); |
| }, |
| |
| /** |
| * Reads the contents of a file identified by options.openRequestId. |
| * @param {!unpacker.types.ReadFileRequestedOptions} options |
| * @param {function(!ArrayBuffer, boolean)} onSuccess Callback to execute on |
| * success. The first parameter is the read data and the second parameter |
| * is 'hasMore'. If it's set to true, then onSuccess must be called again |
| * with the next data to read. |
| * @param {function(!ProviderError)} onError Callback to execute on error. |
| */ |
| onReadFileRequested: function(options, onSuccess, onError) { |
| unpacker.app.ensureVolumeLoaded_(options.fileSystemId) |
| .then(function() { |
| unpacker.app.volumes[options.fileSystemId].onReadFileRequested( |
| options, onSuccess, onError); |
| }) |
| .catch(/** @type {function(*)} */ (onError)); |
| }, |
| |
| /** |
| * Creates a new compressor and compresses entries. |
| * @param {!Object} launchData |
| */ |
| onLaunchedWithPack: function(launchData) { |
| unpacker.app.mountProcessCounter++; |
| |
| // Create a promise to load the NaCL module. |
| if (!unpacker.app.moduleLoadedPromise) { |
| unpacker.app.loadNaclModule(unpacker.app.DEFAULT_MODULE_NMF, |
| unpacker.app.DEFAULT_MODULE_TYPE); |
| } |
| |
| unpacker.app.moduleLoadedPromise |
| .then(function() { |
| var compressor = new unpacker.Compressor( |
| /** @type {!Object} */ (unpacker.app.naclModule), |
| launchData.items); |
| |
| var compressorId = compressor.getCompressorId(); |
| |
| unpacker.app.compressors[compressorId] = compressor; |
| |
| // TODO(takise): Error messages have not been prepared yet for timer |
| // and error processing. |
| |
| // If packing takes significant amount of time, then show a |
| // notification about packing in progress. |
| // var deferredNotificationTimer = setTimeout(function() { |
| // chrome.notifications.create(compressorId.toString(), { |
| // type: 'basic', |
| // iconUrl: chrome.runtime.getManifest().icons[128], |
| // title: entry.name, |
| // message: chrome.i18n.getMessage('packingMessage'), |
| // }, function() {}); |
| // }, unpacker.app.PACKING_NOTIFICATION_DELAY); |
| |
| var onError = function(compressorId) { |
| // clearTimeout(deferredNotificationTimer); |
| // console.error('Packing error: ' + error.message + '.'); |
| // chrome.notifications.create(compressorId.toString(), { |
| // type: 'basic', |
| // iconUrl: chrome.runtime.getManifest().icons[128], |
| // title: entry.name, |
| // message: chrome.i18n.getMessage('packingErrorMessage') |
| // }, function() {}); |
| unpacker.app.cleanupCompressor(compressorId, true /* hasError */); |
| }; |
| |
| var onSuccess = function(compressorId) { |
| // clearTimeout(deferredNotificationTimer); |
| // chrome.notifications.clear(compressorId.toString(), |
| // function() {}); |
| unpacker.app.cleanupCompressor(compressorId, false /* hasError */); |
| }; |
| |
| compressor.compress(onSuccess, onError); |
| }); |
| }, |
| |
| /** |
| * Creates a volume for every opened file with the extension or mime type |
| * declared in the manifest file. |
| * @param {!Object} launchData |
| * @param {function(string)=} opt_onSuccess Callback to execute in case a |
| * volume was loaded successfully. Has one parameter, which is the file |
| * system id of the loaded volume. Can be called multiple times, depending |
| * on how many volumes must be loaded. |
| * @param {function(string)=} opt_onError Callback to execute in case of |
| * failure when loading a volume. Has one parameter, which is the file |
| * system id of the volume that failed to load. Can be called multiple |
| * times, depending on how many volumes must be loaded. |
| */ |
| onLaunchedWithUnpack: function(launchData, opt_onSuccess, opt_onError) { |
| // Increment the counter that indicates the number of ongoing mouot process. |
| unpacker.app.mountProcessCounter++; |
| |
| // Create a promise to load the NaCL module. |
| if (!unpacker.app.moduleLoadedPromise) { |
| unpacker.app.loadNaclModule(unpacker.app.DEFAULT_MODULE_NMF, |
| unpacker.app.DEFAULT_MODULE_TYPE); |
| } |
| |
| unpacker.app.moduleLoadedPromise |
| .then(function() { |
| unpacker.app.mountProcessCounter--; |
| launchData.items.forEach(function(item) { |
| unpacker.app.mountProcessCounter++; |
| chrome.fileSystem.getDisplayPath(item.entry, function( |
| entry, |
| fileSystemId) { |
| // If loading takes significant amount of time, then show a |
| // notification about scanning in progress. |
| var deferredNotificationTimer = setTimeout(function() { |
| chrome.notifications.create(fileSystemId, { |
| type: 'basic', |
| iconUrl: chrome.runtime.getManifest().icons[128], |
| title: entry.name, |
| message: chrome.i18n.getMessage('mountingMessage'), |
| }, function() {}); |
| }, unpacker.app.MOUNTING_NOTIFICATION_DELAY); |
| |
| var onError = function(error, fileSystemId) { |
| clearTimeout(deferredNotificationTimer); |
| console.error('Mount error: ' + error.message + '.'); |
| // Decrement the counter that indicates the number of ongoing |
| // mount process. |
| unpacker.app.mountProcessCounter--; |
| if (error.message === 'EXISTS') { |
| if (opt_onError) |
| opt_onError(fileSystemId); |
| return; |
| } |
| chrome.notifications.create(fileSystemId, { |
| type: 'basic', |
| iconUrl: chrome.runtime.getManifest().icons[128], |
| title: entry.name, |
| message: chrome.i18n.getMessage('otherErrorMessage') |
| }, function() {}); |
| if (opt_onError) |
| opt_onError(fileSystemId); |
| // Cleanup volume resources in order to allow future attempts |
| // to mount the volume. The volume can't be cleaned up in |
| // case of 'EXIST' because we should not clean the other |
| // already mounted volume. |
| unpacker.app.cleanupVolume(fileSystemId); |
| }; |
| |
| var onSuccess = function(fileSystemId) { |
| clearTimeout(deferredNotificationTimer); |
| chrome.notifications.clear(fileSystemId, function() {}); |
| // Decrement the counter that indicates the number of ongoing |
| // mount process. |
| unpacker.app.mountProcessCounter--; |
| if (opt_onSuccess) |
| opt_onSuccess(fileSystemId); |
| }; |
| |
| var loadPromise = unpacker.app.loadVolume_( |
| fileSystemId, entry, {}, '' /* passphrase */); |
| loadPromise.then(function() { |
| // Mount the volume and save its information in local storage |
| // in order to be able to recover the metadata in case of |
| // restarts, system crashes, etc. |
| const mountOptions = { |
| fileSystemId: fileSystemId, |
| displayName: entry.name, |
| openedFilesLimit: 1 |
| }; |
| if (unpacker.app.getChromeMajorVersion_() >= 64) |
| mountOptions.persistent = false; |
| chrome.fileSystemProvider.mount(mountOptions, function() { |
| if (chrome.runtime.lastError) { |
| onError(chrome.runtime.lastError, fileSystemId); |
| return; |
| } |
| // Save state so in case of restarts we are able to correctly |
| // get the archive's metadata. |
| unpacker.app.saveState_([fileSystemId]); |
| onSuccess(fileSystemId); |
| }); |
| }).catch(function(error) { |
| onError(error.stack || error, fileSystemId); |
| return Promise.reject(error); |
| }); |
| |
| unpacker.app.volumeLoadedPromises[fileSystemId] = loadPromise; |
| }.bind(null, item.entry)); |
| }); |
| }) |
| .catch(function(error) { console.error(error.stack || error); }); |
| }, |
| |
| /** |
| * Fired when this extension is launched. |
| * Calls a module designated by launchData.id. |
| * Currently, Verbs API does not support "unpack" option. Thus, any launchData |
| * that does not have "pack" as id is regarded as unpack for now. |
| * @param {!Object} launchData |
| * @param {function(string)=} opt_onSuccess |
| * @param {function(string)=} opt_onError |
| */ |
| onLaunched: function(launchData, opt_onSuccess, opt_onError) { |
| if (launchData.items == null) { |
| // The user tried to launch us directly. |
| console.log('Ignoring launch request w/out items field', {launchData}); |
| return; |
| } |
| |
| if (launchData.id === "pack") |
| unpacker.app.onLaunchedWithPack(launchData); |
| else |
| unpacker.app.onLaunchedWithUnpack(launchData, opt_onSuccess, opt_onError); |
| }, |
| |
| /** |
| * Saves the state before suspending the event page, so we can resume it |
| * once new events arrive. |
| */ |
| onSuspend: function() { |
| unpacker.app.saveState_(Object.keys(unpacker.app.volumes)); |
| }, |
| |
| /** |
| * Gets the major version number of Chrome currently running. |
| * @private |
| * @return {number} |
| */ |
| getChromeMajorVersion_: function() { |
| const r = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)/); |
| console.assert(r, 'Failed to identify Chrome version.'); |
| return parseInt(r[2]); |
| } |
| }; |