Zipping and Unzipping Files in a WinJS Application

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.

About Will Miller

Will Miller joined AIS in 2010 as a Software Engineer. During his time at with the company he has worked on several projects, including a dashboard tool for a marketing agency and a cloud-based business-to-business web application for a well-known jewelry chain. His most recent project is a construction site inspection tool for Windows 8 tablets. Will holds a Bachelor’s degree in Computer Science from the University of Maryland, Baltimore County and a Master’s Degree in Computer Science from Hood College.

  • Devendra Kumar

    Hi Will Miller,

    Thanks for this post,
    I tried with your code to extract my zip file to local folder;
    Unfortunately it is not worked.
    Here I have a question, As you said we are using JSZip library. Is it necessary to call any js file before running unzipAsync function().

    Can you help me about this & waiting for your reply;

    It is better if you upload with a sample for this.

    • Roman

      Did you try to debug? Code mostly works but contains couple bugs: .concat(‘\’) for paths and 19th row in zipAsync:

      var collisions = storage.CreationCollisionOptions;

      wrong enum name: ‘s’ at the end.

  • Roman

    Thx, Will! It is awesome!