Monday, February 24, 2014

Deleting Directories Whose Contents Have Long Names From C#

Deleting Directories Whose Contents Have Long Names In C#

Goal:

To use C# to delete directories that contain files whose fully qualified names are longer than 260 characters.

Deleting a Directory That Contains Contents With Short Names

Suppose we need to delete a directory and all its contents from C#. If the fully qualified names of all its contents are less than 260 characters, then the standard DirectoryInfo.Delete method is sufficient, as we can see from the following test case.

LongFileNamesTest.cs
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.IO;

namespace Timezra.LongFileNames
{
  [TestClass]
  public class LongFileNamesTest
  {
    [TestMethod]
    public void ShouldDeleteALocalDirectoryWithContents()
    {
      var directory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()));
      var fileName = Path.Combine(directory.FullName, Path.GetRandomFileName());
      System.IO.File.Create(fileName).Dispose();

      directory.Delete(true); // Regular Directory Delete

      Assert.IsFalse(directory.Exists);
    }
  }
}

Deleting a Directory That Contains Contents With Long Names

Unfortunately, if the directory contains contents whose fully-qualified names are longer than 260 characters, this same method will fail.

LongFileNamesTest.cs
    [TestMethod]
    public void ShouldDeleteALocalDirectoryWithContentsThatHaveLongNames()
    {
      const int longFileNameLength = 247;
      var directory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()));
      var fileName = Path.Combine(directory.FullName, new string('z', longFileNameLength) + ".txt");
      System.IO.File.Create(fileName).Dispose();

      directory.Delete(true); // Regular Directory Delete

      Assert.IsFalse(directory.Exists);
    }

This test case will fail with a message similar to this because we cannot even create the file with a fully-qualified name longer than 260 characters.

Test Failed - ShouldDeleteALocalDirectoryWithContentsThatHaveLongNames
Result Message:
Test method Timezra.LongFileNames.LongFileNamesTest.ShouldDeleteALocalDirectoryWithContentsThatHaveLongNames threw exception:
System.IO.PathTooLongException: The specified path, file name, or both are too long. The fully qualified file name must be less than 260 characters, and the directory name must be less than 248 characters.
Result StackTrace:
at System.IO.PathHelper.GetFullPathName()
  at System.IO.Path.NormalizePath(String path, Boolean fullCheck, Int32 maxPathLength)
  at System.IO.FileStream.Init(String path, FileMode mode, FileAccess access, Int32 rights, Boolean useRights, FileShare share, Int32 bufferSize, FileOptions options, SECURITY_ATTRIBUTES secAttrs, String msgPath, Boolean bFromProxy, Boolean useLongPath, Boolean checkHost)
  at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share, Int32 bufferSize)
  at System.IO.File.Create(String path)
  at Timezra.LongFileNames.LongFileNamesTest.ShouldDeleteALocalDirectoryWithContentsThatHaveLongNames()

Even if we managed to create the file outside of the test case (say by extracting an archive that contains files with long names), then we would see a more cryptic error on delete.

Test Failed - ShouldDeleteALocalDirectoryWithContentsThatHaveLongNames
Result Message:
Test method Timezra.LongFileNames.LongFileNamesTest.ShouldDeleteALocalDirectoryWithContentsThatHaveLongNames threw exception:
System.IO.DirectoryNotFoundException: Could not find a part of the path....
Result StackTrace:
at System.IO.Directory.DeleteHelper(String fullPath, String userPath, Boolean recursive, Boolean throwOnTopLevelDirectoryNotFound)
  at System.IO.Directory.Delete(String fullPath, String userPath, Boolean recursive, Boolean checkHost)
  at System.IO.DirectoryInfo.Delete(Boolean recursive)
  at Timezra.LongFileNames.LongFileNamesTest.ShouldDeleteALocalDirectoryWithContentsThatHaveLongNames()

We can get around this limit on creating and deleting long files by using the Microsoft Scripting Runtime and a special prefix on our file path. First, our project needs a Reference to COM -> Microsoft Scripting Runtime. Then we can use methods on the FileSystemObject, along with the \\?\ prefix to our path.

LongFileNamesTest.cs
    [TestMethod]
    public void ShouldDeleteALocalDirectoryWithContentsThatHaveLongNames()
    {
      const int longFileNameLength = 247;
      var directory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()));
      var fileName = Path.Combine(directory.FullName, new string('z', longFileNameLength) + ".txt");
      var fso = new Scripting.FileSystemObject();
      fso.CreateTextFile(@"\\?\" + fileName).Close();

      fso.DeleteFolder(@"\\?\" + directory.FullName, true); // Local Directory Delete When Contents Have Long Names

      Assert.IsFalse(directory.Exists);
    }

Deleting a Remote Directory That Contains Contents With Long Names

Suppose we also need to support the deletion of remote directories with contents that are longer than 260 characters. The same principle applies, but our prefix is slightly different, i.e., \\?\UNC. Fortunately, we can test this by converting our local directory path to a UNC directory path.

LongFileNamesTest.cs
    [TestMethod]
    public void ShouldDeleteARemoteDirectoryWithContentsThatHaveLongNames()
    {
      const int longFileNameLength = 247;
      var directory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()));
      var fileName = Path.Combine(directory.FullName, new string('z', longFileNameLength) + ".txt");
      var fso = new Scripting.FileSystemObject();
      fso.CreateTextFile(@"\\?\" + fileName).Close();

      var uncDirectoryName = @"\\" + System.Environment.MachineName + @"\" + directory.FullName.Replace(':', '$');
      fso.DeleteFolder(@"\\?\UNC" + uncDirectoryName.Substring(1), true); // Remote Directory Delete When Contents Have Long Names

      Assert.IsFalse(directory.Exists);
    }