MOSS MVP

I've moved my blog to http://blog.falchionconsulting.com!. Please update your links. This blog is no longer in use--you can find all posts and comments at my new blog; I will no longer be posting to this site and comments have been disabled.

Monday, July 6, 2009

Deploying SharePoint Files Not Handled by the WSP Solution Schema

I was working on a project recently where I had to deploy a settings file to the root of my web applications folder (where the web.config file resides).  If you've ever had to do something like this before then you know that you cannot do this declaratively using the WSP's Solution schema.  The Solution schema is really quite limiting as to where you can actually deploy files - as a result your only option is to create a custom Feature that runs some code when executed (because we certainly don't want to go the xcopy route).

To do this we're going to create a custom Feature which contains all the files that we need to copy and then we'll provision a one-time timer job to copy the file to the target location on each server.

Here's our Feature.xml file:

<?xml version="1.0" encoding="utf-8"?>
<Feature
Id="1960C4A0-7A47-42A8-A382-F7A91214BA39"
Title="Settings Provisioner"
Description="This Feature deploys a settings file to a the web application root."
Version="1.0.0.0"
Scope="WebApplication"
Hidden="false"
ReceiverAssembly="MyCustomFeature, Version=1.0.0.0, Culture=neutral, PublicKeyToken=39b13c54ceef5193"
ReceiverClass="MyCustomFeature.FeatureReceivers.SettingsFeatureReceiver" xmlns="http://schemas.microsoft.com/sharepoint/">
<ElementManifests>
<ElementFile Location="Files\settings.config" />
</ElementManifests>
</Feature>

As you can see we are including a "settings.config" file which is located in a folder called "Files" directly under the Feature folder.  You could easily have any number of files here by simply adding additional ElementFile elements.  Also note that we are linking a feature receiver to the Feature which will execute upon activation and deactivation.

Here's our feature receiver class:

   1: public class SettingsFeatureReceiver : SPFeatureReceiver
   2: {
   3:     /// <summary>
   4:     /// Occurs after a Feature is activated.
   5:     /// </summary>
   6:     /// <param name="properties">An <see cref="T:Microsoft.SharePoint.SPFeatureReceiverProperties"></see> object that represents the properties of the event.</param>
   7:     public override void FeatureActivated(SPFeatureReceiverProperties properties)
   8:     {
   9:         SPWebApplication webApp = properties.Feature.Parent as SPWebApplication;
  10:         try
  11:         {
  12:             TimerJobs.CopySettingsJob job = new TimerJobs.CopySettingsJob(webApp);
  13:             job.SubmitJob(true, properties.Feature.Definition.RootDirectory);
  14:         }
  15:         catch (Exception ex)
  16:         {
  17:             Logger.WriteException(ex);
  18:         }
  19:  
  20:         
  21:     }
  22:  
  23:     /// <summary>
  24:     /// Occurs when a Feature is deactivated.
  25:     /// </summary>
  26:     /// <param name="properties">An <see cref="T:Microsoft.SharePoint.SPFeatureReceiverProperties"></see> object that represents the properties of the event.</param>
  27:     public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
  28:     {
  29:         try
  30:         {
  31:             TimerJobs.CopySettingsJob job = new TimerJobs.CopySettingsJob(webApp);
  32:             job.SubmitJob(false, properties.Feature.Definition.RootDirectory);
  33:         }
  34:         catch (Exception ex)
  35:         {
  36:             Logger.WriteException(ex);
  37:         }
  38:     }
  39:  
  40:     public override void FeatureInstalled(SPFeatureReceiverProperties properties)
  41:     {
  42:         /* no op */
  43:     }
  44:     public override void FeatureUninstalling(SPFeatureReceiverProperties properties)
  45:     {
  46:         /* no op */
  47:     }
  48: }

Notice that within the FeatureActivated method I'm getting a reference to the SPWebApplication object and passing that to a CopySettingsJob class which is our timer job that will do all the work.  On the FeatureDeactivating event you can see similar code but I'm passing in false instead of true to the Submit method.  The Boolean value indicates whether we are activating or deactivating our Feature.  I'm also passing in the path to the Feature folder in the 12 hive as that is where our source files are located.

Lets look at the timer job class now:

   1: public class CopySettingsJob : SPJobDefinition
   2: {
   3:     private const string KEY_ACTIVATING = "Activating";
   4:     private const string KEY_FEATUREFOLDER = "FeatureFolder";
   5:     private const string JOB_NAME = "job-settings-copy-";
   6:     private static readonly string jobId = Guid.NewGuid().ToString();
   7:  
   8:     public CopySettingsJob() : base() { }
   9:  
  10:     /// <summary>
  11:     /// Initializes a new instance of the <see cref="CopySettingsJob"/> class.
  12:     /// </summary>
  13:     /// <param name="webApp">The web app.</param>
  14:     public CopySettingsJob(SPWebApplication webApp)
  15:         : base(JOB_NAME + jobId, webApp, null, SPJobLockType.None)
  16:     {
  17:         Title = "Copy Settings Job";
  18:     }
  19:  
  20:     /// <summary>
  21:     /// Executes the job definition.
  22:     /// </summary>
  23:     /// <param name="targetInstanceId">For target types of <see cref="T:Microsoft.SharePoint.Administration.SPContentDatabase"></see> this is the database ID of the content database being processed by the running job. This value is Guid.Empty for all other target types.</param>
  24:     public override void Execute(Guid targetInstanceId)
  25:     {
  26:         Logger.WriteInformation(string.Format("Starting {0} timer job.", Name));
  27:  
  28:         try
  29:         {
  30:             string settingsFilePath = Path.Combine(Properties[KEY_FEATUREFOLDER].ToString(), "Files\\Settings.config");
  31:             string targetPath = Path.Combine(WebApplication.IisSettings[SPUrlZone.Default].Path.ToString(), "Settings.config");
  32:             if ((bool)Properties[KEY_ACTIVATING])
  33:             {
  34:                 Logger.WriteInformation(string.Format("Copying file from \"{0}\" to \"{1}\".", settingsFilePath, targetPath));
  35:                 File.Copy(settingsFilePath, targetPath, true);
  36:             }
  37:             else
  38:             {
  39:                 Logger.WriteInformation(string.Format("Deleting file from \"{0}\"", targetPath));
  40:                 File.Delete(targetPath);
  41:             }
  42:         }
  43:         catch (Exception ex)
  44:         {
  45:             Logger.WriteException(ex);
  46:             return;
  47:         }
  48:         Logger.WriteSuccessAudit(string.Format("Timer job {0} completed successfully", Name));
  49:     }
  50:  
  51:     /// <summary>
  52:     /// Submits the job.
  53:     /// </summary>
  54:     /// <param name="activating">if set to <c>true</c> [activating].</param>
  55:     public void SubmitJob(bool activating, string featureFolder)
  56:     {
  57:         Properties[KEY_ACTIVATING] = activating;
  58:         Properties[KEY_FEATUREFOLDER] = featureFolder;
  59:         Schedule = new SPOneTimeSchedule(DateTime.Now);
  60:         Title += " (" + jobId + ")";
  61:         Update();
  62:     }
  63: }

As you can see the code simply stores the Feature folder as a property and then sets a one-time schedule.  When the code runs it copies the source file to the target.  Because we're using an SPJobLockType value of "None" in the constructor the code will execute on every server (set it to "Job" if you want it to run on just the server in which the Feature was actually activated).

Of course the code above isn't very generic as it hard codes the settings.config file which isn't very reusable but I wanted to keep this sample nice and simple.  A better approach would be to require an either an XML file to be stored in the Feature folder and then read by the timer job or have the SubmitJob method take in parameters that describe what files to move and where to move them.

One key thing to remember is that this code will run once on each server for every web application on which the Feature has been activated.  If you need a Farm scoped Feature because perhaps you are copying the noise words file for instance then you'll want to change the constructor of the timer job to take in an SPService object and change the FeatureActivated method as shown below:

   1: try
   2: {
   3:     string featurePath = properties.Feature.Definition.RootDirectory;
   4:  
   5:     SPTimerService timerService = SPFarm.Local.TimerService;
   6:     if (null == timerService)
   7:     {
   8:         throw new SPException("The Farms timer service cannot be found.");
   9:     }
  10:     TimerJobs.CopySettingsJob job = timerService.JobDefinitions.GetValue<TimerJobs.CopySettingsJob>(TimerJobs.CopySettingsJob.JOB_NAME);
  11:     if (null == job)
  12:     {
  13:         job = new TimerJobs.CopySettingsJob(timerService);
  14:     }
  15:     job.SubmitJob(true, properties.Feature.Definition.RootDirectory);
  16: }
  17: catch (Exception ex)
  18: {
  19:     Logger.WriteException(ex);
  20: }

Hopefully this simple example helps you to solve your file deployment challenges.

1 comment:

Jason Venema said...

Perfect timing, this is exactly what I need to do. Thanks for the great post!