[C#] Directory.Delete (path, true)를 사용하여 디렉토리를 삭제할 수 없습니다.

.NET 3.5를 사용하고 있는데 다음을 사용하여 디렉토리를 반복적으로 삭제하려고합니다.

Directory.Delete(myPath, true);

파일을 사용 중이거나 권한 문제가있는 경우이를 throw해야하지만 그렇지 않으면 디렉토리와 모든 내용을 삭제해야합니다.

그러나 때때로 나는 이것을 얻는다 :

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 대신 이것을 볼 이유가 있습니까?



답변

편집자 주 : 이 답변에는 유용한 정보가 포함되어 있지만의 작동 방식에 대해서는 실제로 잘못되었습니다 Directory.Delete. 이 답변에 대한 의견과이 질문에 대한 다른 답변을 읽으십시오.


나는 전에이 문제에 부딪쳤다.

문제의 근본 원인은이 함수가 디렉토리 구조 내에있는 파일을 삭제하지 않는다는 것입니다. 따라서 디렉토리 자체를 제거하기 전에 디렉토리 구조 내의 모든 파일을 삭제 한 다음 모든 디렉토리를 삭제하는 함수를 작성해야합니다. 나는 이것이 두 번째 매개 변수에 위배된다는 것을 알고 있지만 훨씬 안전한 접근 방식입니다. 또한 파일을 삭제하기 직전에 파일에서 읽기 전용 액세스 속성을 제거하려고 할 수 있습니다. 그렇지 않으면 예외가 발생합니다.

이 코드를 프로젝트에 넣으십시오.

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:\.


답변

재귀 적으로 디렉토리를 삭제하려고 시도하고 탐색기에서 a디렉토리 a\b가 열려 b있으면 삭제되지만 a이동하고 볼 때 디렉토리가 비어 있어도 ‘디렉토리가 비어 있지 않습니다’라는 오류 가 발생합니다. 모든 응용 프로그램 (탐색기 포함)의 현재 디렉토리는 디렉토리에 대한 핸들을 유지합니다 . 당신이 호출 할 때 Directory.Delete(true), 그것은 아래에서 위로 삭제합니다 b다음 a. b탐색기에서이 열려 있으면 탐색기에서 삭제를 감지하고 b디렉토리를 위쪽으로 변경 cd ..하며 열린 핸들을 정리합니다. 파일 시스템이 비동기 적으로 작동하기 때문에 Directory.Delete탐색기와의 충돌로 인해 작업이 실패합니다.

불완전한 솔루션

필자는 탐색기 스레드가 디렉토리 핸들을 해제 할 수 있도록 현재 스레드를 중단한다는 아이디어와 함께 다음 솔루션을 게시했습니다.

// 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)의 시작 부분에 a 를 추가하여 일반적인 조건에서 발생하고 포착되는 예외 수를 줄일 수 있습니다 try. 또한 시스템로드가 많은 경우 두 가지 Directory.Delete시도 를 모두 수행 하여 실패 할 수있는 위험이 있습니다. 이 솔루션을보다 강력한 재귀 삭제의 시작점으로 고려하십시오.

일반적인 답변

이 솔루션은 Windows 탐색기와 상호 작용하는 특성 만 해결합니다. 견고한 삭제 작업을 원할 경우, 무엇이든 (바이러스 스캐너 등) 언제라도 삭제하려는 항목에 대해 열린 핸들을 가질 수 있다는 점을 명심해야합니다. 따라서 나중에 다시 시도해야합니다. 나중에 얼마나 많이 시도하고 몇 번 시도하는지는 개체를 삭제하는 것이 얼마나 중요한지에 달려 있습니다. 으로 MSDN을 나타냅니다 ,

강력한 파일 반복 코드는 파일 시스템의 많은 복잡성을 고려해야합니다.

NTFS 참조 문서에 대한 링크 만 제공되는이 무고한 진술은 머리카락을 돋보이게해야합니다.

( 편집 : 많이.이 답변은 원래 첫 번째, 불완전한 솔루션 만 가지고있었습니다.)


답변

계속 진행하기 전에 제어 할 수있는 다음 이유를 확인하십시오.

  • 폴더가 프로세스의 현재 디렉토리로 설정되어 있습니까? 그렇다면 먼저 다른 것으로 변경하십시오.
  • 해당 폴더에서 파일을 열거 나 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 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
}

필자의 의견으로는 가짜 실패가 항상 가능하기 때문에 모든 삭제에 이와 같은 도우미를 사용해야합니다. 그러나이 코드를 맹목적으로 복사하는 것이 아니라 사용 사례에 맞게 수정해야합니다.

% LocalAppData % 아래에있는 내 앱에서 생성 된 내부 데이터 폴더에 대해 잘못된 오류가 발생하여 분석은 다음과 같습니다.

  1. 폴더는 내 응용 프로그램에 의해서만 제어되며 사용자는 해당 폴더 내에서 읽기 전용 또는 액세스 할 수없는 것으로 표시하고 그 이유를 다루지 않을 유효한 이유가 없습니다.

  2. 거기에는 사용자가 만든 귀중한 자료가 없으므로 실수로 무언가를 강제로 삭제할 위험이 없습니다.

  3. 내부 데이터 폴더이기 때문에 탐색기에서 폴더가 열릴 것으로 기대하지 않습니다. 적어도 사건을 구체적으로 처리 할 필요가 없다고 생각합니다 (즉, 지원을 통해 사건을 잘 처리하고 있습니다).

  4. 모든 시도가 실패하면 오류를 무시하기로 선택합니다. 최악의 경우, 앱이 새로운 리소스의 압축을 풀지 못하고 충돌이 발생하여 사용자에게 지원을 요청하라는 메시지를 표시합니다. 지원이 자주 발생하지 않는 한 허용됩니다. 또는 앱이 충돌하지 않으면 오래된 데이터가 남겨져 다시 받아 들일 수 있습니다.

  5. 재 시도를 500ms (50 * 10)로 제한합니다. 이것은 실제로 작동하는 임의의 임계 값입니다. 사용자가 응답을 멈췄다 고 생각하면서 앱을 죽이지 않도록 임계 값을 짧게 만들고 싶었습니다. 반면에, 0.5 초는 범죄자가 내 폴더 처리를 마치는 데 충분한 시간입니다. 때로는 Sleep(0)받아 들일 수있는 것으로 보이는 다른 SO 답변으로 판단 할 때, 한 번의 재시도 이상을 경험하는 사용자는 거의 없습니다.

  6. 나는 50ms마다 다시 시도하는데, 이는 또 다른 임의의 숫자입니다. 파일을 삭제하려고 할 때 파일을 처리 (인덱싱, 검사)하면 50ms가 필자의 경우 처리가 완료 될 것으로 예상되는 시점입니다. 또한 50ms는 눈에 띄게 느려지지 않을 정도로 작습니다. 다시 말하지만, Sleep(0)많은 경우에 충분한 것으로 보이므로 너무 지연하고 싶지 않습니다.

  7. 코드는 모든 IO 예외에서 재 시도합니다. 나는 일반적으로 % LocalAppData %에 액세스하는 예외를 기대하지 않으므로, 단순성을 선택하고 합법적 인 예외가 발생할 경우 500ms 지연의 위험을 감수했습니다. 또한 다시 시도하려는 정확한 예외를 감지하는 방법을 알고 싶지 않았습니다.


답변

현대 비동기 답변

허용되는 대답은 잘못되었습니다. 디스크에서 파일을 가져 오는 데 걸리는 시간이 파일을 잠그는 모든 것을 해제하기 때문에 일부 사람들에게는 효과가있을 수 있습니다. 사실 이것은 다른 프로세스 / 스트림 / 액션에 의해 파일이 잠겨 있기 때문에 발생합니다. 다른 답변은 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);
        }
    }
}


답변

언급해야 할 중요한 사항 중 하나는 주석으로 추가했지만 허용되지는 않습니다. 오버로드의 동작이 .NET 3.5에서 .NET 4.0으로 변경되었다는 것입니다.

Directory.Delete(myPath, true);

.NET 4.0부터는 3.5 폴더가 아닌 폴더 자체의 파일을 삭제합니다. 이것은 MSDN 설명서에서도 볼 수 있습니다.

.NET 4.0

지정된 디렉토리지정된 경우 디렉토리의 서브 디렉토리 및 파일을 삭제 합니다 .

.NET 3.5

비어있는 디렉토리 와 표시된 경우 디렉토리의 하위 디렉토리 및 파일을 삭제 합니다 .


답변

델파이에서도 똑같은 문제가있었습니다. 그리고 최종 결과는 내 응용 프로그램이 삭제하려는 디렉토리를 잠그고 있다는 것입니다. 어떻게 든 디렉토리에 쓸 때 디렉토리가 잠겼습니다 (일부 임시 파일).

캐치 22는 삭제하기 전에 부모로 간단한 변경 디렉토리 를 만들었 습니다.


답변

나는이 간단한 비 재귀 적 방법에 대해 아무도 생각하지 않아 놀랐습니다.이 방법은 각 파일의 읽기 전용 속성을 변경할 필요없이 읽기 전용 파일을 포함하는 디렉토리를 삭제할 수 있습니다.

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

(인터넷을 통해 사용할 수있는 cmd 창을 일시적으로 실행하지 않도록 비트를 변경하십시오)