Monday, May 6, 2013

Automatically deploying all Embedded Resources when a Document is deployed with Umbraco Courier.

We use Umbraco and the Courier package to deploy changes from a staging instance to a production instance.  The workflow our editors use is as follows.
  1. Update a document with new changes including embedded images or links to PDF/other files.
  2. Deploy the document only, no recursion/dependancies.
The problem here is our workflow was taking advantage of a Courier bug which has seance been fixed.  Namely that deploying a document without dependencies won't deploy the embedded images/linked files with it.  The editor would need to to go deploy all the linked resources manually first which can be quite impractical, and not logical with the workflow.

The default mode in Courier is actually to deploy all dependent resources recursively up the structure.  That is to package up and deploy all document types, templates, template resources(css files, script files, other pages linked to), and macros used by the document.  The problem here is two fold.  We could have script or css changes on our staging site that we don't want to push to production even though we want to deploy some new copy text for one of the pages.  You would need to un-check all the files you don't want to deploy if deploying dependencies which is impractical and easy to skip over thus causing errors on production.  The other problem is if you have a main menu/links on the template then they will all be added as dependancies and be packaged up and deployed with the document.  Thus it isn't possible to deploy changes to just one page without deploying all the changes to any other linked pages at the same time.

This all said there is a simple solution to change this behavior to what we want.  First of all we add a little Data Resolver to add all linked files and images in the document as resources to the document when it is packaged.  This will package and deploy them with the document irregardless of the dependency settings.  Next we add a bit of code to change the default dependency level to no dependancies instead of all dependancies.  Here are the few code files and simple changes to reproduce this logic.

First add the following files to your App_Code directory.

EmbeddedResources.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Text.RegularExpressions;
using Umbraco.Courier.Core;
using Umbraco.Courier.ItemProviders;
using Umbraco.Courier.Core.Helpers;
using Umbraco.Courier.DataResolvers;

namespace JSP
{
    /// <summary>
    /// Custom provider to add linked media items as resources to a document so that they will deploy even if not dependencies are deployed.
    /// </summary>
    public class EmbeddedResources : ItemDataResolverProvider
    {
        // Use the same properties as localLinks to retection.
        private static List<string> embeddedResourceDataTypes = Context.Current.Settings.GetConfigurationCollection("/configuration/itemDataResolvers/localLinks/add", true);

        // Look for image and links with a href/src linking to the /media tree.
        private static Regex resourceRegex = new Regex("<(a|img)[^>]*(href|src)=['\"]/media([^\"']+)['\"][^>]*>", RegexOptions.IgnoreCase);
        
        public override List<Type> ResolvableTypes
        {
            get { return new List<Type>() { typeof(ContentPropertyData) }; }
        }

        public override bool ShouldExecute(Item item, Umbraco.Courier.Core.Enums.ItemEvent itemEvent)
        {
            ContentPropertyData cpd = (ContentPropertyData)item;
            foreach (var cp in cpd.Data.Where(X => X.Value != null && embeddedResourceDataTypes.Contains(X.DataTypeEditor.ToString().ToLower())))
                if (cp.Value != null && cp.Value.ToString().IndexOf("/media/", StringComparison.OrdinalIgnoreCase) >= 0)
                    return true;

            return false;
        }

        public override void Packaging(Item item)
        {
            int count = 0;
            ContentPropertyData cpd = (ContentPropertyData)item;
            foreach (var cp in cpd.Data.Where(X => X.Value != null && embeddedResourceDataTypes.Contains(X.DataTypeEditor.ToString().ToLower())))
            {
                if (cp.Value != null && cp.Value.ToString().IndexOf("/media/", StringComparison.OrdinalIgnoreCase) >= 0)
                {
                    // Find all matches.
                    MatchCollection matches = resourceRegex.Matches(cp.Value.ToString());
                    count += matches.Count;

                    foreach (Match match in matches)
                    {
                        // Get the path from the capture group.
                        string path = "/media" + match.Groups[3].Value;

                        // Add the file as a resource.
                        item.Resources.Add(path);
                    }
                }
            }
            Logging._Debug(count + " EmbeddedResources files found");
        }
    }
}

CommitItem.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace JSP
{
    /// <summary>
    /// Summary description for Class1
    /// </summary>
    public class CommitItem : Umbraco.Courier.UI.Dialogs.CommitItem
    {
        public CommitItem()
        {
            this.DepthLimit = 0;
        }
    }
}

Next locate and update the /umbraco/plugins/courier/dialogs/CommitItem.aspx file.  On the first line change the Inherits="..." attribute to Inherits="JSP.CommitItem".

Now you are good to go with the new workflow/logic and the default setting is to just deploy the current item and its linked resources, not all dependencies, though this option can still be chosen as needed.

No comments: