The problem I needed to solve was this: Two users were independently creating the same document. Once this was discovered, one of them was chosen to be the master document. But the users didn’t want to lose the other document, even though it was not chosen as the master. The users wanted to merge this document into the history of the master document. Technically, it is not possible to alter the history of any document in SharePoint. So I decided we would create a new third document, which was generated based on the two documents to be merged. By using the SharePoint API, I could control the dates and merge the histories of the two documents into a third one. Once that was done, the original documents could be deleted.
The basic algorithm works like this:
- Choose two documents, one to be the master, one to merge into the master
- Fetch the history of both items into a
List<>
- Sort the list by date
- Loop through the list and write the historical files to a new list item, creating a new history
- Write the current version of the merge document to the new list item
- Write the current version of the master document to the new list item, including the properties
I did try to keep the properties to all the previous versions, but there was some resulting weirdness that went away when that part was removed. Since the metadata wasn’t a strict requirement, but a nice-to-have, I didn’t investigate that any further. YMMV. Here is the code for the algorithm:
protected void MergeDocuments(string siteUrl, string listName,
string keepDocName, string mergeDocName, string newName)
{
var site = new SPSite(siteUrl);
var web = site.OpenWeb();
var list = web.Lists[listName];
var keepUrl = string.Format("{0}/{1}",
list.RootFolder.ServerRelativeUrl, keepDocName);
var discardUrl = string.Format("{0}/{1}",
list.RootFolder.ServerRelativeUrl, mergeDocName);
var keep = web.GetListItem(keepUrl);
var merge = web.GetListItem(discardUrl);
var allVersions = keep.Versions.Cast<SPListItemVersion>().ToList();
allVersions.AddRange(merge.Versions.Cast<SPListItemVersion>());
allVersions.Sort(new SortVersionsByDate());
var newUrl = string.Format("{0}/{1}",
list.RootFolder.ServerRelativeUrl, newName);
foreach (var version in allVersions)
{
if (version.IsCurrentVersion)
continue;
var oldFile = version.ListItem.File.Versions.GetVersionFromID(version.VersionId);
var oldestFile = list.RootFolder.Files.Add(newUrl,
oldFile.OpenBinaryStream(), null, version.CreatedBy.User,
version.CreatedBy.User, version.Created, version.Created,
oldFile.CheckInComment, true);
UpdateListItem(oldestFile.Item, version.Created,
version.Created);
list.Update();
}
WriteFileToSharePoint(list, merge, newUrl, false);
WriteFileToSharePoint(list, keep, newUrl, true);
}
protected void UpdateListItem(SPListItem item, DateTime created, DateTime modified)
{
item[SPBuiltInFieldId.Modified] = modified.ToLocalTime();
item[SPBuiltInFieldId.Created] = created.ToLocalTime();
item.UpdateOverwriteVersion();
}
protected void WriteFileToSharePoint(SPList list, SPListItem doc, string newUrl, bool final)
{
Hashtable props = null;
if (final)
props = doc.Properties;
var lastMergedFile = list.RootFolder.Files.Add(newUrl,
doc.File.OpenBinaryStream(), props, doc.File.Author,
doc.File.Author, doc.File.TimeCreated, doc.File.TimeCreated,
doc.File.CheckInComment, true);
UpdateListItem(lastMergedFile.Item, doc.File.TimeCreated,
doc.File.TimeLastModified);
list.Update();
}
I deployed this as a PowerShell cmdlet, which is explained here and here. Essentially, that route was chosen because the merging was to be performed on request by an administrator. It would be easy enough to wire this up to a ribbon command in the UI though.