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:
- added regex to clean up input url
- added md5 of url as the cache key
- 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"); } } }