Невозможно удалить каталог с помощью Directory.Delete (путь, истина) - PullRequest
359 голосов
/ 30 ноября 2008

Я использую .NET 3.5, пытаюсь рекурсивно удалить каталог, используя:

Directory.Delete(myPath, true);

Насколько я понимаю, это должно произойти, если файлы используются или существует проблема с разрешениями, но в противном случае следует удалить каталог и все его содержимое.

Однако я иногда получаю это:

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)
    ...

Меня не удивляет, что метод иногда выдает, но я удивляюсь, когда получаю именно это сообщение, когда рекурсивное значение истинно. (Я знаю каталог не пустой.)

Есть ли причина, по которой я вижу это вместо AccessViolationException?

Ответы [ 29 ]

219 голосов
/ 01 декабря 2008

Примечание редактора: Хотя этот ответ содержит некоторую полезную информацию, на самом деле он неверен в отношении работы Directory.Delete. Пожалуйста, прочтите комментарии к этому ответу и другие ответы на этот вопрос.


Я сталкивался с этой проблемой раньше.

Корень проблемы в том, что эта функция не удаляет файлы, которые находятся в структуре каталогов. Итак, вам нужно создать функцию, которая удаляет все файлы в структуре каталогов, а затем все каталоги, прежде чем удалять сам каталог. Я знаю, что это идет против второго параметра, но это гораздо более безопасный подход. Кроме того, вы, вероятно, захотите удалить атрибуты доступа READ-ONLY из файлов непосредственно перед их удалением. В противном случае это вызовет исключение.

Просто вставьте этот код в ваш проект.

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);
}

Кроме того, лично я добавляю ограничение на области машины, которые разрешено удалять, потому что вы хотите, чтобы кто-то вызывал эту функцию на C:\WINDOWS (%WinDir%) или C:\.

170 голосов
/ 10 ноября 2009

Если вы пытаетесь рекурсивно удалить каталог a и каталог a\b открыт в проводнике, b будет удалено, но вы получите сообщение об ошибке «каталог не пуст» для a, даже если это пусто, когда вы идете и смотрите. Текущий каталог любого приложения (включая Explorer) сохраняет дескриптор каталога . Когда вы звоните Directory.Delete(true), он удаляет снизу вверх: b, затем a. Если в Проводнике открыто b, Проводник обнаружит удаление b, изменит каталог вверх cd .. и очистит открытые дескрипторы. Поскольку файловая система работает асинхронно, операция Directory.Delete завершается неудачно из-за конфликтов с Explorer.

Неполное решение

Первоначально я опубликовал следующее решение с идеей прерывания текущего потока, чтобы у проводника было время освободить дескриптор каталога.

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

Но это работает только в том случае, если открытый каталог является немедленным дочерним элементом удаляемого каталога. Если в проводнике открыто a\b\c\d, и вы используете его на a, эта методика не будет работать после удаления d и c.

Несколько лучшее решение

Этот метод будет обрабатывать удаление глубокой структуры каталогов, даже если в Проводнике открыт один из каталогов нижнего уровня.

/// <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);
    }
}

Несмотря на дополнительную работу по возврату данных самостоятельно, нам все же приходится обрабатывать UnauthorizedAccessException, которые могут возникнуть по пути. Неясно, прокладывает ли первая попытка удаления второй, удачной, или это просто временная задержка, вызванная выбросом / перехватом исключения, которое позволяет файловой системе наверстать упущенное.

Возможно, вам удастся уменьшить количество исключений, сгенерированных и перехваченных в обычных условиях, добавив Thread.Sleep(0) в начале блока try. Кроме того, существует риск того, что при большой загрузке системы вы можете совершить обе попытки Directory.Delete и потерпеть неудачу. Считайте это решение отправной точкой для более надежного рекурсивного удаления.

Общий ответ

Это решение касается только особенностей взаимодействия с Windows Explorer. Если вам нужна надежная операция удаления, следует помнить, что все (антивирусный сканер и т. Д.) Может иметь открытый дескриптор того, что вы пытаетесь удалить, в любое время. Поэтому вы должны попробовать позже. Сколько позже и сколько раз вы попробуете, зависит от того, насколько важно удалить объект. Как MSDN указывает ,

Надежный код итерации файла должен учитывать множество сложностей файловой системы.

Это невинное утверждение, содержащее только ссылку на справочную документацию NTFS, должно заставить ваши волосы встать.

( Редактировать : много. Этот ответ изначально имел только первое, неполное решение.)

40 голосов
/ 18 февраля 2013

Прежде чем идти дальше, проверьте следующие причины, которые находятся под вашим контролем:

  • Является ли папка текущей папкой вашего процесса? Если да, сначала поменяйте его на что-то другое.
  • Вы открыли файл (или загрузили DLL) из этой папки? (и забыл закрыть / разгрузить его)

В противном случае проверьте следующие законные причины вне вашего контроля:

  • В этой папке есть файлы, помеченные как доступные только для чтения.
  • У вас нет разрешения на удаление некоторых из этих файлов.
  • Файл или подпапка открыты в Проводнике или другом приложении.

Если что-то из перечисленного является проблемой, вы должны понять, почему это происходит, прежде чем пытаться улучшить свой код удаления. Должно ли ваше приложение удалять файлы только для чтения или недоступные файлы? Кто так их пометил и почему?

Как только вы исключите вышеуказанные причины, все еще существует вероятность ложных сбоев. Удаление не удастся, если кто-то держит дескриптор любого из удаляемых файлов или папок, и существует множество причин, по которым кто-то может перечислять папку или читать ее файлы:

  • индексаторы поиска
  • антивирусы
  • программное обеспечение для резервного копирования

Общий подход к случайным сбоям состоит в том, чтобы делать попытки несколько раз, делая паузу между попытками. Вы, очевидно, не хотите продолжать попытки вечно, поэтому вам следует отказаться после определенного количества попыток и либо выбросить исключение, либо проигнорировать ошибку. Как это:

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 /307725/nevozmozhno-udalit-katalog-s-pomoschy-directory-delete-put-istina for more magic
            Thread.Sleep(50);
            continue;
        }
        return;
    }
    // depending on your use case, consider throwing an exception here
}

По моему мнению, такой помощник должен использоваться для всех удалений, потому что ложные сбои всегда возможны. Однако ВЫ ДОЛЖНЫ ПРИНЯТЬ ЭТОТ КОД К ВАШЕМУ ИСПОЛЬЗОВАНИЮ, а не просто слепо скопировать его.

У меня были ложные сбои для внутренней папки данных, созданной моим приложением, расположенной в папке% LocalAppData%, поэтому мой анализ выглядит так:

  1. Папка контролируется исключительно моим приложением, и у пользователя нет веской причины для того, чтобы отмечать вещи как доступные только для чтения или недоступные в этой папке, поэтому я не пытаюсь обработать этот случай.

  2. Там нет ценных созданных пользователем вещей, поэтому нет риска принудительного удаления чего-либо по ошибке.

  3. Будучи внутренней папкой данных, я не ожидаю, что она будет открыта в проводнике, по крайней мере, я не чувствую необходимости специально обрабатывать дело (т.е. я прекрасно справляюсь с этим делом через службу поддержки) .

  4. Если все попытки не удаются, я предпочитаю игнорировать ошибку. В худшем случае приложение не может распаковать некоторые новые ресурсы, вылетает и предлагает пользователю обратиться в службу поддержки, что является приемлемым для меня, если это происходит не часто. Или, если приложение не аварийно завершает работу, оно оставляет некоторые старые данные, что опять-таки приемлемо для меня.

  5. Я решил ограничить количество попыток до 500 мс (50 * 10). Это произвольный порог, который работает на практике; Я хотел, чтобы порог был достаточно коротким, чтобы пользователи не убивали приложение, думая, что оно перестало отвечать. С другой стороны, полсекунды - это достаточно времени, чтобы нарушитель мог закончить обработку моей папки. Судя по другим ответам SO, которые иногда считают даже Sleep(0) приемлемым, очень немногие пользователи когда-либо будут испытывать более одной попытки.

  6. Я повторяю каждые 50 мс, что является другим произвольным числом. Я чувствую, что если файл обрабатывается (индексируется, проверяется), когда я пытаюсь удалить его, 50 мс - это подходящее время, чтобы ожидать завершения обработки в моем случае. Кроме того, 50 мс достаточно мал, чтобы не привести к заметному замедлению; опять же, Sleep(0) кажется достаточным во многих случаях, поэтому мы не хотим слишком долго откладывать.

  7. Код повторяется при любых исключениях ввода-вывода. Обычно я не ожидаю каких-либо исключений при доступе к% LocalAppData%, поэтому я выбрал простоту и принял риск задержки в 500 мс в случае легитимного исключения. Я также не хотел выяснять способ определения точного исключения, к которому я хочу повторить попытку.

15 голосов
/ 24 ноября 2016

Одна важная вещь, о которой следует упомянуть (я добавил ее в качестве комментария, но мне это запрещено), - это поведение перегрузки изменилось с .NET 3.5 на .NET 4.0.

Directory.Delete(myPath, true);

Начиная с .NET 4.0, он удаляет файлы в самой папке, но НЕ в 3.5. Это можно увидеть и в документации MSDN.

.NET 4.0

Удаляет указанный каталог и, если указано, любые подкаталоги и файлы в каталоге.

.NET 3.5

Удаляет пустой каталог и, если указано, любые подкаталоги и файлы в каталоге.

13 голосов
/ 01 декабря 2008

У меня была та же проблема при Delphi. И в результате мое собственное приложение блокировало каталог, который я хотел удалить. Каким-то образом каталог заблокировался, когда я писал в него (некоторые временные файлы).

Подвох 22 состоял в том, что я просто изменил каталог на его родительский, прежде чем удалить его.

11 голосов
/ 02 июня 2017

Современный асинхронный ответ

Принятый ответ просто неверен, он может работать для некоторых людей, потому что время, необходимое для получения файлов с диска, освобождает все, что блокировало файлы. Дело в том, что это происходит потому, что файлы блокируются каким-либо другим процессом / потоком / действием. Другие ответы используют Thread.Sleep (Yuck), чтобы через некоторое время повторить попытку удаления каталога. Этот вопрос нуждается в повторении с более современным ответом.

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;
}

Модульные тесты

Эти тесты показывают пример того, как заблокированный файл может вызвать сбой Directory.Delete, и как метод TryDeleteDirectory, описанный выше, устраняет проблему.

[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);
        }
    }
}
11 голосов
/ 02 июня 2012

Я удивлен, что никто не задумывался об этом простом нерекурсивном методе, который может удалять каталоги, содержащие файлы только для чтения, без необходимости изменять атрибут только для чтения каждого из них.

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

(Измените немного, чтобы не запускать окно cmd, которое доступно по всему интернету)

10 голосов
/ 05 сентября 2014

Вы можете воспроизвести ошибку, запустив:

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");

При попытке удалить каталог «b» выдается исключение IOException «Каталог не пустой». Это глупо, так как мы только что удалили каталог 'c'.

Насколько я понимаю, объяснение состоит в том, что каталог 'c' помечается как удаленный. Но удаление еще не зафиксировано в системе. Система ответила, что работа выполнена, хотя на самом деле она все еще обрабатывается. Система, вероятно, ожидает, что файловый менеджер сфокусируется на родительском каталоге, чтобы зафиксировать удаление.

Если вы посмотрите на исходный код функции Delete (http://referencesource.microsoft.com/#mscorlib/system/io/directory.cs), вы увидите, что она использует встроенную функцию Win32Native.RemoveDirectory. Это поведение "не ждать") отмечается здесь:

Функция RemoveDirectory помечает каталог для удаления при закрытии. Поэтому каталог не удаляется до тех пор, пока не будет закрыт последний дескриптор каталога.

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

Сон и повтор - это решение. См. Решение Ряскля.

7 голосов
/ 11 июня 2009

У меня были странные проблемы с правами на удаление каталогов профиля пользователя (в C: \ Documents and Settings), несмотря на то, что я мог это сделать в оболочке Explorer.

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

Мне не имеет смысла, что "файловая" операция делает с каталогом, но я знаю, что она работает, и мне этого достаточно!

3 голосов
/ 05 августа 2016

Этот ответ основан на: https://stackoverflow.com/a/1703799/184528. Разница с моим кодом заключается в том, что многие рекурсивные каталоги и файлы удаляются только при необходимости, вызов Directory.Delete завершается неудачно с первой попытки (что может произойти) из-за проводника Windows, смотрящего на каталог).

    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);
        } 
    }
...