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