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)  #