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[]> LoadResources (Assembly assembly, string databaseName) { var itemResources = new SortedDictionary<string, byte[]>(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); } } } }
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