Sunday, August 1, 2010

Using Sitecore media cache with custom handlers.

Every certified Sitecore developer knows how media files caching works. When you request image from the media library, you can specify custom parameters like image height, width, scaling, etc. For each parameters set Sitecore stores generated image in the "/App_Data/MediaCache/" folder.

For example, you have a following image in you media library:

/~/media/images/f1

















It's quite large, so you also use small image somewhere at the front-page:

/~/media/images/f1.ashx?h=75&w=100










As a result Sitecore will generate resized file and save on the disk:












Ini file contains the list of images and parameters used to generate them:


[key]
?as=False&bc=0&h=75&mh=0&mw=0&sc=0&thn=False&w=100
[extension]
jpg
[headers]
Content-Type: image/jpeg
[dataFile]
993679bda69f41be9c8543cb1ac25c61.jpg
;-----

Simple, isn't it? But how about using this fancy feature for caching your own data? There are lot of situations when you need a file-based cache, like displaying images retrieved from external system(custom database, CRM, etc.), caching large files that will not fit into RAM and many others.

The solution is as always simple and straightforward. First, we need to define custom processor for the "getMediaStream" pipeline.


    

The following class contains two examples:
1) In the first one we provide externalImageId variable to the handler, that may be used to get the image from the external storage. Then we perform image transformations and return png media stream.
2) In the second example we combine javascripts according to the provided websiteScripts parameter and return the resulting file.

public class CustomContentProcessor
    {
        #region Methods

        public static void Process(GetMediaStreamPipelineArgs args)
        {
            if (args.Options.CustomOptions["externalImageId"] != null)
            {
                GetExternalImage(args);
            }
            if (args.Options.CustomOptions["websiteScripts"] != null)
            {
                GetWebsiteScripts(args);
            }
        }

        private static void GetExternalImage(GetMediaStreamPipelineArgs args)
        {
            // Guid imageId = new Guid(args.Options.CustomOptions["externalImageId"]);
            // Here you can request image from the database, crm or any other external source
            // In this example we'll use predefined base64 string as a data source

            byte[] byteData = Convert.FromBase64String(@"Qk3mBAAAAAAAADYAAAAoAAAAFAAAABQAAAABABgAAAAAALAEAADEDgAAxA4AAAAAAAAAAAAA+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unYZrDuZrDuZrDuZrDuZ7/SAKIyALMAAK0AAK0AAK0AAKwAAKcAAJwARJg+b39qb39q+unY+unY+unY+unYZrDuZrDuZrDuZrDuZ7/SAKIyALMAAK0AAK0AAK0AAKwAAKcAAJwARJg+b39qb39q+unY+unY+unY+unYZrDuZrDuZrDuZrDuZ7/SAKIyALMAAK0AAK0AAK0AAKwAAKcAAJwARJg+b39qb39q+unY+unY+unY+unYZJ7/ZJ7/ZJ7/ZJ7/arPpAIxVAJwLAJ4AAJ4AAJ4AEqolAKEAAKIAAKQAOYYxOYYx+unY+unY+unY+unYZaL/ZaL/ZaL/ZaL/ZbPxAIBtAJtEDqIpDqIpDqIpAJgAAKIYEqspAKMARpE6RpE6+unY+unY+unY+unYXKD/XKD/XKD/XKD/XKrybL/OAI5dAJ0qAJ0qAJ0qAKQLAKIdAJYLEpkgYI5MYI5M+unY+unY+unY+unYYKT/YKT/YKT/YKT/Zqr6bLDfAIJ6AJgsAJgsAJgsAKUAAKIOLJ43T4xQZ19CZ19C+unY+unY+unY+unYUqn8Uqn8Uqn8Uqn8SqrzVrjnVsTIAJhGAJhGAJhGAKMdAKEmAJI4UoRjcFtKcFtK+unY+unY+unY+unYUqn8Uqn8Uqn8Uqn8SqrzVrjnVsTIAJhGAJhGAJhGAKMdAKEmAJI4UoRjcFtKcFtK+unY+unY+unY+unYN7buN7buN7buN7buauz/U/D/ZP/5AJllAJllAJllAJtMAJVEAJlfuPLhXWhAXWhA+unY+unY+unY+unYKb/aKb/aKb/aKb/aV/b/M/3/I/v0TfznTfznTfznc//seP3plffwyPH9mo6Gmo6G+unY+unY+unY+unYM8rFM8rFM8rFM8rFW/n/H/n5K///N//5N//5N//5SPn5YvH/m+X/c16/s2Ccs2Cc+unY+unY+unY+unYAI5mAI5mAI5mAI5mXvr5OP//CPn5P/n/P/n/P/n/YPL/fOP/HV3OkEfU02m902m9+unY+unY+unY+unYAI5mAI5mAI5mAI5mXvr5OP//CPn5P/n/P/n/P/n/YPL/fOP/HV3OkEfU02m902m9+unY+unY+unY+unYAI5mAI5mAI5mAI5mXvr5OP//CPn5P/n/P/n/P/n/YPL/fOP/HV3OkEfU02m902m9+unY+unY+unY+unYAJZTAJZTAJZTAJZTY//qNv/8Ofn/Z+f/Z+f/Z+f/lNz/uM7/bUrdnD3V0Wq60Wq6+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY+unY");
            var image = new MemoryStream(byteData);

            var bm = (Bitmap)Bitmap.FromStream(image);
            var stream = new MemoryStream();
            bm.Save(stream, ImageFormat.Png);
            args.OutputStream = new MediaStream(stream, "png", args.MediaData.MediaItem);
        }

        private static void GetWebsiteScripts(GetMediaStreamPipelineArgs args)
        {
            var scriptsType = args.Options.CustomOptions["websiteScripts"];
            var stream = new MemoryStream();

            switch (scriptsType)
            {
                case "SyntaxHighlighter":
                    {
                        AddFileToStream(stream, "/js/shCore.js");
                        AddFileToStream(stream, "/js/shBrushSql.js");
                        AddFileToStream(stream, "/js/shBrushVb.js");
                        AddFileToStream(stream, "/js/shBrushXml.js");
                        break;
                    }
                case "FancyBox":
                    {
                        AddFileToStream(stream, "/js/jquery.easing-1.3.pack.js");
                        AddFileToStream(stream, "/js/jquery.fancybox-1.3.1.js");
                        AddFileToStream(stream, "/js/jquery.fancybox-1.3.1.pack.js");
                        AddFileToStream(stream, "/js/jquery.mousewheel-3.0.2.pack.js");
                        break;
                    }
                default:
                    {
                        break;
                    }
            }

            // Before sending to the user stream can be minified, etc.
            args.OutputStream = new MediaStream(stream, "js", args.MediaData.MediaItem);
        }

        private static void AddFileToStream(MemoryStream stream, string fileName)
        {
            var fileBytes = File.ReadAllBytes(HttpContext.Current.Server.MapPath(fileName));
            stream.Write(fileBytes, 0, fileBytes.Length);
        }

        #endregion
    }


Now, if you request a dummy media file and pass the externalImageId="any value" parameter, you'll receive a generated image. After few requests your media cache folder will look like:












And the ini file:


[key]
?as=False&bc=0&h=0&mh=0&mw=0&sc=0&thn=False&w=0&externalImageId=guid1
[extension]
png
[headers]
Content-Type: image/png
[dataFile]
93d48f5c08b442018ed75f80981412d8.png
;-----
[key]
?as=False&bc=0&h=0&mh=0&mw=0&sc=0&thn=False&w=0&externalImageId=guid2
[extension]
png
[headers]
Content-Type: image/png
[dataFile]
02bb1a832dc94f3bab2af7a5d72d8134.png
;-----
[key]
?as=False&bc=0&h=0&mh=0&mw=0&sc=0&thn=False&w=0&externalImageId=guid3
[extension]
png
[headers]
Content-Type: image/png
[dataFile]
0d18c2653d694bac8bc1c975485faa29.png
;-----
[key]
?as=False&bc=0&h=0&mh=0&mw=0&sc=0&thn=False&w=0&externalImageId=guid4
[extension]
png
[headers]
Content-Type: image/png
[dataFile]
c5bf077b1cfa453c8cf07c65a6ecd297.png
;-----
[key]
?as=False&bc=0&h=0&mh=0&mw=0&sc=0&thn=False&w=0&externalImageId=guid5
[extension]
png
[headers]
Content-Type: image/png
[dataFile]
8bc15c5151374b7299de8690129bc5eb.png
;-----

And if you pass the websiteScripts parameter, like this: /~/media/images/CustomContent.ashx?websiteScripts=SyntaxHighlighter, you'll get the combined JavaScript that will be cached and taken from disk on the next request:












And the ini file:

[key]
?as=False&bc=0&h=0&mh=0&mw=0&sc=0&thn=False&w=0&websiteScripts=SyntaxHighlighter
[extension]
js
[headers]
Content-Disposition: attachment; filename="CustomContent.js"
Content-Type: application/x-javascript


[dataFile]
d4e72adb9766419db625893a1684217d.js
;-----
[key]
?as=False&bc=0&h=0&mh=0&mw=0&sc=0&thn=False&w=0&websiteScripts=FancyBox
[extension]
js
[headers]
Content-Disposition: attachment; filename="CustomContent.js"
Content-Type: application/x-javascript
[dataFile]
f679efec17844995a7a3d65a2874f34d.js
;-----

In addition to the simple implementation, we get a lot of benefits like easy cache managing (there are jobs to  clear files by schedule, etc.) and robust caching mechanism from Sitecore.Kernel. I think this example really proves that Sitecore is very extensible and developer-friendly.