conflate.ashx v1.1

2007-10-26 @ 16:55#

last week i threw together an ASP.NET handler that accepted a series of CSS or JS files in the URL and returned a single representation to the caller. works cool. now that i'm playing with it a bit, i found a few ways to improve it. thus, release of 1.1 of conflate.ashx

here's a summary of the changes:

  1. added regex to clean up input url
  2. added md5 of url as the cache key
  3. added gzip/deflate compression support

as before, i offer the entire source here in a single shot. feel free to do what you wish. if this gets any larger/more interesting, i'll toss it into SVN and folks can pull it from there.

<%@ WebHandler Language="C#" Class="Conflate" %>

/************************************************************************
 * 
 * title:   conflate.ashx
 * version: 1.0 - 2007-10-23 (mca)
 * version: 1.1 - 2007-10-26 (mca)
 *                - added regex to clean up input url
 *                - added md5 for cache key
 *                - added compression support
 * 
 * usage:   "conflate.ashx?/folder/path/file1.js,/folder/path/file2.js,..."
 *          "conflate.ashx?/folder/path/file1.css,/folder/path/file2.css,..."
 * 
 * notes:   returns a single representation which is a combination of csv list
 *          inserts "error loading ..." msg if file was not found.
 *          ignores "empty" filenames (no load attempts, no errors)
 *          stores results in asp.net cache w/ file dependencies
 *          you modify expires var to control Cache-Control/Expires headers
 *          
 *************************************************************************/
using System;
using System.Web;

using System.IO;
using System.Text;
using System.Web.Caching;
using System.Text.RegularExpressions;
using System.IO.Compression;

public class Conflate : IHttpHandler 
{
    const double expires = 60 * 60 * 24 * 30;   // 30 days
    const string cache_control_fmt = "public,max-age={0}";
    const string expires_fmt = "{0:ddd dd MMM yyyy HH:mm:ss} GMT";
    const string load_err_fmt = "/* error loading {0} */\n";
    
    public void ProcessRequest(HttpContext ctx)
    {
        string files = (ctx.Request.Url.Query.Length > 0 ? ctx.Request.Url.Query.Substring(1) : string.Empty);
        string ctype = (files.IndexOf(".css") != -1 ? "text/css" : (files.IndexOf(".js") != -1 ? "text/javascript" : string.Empty));

        files = Regex.Replace(files, "[,]{2,}", ",");
        files = Regex.Replace(files, "^,(.+)", "$1");
        files = Regex.Replace(files, "(.+),$", "$1");
        
        if(ctype!=string.Empty && files!=string.Empty)
        {
            string data = LoadFiles(ctx, files.Split(','));

            SetCompression(ctx);
            
            ctx.Response.Write(data);
            ctx.Response.StatusCode = 200;
            ctx.Response.ContentType = ctype;

            if (expires != 0)
            {
                ctx.Response.AddHeader("Cache-Control", string.Format(cache_control_fmt, expires));
                ctx.Response.AddHeader("Expires", string.Format(expires_fmt, System.DateTime.UtcNow.AddSeconds(expires)));
            }
        }
        else
        {
            ctx.Response.ContentType = "text/plain";
            ctx.Response.StatusCode = 404;
            ctx.Response.StatusDescription = (ctype == string.Empty ? "no valid content-type" : "no files to process");
            ctx.Response.Write("\n");
        }
        ctx.Response.End();
    }

    public bool IsReusable
    {
        get
        {
            return false;
        }
    }

    private string LoadFiles(HttpContext ctx, string[] files)
    {
        string data = (string)ctx.Cache.Get(md5(ctx.Request.RawUrl));

        if (data == null)
        {
            string[] fnames = (string[])files.Clone();
            StringBuilder sb = new StringBuilder();

            for (int i = 0; i < files.Length; i++)
            {
                files[i] = ctx.Server.MapPath(files[i]);
                if (File.Exists(files[i]))
                {
                    using (TextReader tr = new StreamReader(files[i]))
                    {
                        sb.AppendLine(tr.ReadToEnd());
                    }
                }
                else
                {
                    sb.AppendFormat(load_err_fmt, fnames[i]);
                }
            }
            
            data = sb.ToString();
            
            ctx.Cache.Add(
                md5(ctx.Request.RawUrl),
                data,
                new CacheDependency(files),
                Cache.NoAbsoluteExpiration,
                Cache.NoSlidingExpiration,
                CacheItemPriority.Normal,
                null);
        }

        return data;
    }

    private string md5(string data)
    {
        return Convert.ToBase64String(new System.Security.Cryptography.MD5CryptoServiceProvider().ComputeHash(System.Text.Encoding.Default.GetBytes(data)));
    }

    private void SetCompression(HttpContext ctx)
    {
        if (ctx.Request.Headers["Accept-encoding"] != null && ctx.Request.Headers["Accept-encoding"].Contains("gzip"))
        {
            ctx.Response.Filter = new GZipStream(ctx.Response.Filter, CompressionMode.Compress);
            ctx.Response.AppendHeader("Content-Encoding", "gzip");
        }
        else if (ctx.Request.Headers["Accept-encoding"] != null && ctx.Request.Headers["Accept-encoding"].Contains("deflate"))
        {
            ctx.Response.Filter = new DeflateStream(ctx.Response.Filter, CompressionMode.Compress);
            ctx.Response.AppendHeader("Content-Encoding", "deflate");
        }
    }
}

code