SPFileVersion: Handling SharePoint file versions programmatically




I recently had a requirement around linking to and downloading specific versions of documents stored in SharePoint. There is some rather quirky behaviour around SPFileVersion and how this is achieved and I felt it warranted a post.

Versions

The first thing to be aware of is that there are two distinct version collections: SPFileVersionCollection and SPListItemVersionCollection. These collections can be accessed via the respective assets as you would expect (SPFile.Versions and SPListItem.Versions) and, as is the way in SharePoint, are enumerations of SPFileVersion and SPListItemVersion objects, respectively. These objects, which represent the individual versions, have a number of similar properties and methods but do not share a base class nor an interface.

In order to access specific versions of documents, the approach I took was to store the VersionLabel (a property on both SPFileVersion and SPListItemVersion) and then retrieve the version by using this value in conjunction with GetVersionFromLabel (a method on both SPFileVersion and SPListItemVersion). This works but with a number of caveats:

  1. SPFileVersionCollection does NOT contain an entry for the latest version. This means that if the document has only a single version then this collection will be empty. If you want to access the latest version you must verify that the version label refers to the latest version by comparing it to the first entry in the SPListItemVersionCollection and if it is then access the SPFile directly. If you are accessing properties that are also present on an SPListItemVersion (ie not opening a stream) you can use that collection instead but you must also take note of the next point.
  2. SPListItemVersion.Url behaves unexpectedly in some situations. Assumedly this is to support directing underprivileged users away from minor versions that they are not allowed to see (although I don’t see how it achieves this), the Url for minor versions will be equal to that of the latest minor version when there is no published version. Always use SPFileVersion.Url.
  3. Minor versions get promoted when published, breaking links to the minor version. Example – You have a document currently at version 0.2, with a previous 0.1 version. When you publish this document you still only have two versions: 0.1 and 1.0. You will now fail to locate version 0.2. Don’t be fooled into thinking that using the VersionId property will solve this issue. It simply represents the the version label as an integer (+1 for each minor version, +512 for each major version). The only way I could find to handle this case is to introduce the doc store version which is stored in the property bag of the SPFile: SPFile.Properties["vti_docstoreversion"]. You can see how this works by looking at the code examples I have included but if this specific case isn’t going to be an issue for you then don’t use it and enjoy cleaner code (see here). Also note that the doc store version is only stored in the SPFile property bag and not in the SPListItem property bag.

I have included some code to illustrate how this may be implemented:

public static string GetUrlForVersion(this SPListItem item, string versionLabel, int? docStoreVersion = null)
{
  string relatedDocumentVersionUrl = string.Empty;

  // This check is done in SPFile.GetFileVersion (so we could only perform it when we know we don't have a file)
  // but we do it here first to save effort in the case that we aren't working with a file (to save that more expensive check).
  if (string.IsNullOrEmpty(versionLabel))
  {
    relatedDocumentVersionUrl = item.Url;
  }
  else
  {
    SPFile file = item.File;
    if (file != null)
    {
      SPFileVersion fileVersion;
      bool refersToLatestVersion = file.GetFileVersion(versionLabel, out fileVersion, docStoreVersion);
      if (refersToLatestVersion)
      {
        relatedDocumentVersionUrl = item.Url;
      }
      else if (fileVersion != null)
      {
        relatedDocumentVersionUrl = fileVersion.Url;
      }
    }
    if (string.IsNullOrEmpty(relatedDocumentVersionUrl))
    {
      var itemVersion = item.Versions.GetVersionFromLabel(versionLabel);
      if (itemVersion != null)
      {
        relatedDocumentVersionUrl = itemVersion.Url;
      }
    }
    if (string.IsNullOrEmpty(relatedDocumentVersionUrl))
    {
      throw new SPException(string.Format("The '{0}' version of document '{1}' cannot be found. It may have been deleted.", versionLabel, item.Name));
    }
  }
  return relatedDocumentVersionUrl;
}

public static Stream OpenBinaryStreamForVersion(this SPFile file, string versionLabel, int? docStoreVersion)
{
  Stream bStream = null;

  SPFileVersion fileVersion;
  bool refersToLatestVersion = file.GetFileVersion(versionLabel, out fileVersion, docStoreVersion);
  if (refersToLatestVersion)
  {
    bStream = file.OpenBinaryStream();
  }
  else if (fileVersion != null)
  {
    bStream = fileVersion.OpenBinaryStream();
  }
  return bStream;
}

public static int? GetDocStoreVersion(this SPListItem item)
{
  return GetDocStoreVersion(item.File);
}

public static int? GetDocStoreVersion(this SPFile file)
{
  int? docStoreVersion = null;
  if (file != null)
  {
    docStoreVersion = (int)file.Properties[Constants.Properties.VTI_DOCSTOREVERSION];
  }
  return docStoreVersion;
}

public static int? GetDocStoreVersion(this SPFileVersion fileVersion)
{
  int? docStoreVersion = null;
  if (fileVersion != null)
  {
    docStoreVersion = (int)fileVersion.Properties[Constants.Properties.VTI_DOCSTOREVERSION];
  }
  return docStoreVersion;
}

/// If the fileversion with the provided versionLabel has been promoted via publishing, then we will return the
/// fileversion that has been published (that will not have a mathing versionLabel).
/// NOTE: This can never return the current version as it is not in the SPFile.Versions collection.
/// versionLabel: The version of the document to get
/// docStoreVersion: If provided, will get the version using this value (this doesn't change when a version is published), else will attempt to figure it out.</param>
/// returns: If the file version is the latest version (need to access the SPFile directly)
public static bool GetFileVersion(this SPFile file, string versionLabel, out SPFileVersion fileVersion, int? docStoreVersion = null)
{
  bool refersToLatestVersion = false;
  fileVersion = null;

  int docStoreVersionLatest = file.GetDocStoreVersion().Value;

  if (docStoreVersion.HasValue && docStoreVersion.Value == docStoreVersionLatest)
  {
    // The doc store version matches the current version
    refersToLatestVersion = true;
  }
  else if (string.IsNullOrEmpty(versionLabel) || versionLabel == file.UIVersionLabel)
  {
    // The version we are looking for matches the current version
    refersToLatestVersion = true;
  }
  else if (docStoreVersionLatest < 3)
  {
    // The first doc store version number is 2, therefore it is the only version that there has been
    refersToLatestVersion = true;
  }
  else if (docStoreVersion.HasValue)
  {
    // If a doc store version has been provided we can find it in the previous versions
    fileVersion = file.Versions.Cast<SPFileVersion>().FirstOrDefault(v => v.GetDocStoreVersion() == docStoreVersion.Value);
  }
  else
  {
    // Get previous version by version label
    fileVersion = file.Versions.GetVersionFromLabel(versionLabel);

    if (fileVersion == null)
    {
      // We don't have the docstore version to find but if the previous verions docstore version minus the next version docstor version = 1, then it has been published.
      // Note that this only covers the case where the version gets published, when multiple versions have been deleted this may fail to return a version.
      // ... I have not included this code for brevity ...
    }
  }
  if (!refersToLatestVersion && fileVersion == null)
  {
    string errMsg = "Failed to locate specific file version, URL: {0}, VersionLabel: {1}, DocStoreVersion: {2}";
    string docStoreVersionString = docStoreVersion.HasValue ? docStoreVersion.Value.ToString() : "Unspecified";
    Log.WriteLog(string.Format(errMsg, file.ServerRelativeUrl, versionLabel, docStoreVersionString), TraceSeverity.High, DiagnosticServiceLogger.LogCategories.GroupsAndProjects);
  }
  return refersToLatestVersion;
}

Please leave a comment and if you want to keep reading more about SharePoint don’t forget to subscribe.




One thought on “SPFileVersion: Handling SharePoint file versions programmatically”

Leave a Reply

Your email address will not be published.