Sitecore autoinstalling modules

This is a follow up on yesterdays post regarding autoinstalling NuGet packages into Sitecore.

On request, here is my prototype code on how I use WebActivator to automatically insert/update embedded items into a Sitecore installation. Please note that this is very much prototype code, and it’s really ugly too. But I hope you get the idea of how it works.

First, I’ve started with a (to be extended) class library with some helper methods that’ll handle operations regarding loading items into Sitecore. This class is packed into a separate NuGet package, so that I can reuse it in other Sitecore modules:

using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Resources;
using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Data.Serialization;
using Sitecore.Diagnostics;
using Sitecore.SecurityModel;

namespace Stendahls.SitecoreModuleInstaller
{
    public abstract class SitecoreModuleAutoinstaller
    {
        protected readonly HashSet<string> ForceItems = new HashSet<string>();
        protected readonly HashSet<string> UpdateItems = new HashSet<string>();

        public virtual SortedDictionary<string, byte&#91;&#93;> LoadResources (Assembly assembly, string databaseName)
        {
            var itemResources = new SortedDictionary<string, byte&#91;&#93;>(StringComparer.CurrentCultureIgnoreCase);
            var resourceName = string.Format("{0}.{1}.resources", assembly.GetName().Name, databaseName);

            var stream = assembly.GetManifestResourceStream(resourceName);
            if (stream == null)
            {
                Log.Warn(string.Format("Could not load resource {0}", resourceName), this);
                return null;
            }

            using (var reader = new ResourceReader(stream))
            {
                var enumerator = reader.GetEnumerator();
                while (enumerator.MoveNext())
                {
                    var key = enumerator.Key as string;
                    var data = enumerator.Value as byte[];
                    if (key != null && data != null)
                        itemResources.Add(key, data);
                }
            }

            return itemResources;
        }


        public virtual bool StoreItemToDisk (Database db, string itemPath, byte[] data, bool overwrite)
        {
            var itemReference = new ItemReference(db.Name, itemPath);
            string path = PathUtils.GetFilePath(itemReference.ToString());
            if (!overwrite && File.Exists(path))
                return false;

            string directory = Path.GetDirectoryName(path);
            if (directory != null && !Directory.Exists(directory))
                Directory.CreateDirectory(directory);

            Log.Info(string.Format("Storing {0} to disk at {1} ({2} bytes)", itemPath, path, data.Length), this);
            File.WriteAllBytes(path, data);
            return true;
        }


        public virtual void RestoreItem (Database db, string itemPath, bool force)
        {
            var itemReference = new ItemReference(db.Name, itemPath);
            string path = PathUtils.GetFilePath(itemReference.ToString());
            var loadOptions = new LoadOptions(db) { ForceUpdate = force };

            Log.Info(string.Format("Restoring {0} to {1}", itemPath, db.Name), this);
            using (new SecurityDisabler())
            {
                Manager.LoadItem(path, loadOptions);
            }
        }


        public virtual void ProcessItem(Database db, string itemPath, byte[] data)
        {
            string key = string.Format("/{0}/{1}", db.Name, itemPath.TrimStart('/'));
            if (ForceItems.Contains(key))
            {
                StoreItemToDisk(db, itemPath, data, true);
                RestoreItem(db, itemPath, true);
            }

            if (UpdateItems.Contains(key))
            {
                StoreItemToDisk(db, itemPath, data, true);
                RestoreItem(db, itemPath, false);
            }
        }

        public virtual IEnumerable<string> DatabaseNames
        {
            get { 
                yield return "core";
                yield return "master";
            }
        }

        public abstract Assembly ResourceAssembly { get; }

        public virtual void ProcessInstall ()
        {
            Log.Info("Autoinstalling Sitecore modules", this);
            foreach (string database in DatabaseNames)
            {
                var db = Factory.GetDatabase(database);
                Assert.IsNotNull(db, "Cannot load database {0}", database);
                var resources = LoadResources(ResourceAssembly, db.Name);
                if (resources == null)
                    continue;

                foreach (var resource in resources)
                {
                    string path = resource.Key;
                    if (path.EndsWith(".item"))
                        path = path.Substring(0, path.Length - 5);
                    ProcessItem(db, path, resource.Value);
                }
            }
        }
    }
}

Now, having imported the class above into my module, I create a setup class that’ll do the install work. I use WebActivator to trigger the setup on PostApplicationStart. Below is a sample of such class that I have in one of my modules:

using System.Reflection;
using Stendahls.SitecoreModuleInstaller;

[assembly: WebActivator.PostApplicationStartMethod(typeof(Stendahls.Modules.Tools.Setup.ConfigureSitecore), "Configure")]

namespace Stendahls.Modules.Tools.Setup
{
    public class ConfigureSitecore : SitecoreModuleAutoinstaller
    {
        public static void Configure()
        {
            var configurator = new ConfigureSitecore();
            configurator.ProcessInstall();
        }

        public ConfigureSitecore()
        {
            ForceItems.Add("/core/sitecore/content/Applications/Content Editor/Gutters/Media In Use");
            ForceItems.Add("/core/sitecore/content/Applications/Content Editor/Gutters/Missing Local Version");
            ForceItems.Add("/core/sitecore/content/Applications/Content Editor/Gutters/Related Items Published");
            ForceItems.Add("/master/sitecore/system/Settings/Subitems Sorting/Pure Logical");
            ForceItems.Add("/master/sitecore/system/Settings/Subitems Sorting/Pure Display Name");
        }

        public override Assembly ResourceAssembly
        {
            get { return GetType().Assembly; }
        }
    }
}

In this example I treat those items as master, and any changes in a local Sitecore installation will be overwritten. I can of course add missing items, do updates or run any kind of custom code to update my installation as needed.

The above asumes that I have core.resources and master.resources embedded in my solution. I’ve created those with a Pre-build event like this:

Stendahls.SitecoreItemResourcePacker.exe -d master -i $(SolutionDir)\Tds.Master -r $(ProjectDir)
Stendahls.SitecoreItemResourcePacker.exe -d core -i $(SolutionDir)\Tds.Core -r $(ProjectDir)

And here’s the prototype code for packer application:

using System;
using System.IO;
using System.Resources;
using CommandLine;
using CommandLine.Text;

namespace Stendahls.SitecoreItemResourcePackager
{
    internal class Program
    {
        private sealed class CommandLineArgs : CommandLineOptionsBase
        {
            private string _resourcePath;

            [Option("d", "database", Required = true, HelpText = "Database name")]
            public string Database { get; set; }

            [Option("i", "itempath", Required = true, HelpText = "Item base path")]
            public string ItemPath { get; set; }

            [Option("r", "resourcepath", Required = true, HelpText = "Resource file base path")]
            public string ResourcePath
            {
                get { return _resourcePath; }
                set
                {
                    if (value == null)
                    {
                        _resourcePath = null;
                        return;
                    }
                    string path = value.TrimEnd('\\');
                    if (path.EndsWith("\\sitecore", StringComparison.InvariantCultureIgnoreCase))
                        path = path.Substring(0, path.Length - "\\sitecore".Length);
                    _resourcePath = path;
                }
            }

            [Option("v", "verbose", HelpText = "Verbose output")]
            public bool Verbose { get; set; }

            [HelpOption]
            public string GetUsage ()
            {
                return HelpText.AutoBuild(this, current => HelpText.DefaultParsingErrorsHandler(this, current));
            }
        }

        static void Main(string[] args)
        {
            try
            {
                var options = new CommandLineArgs();
                var parser = new CommandLineParser(new CommandLineParserSettings(Console.Error));
                if (!parser.ParseArguments(args, options))
                {
                    Console.Error.WriteLine(options.GetUsage());
                    Environment.Exit(1);
                }

                string resourceFile = Path.Combine(options.ResourcePath, string.Format("{0}.resources", options.Database));
                string filesBasePath = Path.Combine(options.ItemPath, "sitecore");
                if (options.Verbose)
                {
                    Console.WriteLine("Creating resource file {0}", resourceFile);
                    Console.WriteLine("Scanning {0} for .item files...", filesBasePath);
                }

                int count = 0;
                using (var writer = new ResourceWriter(resourceFile))
                {
                    var files = Directory.GetFiles(filesBasePath, "*.item", SearchOption.AllDirectories);
                    foreach (string file in files)
                    {
                        string key = file.Substring(options.ItemPath.Length).Replace('\\', '/');
                        key = key.Substring(0, key.Length - 5); // Strip out .item
                        var data = File.ReadAllBytes(file);
                        writer.AddResource(key, data);
                        if (options.Verbose)
                        {
                            Console.WriteLine("Item {0} ({1} bytes) added to resource package with key {2}", file, data.Length, key);
                        }
                        count++;
                    }
                }
                Console.WriteLine("Successfully packed {0} items into {1}.resources", count, options.Database);
                Environment.Exit(0);
            }
            catch (Exception ex)
            {
                Console.Error.WriteLine(ex);
                Environment.Exit(1);
            }
        }
    }
}

1 thought on “Sitecore autoinstalling modules

  1. One of the most inspiring posts about Sitecore, NuGet and TDS. I’d been wondering how to achieve something like this for some time and you’ve totally nailed it. Kudos. Simon

Leave a Reply to Simon Cancel reply