Pushing images from Sitecore’s media library to Amazon CDN

I was recently working on a project for a client that wanted to serve all their images via a CDN, but still wanted to maintain all their images in Sitecore.

In addition we needed to import content that already contained links to images that where already hosted on the CDN servers. To do this we will need to add a new field to each media item type to indicate the url of the item once it has been placed on the CDN server.

The best place to add the new required fields is

  • /sitecore/templates/system/Media/Versioned/File
  • /sitecore/templates/system/Media/Unversioned/File

Because each other media type defined within Sitecore inherit from one of these two templates.

Its best to create another template, _MediaContentDeliveryNetwork, with the following fields:

  • UrlOnCdnServer
  • PushedToCdn

And then add the new template to each of the two File templates.

PushedToCdn is required because of the requirement to import images into Sitecore that are already on the CDN Server
To aid with the import, PushedToCdn should be a droplist. So create another template for the export status of each file. This template does not need to contain any fields. Once the template has been created, create the following three items in the content tree

  • ImportedFromCDN
  • PushedToCDN
  • ToBePushedToCDN

Once this is completed, set the datasource of PushedToCdn to the location in the content tree where the above items have been created.
Standard values should be created on each of the File templates, and set PushedToCdn to ToBePushedToCDN

Now that the all the relevant changes within Sitecore have been completed, now need to work on the pipeline that will do all the work

The next step is to create a class that will be used for media items

public class MediaLibraryItem : BaseItem
{
    public string FileName { get; set; }
    public string Path { get; set; }
    public string Extension { get; set; }
    public string Alt { get; set; }
    public string Description { get; set; }
    public string MimeType { get; set; }
    public string Name { get; set; }
    public Guid ItemId { get; set; }
    public Guid ParentItemId { get; set; }
    public Guid TemplateId { get; set; }
    public string Language { get; set; }
    public string ItemUrl { get; set; }
}

public class BaseItem
{
}

and some static classes so we can avoid having code with magic strings

public static class MediaContentDeliveryNetwork
{
    public const string UrlOnCDN = 
        "UrlOnCDN";
    public const string PushedToCDN = 
        "PushedToCDN";
}

public static class MediaContentDeliveryNetworkStatus
{
    public const string ImportedFromCDN = 
        "ImportedFromCDN";
    public const string PushedToCDN = 
        "PushedToCDN";
    public const string ToBePushedToCDN = 
        "ToBePushedToCDN";
}

The next step is to create a class that will save the items onto the CDN. The code is for Amazon’s CDN, but this can be replaced with code to write to any other CDN

public interface ISaveMediaItemToCdnServer
{
    bool SaveFile(string contentType, 
        string fullFileName, 
        Stream mediaStream);
    string GetPathOnCdn(string mediaItemPath, 
        string rootMediaFolderPath);
    string GetFullFolderFileNameForCdn(
        MediaLibraryItem mediaItem, 
        string rootMediaFolderPath);
}

using Amazon;
using Amazon.S3;
using Amazon.S3.Model;

public class SaveMediaItemToCdnServer : 
    ISaveMediaItemToCdnServer
{
    private readonly ISettings _settings

    public SaveMediaItemToCdnServer(ISettings settings)
    {
        _settings = settings;
    }

    public string GetFullFolderFileNameForCdn(
        MediaLibraryItem mediaItem, string rootMediaFolderPath)
    {
        var folderOnCdn = 
            GetPathOnCdn(mediaItem.Path, rootMediaFolderPath);
        var fileName = 
            GetFileNameForCdn(mediaItem);
        return string.Format("{0}/{1}", 
            folderOnCdn, fileName);
    }

    public string GetPathOnCdn(string mediaItemPath, 
        string rootMediaFolderPath)
    {
        return mediaItemPath.Replace(
            string.Format("{0}/", rootMediaFolderPath), 
            string.Empty);
    }

    private string GetFileNameForCdn(
        MediaLibraryItem mediaLibraryItem)
    {
        return string.Format("{0}-{1}.{2}", 
            mediaLibraryItem.Name, 
            mediaLibraryItem.ItemId,
            mediaLibraryItem.Extension);
    }

    public bool SaveFile(string contentType, 
        string fullFileName, 
        Stream mediaStream)
     {
        try
        {
            using (var client = CreateClient())
            {
                var putObject = CreatePutObjectRequest(
                        _settings.AWSBucket, fullFileName, 
                        contentType, mediaStream);
                var response = client.PutObject(putObject);
                return true;
            }
        }
        catch (Exception ex)
        {
        }
    }

    private PutObjectRequest CreatePutObjectRequest(
        string bucketName, string key, 
        string contentType, Stream stream)
    {
        return new PutObjectRequest
        {
            BucketName = bucketName,
            Key = key,
            InputStream = stream,
            CannedACL =  S3CannedACL.PublicRead,
            ContentType = contentType
        };
    }

    private IAmazonS3 CreateClient()
    {
        var config = new AmazonS3Config
        {
            RegionEndpoint = RegionEndpoint.EUWest1
        };
        return AWSClientFactory.CreateAmazonS3Client(
            _settings.AWSAccessKey, 
            _settings.AWSSecretAccessKey, config);
    }
}

The last step is the class to create the save event handler

using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Events;
using Sitecore.Security.Accounts;

public class PushMediaToCdnEventHandler
{
    private readonly ILogger _logger;
    private readonly ISettings _settings;

    private readonly ISaveMediaItemToCdnServer 
        _saveMediaItemToCdnServer;

    private readonly Database db;

    public PushMediaToCdnEventHandler(ILogger logger, 
        ISettings settings,
        ISaveMediaItemToCdnServer saveMediaItemToCdnServer)
    {
        _logger = logger;
        _settings = settings;
        _saveMediaItemToCdnServer = 
            saveMediaItemToCdnServer;
        db = Database.GetDatabase(
            _settings.SitecoreMasterDatabaseName);
    }

    private bool IsItemAMediaItem(Item item)
    {
        return item != null
            && item.Database.Name.ToLower().Equals(_settings.SitecoreMasterDatabaseName)
            && item.Paths.IsMediaItem;
    }

    private bool ItemImportedFromCdn(MediaItem item)
    {
        return
            item.InnerItem[PushedToCDN]
                .Equals(ImportedFromCDN);
    }

    private bool MediaItemIsWithinMediaFolder(MediaItem item)
    {
        var mediaFolderPath = GetMediaFolderPath();
        return (item.Path.Contains(mediaFolderPath));
    }

    private bool IsValidMediaItem(MediaItem item)
    {
        return !string.IsNullOrEmpty(item.Extension);
    }

    public void OnItemSaved(object sender, EventArgs args)
    {
        if (args == null)
        {
            return;
        }

        try
        {
            //take item from args
            var eventArgs = args as SitecoreEventArgs;
            if (eventArgs == null)
            {
                return;
            }

            Item item = eventArgs.Parameters[0] as Item;
            if ( !IsItemAMediaItem(item) )
            {
                return;
            }

            MediaItem mediaItem = item;
            if ( ItemImportedFromCdn(mediaItem) )
            {
                return;
            }

            if (MediaItemIsWithinMediaFolder(mediaItem))
            {
                return;
            }

            if ( !IsValidMediaItem(mediaItem) )
            {
                return;
            }

            Stream mediaStream = mediaItem.GetMediaStream();
            if (mediaStream == null || mediaStream.Length == 0)
            {
                return;
            }
            BeginUpload(mediaItem, mediaStream);
        }
        catch (Exception ex)
        {
        }
    }

    private MediaLibraryItem GetMediaItem(MediaItem item)
    {
        var dataToReturn = new MediaLibraryItem
        {
            Name = item.Name,
            ItemId = item.ID.ToGuid(),
            ParentItemId = 
                item.InnerItem.ParentID.ToGuid(),
            TemplateId = 
                item.InnerItem.TemplateID.ToGuid(),
            Language = 
                item.InnerItem.Language.ToString(),
            Path = item.InnerItem.Paths.ParentPath,
            FileName = string.Format("{0}.{1}", 
                item.Name, item.Extension).ToLower(),
            Extension = item.Extension
        };
        return dataToReturn;
    }

    private string GetRootMediaFolderPath()
    {
        var rootMediaLibrary = db.GetItem(new ID(ItemIds.MediaLibrary.ImagesFolder));
        return rootMediaLibrary.Paths.Path;
    }

    private string GetMediaFolderPath()
    {
        var rootMediaLibrary = db.GetItem(
            new ID(ItemIds.MediaLibrary.Images.Media));
        return rootMediaLibrary.Paths.Path;
    }

    private void BeginUpload(MediaItem item, Stream mediaStream)
    {
        var mediaItem = GetMediaItem(item);
        var fileNameOnS3 = 
            _saveMediaItemToCdnServer.
                GetFullFolderFileNameForCdn(
                        mediaItem, 
                        GetRootMediaFolderPath());

        if (_saveMediaItemToCdnServer.SaveFile(
            item.MimeType, 
            fileNameOnS3, 
            mediaStream))
        {
            string fullUrlToFileOnCdn = 
                string.Format("{0}/{1}", 
                _settings.CDNHostName, 
                fileNameOnS3);
            SaveMediaItemDetails(item, fullUrlToFileOnCdn);
        }
    }

    private void SaveMediaItemDetails(
            MediaItem mediaItem, 
            string urlOnCdn)
    {
        var scUser = User.FromName(
            _settings.ImportUser, false);
        using (new UserSwitcher(scUser))
        {
            mediaItem.InnerItem.Editing.BeginEdit();
            mediaItem.InnerItem[PushedToCDN] =
                PushedToCDN;
            mediaItem.InnerItem[UrlOnCDN] = 
                urlOnCdn;
            mediaItem.InnerItem.Editing.EndEdit();
        }
    }
}

All that is now required is to hook up the event so that Sitecore knows about the save event

<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/" >
  <sitecore>
    <events>
      <event name="item:saved">
        <handler type="Events.Save.PushMediaToCdnEventHandler, Events.Save" method="OnItemSaved"></handler>
      </event>
    </events>
  </sitecore>
</configuration>

In Part Two I will update the media presenter so that Sitecore will render the url from the CDN server instead of the default media url that Sitecore will generate.

Advertisements

My musing about anything and everything

Tagged with: , ,
Posted in Sitecore
One comment on “Pushing images from Sitecore’s media library to Amazon CDN
  1. […] In my previous post, Pushing Images from Sitecore’s media Library to Amazon’s CDN […]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Enter your email address to follow this blog and receive notifications of new posts by email.

Join 9 other followers

%d bloggers like this: