Friday, July 15, 2005

My friend and colleague Pascal is selling one of his PDC 2000 souvenirs.

Bottle.jpg

It's an unopened original PDC 2000 bottle of water!!! Who will be the lucky buyer? :-)


Fun

7/15/2005 10:00:10 AM (Eastern Daylight Time, UTC-04:00)  #   
 Thursday, July 14, 2005

I'm currently working hard on the FileSystem support for Xceed FTP for .NET, and I had to share with you a screenshot of one of my tests. My XceedBox.NET sample is now fully supporting FtpFile and FtpFolder mapping.

XceedBoxNET.png

As you can see, my working folder is a zip file located in a folder on an FTP server. The "type" command above has no idea what kind of file it is displaying. It receives an AbstractFile, gets a Stream on it via OpenRead, reads and displays the contents. That AbstractFile is actually a ZippedFile found in a ZippedFolder constructed around an FtpFile found in an FtpFolder.


.NET | FileSystem | FTP

7/14/2005 4:04:21 PM (Eastern Daylight Time, UTC-04:00)  #   
 Monday, July 04, 2005

My friend and colleague Pascal is a big fan of AppRocket. He bought it and is very very satisfied with the software. For those who don't know what is does, let's say it indexes file and folder names of your choice for quick access using a few keyboard letters. For example, by default, it indexes the Start Menu. So typing "PNT" proposes a list of applications or documents to open, starting with Paint.

He convinced me to try it, and I immediately was attracted with the concept. Getting away from the dreadful mouse clicks is a benediction. But ever since installation, I was regularly frustrated with many aspects of the software. So today, I uninstalled it.

First, after installation, my Windows XP Firewall warns me AppRocket wants to listen on a port. What the...? It's a keyboard shortcut program. How come it requires listening? I sent an email to Candy Labs support, and never got any answer.

Second, the application's configuration dialog box does not display correctly when you are using a theme different from the default Windows XP "Luna" (Blue, Silver or Olive). Everytime I want to change something in the configuration, like adding a new path to index, I must switch from my CodeOpus theme to Luna, change stuff, then get back to CodeOpus. I contacted Candy Labs support again... No answer.

As if that wasn't enough, one time I opened the Control Panel's Display Settings by typing "DIS" in AppRocket (yes, it also indexes the Control Panel), then changed the theme to Luna because I wanted to add a path to index. But AppRocket crached with an unhandled exception. Hurray! They are forcing me to switch themes, then crash on me when I do!

The last nail got hit when I monitored the IP connections AppRocket was trying to make, to see why it was listening on a port. It's listening on port 9001, and up to now have no idea why. But I also noticed that what appears to be auto-update-related functionality is trying to connect to http://64.127.102.250/. If you go check the home page, it's really nothing to inspire confidance in the product's seriousness, not to count the lack of any activity on Candy Labs' site.


Fun

7/4/2005 10:31:44 AM (Eastern Daylight Time, UTC-04:00)  #   
 Tuesday, June 28, 2005

Google Earth (aka Keyhole 3) is out, and free! Go check it out!


Fun

6/28/2005 3:28:27 PM (Eastern Daylight Time, UTC-04:00)  #   
 Thursday, June 16, 2005

Customer requests come in waves, as if fashion was driving the development industry. Lately, many customers were trying to compress log files. I've deviced it was time for a little sample.

The idea was to encode each string message in unicode and compress it in a plain file, one after the other. I could have used a zip file with each file entry representing a message, but for small messages, the zip headers would take too much space for nothing, wasting the need for compression in the first place.

The deflate compression method has one nice feature: it can detect the end of the compressed data while decompressing, without knowing the total compressed size. That's why the CompressedStream class exposes a GetRemainingStream method for retrieving a Stream reference on the rest of the data in the inner stream.

I've kept the sample real simple, so you get the general idea:

using System;
using System.IO;
using Xceed.Compression;
 
namespace CompressedLogExample
{
  public class CompressedLog
  {
    public CompressedLog( string filename )
    {
      if( filename == null )
        throw new ArgumentNullException( "filename" );
 
      if( filename.Length == 0 )
        throw new ArgumentException( "The filename cannot be empty.", "filename" );
 
      Xceed.Compression.Licenser.LicenseKey = "SAMPLE-APPLICATION-KEY";
      m_filename = filename;
    }
 
    public void AddMessage( string message )
    {
      if( message == null )
        throw new ArgumentNullException( "message" );
 
      lock( m_lock )
      {
        using( Stream fileStream = new FileStream( m_filename, FileMode.Append ) )
        {
          using( CompressedStream compStream = new CompressedStream( fileStream ) )
          {
            byte[] encodedMessage = System.Text.Encoding.Unicode.GetBytes(
              DateTime.Now.ToString() + Environment.NewLine + message );
 
            compStream.Write( encodedMessage, 0, encodedMessage.Length );
          }
        }
      }
    }
 
    public void DisplayMessages( TextWriter writer )
    {
      if( writer == null )
        throw new ArgumentNullException( "writer" );
 
      lock( m_lock )
      {
        try
        {
          using( Stream originalStream = new FileStream( m_filename, FileMode.Open ) )
          {
            Stream workStream = originalStream;
 
            do
            {
              using( CompressedStream compStream = new CompressedStream( workStream ) )
              {
                // We don't want compStream to close sourceStream!
                compStream.Transient = true;
 
                using( StreamReader reader = new StreamReader( 
                         compStream, System.Text.Encoding.Unicode ) )
                {
                  writer.WriteLine( reader.ReadToEnd() );
                  writer.WriteLine( "---" );
 
                  // Before closing the reader (thus compStream), acquire a stream on
                  // the rest of the data if present.
                  workStream = compStream.GetRemainingStream();
 
                  // We must do this AFTER calling GetRemainingStream since compStream
                  // may have read more from its inner stream than necessary.
                  if( workStream.Position == workStream.Length )
                  {
                    workStream = null;
                  }
                }
              }
            }
            while( workStream != null );
          }
        }
        catch( FileNotFoundException )
        {
          writer.WriteLine( "The log is empty." );
        }
      }
    }
 
    private string m_filename = string.Empty;
    private object m_lock = new object();
  }
}

Unfortunately, you can't replace the CompressedStream with a formatted stream like GZipCompressedStream, because they do not expose a GetRemainingStream method yet. Too bad, since the GZipCompressedStream can store a few minimal informations in its header. I'll have to open a feature request about that!

Here is some sample code for using this CompressedLog class:

    static void Main(string[] args)
    {
      CompressedLog log = new CompressedLog( @"d:\temp\log.cmp" );
 
      while( true )
      {
        Console.WriteLine( "Write your next message below (empty message to quit):" );
 
        string line = Console.ReadLine();
 
        if( line.Length == 0 )
          break;
 
        log.AddMessage( line );
      }
 
      Console.WriteLine();
      Console.WriteLine( "Your messages were:" );
 
      log.DisplayMessages( Console.Out );
 
      Console.WriteLine( "Press <Enter> to quit." );
      Console.ReadLine();
    }


6/16/2005 3:04:00 PM (Eastern Daylight Time, UTC-04:00)  #   
 Monday, June 13, 2005

I just learned through Scott and Simon that you can add new fonts to the Windows Console. Great, I've added my ProggySquare font to the list. See how great my XceedBox.NET sample looks:

XceedBox.jpg

Neat!


Fun

6/13/2005 9:40:41 AM (Eastern Daylight Time, UTC-04:00)  #   
 Thursday, June 09, 2005

In the new package v1.2.5309 which will be available for download next week resides a new feature you won't see much emphasis about, but which I was very eager to complete. You can now create a ZipArchive instance around an AbstractFile that does not support reading from.

(drum roll) ... (looking around) ... Nobody's applauding? That's because you probably don't know yet how useful this can be.

Most ASP.NET applications that wish to create zip files on the fly and send them in the response are either stuck with creating those zip files on disk in a temporary filename, or create them in a MemoryFile, then copy that MemoryFile in the response stream.

However, the StreamFile class was created for such purposes of exposing any existing Stream as an AbstractFile. You already could create a StreamFile around the Response's OutputStream. But passing that StreamFile to the ZipArchive's constructor would fail, because it can't read from it. Instead of assuming an empty zip file, it miserably failed. Shame.

No more... Since version 2.2.5302, it will assume the zip file is empty. So code like this works perfectly:

    public void ProcessRequest(HttpContext context)
    {
      context.Response.ContentType = "application/zip";
      context.Response.AddHeader( "Content-Disposition", "attachment; filename=images.bmp" );
 
      ZipArchive archive = new ZipArchive( new StreamFile( context.Response.OutputStream ) );
      DiskFolder source = new DiskFolder( context.Request.MapPath( "." ) );
 
      source.CopyFilesTo( archive, false, false, "*.bmp" );
    }

The same problem appeared when trying to combine Xceed Zip for .NET with Xceed FTP for .NET, to upload zip files directly on the FTP server. Though the FtpClient class exposes a very useful GetUploadStream method to get a direct stream on the data connection, code like this previously failed.

          using( Stream upload = client.GetUploadStream( "images.zip" ) )
          {
            ZipArchive archive = new ZipArchive( new StreamFile( upload ) );
            DiskFolder source = new DiskFolder( @"d:\images\" );
 
            source.CopyFilesTo( archive, false, false, "*.bmp" );
          }

Talk about short and sweet uploads of zip files!


.NET | FileSystem | FTP | Zip

6/9/2005 2:56:43 PM (Eastern Daylight Time, UTC-04:00)  #   
 Thursday, May 19, 2005

Lately, people have been asking us how to abort a zipping operation with Xceed Zip for .NET. The official answer is "you can't", as there is no method or property exposed for this task, as opposed to Xceed Zip ActiveX with its simple Abort property. But the truth is you can, with relatively little coding.

Before we get into how to abort, let's talk a little bit about the ZipArchive's TempFolder property. By default, it points to the same folder as the static ZipArchive.DefaultTempFolder property, which itself points to the user's temp folder, as exposed by System.IO.Path.GetTempPath().

Though the library is designed to delete any file it creates in the temporary folder, this can occur only when instances get finalized if the operation failed in the middle of the process.

A good coding pattern I like to use is the following:

    ZipArchive zip = new ZipArchive( new DiskFile( @"d:\temp\backup.zip" ) );
    zip.TempFolder = zip.TempFolder.CreateFolder( Guid.NewGuid().ToString() );
 
    try
    {
      using( AutoBatchUpdate auto = new AutoBatchUpdate( zip ) )
      {
        DiskFolder source = new DiskFolder( @"d:\Data" );
        source.CopyTo( zip, true );
      }
    }
    finally
    {
      zip.TempFolder.Delete();
    }

This makes sure no temp file survive a zipping cycle. And with that pattern, I can set the "default" temporary location once using the static DefaultTempFolder property, and each instance will use a unique folder within this starting point.

Now that my zipping operations are cleaning their traces, we're ready to talk about aborting. Some key concepts:

  • The library isn't pumping messages, and does not offer async operations. If you want your WinForms application's "Abort" button to react, you will have to pump messages yourself somewhere.
  • There are three major operations behind the creation or modification of a zip file:
    • Compressing each new file.
    • Moving each file to keep from the original zip file (if updating an existing zip file).
    • Building the target zip file by appending data created by the above two steps.
  • Zip and FileSystem events get raised at many levels, so you should pass your ZipEvents instance everywhere an overload accepting a FileSystemEvents or ZipEvents instance exists.

Your "Abort" button (or any abort input you like) will simply raise a flag. It can't do more.

    private bool m_abort = false;
 
    private void AbortButton_Click(object sender, System.EventArgs e)
    {
      m_abort = true;
    }

Then you handle three events matching the forementioned three steps, pump messages to keep a responsive application, and check if the flag is raised. You can safely use the same method for handling the three events.

    private void CheckAbort_ByteProgression(object sender, ByteProgressionEventArgs e)
    {
      if( m_abort )
        throw new ApplicationException( "The user aborted the operation." );
 
      Application.DoEvents();
    }

As you can see, if the flag is raised, I'm throwing an ApplicationException. This will result in a System.Reflection.TargetInvocationException being thrown by the originally called method. To get a well-behaved application, you obviously want to trap any exception the FileSystem could throw. You can catch any TargetInvocationException to display an "operation aborted" message. Here's my code for the full operation:

    private void StartButton_Click(object sender, System.EventArgs e)
    {
      m_abort = false;
      StartButton.Enabled = false;
      AbortButton.Enabled = true;
 
      try
      {
        ZipEvents events = new ZipEvents();
 
        // Advise for the three main events for checking abort flag.
        events.ByteProgression += 
          new ByteProgressionEventHandler( CheckAbort_ByteProgression );
        events.GatheringZipContentByteProgression += 
          new GatheringZipContentByteProgressionEventHandler( CheckAbort_ByteProgression );
        events.BuildingZipByteProgression += 
          new BuildingZipByteProgressionEventHandler( CheckAbort_ByteProgression );
 
        // What's cool with delegates is that you can separate logic from UI.
        events.ByteProgression += 
          new ByteProgressionEventHandler( UpdateUI_ByteProgression );
 
        ZipArchive zip = new ZipArchive( 
          events, null, new DiskFile( @"d:\temp\backup.zip" ) );
 
        zip.TempFolder = zip.TempFolder.CreateFolder( Guid.NewGuid().ToString() );
 
        try
        {
          using( AutoBatchUpdate auto = new AutoBatchUpdate( zip, events, null ) )
          {
            DiskFolder source = new DiskFolder( @"d:\Data" );
            source.CopyTo( events, null, zip, true );
          }
        }
        finally
        {
          zip.TempFolder.Delete();
 
          // Clean up events.
          events.ByteProgression -= 
            new ByteProgressionEventHandler( CheckAbort_ByteProgression );
          events.GatheringZipContentByteProgression -= 
            new GatheringZipContentByteProgressionEventHandler( CheckAbort_ByteProgression );
          events.BuildingZipByteProgression -= 
            new BuildingZipByteProgressionEventHandler( CheckAbort_ByteProgression );
 
          events.ByteProgression -= 
            new ByteProgressionEventHandler( UpdateUI_ByteProgression );
        }
      }
      catch( System.Reflection.TargetInvocationException except )
      {
        MessageBox.Show( except.InnerException.Message, "Abort" );
      }
      catch( Exception except )
      {
        MessageBox.Show( except.Message, "Error" );
      }
      finally
      {
        AbortButton.Enabled = false;
        StartButton.Enabled = true;
        m_abort = false;
      }
    }

Things to notice:

  • I'm passing my "events" instance to:
    • The ZipArchive's ctor. You could handle the ReadingZipItemProgression events.
    • The AutoBatchUpdate ctor, which will in turn pass it to both BeginUpdate and EndUpdate. The later method will generate the GatheringZipContentByteProgression and BuildingZipByteProgression events.
    • The CopyTo method. It will generate the ByteProgression events.
  • I'm advising two times for the ByteProgression events, once for handling abort conditions, and another for updating my UI. This is a cool way to leverage delegates and separate the logic from the UI.

.NET | Zip

5/19/2005 4:53:12 PM (Eastern Daylight Time, UTC-04:00)  #