I recently encountered a requirement to programmatically zip multiple files into an archive in a Windows 8 JavaScript/HTML app. The intent was to shrink the several large files as much as possible for eventual submission to a central server. This operation needed to occur without the user’s direct involvement as part of a larger data transmission process.
While the Windows.Storage.Compression namespace does provide an interface for compressing individual files, there is no native support for creating a multi-file archive. In order to implement this feature I chose to use the third-party JSZip library, which is a light wrapper around the zLib library.
The following code uses JSZip to asynchronously create a zip archive in local storage from one or more input files.
var storage = Windows.Storage; // Alias for readability function zipAsync(filePaths, zipName) { if (!Array.isArray(filePaths)) { filePaths = [filePaths]; // zipName is optional when a single file is provided. // Default to that single file's name. if (zipName == null) { zipName = filePaths.match(/[-_\w]+[.][\w]+$/i)[0]; } } // Create new zip file in local storage var localFolder = storage.ApplicationData.current.localFolder; var zipFileName = zipName.indexOf('.zip') == -1 ? zipName.concat('.zip') : zipName; var collisions = storage.CreationCollisionOptions; return localFolder .createFileAsync(zipFileName, collisions.replaceExisting) .then(function (file) { // Create the zip data in memory var zip = new JSZip(); // Process each input file, adding it to the zip data var promises = []; _.each(filePaths, function (path) { promises.push(addFileToZipAsync(path, zip)); }); // 'Process' them and then manually apply the template return WinJS.Promise.join(promises).then(function () { //Generate the compressed zip contents var contentBytes = zip.generate( { compression: 'DEFLATE', type: 'uint8array' }); //Write the zip data to the file in local storage return storage.FileIO .writeBytesAsync(file, contentBytes) .then(function () { return file; }); }); }); }
The zipAsync function begins using the Windows.Storage namespace to create the destination .zip file in local storage. I then iterate (using Underscore.js) over the input file paths and add each file to the in-memory JSZip archive, in the addFileToZipAsync method (shown below). The addFileToZipAsync function returns a WinJS Promise, allowing the files to be added asynchronously before JSZip compresses the archive contents into a Uint8Array. This Uint8Array is then written to the destination file via the Windows.Storage writeBytesAsync method. Note that the zipAsync method returns a Promise, allowing client code to handle this asynchronous process as desired.
The addFileToZipAsync method and its supporting getFileAsUint8Array helper are shown below.
function addFileToZipAsync(filePath, zip) { return storage.StorageFile.getFileFromPathAsync(filePath) .then(function (file) { return getFileAsUint8Array(file) .then(function (fileContents) { //Add the file to the zip archive zip.file(file.displayName, fileContents); }); }); }; function getFileAsUint8Array(file) { return storage.FileIO.readBufferAsync(file) .then(function (buffer) { //Read the file into a byte array var fileContents = new Uint8Array(buffer.length); var dataReader = storage.Streams.DataReader.fromBuffer(buffer); dataReader.readBytes(fileContents); dataReader.close(); return fileContents; }); }
The unzipAsync method supports unzipping an existing archive by reversing the process described in zipAsync. It reads the archive file as a Uint8Array using the getFileAsUint8Array method, then constructs a JSZip archive from the compressed data. Using JSZip, we can identify the compressed files and iterate over them, extracting each into local storage.
function unzipAsync(filePath, replaceIfExists) { var fileCollisionOption = replaceIfExists ? storage.CreationCollisionOption.replaceExisting : storage.CreationCollisionOption.failIfExists; return storage.StorageFile .getFileFromPathAsync(filePath) .then(getFileAsUint8Array) .then(function (zipFileContents) { //Create the zip data in memory var zip = new JSZip(zipFileContents); //Extract files var promises = []; var lf = storage.ApplicationData.current.localFolder; _.each(zip.files, function (zippedFile) { //Create new file promises.push(lf .createFileAsync(zippedFile.name, fileCollisionOption) .then(function (localStorageFile) { //Copy the zipped file's contents into the local storage file var fileContents = zip.file(zippedFile.name).asUint8Array(); return storage.FileIO .writeBytesAsync(localStorageFile, fileContents); }) ); }); return WinJS.Promise.join(promises); }); }
Example usage:
function zipTest() { //Zip the DB file, then extract it var filePaths = [ /* File paths here */]; Zip.zipAsync(filePaths, 'testZip.zip') .then(function (file) { //Zip successful! var local = storage.ApplicationData.current.localFolder; var archivePath = local.path.concat('/').concat(file.displayName); return Zip.unzipAsync(archivePath, true) .then(function (file) { //Unzip successful! }); }); }
Using this approach, we can zip and unzip files in local storage both programmatically and asynchronously, hiding the process from the end user and minimizing its impact on the application.
References
JSZip: http://stuk.github.io/jszip/
Windows.Storage namespace: http://msdn.microsoft.com/en-us/library/windows/apps/windows.storage.aspx
Underscore.js: http://underscorejs.org/
Special thanks to Tom McKearney for reviewing this post.