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