Get more out of the file system


Applies to:
  • Disk management
  • C#
  • Interop
  • P/Invoke
  • System.Management
  • Visual Studio .NET 2002
Summary: About a year ago I wrote an initial article about how to interact with files and folders in .NET. This article elaborates on the topics discussed there. Specifically, I want to go further this time when it comes to different ways for accessing and managing disk drives (floppy, hard disk, network or other) in your computer system.

Download the sample code for this article:

Unsolved

A number of scenarios were left unmentioned in my initial article on filemanagent in .NET. I showed how to build a list of available diskdrives, but not how to get more information about those drives. How can you check the drive type (floppy, CD-ROM,...) or how do you determine the amount of available space? In this follow-up article, three possibilities are shown to obtain advanced information about storage media, along with pros and cons for each.

Catching up

The System.IO namespace does not contain a Drive object to gather specific information for storage media. While System.IO.Directory has a static method named GetLogicalDrives, it returns a string array. More than a simple list with drive-letters is not possible using this approach.
In order to get more information about a drive, you can import the Microsoft Scripting library into your project. Make a reference to the COM component “Microsoft Scripting Library” and indicate that you want to use the Scripting namespace:
using Scripting;

Now you have the Scripting.Drive object at your disposal, which provides significantly more information than the static System.IO.Directory.GetLogicalDrives() method:


A comprehensive list of available drives with their properties can be obtained as follows:
Console.WriteLine("{0,-5} {1,-6} {2,-5} {3,-20} {4,-15} {5,-15}",
"Drive", "Type", "Ready", "Volume name",
"Total space", "Available space");
Console.WriteLine("-----+------+-----+"
+ "--------------------+---------------+----------------");

FileSystemObject fso = new FileSystemObject();
foreach (Drive dr in fso.Drives)
{
  Console.Write(" {0,-3} {1,-6} ",
    dr.DriveLetter, dr.DriveType);
  if (dr.IsReady)
  {
    Console.WriteLine(" Yes {0,-20} {1,15} {2,15}",
      dr.VolumeName,
      Convert.ToDouble(dr.TotalSize).ToString("#,#"),
      Convert.ToDouble(dr.AvailableSpace).ToString("#,#"));
  }
  else
  {
    Console.WriteLine(" No");
  }
}



The Scripting library is a COM object. This means that various convertions need to take place to send objects and variables back and forth between COM and .NET (marshaling). Furthermore, there is no guarantee that the library will still be supported in .NET in the future. The Scripting library still imposes implicit restrictions: you can only access the features that are exposed to you. When a new API call is added to Windows, you will not have access to it until Microsoft comes out with a new version of the library. At that time, you will be able to update the library (as well as perform the update on each computer system that runs your software).

Application Programmer’s Interface – API

There is an obvious alternative to the Scripting library. After all, the library only operates as a broker sending back and forth messages between the client using it and the underlying Windows Application Programmer’s Interface – API. The reason why we started off using the library is that it is easy to use and that some programming languages have difficulties directly calling upon the API. This is no longer the case in .NET. With the [DllImport] attribute you can access virtually every part of the Windows API (Kernel, GDI and User). To refurbish the above code using API-calls, you need a number of functions: GetLogicalDrives(), GetVolumeInformation(), GetFreeDiskspaceEx() en GetDriveType(). These are all exposed through kernel32.dll.
In order to use these functions in your project, you need to reference them in your code using the DllImport attribute, which is defined in the System.Runtime.InteropServices namespace.
using System.Runtime.InteropServices;

Now you are all set to import the required API-calls. Each one needs to be specified seperately:
[DllImport("kernel32.dll")]
  static extern uint GetLogicalDrives();
[DllImport("kernel32.dll" , CharSet=CharSet.Auto)]
  static extern bool GetVolumeInformation(string vol,
    StringBuilder name, uint name_size,
    out uint serial, out uint max_fn_length, out uint flags,
    StringBuilder fs, uint fs_size);
[DllImport("kernel32.dll" , CharSet=CharSet.Auto)]
  static extern uint GetDriveType(string vol);
[DllImport("kernel32.dll" , CharSet=CharSet.Auto)]
  static extern int GetDiskFreeSpaceEx(string vol,
    out ulong available, out ulong total, out ulong free);

The following list helps converting API to .NET datatypes:
API datatype.NET datatypeC# datatype
BOOLSystem.Booleanbool
DWORDSystem.UInt32uint
LPCTSTRSystem.Stringstring
LPDWORDSystem.UInt32 (by-ref)uint (by-ref)
LPTSTRSystem.Text.StringBuilderSystem.Text.StringBuilder
PULARGE_INTEGERSystem.UInt64Ulong

More information about interacting with the Windows API from within .NET (P/Invoke) can be found at http://msdn.microsoft.com/msdnmag/issues/03/07/NET/.
In order to use the StringBuilder class, you will also need the System.Text namespace:
using System.Text;

To get the same output as in our first example we write the following code:
Console.WriteLine("{0,-5} {1,-10} {2,-5} {3,-20} {4,-15} {5,-15}",
  "Drive", "Type", "Ready", "Volume name",
  "Total space", "Available space");
Console.WriteLine("-----+----------+-----+"
  + "--------------------+---------------+----------------");
uint bitmask = GetLogicalDrives();

for (int i = 0; i < 31; i++)
{
  if ((bitmask & 1) == 1)
  {
    char driveletter = (char)(i+65);

    StringBuilder volumename = new StringBuilder(256);
    uint serialnumber = 0, fn_length = 0, flags = 0;
    StringBuilder fstype = new StringBuilder(256);

    uint drivetype = GetDriveType(driveletter + @":\");

    bool res = GetVolumeInformation(driveletter + @":\",
      volumename, (uint)volumename.Capacity - 1,
      out serialnumber, out fn_length, out flags,
      fstype, (uint)fstype.Capacity -1);

    Console.Write(" {0,-3} {1,-10} ",
      driveletter, InterpretDrivetype(drivetype));

    ulong space_available = 0, space_total = 0, space_free = 0;

    int spaceres = GetDiskFreeSpaceEx(driveletter + @":\",
      out space_available, out space_total, out space_free);

    if (spaceres == 1)
    {
      Console.WriteLine(" Yes {0,-20} {1,15} {2,15}",
        volumename, Convert.ToDouble(space_total).ToString("#,#"),
        Convert.ToDouble(space_free).ToString("#,#"));
    }
    else
    {
      Console.WriteLine(" No");
    }
  }
  bitmask >>= 1;
}

Access to the Windows API is very fast. Keep in mind that the Scripting library eventually also calls upon the API, probably even using the same functions. By calling upon the API directly, you actually cut out the middle man (the library), which explains the improved performance. Even when you use regular .NET Framework code, the Windows API is called eventually from within the CLR (the CLR offers one big advantage though: if your program runs under the CLR, it will work on any system with a CLR).
The solution proposed here has disadvantages as well. The code is more complicated. A little research is needed to search de API documentation to find the right calls and the right .dll file. Since the API is called directly, your program is bound to the operating systems that support it. The use of functions such as GetDiskFreeSpaceEx() suggests a potential future problem: The different between GetDiskFreeSpace() and GetDiskFreeSpaceEx() is that the first one only work with storage devices up to 2 GB. The API-calls with the “Ex” suffix where implemented when 16-bit fields were no longer sufficient to return correct information. It is possible that in the future 32-bit or even 64-bit integers will no longer suffice either.
Some of these issues can be solved by bundling all functions that interface directly with the API into a wrapper function or class. This does not make your code any simpler, but it does provide a friendlier interface to interact with it. It also allows beginning programmers to use advanced operating system features as a “black box”. You might write different versions of the class for different operating systems. When the calls at some point are no longer relevant (32 bit > 64 bit > 128 bit...), all you need to do is alter the code in one central location.
A Drive class could look as follows: First, a static method to obtain a list of all available drives.
public static char[] AvailableDrives()
{
StringBuilder sb = new StringBuilder("", 32);
  uint bitmask = GetLogicalDrives();
  for (int i = 0; i < 31; i++)
  {
    if ((bitmask & 1) == 1)
    {
      sb.Append((char)(i+65));
    }
    bitmask >>= 1;
  }
  return sb.ToString().ToCharArray();
}

The class constructor is provided with one argument: the letter of the drive that needs to be investigated. A private default constructor is also added. This prevents that a Drive object can be created without specifying a drive letter. If a default constructor is not explicitely set to private, the compiler will automatically supply a default public constructor, something that is undesirable in this case:
// define private members: m_DriveLetter, m_DriveType, m_AvailableSpace etc...
private Drive() { // empty constructor }
public Drive(char letter)
{
  m_DriveLetter = letter;

  m_DriveType = InterpretDrivetype(GetDriveType(letter + @":\"));

  int spaceres = GetDiskFreeSpaceEx(letter + @":\",
    out m_AvailableSpace, out m_TotalSpace, out m_FreeSpace);
  m_IsReady = (spaceres == 1) ? true: false;

  StringBuilder volumename = new StringBuilder(256);
  uint serialnumber = 0, fn_length = 0, flags = 0;
  StringBuilder fstype = new StringBuilder(256);

  bool res = GetVolumeInformation(letter + @":\",
    volumename, (uint)volumename.Capacity - 1,
    out serialnumber, out fn_length, out flags,
    fstype, (uint)fstype.Capacity -1);
  m_VolumeName = volumename.ToString();
}

A number of instance properties allow the programmer to obtain specifics about a drive:
public char DriveLetter
{
  get { return m_DriveLetter; }
}
public bool IsReady
{
  get { return m_IsReady; }
}
public string DriveType
{
  get { return m_DriveType; }
}
public string VolumeName
{
  get { return m_VolumeName; }
}
public string FileSystem
{
  get { return m_FileSystem; }
}
public ulong TotalSpace
{
  get { return m_TotalSpace; }
}
public ulong FreeSpace
{
  get { return m_FreeSpace; }
}
public ulong AvailableSpace
{
  get { return m_AvailableSpace; }
}

Windows Management Instrumentation – WMI

There is a third way to get to extended drive information. Windows Management Instrumentation – WMI – offers an object-oriented interface to the Windows operating system. Every aspect of the operating system is represented by an object that can be accessed and queried in the same uniform way. For this, WMI has a standard set of methods and classes to retrieve information. They are available to the programmer through the System.Management namespace, which can be used by your project after adding a reference to the System.Management library:


Now the namespace is available to reference from your code:
using System.Management;

WMI consists of a set of classes that represent system-objects. In order to manage storage media, you need the Win32_LogicalDisk class:
ManagementClass log_disk = new ManagementClass("Win32_LogicalDisk");

This results in a collection of all available drives on your system:
ManagementObjectCollection drives = log_disk.GetInstances();

Both instructions may be combined in one statement:
ManagementObjectCollection drives =
  new ManagementClass("Win32_LogicalDisk").GetInstances();

You can also request the properties of a specific drive more directly:
ManagementObject drv = new ManagementObject("Win32_LogicalDisk.DeviceID='c:'");
drv.Get();

You can download the WMI Tools from the Microsoft website. These allow you to interactively browse through the available WMI classes on your computer. You can examine the properties of each one. As is clear from the following screenshot, there is a lot more information available through WMI than we obtained earlier through the Scripting library. There is also more information than we retrieved using the API, since we only used a few calls.


Whether or not a drive is ready should be verifiable using either the fields Availability, Status or StatusInfo. In practice however, these fields often have the value <empty> (or null), regardless of whether a CD-ROM drive has a CD loaded. As an alternative you can check the Size property. If this property has a value, the drive is ready. If it has the <empty> value, it is not ready and you should not try to read from or write to the device. You can substitute the Size property with others such as FreeSpace or FileSystem. These will also be equal to <empty>, allowing you to check for availability through error handling:
Console.WriteLine("{0,-5} {1,-10} {2,-5} {3,-20} {4,-15} {5,-15}",
  "Drive", "Type", "Ready", "Volume name",
  "Total space", "Available space");
Console.WriteLine("-----+----------+-----+"
  + "--------------------+---------------+----------------");

ManagementObjectCollection drives =
  new ManagementClass("Win32_LogicalDisk").GetInstances();

foreach (ManagementObject drive in drives)
{
  Console.Write(" {0,-3} {1,-10} ",
    drive["DeviceID"],
  InterpretDrivetype(Convert.ToUInt16(drive["DriveType"])));
  try
  {
    Console.WriteLine(" Yes {0,-20} {1,15} {2,15}",
      drive["VolumeName"].ToString(),
      Convert.ToDouble(drive["Size"]).ToString("#,#"),
      Convert.ToDouble(drive["FreeSpace"]).ToString("#,#"));
  }
  catch (Exception)
  {
    Console.WriteLine(" No ");
  }
}

The reason this works is because an exception is thrown when the Convert class tries to convert an <empty> value to a number.
We have only listed a limited number of WMI properties so that it is easy to compare with the Scripting and API code. It is clear that other WMI properties for storage media can be accessed and displayed in the same way.
It is possible to write a similar wrapper-class for our WMI approach as we did for the API. However, as you require more properties, you will start to notice that your code eventually differs little from working with the System.Management object directly. Unless you want to use complicated logic with WMI data (which is not the case here), it is just as easy to work with the objects already provided through the .NET Framework.
The “W” in WMI stands for Windows, which means that the System.Management namespace may very well be specifically tied to the Windows Operating System and stay that way in the future.
Through WMI, potential problems are easier to handle in comparison with the API. An exception will be thrown if a non-existing class is requested. This can be caught and dealt with accordingly:
try
{
  ManagementObjectCollection drives =
    new ManagementClass("Some_NonExisting_Class").GetInstances();
}
catch (ManagementException)
{
  Console.WriteLine("WMI Class not found");
}

Decisions

Three methods were shown to obtain advanced properties of storage media. Which method you implement in your project depends on the environment in which your software is developed and in which it will be used. No situation seems to justify the use of the Scripting library anymore. In .NET the library can be considered a legacy application offering only limited functionality compared to the Windows API and the extended information exposed through WMI.
Choosing between the API and WMI is less easy. The API offers maximum performance, at the expense of transparancy and becoming dependent to an operating system. These apparent disadvantages can be minimized by building wrapper-classes around your critical code.
In contrast, WMI and the System.Management namespace give you a framework which opens up almost every aspect of Windows to you directly, without having to know every API-call for every single variable. This is particularly useful when working with large amounts of data from different sources. For large-scale projects with the qualified specialist people however it may still be worthwhile converting original WMI-code to API-calls.

About the author

In 1989, Yves Sucaet started working with computers and GW Basic. In 1997 this led to a first real job at Alcatel e-COM. Afterwards, he spent some time in New Zealand to build generic e-business solutions. When this was finished, he started working at a Fortune 500 company. Currently he is linked to Troy State University in the USA as an assistant and is researching on bio-computing. You can reach him at yves.sucaet@usa.net.