I have been implementing workarounds since SharePoint 2007 to overwrite an unghosted (or customized) file that already exists at a specified URL for a File element within a Module element of a Feature element manifest (or site definition which I stopped using extensively in SharePoint 2010 preferring web templates).
Updating existing files when upgrading our SharePoint custom Features can be accomplished by provisioning ghosted files using the Type attribute set to Ghostable or GhostableInLibrary. For unghosted (or customized) files a common trap is setting IgnoreIfAlreadyExists=”TRUE” expecting existing files to be overwritten. The MSDN Library documentation description for the File element IgnoreIfAlreadyExists attribute contributes to the confusion because it does not behave as expected:
Optional Boolean. TRUE to provision the view even if the file already exists at the specified URL; otherwise FALSE.
To overcome the limitation of not being able to update unghosted files using the IgnoreIfAlreadyExists attribute which is a very common scenario for SharePoint Online (where we do not have a reference to ghosted files on the file system) read on…
There are two possible approaches depending on whether your unghosted files exist in SharePoint 2010 or SharePoint 2013. For SharePoint 2013 you can now use the ReplaceContent attribute (which is not yet documented in MSDN Library) in the file element like this:
<File Path=”Branding.css” Url=”Style Library/Branding.css” ReplaceContent=”TRUE” />
One limitation I noticed is ReplaceContent will not overwrite files that are checked-out to a different user than the person provisioning the file. To overcome this scenario in SharePoint 2013 and update unghosted files in SharePoint 2010 we need to fall-back to custom code in a feature receiver.
We can use the GetElementDefinitions method of the SPFeatureDefinition returned by SPFeatureReceiverProperties to identify Feature element files to overwrite in full trust solutions only. Identifying the Feature element files in SharePoint Online is a little bit more challenging as both the GetElementDefinitions method and RootDirectory property are not available in Sandbox solutions.
In order to read the element manifest and overwrite already existing unghosted files in SharePoint Online we need to provision the element manifest file and additional copies of the element files (remember IgnoreIfAlreadyExists=”TRUE” does not replace existing files).
<File Path=”Branding.css” Url=”Style Library/Branding.css” IgnoreIfAlreadyExist=”TRUE” />
<File Path=”Elements.xml” Url=”Branding_$SharePoint.Feature.Id$.xml” Type=”Ghostable” />
<File Path=”Branding.css” Url=”Style Library/Branding.css.copy” IgnoreIfAlreadyExist=”TRUE” />
The element manifest file is provisioned to the root folder of the parent web as {Prefix Name}_{Parent Feature ID}. $SharePoint.Feature.Id$ replaceable token is used to provide the parent feature id. If the Feature definition has multiple element manifest files that require element files to be overwritten we must provide a unique {Prefix Name} for each element manifest.
A copy of each element file to be overwritten in the element manifest is provisioned with a .copy file extension. This allows the feature receiver custom code to parse the provisioned element manifest file to identify which element files to overwrite and more importantly retrieve .copy element files to replace existing unghosted files with.
We created a helper function to process the provisioned element manifest and .copy element files to overwrite existing unghosted files:
public static void CopyFiles(SPWeb web, Guid featureID)
{
// create a regular expression pattern for the feature element manifest files
string pattern = string.Format(@”^.+_{0}.xml$”, featureID);
Regex fileNameRE = new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase);
// get the feature element manifest files from the root folder of the site
SPFile[] manifestFiles = web.RootFolder.Files.Cast<SPFile>().Where(f => fileNameRE.IsMatch(f.Name)).ToArray();
try
{
// iterate the feature element manifest files
foreach (SPFile manifestFile in manifestFiles)
{
// load the contents of the element manifest file in an XDocument
MemoryStream mStream = new MemoryStream(manifestFile.OpenBinary());
StreamReader reader = new StreamReader(mStream, true);
XDocument manifestDoc = XDocument.Load(reader, LoadOptions.None);
/ / iterate over the ‘Module’ and ‘File’ elements in the XDocument, concatenating their Url attributes in a smart way so that we grab the site relative file Url-s
string[] fileUrls = manifestDoc.Root.Elements(WS + “Module”).SelectMany(me => me.Elements(WS + “File”), (me, fe) => string.Join(“/”, new XAttribute[] { me.Attribute(“Url”), fe.Attribute(“Url”) }.Select(attr => attr != null ? attr.Value : null).Where(val => !string.IsNullOrEmpty(val)).ToArray())).Where(x => x.EndsWith(“.copy”)).ToArray();
bool copyFilesCheckedOut = false;
// Check for checked-out .copy files
foreach (string fileUrl in fileUrls)
{
string copyFileUrl = fileUrl.Substring(0, fileUrl.Length – “.copy”.Length);
SPFile file = web.GetFile(copyFileUrl);
if (file.Exists && file.CheckOutType != SPFile.SPCheckOutType.None)
{
copyFilesCheckedOut = true;
break;
}
}
// Overwrite existing unghosted files
foreach (string fileUrl in fileUrls)
{
// get the .copy file
SPFile file = web.GetFile(fileUrl);
if (!copyFilesCheckedOut)
{
// get the existing unghosted file
string copyFileUrl = fileUrl.Substring(0, fileUrl.Length – “.copy”.Length);
// SPFile.CopyTo() does not preserve file version history;
// file.CopyTo(copyFileUrl, true);
// overwrite existing unghosted file preserving version history
SPFile copyFile = web.GetFile(copyFileUrl);
copyFile.CheckOut();
copyFile.SaveBinary(file.OpenBinary());
copyFile.CheckIn(string.Empty, SPCheckinType.MinorCheckIn);
// depending on the settings of the parent document library we may need to check in and/or (publish or approve) the file
if (copyFile.Level == SPFileLevel.Draft)
{
if (copyFile.DocumentLibrary.EnableModeration)
{
copyFile.Approve(string.Empty);
}
else
{
copyFile.Publish(string.Empty);
}
}
}
// remove the .copy file so we can provision newer versions
file.Delete();
}
}
}
finally
{
// remove feature element manifest files from the site root folder so we can provision newer versions
foreach (SPFile manifestFile in manifestFiles)
{
manifestFile.Delete();
}
}
}
Finally we add the feature receiver custom code to call the helper function:
public override void FeatureActivated(SPFeatureReceiverProperties properties)
{
SPWeb web = null;
if (properties.Feature.Parent is SPWeb)
{
web = (SPWeb)properties.Feature.Parent;
}
else if (properties.Feature.Parent is SPSite)
{
web = ((SPSite)properties.Feature.Parent).RootWeb;
}
if (web!=null)
{
CopyFiles(web, properties.Feature.DefinitionId);
}
}
The feature receiver custom code approach for SharePoint 2013 allows us to validate all the element files to be overwritten are checked-in before replacing them. This is particularly important for client-side Apps where failure to overwrite all App .html, .js and .css files can lead to unpredictable behaviour. The CopyFiles function can also be modified to override checked out files and force overwrites.
A note of caution about using the feature receiver custom code in SharePoint Online. Sandboxed code execution during a single request cannot exceed 30 seconds or the user code service will recycle the application domain and the request will return an error. This may require splitting large numbers of element files that need to be overwritten into multiple Features and element manifests.
If you need to overwrite element files in SharePoint 2013 start using the new ReplaceContent attribute unless you are experiencing issues with overwriting checked-out files. If you need to overwrite element files in SharePoint 2010 I hope you found this blog post useful.
Excellent article, thank you.
I have a need to update some files that became “unghosted” and I’m hoping that your code is the solution, but one line in the CopyFiles method is causing me problems:
string[] fileUrls = manifestDoc.Root.Elements(WS + “Module”).SelectMany(me => me.Elements(WS + “File”), (me, fe) => string.Join(“/”, new XAttribute[] { me.Attribute(“Url”), fe.Attribute(“Url”) }.Select(attr => attr != null ? attr.Value : null).Where(val => !string.IsNullOrEmpty(val)).ToArray())).Where(x => x.EndsWith(“.copy”)).ToArray();
What is the WS referring to? When I try to implement the method, VS tells me it finds no reference to WS.
Thanks again.
WS in this case refers the xml namespace for the nodes in the Elements.xml file. Try adding the following to the top of the CopyFiles method:
XNamespace WS = “http://schemas.microsoft.com/sharepoint/”;
excellent solution, thank very much.