Deleting Folders

RemoveDirectory requires the input folder to be empty. That typically requires repeatedly FileFind’ing the folder contents (either with the MFC wrapper or directly with the Win32 API) and DeleteFile‘ing. Things soon get interesting when you discover you need more code to detect subfolders and recursively empty and delete them – the code for a simple task seems to get out of hand.

Sarath suggests a seemingly more pleasant way, SHFileOperation.  A quick rehash:

bool DeleteDirectory( CString strPath )
{
  strPath += _T( ‘\ 0′ );

  SHFILEOPSTRUCT strOper = { 0 };
  strOper.hwnd = NULL;
  strOper.wFunc = FO_DELETE;
  strOper.pFrom = strPath;
  strOper.fFlags = FOF_SILENT | FOF_NOCONFIRMATION;

  if ( 0 == SHFileOperation ( &strOper ))
  {
    return true;
  }
  return false;
}

This is an attractive alternative indeed, but turns out it has a quasi-bug, a hidden gotcha, bizarre error-reporting and some lacking capabilities.

1. Quasi-bug

MSDN mentions that this sort of SHFileOperation usage must be followed by an SHChangeNotify call. The code should read:

...
if ( 0 == SHFileOperation ( &strOper ))
{
  SHChangeNotify(SHCNE_RMDIR, SHCNF_PATH, strOper.pFrom, NULL);
  return true;
}
...

I call this a quasi-bug because (a) I’ve no idea how SHChangeNotify affects the shell, (b) a toy-test I just did shows that windows explorer immediately picks up this SHFileOperation change without an explicit SHChangeNotify, (c) Not only Sarath but also Jonathan Wood omits the SHChangeNotify call right there on MSDN, and finally (d) this just seems a silly API design. SHFileOperation is a shell API – it can easily (probably has no choice but to-) notify the shell himself of whatever needs notifying, and I cannot imagine a scenario where a user might prefer to skip such a notification.   Gotta ask about this at Raymond’s some day.

2. Hidden Gotcha

You should never use relative paths as an input to SHFileOperation, as (quoting MSDN) “Using it with relative path names is not thread safe”. Apparently the implementation somehow is thread safe for absolute path. Must be some arcane file system issue buried deep inside.

3. Bizarre Error Reporting

Quoting again:

Do not use GetLastError with the return values of this function.

To examine the nonzero values for troubleshooting purposes, they largely map to those defined in Winerror.h. However, several of its possible return values are based on pre-Win32 error codes, which in some cases overlap the later Winerror.h values without matching their meaning. …   for these specific values only these meanings should be accepted over the Winerror.h codes. However, these values are provided with these warnings:

  • These are pre-Win32 error codes and are no longer supported or defined in any public header file. To use them, you must either define them yourself or compare against the numerical value.
  • These error codes are subject to change and have historically done so.
  • These values are provided only as an aid in debugging. They should not be regarded as definitive…

Feels like an all-but-deprecated API, and it is indeed superseded by IFileOperation since Vista.

4. Lacking Capabilities

Things get even more interesting when your folder contains read-only or hidden files. If you do enumerate and delete the folder contents yourself this is easily rectifiable by a SetFileAttributes call. The shell API has no way (that I know of) to achieve similar functionality.

Bottom Line

For any real production code I whole heartily recommend against SHFileOperation calls. Using it has real potential of dooming your code users and maintainers for weird, time consuming bugs.

It’s really not that terrible a bullet to bite – just a few dozen more code lines. Even better, you can find them here (MFC version):


VOID MakeWritable(CONST CString& filename)
{
  DWORD dwAttrs = ::GetFileAttributes(filename);
  if (dwAttrs==INVALID_FILE_ATTRIBUTES) return;

  if (dwAttrs & FILE_ATTRIBUTE_READONLY)
  {
    ::SetFileAttributes(filename,
    dwAttrs & (~FILE_ATTRIBUTE_READONLY));
  }
}

BOOL DeleteDirectory(CONST CString& sFolder)
{
  CFileFind   ff;
  CString     sCurFile;
  BOOL bMore = ff.FindFile(sFolder + _T("\\*.*"));

  // Empty the folder, before removing it
  while (bMore)
  {
    bMore = ff.FindNextFile();
    if (ff.IsDirectory())
    {
      if (!ff.IsDots())
        DeleteDirectory(ff.GetFilePath());
    }
    else
    {
      sCurFile = ff.GetFilePath();
      MakeWritable(sCurFile);

      if (!::DeleteFile(sCurFile))
      {
        LogLastError(); // just a placeholder - recover whichever way you want
        return FALSE;
      }
    }
  }

  // RemoveDirectory fails without this one!  CFileFind locks file system resources.
  ff.Close();

  if(! ::RemoveDirectory(sFolder))
  {
    LogLastError();
    return FALSE;
  }
  return TRUE;
}
Advertisement
This entry was posted in Win32. Bookmark the permalink.

4 Responses to Deleting Folders

  1. rmn says:

    Ouch, a goto :P
    Your code will function _exactly_ the same if you remove lines 18-19:

     
    if(!bMore)
      goto rmdir;
    

    since the loop will not trigger even once if bMore is false.

    Other than that, nice article. WinAPI is so much uglier than *nix.. You could go for boost.filesystem (http://www.boost.org/doc/libs/1_41_0/libs/filesystem/doc/index.htm) which wraps WinAPI and other interfaces for supplying both a portable interface and an easier one to use.

  2. Ofek says:

    I actually had the guts to argue in favor of GOTO right there on stackoverflow… as my links there say, I think Dijkstra’s catchy title (which btw, the journal editor phrased and not Dijkstra himself) is often misunderstood.
    Other then that, you’re absolutely right, and its redundant here :)
    I’ll fix the code immediately.

  3. Nilesh Padbidri says:

    The recursive implementation too, should not be part of real production code.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s