conflate.ashx - v1.2

2007-11-13 @ 11:22#

i updated the conflate.ashx script to include a time-stamp and an optional revision number. this will make make it easier to 'tickle' the cache with a new version id and more reliably output the most recent version of the output to clients. the new query line can now look like this:

conflate.ashx?{v1.6}/css/sample.css,/css/sample2.css,...

the new time-stamp is added to the output, too. this will help with troubleshooting the content. the first line of the output now looks like this:

/* conflated: Tue 13 Nov 2007 16:09:55 GMT {v11} */

below is the latest version of th entire conflate.ashx script"

<%@ 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
 *          1.2 - 2007-11-13 (mca)
 *                - added optional {version} on queryline
 *                - added 'conflated: (GMT) (version)'  comment line at top
 * 
 * usage:   "conflate.ashx?/folder/path/file1.js,/folder/path/file2.js,..."
 *          "conflate.ashx?{v1.1}/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";
    const string conflated_fmt = "/* conflated: " + expires_fmt + " {1} */\n";

    string version = string.Empty;
        
    public void ProcessRequest(HttpContext ctx)
    {
        string files = ctx.Server.UrlDecode((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));

        // get version, if it's there
        try
        {
            version = Regex.Match(files, @"(\{.*\})", RegexOptions.IgnoreCase).Value;
        }
        catch (Exception ex)
        {
            version = string.Empty;
        }

        // clean up query line
        files = Regex.Replace(files, @"(\{.*\})", "");  // drop version
        files = Regex.Replace(files, "[,]{2,}", ",");   // remove duplicate commas
        files = Regex.Replace(files, "^,(.+)", "$1");   // remove leading comma
        files = Regex.Replace(files, "(.+),$", "$1");   // remove trailing comma
        
        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();

            sb.AppendFormat(conflated_fmt, System.DateTime.UtcNow, version);

            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)
    {
        string accept = (ctx.Request.Headers["Accept-encoding"] != null ? ctx.Request.Headers["Accept-encoding"] : string.Empty);

        if (accept.Contains("gzip"))
        {
            ctx.Response.Filter = new GZipStream(ctx.Response.Filter, CompressionMode.Compress);
            ctx.Response.AppendHeader("Content-Encoding", "gzip");
            return;
        }

        if (accept.Contains("deflate"))
        {
            ctx.Response.Filter = new DeflateStream(ctx.Response.Filter, CompressionMode.Compress);
            ctx.Response.AppendHeader("Content-Encoding", "deflate");
            return;
        }

        // if no match found
        return;
    }
}

code