Tuesday, March 08, 2005

Warning: Do not try this at home!

A few days ago, Pierre-Luc at support asked me if Xceed Zip for .NET was thread safe. I knew from his look that he was expecting a "yes" or a "no". At least, that's what the client who asked him the same question expected.

My first answer was more in nuances: Though the library was made to be safely accessible from multiple threads at the same time, by the nature of the sequential format of the zip file, it is not possible to work on the same zip file from multiple threads.

He nodded with approbation, confirming me his client wasn't trying such crazy action, but simply dealing with a multi-threaded application where each thread may be zipping in its own private file. I gave him my benediction: In that case, yes, Xceed Zip for .NET is thread safe.

Pierre-Luc wasn't two feet away when I was illuminated by an idea. It wouldn't be that crazy to try zipping into the same zip file from multiple threads. How neat would it be to benefit from multi processor or hyperthreading machines for zipping a single file? Guess what... you can! You shouldn't... but you can! Don't ask us to support this scenario... but you can!

Here's the deal. Any ZipArchive you modify gets updated when the last modify operation occurs. If you know you're about to make more than one modification to a single zip file, you should first call BeginUpdate, do all modifications, and finally call EndUpdate. The zip file will only get rebuilt on that final call. The files you copy into the zip file before EndUpdate will be compressed and stored in temp files within the ZipArchive's TempFolder.

That means any copy operation you perform within a BeginUpdate/EndUpdate block are atomic, and only involve compressing the sources into independant temp files. You see where I'm heading? How about spawning threads within that block, each thread copying its own source, and waiting for all threads to finish before calling EndUpdate?

I had to try it. I started with a class implementing IAsyncResult, which would be managing the copy operation on a separate thread:

  internal class AsyncCopyResult : IAsyncResult

  {

    public AsyncCopyResult(

      AbstractFolder source,

      AbstractFolder dest,

      AsyncCallback callback,

      object state )

    {

      m_source = source;

      m_dest = dest;

      m_callback = callback;

      m_state = state;

 

      m_thread = new Thread( new ThreadStart( this.ThreadProc ) );

      m_completed = new ManualResetEvent( false );

    }

 

    public void Begin()

    {

      m_completed.Reset();

      m_thread.Start();

    }

 

    public void End()

    {

      // We must not join thread since we may get called by callback, itself

      // within thread.

      m_completed.WaitOne();

 

      if( m_result != null )

        throw m_result;

    }

 

    #region IAsyncResult IMPLEMENTATION

 

    public object AsyncState

    {

      get { return m_state; }

    }

 

    public bool CompletedSynchronously

    {

      get { return false; }

    }

 

    public WaitHandle AsyncWaitHandle

    {

      get { return m_completed; }

    }

 

    public bool IsCompleted

    {

      get { return m_completed.WaitOne( 1, false ); }

    }

 

    #endregion

 

    private void ThreadProc()

    {

      try

      {

        m_result = null;

 

        if( m_source == null )

          throw new ArgumentNullException( "source" );

 

        if( m_dest == null )

          throw new ArgumentNullException( "dest" );

 

        if( m_source.IsRoot )

        {

          m_source.CopyFilesTo( m_dest, true, true );

        }

        else

        {

          m_source.CopyTo( m_dest, true );

        }

      }

      catch( Exception except )

      {

        m_result = except;

      }

 

      m_completed.Set();

 

      if( m_callback != null )

      {

        try

        {

          // Important note: This callback may be calling End.

          // Thus End's implementation should not wait for thread,

          // but for handle.

          m_callback( this );

        }

        catch

        {

          System.Diagnostics.Debug.WriteLine( "Unhandled exception within callback." );

        }

      }

    }

 

    private Thread m_thread = null;

    private ManualResetEvent m_completed = null;

 

    private AbstractFolder m_source = null;

    private AbstractFolder m_dest = null;

    private AsyncCallback m_callback = null;

    private object m_state = null;

 

    private Exception m_result = null;

  }

The ThreadProc method is simply copying the source folder into the destination folder. The rest is plumbing for implementing IAsyncResult. In my main class, I created a static method that uses the AsyncCopyResult class like this:

    private static void CopyMultipleFolders(

      AbstractFolder[] sources,

      AbstractFolder dest )

    {

      // I'm using AutoBatchUpdate with the using directive, an easy way

      // to call BeginUpdate and EndUpdate only if the folder implements

      // IBatchUpdateable.

      using( AutoBatchUpdate auto = new AutoBatchUpdate( dest ) )

      {

        AsyncCopyResult[] results = new AsyncCopyResult[ sources.Length ];

 

        // First create the threads and state objects.

        for( int i=0; i<sources.Length; i++ )

        {

          results[ i ] = new AsyncCopyResult( sources[ i ], dest, null, null );

        }

 

        // Then launch each thread

        foreach( AsyncCopyResult result in results )

        {

          result.Begin();

        }

 

        // We can't call WaitAll on an STA thread, but it doesn't matter.

        // We wait for each one separately.

        foreach( AsyncCopyResult result in results )

        {

          result.AsyncWaitHandle.WaitOne();

 

          try

          {

            result.End();

          }

          catch( Exception except )

          {

            Console.WriteLine( except.Message );

          }

        }

      }

    }

Once each thread is done copying its own source folder into the destination folder, the AutoBatchUpdate class calls EndUpdate on the destination folder (if it implements IBatchUpdateable). In the case of a zip file destination, the final zip file is built by reassembling already compressed temp files. Here's an example of how to call CopyMultipleFolders:

        AbstractFile target = new DiskFile( @"d:\temp\multi.zip" );

 

        if( target.Exists )

          target.Delete();

 

        ZipArchive zip = new ZipArchive( target );

        AbstractFolder firstSource = new DiskFolder( @"d:\Downloads" );

        AbstractFolder secondSource = new DiskFolder( @"d:\Music" );

 

        CopyMultipleFolders( new AbstractFolder[] { firstSource, secondSource }, zip );

The best thing is that this method works for any kind of AbstractFolder, source or destination. If you're confident the size of the zip file isn't too large, you can improve performance by setting the ZipArchive's TempFolder to a new MemoryFolder.

But remember: Don't try this! I didn't tell you it was possible.



3/8/2005 10:56:48 AM (Eastern Standard Time, UTC-05:00)  #   
 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? :-)



2/23/2005 5:12:59 PM (Eastern Standard Time, UTC-05:00)  #   
 Friday, February 18, 2005

A new Mono release today! As usual, I uninstall the previous release, install the new one, and test a few Xceed Zip for .NET console samples. Works perfectly!

But today I decided to try my Xceed FileSystem-based Command Prompt under Mono. Wow! Everything works almost perfectly. Zip, Ftp, Ram drive. Only a glitch with the Isolated Storage. This sample uses Mentalis.org's ConsoleAttributes library to customize the look and feel of the console. It works perfectly well with Mono (on Windows).

Any Mono users out there using Xceed products?


FileSystem | FTP | Zip | Mono

2/18/2005 10:33:14 AM (Eastern Standard Time, UTC-05:00)  #   
 Tuesday, February 08, 2005

I'm playing with Google Maps, and it's amazingly easy to use and so well implemented. Dragging the map, pop-up balloons, directions, searching in a free textbox.

Here's Xceed Software. Here's the path I take every morning and evening.

Great job, Google!


Fun

2/8/2005 9:39:28 AM (Eastern Standard Time, UTC-05:00)  #   
 Wednesday, February 02, 2005

I've been working part time (translation: I should be working on something else) on a new sample: my own Command Prompt. I know, I'm reinventing the wheel, not to count that Microsoft will launch a new one called msh (codename Monad). But it was more a concept or proof around exposing AbstractFolder and AbstractFile within a command prompt.

E:\>dir

  Directory of E:\

      DATE     TIME     SIZE or TYPE NAME
27/12/2004  4:23 PM         [FOLDER] Backup
03/11/2004 10:33 AM         [FOLDER] Chart30
24/11/2004 10:09 AM         [FOLDER] CLR Profiler
11/01/2005  5:24 PM         [FOLDER] Config.Msi
24/11/2004 10:10 AM         [FOLDER] My Music
12/01/2005  4:13 PM         [FOLDER] My Pictures
10/09/2004  1:52 PM         [FOLDER] RECYCLER
30/09/2004  8:40 PM         [FOLDER] System Volume Information
02/02/2005  2:49 PM         [FOLDER] temp
03/11/2004 10:33 AM         [FOLDER] XceedProjectsNET
25/01/2005  7:06 AM              143 toto.txt

  Files: 1  Folders: 14  Total file size: 143

E:\>copy toto.txt temp
 100%
E:\>cd temp
E:\temp\>

As you can see, I can list the contents of folders, copy files, and change the working folder. The application simply manages a working "AbstractFolder", and enables commands to act on that folder (or an AbstractFolder obtained from an absolute path).

The sample quicky evolved into a prototype for upcoming features. Among other things, I needed a way to recognize a path like "E:\temp\test.zip\images" as a ZippedFolder within a zip file. Let's stop the talking, and show some traces:

E:\temp\>md test.zip
E:\temp\>md test.zip\images
E:\temp\>copy "..\My Pictures\Chalet\*" test.zip\images
 100%
E:\temp\>

What have I done here? Create a folder named "test.zip"? Well, the "md" command recognized the ".zip" extension as a request to create a new empty zip file. The second "md" command actually created a new folder within the zip file. And the paths can freely use the zip filename as a folder part for any command, as shown with the copy example. If we display the contents of "E:\temp", we see the two expected files:

E:\temp\>dir

  Directory of E:\temp\

      DATE     TIME     SIZE or TYPE NAME
02/02/2005  3:00 PM         40068736 test.zip
25/01/2005  7:06 AM              143 toto.txt

  Files: 2  Folders: 0  Total file size: 40068879

E:\temp\>

As you can see, "test.zip" is really a file (DiskFile) within "E:\temp" (DiskFolder). What happens if I try changing the current folder into that zip file?

E:\temp\>cd test.zip
E:\temp\test.zip\>dir

  Directory of E:\temp\test.zip\

      DATE     TIME     SIZE or TYPE NAME
02/02/2005  3:00 PM         [FOLDER] images

  Files: 0  Folders: 1  Total file size: 0

E:\temp\test.zip\>cd images
E:\temp\test.zip\images\>dir

  Directory of E:\temp\test.zip\images\

      DATE     TIME     SIZE or TYPE NAME
06/08/2000  4:40 PM          6400006 Chaises.bmp
06/08/2000  4:35 PM          6348550 Chute.bmp
06/08/2000  4:29 PM          6337678 Ciel1.bmp
06/08/2000  4:30 PM          6396226 Ciel2.bmp
06/08/2000  4:33 PM          6414418 Ciel3.bmp
06/08/2000  4:38 PM          6524278 Couple.bmp
06/08/2000  4:37 PM          6388054 Martine.bmp
06/08/2000  4:32 PM          6405478 Ombre.bmp
06/08/2000  4:41 PM          6359254 Rochers.bmp

  Files: 9  Folders: 0  Total file size: 57573942

E:\temp\test.zip\images\>

The zip file is exposed as a folder, because the path "E:\temp\test.zip" was recognized and mapped to a ZippedFolder around a DiskFile. And "images" is nothing more than a subfolder within that root ZippedFolder, actually something like:

new ZippedFolder( new DiskFile( @"E:\temp\test.zip" ), @"\images" );

Ok, let's get into serious things:

E:\temp\test.zip\images\>cd ..\..
E:\temp\>copy *.zip RAM:\
 100%
E:\temp\>cd RAM:\test.zip\images
RAM:\test.zip\images\>dir m*.bmp

  Directory of RAM:\test.zip\images\

      DATE     TIME     SIZE or TYPE NAME
06/08/2000  4:37 PM          6388054 Martine.bmp

  Files: 1  Folders: 0  Total file size: 6388054

RAM:\test.zip\images\>

My Command Prompt exposes a root MemoryFolder called "RAM:\", which I can freely use. The commands act the same, no matter if I'm deeling with a ZippedFolder around a DiskFile or a MemoryFile. Want more?

RAM:\test.zip\images\>cd ftp://vermouth
ftp://vermouth\>dir

  Directory of ftp://vermouth\

      DATE     TIME     SIZE or TYPE NAME

  Files: 0  Folders: 0  Total file size: 0

ftp://vermouth\>md foobar.zip
ftp://vermouth\>copy "E:\My Music\WMA\Mes Aieux" foobar.zip
 100%
ftp://vermouth\>dir

  Directory of ftp://vermouth\

      DATE     TIME     SIZE or TYPE NAME
02/02/2005  3:20 PM         62936759 foobar.zip

  Files: 1  Folders: 0  Total file size: 62936759

ftp://vermouth\>dir c:\inetpub\ftproot

  Directory of c:\inetpub\ftproot\

      DATE     TIME     SIZE or TYPE NAME
02/02/2005  3:20 PM         62936759 foobar.zip

  Files: 1  Folders: 0  Total file size: 62936759

ftp://vermouth\>

FTP servers are threated as any other kind of AbstractFolder. The application simply recognize the "FTP:" prefix as a signature for a root FtpFolder, as it did with "RAM:" exposed as a MemoryFolder. The command implementations don't care what kind of AbstractFolder or AbstractFile they are dealing with.

The engine behind this involves FileSystemMapper-derived classes. They mainly get asked two kinds of questions:

Question 1: Do you recognize this path as a root?

If so, they remove the part of the path they could recognize as a root folder, and return the matching AbstractFolder.

Examples of mappers and their responsability:

  • DiskMapper : Drive letters and UNC paths (yes, you can "cd" into a UNC path!)
  • FtpMapper : The "FTP:" prefix with server name, and optional username and password (e.g. ftp://user:pass@vermouth:9999)
  • IsolatedStorageMapper : A custom prefix name like "STORE:" (that's the one my sample app supports).
  • MemoryMapper : A custom prefix used to create the initial root MemoryFolder, like "RAM:" (that's the one my app supports). You can create more than one MemoryMapper to have more than one ram drive.

Question 2: Can you represent this AbstractFile as an AbstractFolder?

If so, they simply return the matching AbstractFolder.

An example of such a mapper:

  • ZipFileMapper : It simply checks if the provided AbstractFile exists, then tries to create a ZipArchive around that AbstractFile in a try/catch. If it succeeds, it returns this ZipArchive (which derives from ZippedFolder).

Curiously, today I came across a post on our forums asking how to detect if a file is really a zip file. I gave this man the "new ZipArchive within a try/catch" solution, and he came back, as I feared, with concerns with the time wasted catching an exception for all those non-zip files. It's actually one of the bottlenecks of my Command Prompt sample. A lot of time is wasted throwing an exception for all non-zip files my app comes across. Well, I guess I'll have to work sooner than later on a new "ZipArchive.IsZipFile" method! :-)

Now, you have to convince my boss I should put more time on this sample and these new FileSystem features! Does mapping absolute paths like shown above to their proper AbstractFolder or AbstractFile something that could be usefull for you?


FileSystem | FTP | Samples | Zip

2/2/2005 3:55:51 PM (Eastern Standard Time, UTC-05:00)  #   
 Wednesday, January 19, 2005

Scott Hanselman just posted about a case-{in}sensitivity problem he just went through. That reminded me I wanted to talk to you about case-sensitivity in Xceed Zip for .NET and the FileSystem. I remember back in the design days, we debated long and hard on if the FileSystem should be case-sensitive or not. Once we decided to support both, the debate continued about what should be the default behavior.

The conclusions were simple:

  1. Immitate by default.
  2. Uniformity within single product.
  3. Know the differences.

Since System.IO was case-insensitive (and moreover the whole Windows operating system), we had to be case-insensitive by default. Thus, if you have files "first.txt" and "second.TXT" in a folder, the code below will return you two files:

DiskFolder disk = new DiskFolder( @"t:\" );
AbstractFile[] files = disk.GetFiles( false, "*.txt" );

The same way, if you have both files in a zip file, the following code will return both:

ZipArchive zip = new ZipArchive( new DiskFile( @"t:\texts.zip" ) );
AbstractFile[] files = zip.GetFiles( false, "*.txt" );

Now, where it's getting tricky is that you will never have a folder on disk containing both "second.TXT" and "second.txt". The system won't let you create the second one. Thus the following code returns an existing file who's FullName is all lower case, even if the real file has an upper-case extension:

DiskFolder disk = new DiskFolder( @"t:\" );
AbstractFile file1 = disk.GetFile( "second.txt" );

You asked the "Disk" world for file "second.txt", and this world has recognized "second.TXT" as matching your request.

To obey to rule #2, the following code does exactly the same, even though the file stored in the zip file has its extension all upper case:

ZipArchive zip = new ZipArchive( new DiskFile( @"t:\texts.zip" ) );
AbstractFile file2 = zip.GetFile( "second.txt" );

But in a zip file, which can come from a different operating system, you potentially could end up with a zip file containing both. What would happen? I've created such a zip file for our tests, by adding "second.TXT" and "foobar.txt" to a zip file, and hex-editing "foobar" to "second":

second.zip (.23 KB)

When opening this file in WinZip, I can see both files. But when unzipping, it will unzip the first, then try to unzip the second over the first. You just can't unzip both in two separate files. Furthermore, trying to unzip any single one from within the classic view will always unzip both over the same file on disk.

How does Xceed Zip for .NET deal with such zip fles? Try the following code:

ZipArchive zip = new ZipArchive( new DiskFile( @"t:\second.zip" ) );
 
foreach( AbstractFile file in zip.GetFiles( false ) )
{
  Console.WriteLine( file.FullName );
}

The output is:

\second(1).txt
\second.TXT

Any file that case-insensitively matches another file gets appended a number. This is not a perfect solution, as there is never a perfect solution. To support rule #2, DiskFolder and ZippedFolder instances had to behave the same. This post and the documentation tries to address rule #3 :-)

Now, some of you want to always look for exact matches. You simply need to prepend the string mask with a ">", as in "I want a more precise match" (1). The following code will match a single file:

DiskFolder disk = new DiskFolder( @"t:\" );
AbstractFile[] files = disk.GetFiles( false, ">*.txt" );

The idea with System.String filter parameters is that we replace them with a NameFilter, which is the one responsible for that ">" trick. It only works with methods accepting filters (GetFiles, GetFolders, CopyFilesTo, MoveFilesTo). Methods like GetFile can only return a single instance (actually always returns an instance which may exist or not). Those methods return the single and unique AbstractFile matching your string, based on the world this AbstractFolder belongs to.

(1): We actually debated between using "<" as in "match less items" or ">" as in "a more precise match". I think we ended up tossing a coin! :-)



1/19/2005 10:50:47 AM (Eastern Standard Time, UTC-05:00)  #   
 Tuesday, January 18, 2005

I'm currently doing some tests on an alpha version of Xceed FTP for .NET, supporting the Xceed FileSystem. It's so wonderful to be able to manipulate files and folders no mather where they reside. Take this generic directory listing method:

private static void DisplayListing( AbstractFolder folder )
{
  if( !folder.Exists )
  {
    Console.WriteLine( "\n  Folder {0} does not exist.\n", folder.FullName );
  }
  else
  {
    FileSystemItem[] items = folder.GetItems( false );
    long totalSize = 0;
    int fileCount = 0;
 
    Console.WriteLine( "\n  Folder listing of {0}\n", folder.FullName );
 
    foreach( FileSystemItem item in items )
    {
      Console.Write( "{0} {1} ", 
        item.LastWriteDateTime.ToShortDateString(),
        item.LastWriteDateTime.ToShortTimeString() );
 
      AbstractFile file = item as AbstractFile;
 
      if( file == null )
      {
        Console.Write( "            " );
      }
      else
      {
        Console.Write( "{0,16} ", file.Size.ToString( "N0" ) );
        totalSize += file.Size;
        ++fileCount;
      }
 
      Console.WriteLine( item.Name );
    }
 
    int folderCount = items.Length - fileCount;
 
    Console.WriteLine( "\n  {0} file{1}, {2} folder{3}, {4} bytes\n",
      fileCount.ToString(),
      ( fileCount == 1 ) ? string.Empty : "s",
      folderCount.ToString(),
      ( folderCount == 1 ) ? string.Empty : "s",
      totalSize.ToString() );
  }
}

As you can see, the code does not need to know what exactly is that AbstractFolder. People familiar with Xceed Zip for .NET already know we could call the above method like this:

DiskFolder folder = new DiskFolder( @"C:\Program Files\Microsoft SDKs\WinFX" );
DisplayListing( folder );

And obtain this kind of output:


  Folder listing of C:\Program Files\Microsoft SDKs\WinFX\
 24/11/2004   9:29 AM   <DIR>          bin
 24/11/2004   9:27 AM   <DIR>          Help
 24/11/2004   9:27 AM   <DIR>          License
 24/11/2004   9:27 AM   <DIR>          misc
 24/11/2004   9:29 AM   <DIR>          Setup
 24/11/2004   9:29 AM   <DIR>          VS Install Directory
 11/11/2004   6:03 PM           16,001 ReleaseNotes.htm
 12/01/2005   2:58 PM           17,297 SetEnv.cmd
  2 files, 6 folders, 33298 bytes

Or call the same method like this:

ZippedFolder folder = new ZippedFolder( 
  new DiskFile( @"D:\sample.zip" ), "ContMenuExt" );
DisplayListing( folder );

To get this output:


  Folder listing of \ContMenuExt\
 08/01/2001  11:29 AM            8,091 ContextMenu.cpp
 02/01/2001   4:15 PM            1,005 ContextMenu.h
 08/01/2001  11:32 AM            7,820 ContextMenuExt.cpp
 28/11/2000  11:23 AM              225 ContextMenuExt.def
 02/01/2001   5:02 PM            5,301 ContextMenuExt.dsp
 28/11/2000  11:23 AM              551 ContextMenuExt.dsw
 28/11/2000  11:23 AM              742 ContextMenuExt.h
 02/01/2001   4:12 PM            1,440 ContextMenuExt.rc
 08/01/2001   3:29 PM                2 ReadMe.txt
 02/01/2001   4:12 PM            1,195 resource.h
  10 files, 0 folders, 26372 bytes

With the upcoming version of Xceed FTP for .NET, it won't be more difficult to display the contents of a folder located on an FTP server:

FtpConnectionInfo info = new FtpConnectionInfo( "ftp.microsoft.com" );
FtpFolder folder = new FtpFolder( info );
DisplayListing( folder );

  Folder listing of \
 25/11/2002  12:00 AM   <DIR>          bussys
 21/05/2001  12:00 AM   <DIR>          deskapps
 20/04/2001  12:00 AM   <DIR>          developr
 18/11/2002  12:00 AM   <DIR>          KBHelp
 02/07/2002  12:00 AM   <DIR>          MISC
 16/12/2002  12:00 AM   <DIR>          MISC1
 25/02/2000  12:00 AM   <DIR>          peropsys
 02/01/2001  12:00 AM   <DIR>          Products
 04/04/2003  12:00 AM   <DIR>          PSS
 21/09/2000  12:00 AM   <DIR>          ResKit
 25/02/2000  12:00 AM   <DIR>          Services
 25/02/2000  12:00 AM   <DIR>          Softlib
  0 files, 12 folders, 0 bytes

As a mather of fact, stuff like this already works fine on my machine:

FtpConnectionInfo info = new FtpConnectionInfo( "ftp.cam.org", "***", "***" );
FtpFile ftpFile = new FtpFile( info, @"\pub\Photos.zip" );
 
if( ftpFile.Exists )
  ftpFile.Delete();
 
DiskFolder sourceFolder = new DiskFolder( @"E:\My Pictures\Clément\Petites" );
ZipArchive destFolder = new ZipArchive( ftpFile );
 
sourceFolder.CopyFilesTo( destFolder, true, true );
 
DisplayListing( destFolder );
DisplayListing( ftpFile.ParentFolder );

This is the output:


  Folder listing of \
 16/09/2004   9:39 AM           11,820 Buzz.jpg
 16/09/2004   9:40 AM           11,143 Chalet - Bercé.jpg
 16/09/2004   9:42 AM           15,749 Clément et Michel.jpg
 16/09/2004   9:43 AM           18,473 Clément et Papa.jpg
 16/09/2004   9:42 AM           15,004 Clément et Valérie.jpg
 14/02/2003   4:17 PM            7,984 clément1.jpg
 14/02/2003   4:18 PM           11,288 clément2.jpg
 14/02/2003   4:18 PM           10,648 clément3.jpg
 11/08/2004   3:25 PM           18,499 Famille.jpg
 11/08/2004   3:23 PM           36,734 Fier.jpg
 16/09/2004   9:40 AM           14,959 Grande discussion.jpg
 16/09/2004   9:41 AM           13,426 Maman et Clément.jpg
 11/08/2004   3:25 PM           24,594 Piscine.jpg
  13 files, 0 folders, 210321 bytes

  Folder listing of \pub\
 27/12/2004  11:06 AM            7,986 Builds du 2004-09-27.htm
 12/01/2005   4:13 PM          208,439 Clement.zip
 27/12/2004  11:06 AM            1,068 FileSystem.txt
 18/01/2005   2:44 PM          208,423 Photos.zip
 27/12/2004  11:06 AM   <DIR>          Second
 27/12/2004  11:06 AM           11,723 VSSWarning.jpg
 27/12/2004  11:06 AM          117,695 appnote.txt
 27/12/2004  11:06 AM               96 vssver.scc
  7 files, 1 folder, 555430 bytes

Am I the only one to find this cool? q;-)


FileSystem | FTP | Zip

1/18/2005 2:49:50 PM (Eastern Standard Time, UTC-05:00)  #   

It's been a long time since my last post. I feel like I should post more often, even if the subject may not get your attention. But I have mixed emotions about blogging just for blogging.

Robert Scoble is an hardant promotor of corporate blogging. And prolific. But when he says stuff like "Talk is cheap. Doing is divine." just because he has nothing new to say about Longhorn, he's shooting his foot. How credible is that statement?



1/18/2005 1:42:25 PM (Eastern Standard Time, UTC-05:00)  #   
 Tuesday, December 14, 2004

I must agree 100% with Frans: While giants are busy fighting on the Desktop Search front, WinFS gets delayed, which should have been the real front. People here at Xceed know what I think about WinFS. While others here were all excited about Avalon and Indigo, I was the first to tell them the real innovation and revolution was WinFS. I'm sorry MS, but WinFS should have been the main Pillar of Longhorn! Instead, WinFS topics on MSDN all start with this remark:

UPDATE: In spite of what may be stated in this content, WinFS is not a feature that will come with the Longhorn Operating System. However, WinFS will be available on the Windows platform at some future date, which is why this content continues to be provided for your information.

Sure, desktop search tools help make my life easier. But it's a temporary solution... And temporary solutions tend to become permanent too easily...

On my TODO list: Post about the WinFS model and its implications on archives like zip files, which are not ready to store data, relationships, and metadata.



12/14/2004 9:43:39 AM (Eastern Standard Time, UTC-05:00)  #