Handling long paths in .net

If you program in .net you should know by now that .net does not support long paths: Long Paths in .NET http://blogs.msdn.com/b/bclteam/archive/2007/02/13/long-paths-in-net-part-1-of-3-kim-hamilton.aspx

So why does this rarely cause problems?

The reality is that long paths are rare. This is due to the fact that Windows (mostly) prevents you from creating long paths and also that it takes a lot of characters to create a long path. So in most scenarios it is fine to just pretend they don’t exist. Then when some process fails due to long paths we can come in, manually shorten the path and rerun the process.

What about when we expect them to exist?

There are certain scenarios where long paths can be common. The main one I have experienced is mapped network drives in a Windows network. For example, a user’s profile might exist on a network drive.

\\server\location\profiles\username

That path is then mapped to a drive letter on the users’ machine, for example, “H:\”. So from the perspective of the user’s machine, the path is extremely short. This means a path on the user’s machine might not be a long path, but when you use that path from the perspective of the server you may hit the long path issue.

So how do we work with these paths in .net?

If you are performing simple operations (delete, move, rename etc.) you can use some low level Windows APIs to deal with long paths. For example DeleteFile http://www.pinvoke.net/default.aspx/kernel32/deletefile.html

[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool DeleteFile(string lpFileName);

Or, you can use one of the libraries that wrap these APIs like the "Base Class Libraries" project from Microsoft http://bcl.codeplex.com/. The problem with these types of libraries is that they only work if you are doing all the file manipulation in your code. They do not support the scenario where another third party libary is doing the file manipulation. For example, you are using a .net zip library. The zip library does not respect long paths and will fail when it encounters them.

So the other option is to shorten the path in the same way that caused the problem, mapping a drive to a folder. To do this we can call the Windows API DefineDosDevice http://pinvoke.net/default.aspx/kernel32/DefineDosDevice.html which is the equivalent to the Windows “subst” command http://en.wikipedia.org/wiki/Subst

A C# wrapper around DefineDosDevice

So a common class I use is a wrapper around DefineDosDevice. The idea of this wrapper class is to map a drive to part of the long path, hence shortening the path, and then dispose of it. During this time any operations can be performed directly against the (shorter) mapped drive path.

public class Subster : IDisposable
{
    char? driveLetter;

    public char DriveLetter
    {
        get { return driveLetter.Value; }
    }

    public Subster(string path)
    {
        driveLetter = GetDriveLetter();
        AddSubst(path);
    }

    char GetDriveLetter()
    {
        var existingDrives = DriveInfo.GetDrives().Select(x => x.Name[0]).ToList();
        for (var letter = 'A'; letter <= 'Z'; letter++)
        {
            var tempDriveLetter = letter;
            if (!existingDrives.Contains(tempDriveLetter))
            {
                return tempDriveLetter;
            }
        }
        throw new Exception("All drive letters reserved.");
    }


    [DllImport("kernel32.dll", SetLastError = true)]
    static extern bool DefineDosDevice(uint dwFlags, string lpDeviceName, string lpTargetPath);

    void AddSubst(string path)
    {
        if (!DefineDosDevice(0, driveLetter + ":", path))
        {
            throw new Win32Exception();
        }
    }

    void RemoveSubst(char driveLetter)
    {
        if (!DefineDosDevice(0x00000002, driveLetter + ":", null))
        {
            throw new Win32Exception();
        }
    }

    public void Dispose()
    {
        if (driveLetter != null)
        {
            RemoveSubst(driveLetter.Value);
        }
    }
}

This class can be used like this

//place in a using so Subster knows when to un-map the drive
using (var subster = new Subster(@"D:\LongDirectoryName"))
{
    Debug.WriteLine(subster.DriveLetter);
    //peform operations against the shorter "DriveLetter"
}

The solution source can be found here http://code.google.com/p/simonsexperiments/source/browse/ under SubstTests

Posted by: Simon Cropp
Last revised: 24 Dec, 2011 05:23 AM History

Comments

No comments yet. Be the first!

No new comments are allowed on this post.