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");
}
}
}