Wednesday, February 23, 2005

I had to take a look at an issue with a spanned zip file. The client graciously sent us the set of zip files he had on floppies. He copied each floppy in subfolders named "Disk1", "Disk2", "Disk3" and "Disk4", zipped those folders and sent us the resulting single huge zip file. Pretty standard.

I was about to copy each part back on floppies when I realized I was passing by a huge feature Xceed Zip for .NET offers regarding spanning and splitting: the ability to specify any AbstractFile, wherever it's located, when a new "disk" is required.

Before I show you how I skipped the unpleasant task of copying each part on separate floppies, let's first take a look at how you can support traditional spanning using Xceed Zip for .NET. The code below is what I call a minimum spanning implementation:

private static void Zip( AbstractFile zipFile, AbstractFolder sourceFolder )
{
  // Prepare ZipEvents object that will be passed to every method call.
  ZipEvents events = new ZipEvents();
  events.DiskRequired += new DiskRequiredEventHandler( ZipEvents_DiskRequired );
 
  // Create the target ZipArchive, and prepare for batch modifications.
  ZipArchive zip = new ZipArchive( events, null, zipFile );
  zip.BeginUpdate( events, null );
 
  try
  {
    // Allow this zip file to span.
    zip.AllowSpanning = true;
 
    // Zip this folder's contents
    sourceFolder.CopyFilesTo( events, null, zip, true, true );
  }
  finally
  {
    // Complete the batch modification of this zip file.
    zip.EndUpdate( events, null );
    events.DiskRequired -= new DiskRequiredEventHandler( ZipEvents_DiskRequired );
  }
}
 
private static void Unzip( AbstractFile zipFile, AbstractFolder destFolder )
{
  // Prepare ZipEvents object that will be passed to every method call.
  ZipEvents events = new ZipEvents();
  events.DiskRequired += new DiskRequiredEventHandler( ZipEvents_DiskRequired );
 
  // Create the source ZipArchive.
  ZipArchive zip = new ZipArchive( events, null, zipFile );
 
  try
  {
    // Unzip to destination folder.
    zip.CopyFilesTo( events, null, destFolder, true, true );
  }
  finally
  {
    events.DiskRequired -= new DiskRequiredEventHandler( ZipEvents_DiskRequired );
  }
}
 
private static void ZipEvents_DiskRequired(object sender, DiskRequiredEventArgs e)
{
  // Let the user know we need that disk and wait for feedback.
  Console.WriteLine( "Please insert disk #{0}, then press .", 
    e.DiskNumber.ToString() );
  Console.ReadLine();
  e.Action = DiskRequiredAction.Continue;
}

It's pretty straightforward. You simply give the user the time to insert the required disk. Don't forget that when unzipping, you must make sure the last zip file is the one available before creating your ZipArchive around it. There is no "insert last disk" event with Xceed Zip for .NET.

Now, back to my task. If you take a look at the DiskRequiredEventArgs parameter of the DiskRequired event, you see it exposes a "ZipFile" property of type AbstractFile. That's the AbstractFile for the part it's trying to locate. The above implementation requires a pause, to give time to the user to insert the correct disk. But what if the correct zip part is already available somewhere else? How about this implementation:

private static void ZipEvents_DiskRequired(object sender, DiskRequiredEventArgs e)
{
  // Let's check if the current zip file is located in a "DiskN" subfolder
  AbstractFolder subfolder = e.ZipFile.ParentFolder;
 
  if(  ( !subfolder.IsRoot )
    && ( subfolder.Name.ToUpper().StartsWith( "DISK" ) ) )
  {
    subfolder = subfolder.ParentFolder.GetFolder( 
      "Disk" + e.DiskNumber.ToString() );
 
    if( subfolder.Exists )
    {
      AbstractFile newZipFile = subfolder.GetFile( e.ZipFile.Name );
 
      if( newZipFile.Exists )
      {
        // No need to ask the user for the correct zip part, we found it!
        e.ZipFile = newZipFile;
        e.Action = DiskRequiredAction.Continue;
      }
    }
  }
 
  if( e.Action != DiskRequiredAction.Continue )
  {
    // Let the user know we need that disk and wait for feedback.
    Console.WriteLine( "Please insert disk #{0}, then press .", 
      e.DiskNumber.ToString() );
    Console.ReadLine();
    e.Action = DiskRequiredAction.Continue;
  }
}

As you can see, I have full control on what AbstractFile I provide to the library as the Nth zip file part. Since I have unzipped my client's zip file parts in my "D:\temp" folder, I can now call my Unzip method like this:

Unzip( 
  new DiskFile( @"d:\temp\Disk4\test.zip" ), 
  new DiskFolder( @"d:\temp\Unzipped" ) );

Then, the obvious striked me! Why did I unzip his "Floppies.zip" zip file in my "D:\temp" folder??? It's even simpler than I thought:

Unzip( 
  new ZippedFile( new DiskFile( @"d:\temp\floppies.zip" ), @"\Disk4\test.zip" ), 
  new DiskFolder( @"d:\temp\Unzipped" ) );

I'm unzipping zip file parts stored in a single zip file, without the need to have those parts really on floppies or even on disk. Wow! Files are files, folders are folders, no matter where they reside. Are you starting to get the idea behind the FileSystem? :-)