How do I get from a file path to the volume that holds it?

Raymond Chen

Say you have the path to a file and you want to access the volume that the file resides on.

Warning: All error checking is removed for expository purposes.

The first step on our journey is getting from the path to the volume mount point. This tells us where the root of the volume got inserted into the namespace.

TCHAR volumePath[MAX_PATH]; // for expository purposes
GetVolumePathName(filePath, volumePath, ARRAYSIZE(volumePath));

This information might be useful in its own right, but for us, it’s just a stepping stone to the next piece of information: The volume name.

TCHAR volumeName[MAX_PATH]; // for expository purposes
GetVolumeNameForVolumeMountPoint(volumePath, volumeName, ARRAYSIZE(volumeName));

The volume name is returned in the form \\?\Volume{guid}\, with a trailing backslash. Note that this call will fail if the path is not a local drive.

Now things get weird.

If you pass that path to the CreateFile function with the trailing backslash intact, then you are opening a handle to the root directory of the volume.

If you pass that path to the CreateFile function with the trailing backslash removed, then you are opening a handle to the volume itself.

Depending on which operation you want to perform on the volume, you either must have or must not have that trailing backslash.

In our case, we want to open a handle to the volume itself, so we need to remove that trailing backslash. The call to CreateFile looks like this:

HANDLE handle = CreateFile(volumeNameWithoutTrailingBackslash,
    0, /* no special access requested */
    FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
    nullptr, /* no custom security */
    OPEN_EXISTING,
    FILE_FLAG_BACKUP_SEMANTICS,
    nullptr); /* template */

Volume query operations do not require any specific level of access, so we’ll ask for no special access. Which is good, because regular non-elevated code doesn’t really have much in the way of special access to volumes. You won’t able to get GENERIC_READ, much less GENERIC_WRITE. (You’ll be able to get FILE_READ_ATTRIBUTES, if that’s any consolation.)

You also need to request backup semantics in order to open a volume.

We can put all of this together into a function called, say, Get­Volume­Handle­For­File. For RAII, I’m going to use wil.

wil::unique_hfile GetVolumeHandleForFile(PCWSTR filePath)
{
  wchar_t volumePath[MAX_PATH];
  THROW_IF_WIN32_BOOL_FALSE(GetVolumePathName(filePath,
                                volumePath, ARRAYSIZE(volumePath)));

  wchar_t volumeName[MAX_PATH];
  THROW_IF_WIN32_BOOL_FALSE(GetVolumeNameForVolumeMountPoint(volumePath,
                                volumeName, ARRAYSIZE(volumeName)));

  auto length = wcslen(volumeName);
  if (length && volumeName[length - 1] == L'\\')
  {
    volumeName[length - 1] = L'\0';
  }

  wil::unique_hfile result{ CreateFile(volumeName, 0,
                FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
                nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr) };
  THROW_LAST_ERROR_IF(!result);
  return result;
}

Now that you have the volume handle, you can ask the volume for information. We’ll look at that next time.

6 comments

Discussion is closed. Login to edit/delete existing comments.

  • 紅樓鍮 0

    Are the Windows people still not using std::array in place of C-style arrays?

    • Danstur 0

      If you can come up with a single advantage in the given code of using std::array instead of a normal array I’m all ears.

      Otherwise you simply make the code more complicated for no good reason. Most people would think this is not a good thing even if it allows you to play with a new toy.

      • 紅樓鍮 0

        you can call .size() on an array to get its size

        I don’t think std::array is in any way more complicated than C-style arrays; it’s the converse in fact. It has none of the surprising behaviors (decaying, not copy constructible/assignable) that C-style arrays have. I think most people will agree volumePath.data(), volumePath.size() is more comprehensible than volumePath, ARRAYSIZE(volumePath) which involves both decaying and a non-standard macro.

        • Raymond ChenMicrosoft employee 0

          I try to stick to C-friendly code, using C++ only where necessary (like for RAII), because it reduces confusion. Note also that .size() returns a size_t, so you’re going to have to do a volumePath.data(), static_cast<DWORD>(volumePath.size()), which is getting unwieldy.

  • Dan Bugglin 0

    I’ve tried to do this in .NET Framework before, and was surprised to find there seems to be no way to get the volume mount point for an arbitrary path, and DriveInfo only supports drive letters. I ended up wrapping some WinAPI calls with PInvoke.

    I used GetVolumePathName, but then I jumped right to GetVolumeInformation rather than getting the actual volume name and opening a handle. I’ll be interested in seeing what you’re doing and what sort of information you can pull (what I got was enough for me IIRC).

    I just checked .NET Core and it still seems to have that problem. The docs for DriveInfo still say it only accepts drive letters. Odd since I would think they would want to rework that given the opportunity since MacOS and Linux both primarily mount volumes in folders.

  • Mystery Man 0

    The “GetVolumePathNameW function” page on Microsoft Docs looks damaged. Its examples are missing too.

Feedback usabilla icon