[聚合问答] Cannot delete directory with Directory.Delete(path, true)

c#,.net,exception,io 2017-11-30 8 阅读

I'm using .NET 3.5, trying to recursively delete a directory using:

Directory.Delete(myPath, true);

My understanding is that this should throw if files are in use or there is a permissions problem, but otherwise it should delete the directory and all of its contents.

However, I occasionally get this:

System.IO.IOException: The directory is not empty.
    at System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath)
    at System.IO.Directory.DeleteHelper(String fullPath, String userPath, Boolean recursive)
    at System.IO.Directory.Delete(String fullPath, String userPath, Boolean recursive)
    ...

I'm not surprised that the method sometimes throws, but I'm surprised to get this particular message when recursive is true. (I know the directory is not empty.)

Is there a reason I'd see this instead of AccessViolationException?

19个回答

193

Editor's note: Although this answer contains some useful information, it is factually incorrect about the workings of Directory.Delete. Please read the comments for this answer, and other answers to this question.


I ran into this problem before.

The root of the problem is that this function does not delete files that are within the directory structure. So what you'll need to do is create a function that deletes all the files within the directory structure then all the directories before removing the directory itself. I know this goes against the second parameter but it's a much safer approach. In addition, you will probably want to remove READ-ONLY access attributes from the files right before you delete them. Otherwise that will raise an exception.

Just slap this code into your project.

public static void DeleteDirectory(string target_dir)
{
    string[] files = Directory.GetFiles(target_dir);
    string[] dirs = Directory.GetDirectories(target_dir);

    foreach (string file in files)
    {
        File.SetAttributes(file, FileAttributes.Normal);
        File.Delete(file);
    }

    foreach (string dir in dirs)
    {
        DeleteDirectory(dir);
    }

    Directory.Delete(target_dir, false);
}

Also, for me I personally add a restriction on areas of the machine that are allowed to be deleted because do you want someone to call this function on C:\WINDOWS (%WinDir%) or C:\.

2017-11-30
148

If you are trying to recursively delete directory a and directory a\b is open in Explorer, b will be deleted but you will get the error 'directory is not empty' for a even though it is empty when you go and look. The current directory of any application (including Explorer) retains a handle to the directory. When you call Directory.Delete(true), it deletes from bottom up: b, then a. If b is open in Explorer, Explorer will detect the deletion of b, change directory upwards cd .. and clean up open handles. Since the file system operates asynchronously, the Directory.Delete operation fails due to conflicts with Explorer.

Incomplete solution

I originally posted the following solution, with the idea of interrupting the current thread to allow Explorer time to release the directory handle.

// incomplete!
try
{
    Directory.Delete(path, true);
}
catch (IOException)
{
    Thread.Sleep(0);
    Directory.Delete(path, true);
}

But this only works if the open directory is the immediate child of the directory you are deleting. If a\b\c\d is open in Explorer and you use this on a, this technique will fail after deleting d and c.

A somewhat better solution

This method will handle deletion of a deep directory structure even if one of the lower-level directories is open in Explorer.

/// <summary>
/// Depth-first recursive delete, with handling for descendant 
/// directories open in Windows Explorer.
/// </summary>
public static void DeleteDirectory(string path)
{
    foreach (string directory in Directory.GetDirectories(path))
    {
        DeleteDirectory(directory);
    }

    try
    {
        Directory.Delete(path, true);
    }
    catch (IOException) 
    {
        Directory.Delete(path, true);
    }
    catch (UnauthorizedAccessException)
    {
        Directory.Delete(path, true);
    }
}

Despite the extra work of recursing on our own, we still have to handle the UnauthorizedAccessException that can occur along the way. It's not clear whether the first deletion attempt is paving the way for the second, successful one, or if it's merely the timing delay introduced by the throwing/catching an exception that allows the file system to catch up.

You might be able to reduce the number of exceptions thrown and caught under typical conditions by adding a Thread.Sleep(0) at the beginning of the try block. Additionally, there is a risk that under heavy system load, you could fly through both of the Directory.Delete attempts and fail. Consider this solution a starting point for more robust recursive deletion.

General answer

This solution only addresses the peculiarities of interacting with Windows Explorer. If you want a rock-solid delete operation, one thing to keep in mind is that anything (virus scanner, whatever) could have an open handle to what you are trying to delete, at any time. So you have to try again later. How much later, and how many times you try, depends on how important it is that the object be deleted. As MSDN indicates,

Robust file iteration code must take into account many complexities of the file system.

This innocent statement, supplied with only a link to the NTFS reference documentation, ought to make your hairs stand up.

(Edit: A lot. This answer originally only had the first, incomplete solution.)

2017-11-30
30

Before going further, check for the following reasons that are under your control:

  • Is the folder set as a current directory of your process? If yes, change it to something else first.
  • Have you opened a file (or loaded a DLL) from that folder? (and forgot to close/unload it)

Otherwise, check for the following legitimate reasons outside of your control:

  • There are files marked as read-only in that folder.
  • You don't have a deletion permission to some of those files.
  • The file or subfolder is open in Explorer or another app.

If any of the above is the problem, you should understand why it happens before trying to improve your deletion code. Should your app be deleting read-only or inaccessible files? Who marked them that way, and why?

Once you have ruled out the above reasons, there's still a possibility of spurious failures. The deletion will fail if anyone holds a handle to any of the files or folders being deleted, and there are many reasons why someone may be enumerating the folder or reading its files:

  • search indexers
  • anti-viruses
  • backup software

The general approach to deal with spurious failures is to try multiple times, pausing between the attempts. You obviously don't want to keep trying forever, so you should give up after a certain number of attempts and either throw an exception or ignore the error. Like this:

private static void DeleteRecursivelyWithMagicDust(string destinationDir) {
    const int magicDust = 10;
    for (var gnomes = 1; gnomes <= magicDust; gnomes++) {
        try {
            Directory.Delete(destinationDir, true);
        } catch (DirectoryNotFoundException) {
            return;  // good!
        } catch (IOException) { // System.IO.IOException: The directory is not empty
            System.Diagnostics.Debug.WriteLine("Gnomes prevent deletion of {0}! Applying magic dust, attempt #{1}.", destinationDir, gnomes);

            // see http://stackoverflow.com/questions/329355/cannot-delete-directory-with-directory-deletepath-true for more magic
            Thread.Sleep(50);
            continue;
        }
        return;
    }
    // depending on your use case, consider throwing an exception here
}

In my opinion, a helper like that should be used for all deletions because spurious failures are always possible. However, YOU SHOULD ADAPT THIS CODE TO YOUR USE CASE, not just blindly copy it.

I had spurious failures for an internal data folder generated by my app, located under %LocalAppData%, so my analysis goes like this:

  1. The folder is controlled solely by my application, and the user has no valid reason to go and mark things as read-only or inaccessible inside that folder, so I don't try to handle that case.

  2. There's no valuable user-created stuff in there, so there's no risk of forcefully deleting something by mistake.

  3. Being an internal data folder, I don't expect it to be open in explorer, at least I don't feel the need to specifically handle the case (i.e. I'm fine handling that case via support).

  4. If all attempts fail, I choose to ignore the error. Worst case, the app fails to unpack some newer resources, crashes and prompts the user to contact support, which is acceptable to me as long as it does not happen often. Or, if the app does not crash, it will leave some old data behind, which again is acceptable to me.

  5. I choose to limit retries to 500ms (50 * 10). This is an arbitrary threshold which works in practice; I wanted the threshold to be short enough so that users wouldn't kill the app, thinking that it has stopped responding. On the other hand, half a second is plenty of time for the offender to finish processing my folder. Judging from other SO answers which sometimes find even Sleep(0) to be acceptable, very few users will ever experience more than a single retry.

  6. I retry every 50ms, which is another arbitrary number. I feel that if a file is being processed (indexed, checked) when I try to delete it, 50ms is about the right time to expect the processing to be completed in my case. Also, 50ms is small enough to not result in a noticeable slowdown; again, Sleep(0) seems to be enough in many cases, so we don't want to delay too much.

  7. The code retries on any IO exceptions. I don't normally expect any exceptions accessing %LocalAppData%, so I chose simplicity and accepted the risk of a 500ms delay in case a legitimate exception happens. I also didn't want to figure out a way to detect the exact exception that I want to retry on.

2017-11-30
12

I had the very same problem under Delphi. And the end result was that my own application was locking the directory I wanted to delete. Somehow the directory got locked when I was writing to it (some temporary files).

The catch 22 was, I made a simple change directory to it's parent before deleting it.

2017-11-30
8

I'm surprised that no one thought of this simple non-recursive method, which can delete directories containing read only files, without needing to change read only attribute of each of them.

Process.Start("cmd.exe", "/c " + @"rmdir /s/q C:\Test\TestDirectoryContainingReadOnlyFiles"); 

(Change a bit to not to fire a cmd window momentarily, which is available all over the internet)

2017-11-30
7

You can reproduce the error by running:

Directory.CreateDirectory(@"C:\Temp\a\b\c\");
Process.Start(@"C:\Temp\a\b\c\");
Thread.Sleep(1000);
Directory.Delete(@"C:\Temp\a\b\c");
Directory.Delete(@"C:\Temp\a\b");
Directory.Delete(@"C:\Temp\a");

When trying to delete directory 'b', it throws the IOException "The directory is not empty". That's stupid since we just deleted the directory 'c'.

From my understanding, the explanation is that directory 'c' is stamped as deleted. But the delete is not yet commited in the system. The system has reply the job is done, while in fact, it is still processing. The system probably wait the file explorer has focus on the parent directory to commit the delete.

If you look on the source code of the Delete function (http://referencesource.microsoft.com/#mscorlib/system/io/directory.cs) you will see it uses the native Win32Native.RemoveDirectory function. This do-not-wait behavior is noted here :

The RemoveDirectory function marks a directory for deletion on close. Therefore, the directory is not removed until the last handle to the directory is closed.

(http://msdn.microsoft.com/en-us/library/windows/desktop/aa365488(v=vs.85).aspx)

Sleep and retry is the solution. Cf the ryascl's solution.

2017-11-30
6

I had a those weird permission problems deleting User Profile directories (in C:\Documents and Settings) despite being able to do so in the Explorer shell.

File.SetAttributes(target_dir, FileAttributes.Normal);
Directory.Delete(target_dir, false);

It makes no sense to me what a "file" operation does on a directory, but I know that it works and that's enough for me!

2017-11-30
6

One important thing which should be mentioned (I'd added it as a comment but I'm not allowed to) is that the overload's behavior changed from .NET 3.5 to .NET 4.0.

Directory.Delete(myPath, true);

Starting from .NET 4.0 it deletes files in the folder itself but NOT in 3.5. This can be seen in the MSDN documentation as well.

.NET 4.0

Deletes the specified directory and, if indicated, any subdirectories and files in the directory.

.NET 3.5

Deletes an empty directory and, if indicated, any subdirectories and files in the directory.

2017-11-30
3

This answer is based on: https://stackoverflow.com/a/1703799/184528. The difference with my code, is that we only recurse many delete sub-directories and files when necessary a call to Directory.Delete fails on a first attempt (which can happen because of windows explorer looking at a directory).

    public static void DeleteDirectory(string dir, bool secondAttempt = false)
    {
        // If this is a second try, we are going to manually 
        // delete the files and sub-directories. 
        if (secondAttempt)
        {
            // Interrupt the current thread to allow Explorer time to release a directory handle
            Thread.Sleep(0);

            // Delete any files in the directory 
            foreach (var f in Directory.GetFiles(dir, "*.*", SearchOption.TopDirectoryOnly))
                File.Delete(f);

            // Try manually recursing and deleting sub-directories 
            foreach (var d in Directory.GetDirectories(dir))
                DeleteDirectory(d);

            // Now we try to delete the current directory
            Directory.Delete(dir, false);
            return;
        }

        try
        {
            // First attempt: use the standard MSDN approach.
            // This will throw an exception a directory is open in explorer
            Directory.Delete(dir, true);
        }
        catch (IOException)
        {
            // Try again to delete the directory manually recursing. 
            DeleteDirectory(dir, true);
        }
        catch (UnauthorizedAccessException)
        {
            // Try again to delete the directory manually recursing. 
            DeleteDirectory(dir, true);
        } 
    }

2017-11-30
3

Modern Async Answer

The accepted answer is just plain wrong, it might work for some people because the time taken to get files from disk frees up whatever was locking the files. The fact is, this happens because files get locked by some other process/stream/action. The other answers use Thread.Sleep (Yuck) to retry deleting the directory after some time. This question needs revisiting with a more modern answer.

public static async Task<bool> TryDeleteDirectory(
   string directoryPath,
   int maxRetries = 10,
   int millisecondsDelay = 30)
{
    if (directoryPath == null)
        throw new ArgumentNullException(directoryPath);
    if (maxRetries < 1)
        throw new ArgumentOutOfRangeException(nameof(maxRetries));
    if (millisecondsDelay < 1)
        throw new ArgumentOutOfRangeException(nameof(millisecondsDelay));

    for (int i = 0; i < maxRetries; ++i)
    {
        try
        {
            if (Directory.Exists(directoryPath))
            {
                Directory.Delete(directoryPath, true);
            }

            return true;
        }
        catch (IOException)
        {
            await Task.Delay(millisecondsDelay);
        }
        catch (UnauthorizedAccessException)
        {
            await Task.Delay(millisecondsDelay);
        }
    }

    return false;
}

Unit Tests

These tests show an example of how a locked file can cause the Directory.Delete to fail and how the TryDeleteDirectory method above fixes the problem.

[Fact]
public async Task TryDeleteDirectory_FileLocked_DirectoryNotDeletedReturnsFalse()
{
    var directoryPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
    var subDirectoryPath = Path.Combine(Path.GetTempPath(), "SubDirectory");
    var filePath = Path.Combine(directoryPath, "File.txt");

    try
    {
        Directory.CreateDirectory(directoryPath);
        Directory.CreateDirectory(subDirectoryPath);

        using (var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.Write))
        {
            var result = await TryDeleteDirectory(directoryPath, 3, 30);
            Assert.False(result);
            Assert.True(Directory.Exists(directoryPath));
        }
    }
    finally
    {
        if (Directory.Exists(directoryPath))
        {
            Directory.Delete(directoryPath, true);
        }
    }
}

[Fact]
public async Task TryDeleteDirectory_FileLockedThenReleased_DirectoryDeletedReturnsTrue()
{
    var directoryPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
    var subDirectoryPath = Path.Combine(Path.GetTempPath(), "SubDirectory");
    var filePath = Path.Combine(directoryPath, "File.txt");

    try
    {
        Directory.CreateDirectory(directoryPath);
        Directory.CreateDirectory(subDirectoryPath);

        Task<bool> task;
        using (var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.Write))
        {
            task = TryDeleteDirectory(directoryPath, 3, 30);
            await Task.Delay(30);
            Assert.True(Directory.Exists(directoryPath));
        }

        var result = await task;
        Assert.True(result);
        Assert.False(Directory.Exists(directoryPath));
    }
    finally
    {
        if (Directory.Exists(directoryPath))
        {
            Directory.Delete(directoryPath, true);
        }
    }
}

2017-11-30
2

I have spent few hours to solve this problem and other exceptions with deleting the directory. This is my solution

 public static void DeleteDirectory(string target_dir)
    {
        DeleteDirectoryFiles(target_dir);
        while (Directory.Exists(target_dir))
        {
            lock (_lock)
            {
                DeleteDirectoryDirs(target_dir);
            }
        }
    }

    private static void DeleteDirectoryDirs(string target_dir)
    {
        System.Threading.Thread.Sleep(100);

        if (Directory.Exists(target_dir))
        {

            string[] dirs = Directory.GetDirectories(target_dir);

            if (dirs.Length == 0)
                Directory.Delete(target_dir, false);
            else
                foreach (string dir in dirs)
                    DeleteDirectoryDirs(dir);
        }
    }

    private static void DeleteDirectoryFiles(string target_dir)
    {
        string[] files = Directory.GetFiles(target_dir);
        string[] dirs = Directory.GetDirectories(target_dir);

        foreach (string file in files)
        {
            File.SetAttributes(file, FileAttributes.Normal);
            File.Delete(file);
        }

        foreach (string dir in dirs)
        {
            DeleteDirectoryFiles(dir);
        }
    }

This code has the small delay, which is not important for my application. But be careful, the delay may be a problem for you if you have a lot of subdirectories inside the directory you want to delete.

2017-11-30
2

Recursive directory deletion that does not delete files is certainly unexpected. My fix for that:

public class IOUtils
{
    public static void DeleteDirectory(string directory)
    {
        Directory.GetFiles(directory, "*", SearchOption.AllDirectories).ForEach(File.Delete);
        Directory.Delete(directory, true);
    }
}

I experienced cases where this helped, but generally, Directory.Delete deletes files inside directories upon recursive deletion, as documented in msdn.

From time to time I encounter this irregular behavior also as a user of Windows Explorer: Sometimes I cannot delete a folder (it think the nonsensical message is "access denied") but when I drill down and delete lower items I can then delete the upper items as well. So I guess the code above deals with an OS anomaly - not with a base class library issue.

2017-11-30
1

Is it possible you have a race condition where another thread or process is adding files to the directory:

The sequence would be:

Deleter process A:

  1. Empty the directory
  2. Delete the (now empty) directory.

If someone else adds a file between 1 & 2, then maybe 2 would throw the exception listed?

2017-11-30
1

It appears that having the path or subfolder selected in Windows Explorer is enough to block a single execution of Directory.Delete(path, true), throwing an IOException as described above and dying instead of booting Windows Explorer out to a parent folder and proceding as expected.

2017-11-30
1

I had this problem today. It was happening because I had windows explorer open to the directory that was trying to be deleted, causing the recursive call the fail and thus the IOException. Make sure there are no handles open to the directory.

Also, MSDN is clear that you don't have to write your own recusion: http://msdn.microsoft.com/en-us/library/fxeahc5f.aspx

2017-11-30
1

I've had this same problem with Windows Workflow Foundation on a build server with TFS2012. Internally, the workflow called Directory.Delete() with the recursive flag set to true. It appears to be network related in our case.

We were deleting a binary drop folder on a network share before re-creating and re-populating it with the latest binaries. Every other build would fail. When opening the drop folder after a failed build, the folder was empty, which indicates that every aspect of the Directory.Delete() call was successful except for deleting the actually directory.

The problem appears to be caused by the asynchronous nature of network file communications. The build server told the file server to delete all of the files and the file server reported that it had, even though it wasn't completely finished. Then the build server requested that the directory be deleted and the file server rejected the request because it hadn't completely finished deleting the files.

Two possible solutions in our case:

  • Build up the recursive deletion in our own code with delays and verifications between each step
  • Retry up to X times after an IOException, giving a delay before trying again

The latter method is quick and dirty but seems to do the trick.

2017-11-30
1

This is because of FileChangesNotifications.

It happens since ASP.NET 2.0. When you delete some folder within an app, it gets restarted. You can see it yourself, using ASP.NET Health Monitoring.

Just add this code to your web.config/configuration/system.web:

<healthMonitoring enabled="true">
  <rules>
    <add name="MyAppLogEvents" eventName="Application Lifetime Events" provider="EventLogProvider" profile="Critical"/>
  </rules>
</healthMonitoring>


After that check out Windows Log -> Application. What is going on:

When you delete folder, if there is any sub-folder, Delete(path, true) deletes sub-folder first. It is enough for FileChangesMonitor to know about removal and shut down your app. Meanwhile your main directory is not deleted yet. This is the event from Log:


enter image description here


Delete() didn't finish its work and because app is shutting down, it raises an exception:

enter image description here

When you do not have any subfolders in a folder that you are deleting, Delete() just deletes all files and that folder, app is getting restarted too, but you don't get any exceptions, because app restart doesn't interrupt anything. But still, you lose all in-process sessions, app doesn't response to requests when restarting, etc.

What now?

There are some workarounds and tweaks to disable this behaviour, Directory Junction, Turning Off FCN with Registry, Stopping FileChangesMonitor using Reflection (since there is no exposed method), but they all don't seem to be right, because FCN is there for a reason. It is looking after structure of your app, which is not structure of your data. Short answer is: place folders you want to delete outside of your app. FileChangesMonitor will get no notifications and your app will not be restarted every time. You will get no exceptions. To get them visible from the web there are two ways:

  1. Make a controller that handles incoming calls and then serves files back by reading from folder outside an app (outside wwwroot).

  2. If your project is big and performance is most important, set up separate small and fast webserver for serving static content. Thus you will leave to IIS his specific job. It could be on the same machine (mongoose for Windows) or another machine (nginx for Linux). Good news is you don't have to pay extra microsoft license to set up static content server on linux.

Hope this helps.

2017-11-30
1

As mentioned above the "accepted" solution fails on reparse points - yet people still mark it up(???). There's a much shorter solution that properly replicates the functionality:

public static void rmdir(string target, bool recursive)
{
    string tfilename = Path.GetDirectoryName(target) +
        (target.Contains(Path.DirectorySeparatorChar.ToString()) ? Path.DirectorySeparatorChar.ToString() : string.Empty) +
        Path.GetRandomFileName();
    Directory.Move(target, tfilename);
    Directory.Delete(tfilename, recursive);
}

I know, doesn't handle the permissions cases mentioned later, but for all intents and purposes FAR BETTER provides the expected functionality of the original/stock Directory.Delete() - and with a lot less code too.

You can safely carry on processing because the old dir will be out of the way ...even if not gone because the 'file system is still catching up' (or whatever excuse MS gave for providing a broken function).

As a benefit, if you know your target directory is large/deep and don't want to wait (or bother with exceptions) the last line can be replaced with:

    ThreadPool.QueueUserWorkItem((o) => { Directory.Delete(tfilename, recursive); });

You are still safe to carry on working.

2017-11-30
1

This problem can appear on Windows when there are files in a directory (or in any subdirectory) which path length is greater than 260 symbols.

In such cases you need to delete \\\\?\C:\mydir instead of C:\mydir. About the 260 symbols limit you can read here.

2017-11-30

注:本文内容来自互联网,旨在为开发者提供分享、交流的平台。如有涉及文章版本等事宜,请你联系站长进行处理。