Just as I was to place in some finishing touches on what was the first version of this blog post when I come across Tim Braga’s post on the same topic. Tim covers the details they performed to integrate their site with their CDN provider. This is a great post on the subject. So with that, most of my original copy was thrown aside and this became a compare and contrast post instead. Tim’s article is great and detailed in the approach he took, and I took a bit different of a path.
While CDNs are nothing new, I was in a unique situation on a project in which one of our clients had not yet decided whom their CDN provider would be. Each provider had a different approach to integration with their product. Some used FTP, some used services, some used an HTTP pull process, and the list goes on and on. My frontal lobe thinking kicked in. It would be much easier to build this to allow for any integration possibility and provide easier configuration.
So, I wanted to utilize a factory pattern to allow the ability to implement and utilize different types of CDN providers with minimal effort. I also wanted the elements of the integration including credentials, URL prefixes, etc. to be configurable from Sitecore. And finally I wanted to segregate my environments (development, QA, production) with minimal modification of the code base, and the ability to “mock” a CDN locally if I wanted to for development activities. I agree with Tim that the process wasn’t terribly difficult. It just needed some thought.
In regard to templates, Tim took an approach to extend the Sitecore media file templates. This way each individual file can be opted in to use the CDN or be excluded from it. I really like this ability for a couple of different reasons. My approach was an all or nothing for the media files. I can think of a potential example where I would want to exclude a file from being served up from the CDN, but most of the time I’m thinking a majority of the media files will go up. Still it is a nice approach at the granular level.
We created new templates to serve as a Sitecore configuration and related CDN processer configuration that include fields to support the identification of what type of CDN processor the factory class should distribute and common fields like URL prefixes, credentials, and any unique processing flags needed. I also included some fields to indicate if I was mocking a CDN locally.
For processing of the media items to the CDN, Tim used a hook on the Item:Saved event. It is a clean approach for processing. I had the publishing pipeline in my sights. So I created a pipeline processor to call the factory generated instance of my CDN processor.
My thinking here about logic was check if the item is a media item, get my CDN processor if configured, and process the item through that CDN processor.
public class CDNPublishing : PublishItemProcessor { public override void Process(PublishItemContext context) { Assert.ArgumentNotNull(context, "context"); var cdnID = Settings.GetSetting(SolutionConstants.SitecoreCDNEnvironment); if (cdnID != null) { var cdnItem = context.PublishHelper.GetSourceItem(ID.Parse(cdnID)); if (cdnItem.Fields["CDNType"] != null) { // get Sitecore Item for publishing context var contextItem = context.PublishHelper.GetSourceItem(context.ItemId); if (contextItem != null) { //check if media item if ((contextItem.Paths.IsMediaItem) && (contextItem.TemplateID.ToString() != SolutionConstants.MediaFolder)) { var typeClass = context.PublishHelper.GetSourceItem(ID.Parse(cdnItem.Fields["CDNType"].Value)); if (typeClass != null) { if (typeClass.TemplateID.ToString() == SolutionConstants.CDNTypesTemplate) { // instatiate class CDNProcessor processor = CDNFactory.GetCDN(typeClass); if (processor != null) { //execute process var cdnSuccess = processor.Process(cdnItem, contextItem, context); if (!cdnSuccess) { Exception cdnEx = new Exception(string.Format("CDN Processing failed for {0}-{1}.", contextItem.ID, contextItem.Name)); Sitecore.Diagnostics.Log.Error(cdnEx.Message, cdnEx, this); context.Job.Status.Failed = true; context.Job.Status.Messages.Add(cdnEx.Message); } } } } } } } } } }
What if my item didn’t succeed in being published to my CDN? No worries, I send a message to be displayed in the publishing results dialog along with logging the error with the item information.
The CDN processor abstract class is just a collection of CRUD operations and I rely on the publish action to determine what I should be doing with the item.
public class CDNFileProcessor : CDNProcessor { private Item _CDNSettings; private Item _MediaItem; private PublishItemContext _Context; private string _filePrefix; ///<summary> /// Process media files appropriately based on provider ///</summary> ///<param name="cdnSettings"></param> ///<param name="mediaItem"></param> ///<param name="context"></param> ///<returns></returns> public override bool Process(Item cdnSettings, Item mediaItem, PublishItemContext context) { _CDNSettings = cdnSettings; _MediaItem = mediaItem; _Context = context; bool result = true; var useLocalRepository = (_CDNSettings.Fields["UseLocalRepository"].Value == "1") ? true : false; if (useLocalRepository) { _filePrefix = _CDNSettings.Fields["LocalRepositoryMediaFilePath"].Value; // determine what to do by publishing action if (context.Action == Sitecore.Publishing.PublishAction.None) result = Add(); if ((context.Action == Sitecore.Publishing.PublishAction.PublishSharedFields) || (context.Action == Sitecore.Publishing.PublishAction.PublishVersion)) result = Replace(); if (context.Action == Sitecore.Publishing.PublishAction.DeleteTargetItem) result = Delete(); } return result; } protected override bool Add() { bool result = false; // extract file name var mediaItem = (MediaItem)_MediaItem; if (_filePrefix != null) { var fullFile = GetFullFilePath(mediaItem, _filePrefix); // determine if media item is not in repository if (!File.Exists(fullFile)) { // write file to repository // insert your logic here } else { result = Replace(); } } return result; } protected override bool Replace() { bool result = false; // extract file name var mediaItem = (MediaItem)_MediaItem; if (_filePrefix != null) { var fullFile = GetFullFilePath(mediaItem, _filePrefix); // determine if media item is not in repository if (!File.Exists(fullFile)) { result = Add(); } else { // update file to repository result = Delete(); if (result) result = Add(); } } return result; } protected override bool Delete() { bool result = false; // extract file name var mediaItem = (MediaItem)_MediaItem; if (_filePrefix != null) { var fullFile = GetFullFilePath(mediaItem, _filePrefix); // determine if media item is not in repository if (File.Exists(fullFile)) { try { File.Delete(fullFile); result = true; } catch (Exception) { result = false; } } } return result; } }
Much like Tim, for this specific case I am getting the media item blob, converting to a file, and putting it where it needs to go. In our case for a local repository, I am writing the file from the binary stream to the folder location locally for development. If I need to do something specific for another CDN, I can just create another concrete CDN processor class with the appropriate implementation for FTP, service call, or whatever is necessary.
This brings us to the media provider. I liked how Tim used a hook for the custom media provider. When I was thinking of this solution – it never occurred to me to leverage this. I made less modifications to my media provider based on having my prefixes defined in the CDN processor. While I did perform an override on the GetMediaURL method it was just to make sure my absolute path was set to false. Most of what I had completed was with the override with the MediaLinkPrefix property. Since my prefix values are defined in the CDN processor, just grab them from there. If that value isn’t available, then take the Sitecore configuration setting value.
///<summary> /// Gets the media link prefix. ///Overriding use config setting as default and looking at CDN record ///</summary> ///<value>The media link prefix.</value> public override string MediaLinkPrefix { get { var result = base.MediaLinkPrefix; if (Context.Site != null) { if (ValidSite(Context.Site.Name)) { result = GetMediaPrefix(); } } return result; } } ///<summary> /// Check CDN settings to see if CDN media prefix is to be used ///</summary> ///<returns></returns> public string GetMediaPrefix() { var result = this.Config.MediaLinkPrefix; // default to Sitecore config setting var cdnID = Settings.GetSetting(SolutionConstants.SitecoreCDNEnvironment); if (cdnID != null) { var cdnItem = contextService.GetItem<ICDNSettings>(cdnID); if(cdnItem != null) { if(cdnItem.UseLocalRepository && !string.IsNullOrEmpty(cdnItem.LocalDomainMediaPrefix)) result = cdnItem.LocalDomainMediaPrefix; else if (!string.IsNullOrEmpty(cdnItem.CDNDomainMediaPrefix)) // have we identified a CDN prefix for CDN use result = cdnItem.CDNDomainMediaPrefix; } } return result; }
Populating my CDN just requires a republish of the media library. Tim accomplished the same task via a utility page which is a quite common practice.
I did have problems with the utilization of my ORM within the publishing context. It wasn’t an impact to the overall result, but I think of it more like taking the “scenic route” in getting Sitecore item information.
I think your code blocks are getting intermingled with HTML?