Peter Keating

Developer from the New Forest in the South of England.

Compressing Files in AIR


I was doing some re-engineering on a client project the other day and thought the implementation was worthy of sharing and hopefully could benefit others. The client project is a desktop application that uses the AIR runtime. Part of the application involves archiving a collection of files into a zip file. When initially developing the project we used a well put together AS3 Zip Library by David Chung (nochump). Unfortunately the AS3 Zip Library caused problems due to its synchronous approach to zipping files. The application would hang when zipping large files, potentially crash due to running out of memory and it was also slow.

Solution

The solution would be to change the synchronous compression process into an asynchronous approach thus preventing the AIR application to not respond. Unfortunately I was unable to find an AS3 library that fits the asynchronous compression requirement so it was time to use the NativeProcess classes that were introduced in the AIR 2.0 SDK. The NativeProcess class allows calls to be made to the command line to execute native apps, in this case an archiving tool.

7zip

The archiving tool I chose to use was 7zip. 7zip fit the bill perfectly, it's free, offers a command line version, available across a magnitude of operating systems and its quick. Although in this example I have chosen to use 7zip, you could easily adapt the code to use a different native executable, it would just be a case of changing the native executable path and the arguments that are passed in. The command line version for Windows was easy to find on the downloads page of the official website. The client application is also available on Mac, so an unix executable was also needed. It took some searching but I was able to find a download for a version for mac that acted exactly the same as the official command line version.

Start with a Test

In order to prove the implementation I have created an AIR project that uses FlexUnit test runner. The test runner runs a test suite that performs an integration test to prove that the zip file is present. Probably could make this test a bit tighter by checking that the zip file created contains the expected archived files, but for this example checking that the zip file exists is sufficient for my liking. The test (code shown below) is an asynchronous test, due to the asynchronous nature of the implementation. I have used Robert Penner's excellent as3-signals library as an alternative to events to notify listeners when the compression has completed. I used eidiot's useful as3-signals-utilities-async library in order to asynchronously test the code that uses signals. The library provides the handleSignal method that waits for a signal dispatch and then passes useful variables to a specified method when the signal is dispatched, otherwise the test will fail when the timeout limit is reached.

/**
 * Tests that when the zip has finished, the completeSignal is
 * dispatched to a method that will assert that the zip file
 * is present as expected.
 */
 [Test(async)]
 public function zip_filesAreArchivedInZipFile(): void  
 {
     var fileCompression: FileCompression = new FileCompression();

     handleSignal(this, fileCompression.completeSignal, assertZipFileExists, TIMEOUT);

     fileCompression.zip(dummyFiles(), zipFile);
  }

  /**
   * Proves that the code works by asserting that
   * the zip file exists.
   */
  private function assertZipFileExists(e: SignalAsyncEvent, data: Object): void
  {
    assertThat(zipFile.exists, isTrue());
  }

Implementation

Now time to make the test above past by implementing the zip method. The NativeProcess class in AIR is simple to use, all it requires is an instance of NativeProcessStartupInfo that contains the path to the native executable and the arguments it requires. 7zip has plenty of command line options, for our requirement the a option, that stands for archive and allows us to add files to an archive. The next option that is added to a vector of arguments is -tzip (others also shown at previous anchor tag) that specifies 7zip to create a .zip file. An obvious argument required is the name of the zip file to create, then any arguments specified after that are files to be included in the archive. In the example code I have only added an event listener to the native process for the process exit, aka when the task has completed. It would be better practice to handle errors and possibly provide feedback that is given by the native process, but for this example it isn't really necessary. Once all the arguments and event listeners have been added it is simply a case of telling the native process to start.

/**
 * Creates the arguments for the native exectuable.
 */
private function createArgs(files: Vector.<File>, zipFile: File): Vector.<String>
{
    var args: Vector.<String> = new Vector.<String>();

    // flag tells 7zip to archive
    args.push("a");

    // defines to create a .zip acrhive
    args.push("-tzip");

    // path to save the zip file to
    args.push(zipFile.nativePath);

    // path of all the files to include in the archive
    for each(var file: File in files)
    {
        args.push(file.nativePath);
    }

    return args;
}

/**
 * Handles the competition of the native process by disposing
 * of the NativeProcess instance created, and then dispatched
 * the complete signal.
 */
private function onExit(e: NativeProcessExitEvent): void
{
    (e.target as NativeProcess).closeInput();
    (e.target as NativeProcess).exit(true);

    complete();
}

/**
 * Creates a zip file.
 * 
 * @filesToArchive     Collection of files to include in the archive.
 * @zipFile            .zip file that will be created.
 */
public function zip(filesToArchive: Vector.<File>, zipFile: File): void
{
    var nativeProcessStartupInfo:NativeProcessStartupInfo = new NativeProcessStartupInfo();
    nativeProcessStartupInfo.executable = File.applicationDirectory.resolvePath(EXE);;

    nativeProcessStartupInfo.arguments = createArgs(filesToArchive, zipFile);

    var nativeProcess: NativeProcess = new NativeProcess();
    nativeProcess.addEventListener(NativeProcessExitEvent.EXIT, onExit);
    nativeProcess.start(nativeProcessStartupInfo);
}

Little Trip Ups

There are always little trip ups that halt your development, even in the smallest examples. Where this was the first AIR project I have started from scratch for a while I got caught by an error thrown when using the NativeProcess class. This error was thrown because the application was not configured to use extended desktop privileges. In order to give the application extended desktop privileges, the application config should have the supportedProfiles as shown below.

<!-- We recommend omitting the supportedProfiles element, -->
<!-- which in turn permits your application to be deployed to all -->
<!-- devices supported by AIR. If you wish to restrict deployment -->
<!-- (i.e., to only mobile devices) then add this element and list -->
<!-- only the profiles which your application does support. -->
<supportedProfiles>extendedDesktop desktop</supportedProfiles>

The other snag that got me with the client application was getting it to function on mac. The problem was being caused due to the unix executable not having execute permissions, which was causing the native process to throw an error to say that the unix executable could not be found. In order to change the permissions of a unix executable simply use the command below in terminal.

  chmod +x 7za

Wrap Up

This was quite a simple re-engineer but the benefits to the client application are clearly noticeable, with a quicker and more reliable compression that doesn't cause the application to not respond. All the code for my this example application is hosted on github and feel free to use it.

Happy Easter Folks!

Back to Posts

-->