Thursday, July 06, 2006
« Busy on the House | Main | Blog Added to FeedBurner »

Well, my quiz from about 3 weeks ago didn't really elicit the response I was hoping.  However, for the benefit of those that have been wondering, I've decided to post my results.  As you may recall, the quiz was a seemingly simple one: to determine if a directory is empty (e.g. that it has no contents: files or folders).

Almost alarmingly, there is nothing built into the .NET Framework for such an operation.  One solution (which was proposed in the preceding post) is to evaluate the results of the Directory.GetDirectories() and Directory.GetFiles() methods.  Doing this, however, forces the runtime to enumerate all of the contents of the folder only to be discarded once we see determine that there were indeed files and folders.  There's no way (that I've seen) to call a Directory.IsEmpty() method or even a Directory.GetDirectoryCount() or Directory.GetFileCount() (though those might also force enumeration internally if they existed).

I have a solution that I propose as being better than the contrived example from the original post, though it is a little lengthier.  I haven't run any stringent analysis on its performance though I have to believe that it's faster than the method that relies on enumeration.  I also imagine that its performance will be substantially better over a network.

This solution relies on some built-into-the-Windows-kernel API calls to walk the contents of a directory (for there isn't any functionality there either to see if a directory is empty).  As soon as a file or folder is found it quits and indicates that the folder is non-empty.

private static bool isDirectoryEmpty(string directory) {
   IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1);
   NativeMethods.WIN32_FIND_DATA findData;
   IntPtr handle = IntPtr.Zero;
  
   try {
      handle = NativeMethods.FindFirstFile(@"
\\?\" + Path.Combine(directory, "*"), out findData);
      if ( INVALID_HANDLE_VALUE == handle ) {
         // call GetLastError() here to be explicit on the reason of the failure
         // for now, simply assume that the directory is not empty.

         return false;
      }
  
      // spin on the directory until a file/directory other than the current (.) or parent (..) is found
      // if something is found, quit immediately - we've determined that the folder is not empty
      do {
         if ( "." != findData.cFileName && ".." != findData.cFileName ) return false;
      } while ( NativeMethods.FindNextFile(handle, out findData) );
     
      // no file or directory was found, so return that it is indeed empty.
      return true;
   }
   finally {
      if ( IntPtr.Zero != handle )
         NativeMethods.FindClose(handle);
   }
}

This code makes use of a NativeMethods class which I define below.  It contains the system API calls that are used to walk the directory contents.

internal static class NativeMethods {
   public const int MAX_PATH = 260;
   public const int MAX_ALTERNATE = 14;

   [StructLayout(LayoutKind.Sequential)]
   public struct FILETIME {
      public uint dwLowDateTime;
      public uint dwHighDateTime;
   };

   [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
   public struct WIN32_FIND_DATA {
      public FileAttributes dwFileAttributes;
      public FILETIME ftCreationTime;
      public FILETIME ftLastAccessTime;
      public FILETIME ftLastWriteTime;
      public int nFileSizeHigh;
      public int nFileSizeLow;
      public int dwReserved0;
      public int dwReserved1;
      [MarshalAs(UnmanagedType.ByValTStr, SizeConst=MAX_PATH)]
      public string cFileName;
      [MarshalAs(UnmanagedType.ByValTStr, SizeConst=MAX_ALTERNATE)]
      public string cAlternate;
   }

   [DllImport("kernel32", CharSet=CharSet.Unicode)]
   public static extern IntPtr FindFirstFile(string lpFileName, out WIN32_FIND_DATA lpFindFileData);

   [DllImport("kernel32", CharSet=CharSet.Unicode)]
   public static extern bool FindNextFile(IntPtr hFindFile, out WIN32_FIND_DATA lpFindFileData);

   [DllImport("kernel32")]
   public static extern bool FindClose(IntPtr hFindFile);
}

NOTE:  Please let me know if I overlooked something obvious.  I've not done stringent analysis with this code but wanted to put it out there as a proposed solution for determining that a directory is empty in an efficient manner.

Thoughts?