Принудительное удаление файлов и каталогов в PowerShell иногда не удается, но не всегда.

Вопрос или проблема

Я пытаюсь рекурсивно удалить каталог с помощью rm -Force -Recurse somedirectory, но получаю несколько ошибок “Каталог не пуст”. Если я повторяю ту же команду, она выполняется успешно.

Пример:

PS I:\Documents and Settings\m\My Documents\prg\net> rm -Force -Recurse .\FileHelpers
Remove-Item : Не удается удалить элемент I:\Documents and Settings\m\My Documents\prg\net\FileHelpers\FileHelpers.Tests\Data\RunTime\_svn: Каталог не пуст.
At line:1 char:3
+ rm <<<<  -Force -Recurse .\FileHelpers
    + CategoryInfo          : WriteError: (_svn:DirectoryInfo) [Remove-Item], IOException
    + FullyQualifiedErrorId : RemoveFileSystemItemIOError,Microsoft.PowerShell.Commands.RemoveItemCommand
Remove-Item : Не удается удалить элемент I:\Documents and Settings\m\My Documents\prg\net\FileHelpers\FileHelpers.Tests\Data\RunTime: Каталог не пуст.
At line:1 char:3
+ rm <<<<  -Force -Recurse .\FileHelpers
    + CategoryInfo          : WriteError: (RunTime:DirectoryInfo) [Remove-Item], IOException
    + FullyQualifiedErrorId : RemoveFileSystemItemIOError,Microsoft.PowerShell.Commands.RemoveItemCommand
Remove-Item : Не удается удалить элемент I:\Documents and Settings\m\My Documents\prg\net\FileHelpers\FileHelpers.Tests\Data: Каталог не пуст.
At line:1 char:3
+ rm <<<<  -Force -Recurse .\FileHelpers
    + CategoryInfo          : WriteError: (Data:DirectoryInfo) [Remove-Item], IOException
    + FullyQualifiedErrorId : RemoveFileSystemItemIOError,Microsoft.PowerShell.Commands.RemoveItemCommand
Remove-Item : Не удается удалить элемент I:\Documents and Settings\m\My Documents\prg\net\FileHelpers\FileHelpers.Tests: Каталог не пуст.
At line:1 char:3
+ rm <<<<  -Force -Recurse .\FileHelpers
    + CategoryInfo          : WriteError: (FileHelpers.Tests:DirectoryInfo) [Remove-Item], IOException
    + FullyQualifiedErrorId : RemoveFileSystemItemIOError,Microsoft.PowerShell.Commands.RemoveItemCommand
Remove-Item : Не удается удалить элемент I:\Documents and Settings\m\My Documents\prg\net\FileHelpers\Libs\nunit\_svn: Каталог не пуст.
At line:1 char:3
+ rm <<<<  -Force -Recurse .\FileHelpers
    + CategoryInfo          : WriteError: (_svn:DirectoryInfo) [Remove-Item], IOException
    + FullyQualifiedErrorId : RemoveFileSystemItemIOError,Microsoft.PowerShell.Commands.RemoveItemCommand
Remove-Item : Не удается удалить элемент I:\Documents and Settings\m\My Documents\prg\net\FileHelpers\Libs\nunit: Каталог не пуст.
At line:1 char:3
+ rm <<<<  -Force -Recurse .\FileHelpers
    + CategoryInfo          : WriteError: (nunit:DirectoryInfo) [Remove-Item], IOException
    + FullyQualifiedErrorId : RemoveFileSystemItemIOError,Microsoft.PowerShell.Commands.RemoveItemCommand
Remove-Item : Не удается удалить элемент I:\Documents and Settings\m\My Documents\prg\net\FileHelpers\Libs: Каталог не пуст.
At line:1 char:3
+ rm <<<<  -Force -Recurse .\FileHelpers
    + CategoryInfo          : WriteError: (Libs:DirectoryInfo) [Remove-Item], IOException
    + FullyQualifiedErrorId : RemoveFileSystemItemIOError,Microsoft.PowerShell.Commands.RemoveItemCommand
Remove-Item : Не удается удалить элемент I:\Documents and Settings\m\My Documents\prg\net\FileHelpers: Каталог не пуст.
At line:1 char:3
+ rm <<<<  -Force -Recurse .\FileHelpers
    + CategoryInfo          : WriteError: (I:\Documents an...net\FileHelpers:DirectoryInfo) [Remove-Item], IOException
    + FullyQualifiedErrorId : RemoveFileSystemItemIOError,Microsoft.PowerShell.Commands.RemoveItemCommand
PS I:\Documents and Settings\m\My Documents\prg\net> rm -Force -Recurse .\FileHelpers
PS I:\Documents and Settings\m\My Documents\prg\net>

Конечно, это происходит не всегда. Также это происходит не только с каталогами _svn, и у меня нет кэша TortoiseSVN или чего-то подобного, так что ничто не блокирует каталог.

Какие-нибудь идеи?

help Remove-Item говорит:

Параметр Recurse в этом cmdlet работает неправильно.

и

Поскольку параметр Recurse в этом cmdlet неисправен, команда использует cmdlet Get-Childitem для получения желаемых файлов и использует оператор конвейера для передачи их в cmdlet Remove-Item.

и предлагает следующую альтернативу в качестве примера:

get-childitem * -include *.csv -recurse | remove-item

Таким образом, вы должны пропускать get-childitem -recurse в remove-item.

@JamesCW: Проблема все еще существует в PowerShell 4.0

Я попробовал другое решение, и оно сработало: используйте cmd.exe:

&cmd.exe /c rd /s /q $somedirectory

Обновление: Начиная с (по крайней мере) версии Windows 10 20H2 (я не знаю, Windows Server какой версии и сборки это соответствует; запустите winver.exe, чтобы проверить вашу версию и сборку), функция Windows API DeleteFile теперь демонстрирует синхронное поведение, которое неявно решает проблемы с Remove-Item в PowerShell и .NET System.IO.File.Delete / System.IO.Directory.Delete (но, любопытно, не с cmd.exe rd /s).


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

Remove-Item -Recurse неожиданно асинхронен, в конечном итоге потому, что методы Windows API для удаления файлов и каталогов по своей сути асинхронны и Remove-Item не учитывает это.

Это периодически и непредсказуемо проявляется одним из двух способов:

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

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

Проблема затрагивает не только Remove-Item в PowerShell, но также rd /s в cmd.exe, а также [System.IO.Directory]::Delete() в .NET:

Начиная с Windows PowerShell v5.1 / PowerShell Core 6.2.0-preview.1 / cmd.exe 10.0.17134.407 / .NET Framework 4.7.03056, .NET Core 2.1, Remove-Item, rd /s, и [System.IO.Directory]::Delete() не работают надежно, так как они не учитывают асинхронное поведение функций удаления файлов/каталогов в Windows API:

Для пользовательской функции PowerShell, которая обеспечивает надежное синхронное решение, см. этот ответ на SO.

ETA 20181217: PSVersion 4.0 и более поздние версии все еще будут выходить из строя в некоторых обстоятельствах, см. альтернативный ответ от Mehrdad Mirreza, и отчет о проблеме, поданный mklement

mklement предоставляет концептуальное решение в этом ответе на SO, пока ошибка ждет официального исправления

Новая версия PowerShell (PSVersion 4.0) полностью решила эту проблему, и Remove-Item "targetdirectory" -Recurse -Force работает без проблем с синхронизацией.

Вы можете проверить вашу версию, запустив $PSVersiontable в среде ISE или командной строке PowerShell. Версия 4.0 поставляется с Windows 8.1 и Server 2012 R2, и ее можно установить на более ранние версии Windows.

Ого. Много ответов. Я честно предпочитаю этот более всех остальных. Он супер простой, полный, читаемый и работает на любой Windows машине. Он использует (надежную) рекурсивную функцию удаления .NET, и если он вылетает по какой-то причине, он выдает верное исключение, которое может быть обработано с помощью блока try/catch.

$fullPath = (Resolve-Path "directory\to\remove").ProviderPath
[IO.Directory]::Delete($fullPath, $true)

Обратите внимание, что строка с Resolve-Path важна, поскольку .NET не знает о вашем текущем каталоге при разрешении относительных путей к файлам. Вот и вся тонкость, о которой стоит помнить.

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

Итак, теперь я использую кое-что, что поймает исключение, подождет и попытается снова (3 раза):

Пока я использую следующее:

function EmptyDirectory($directory = $(throw "Required parameter missing")) {

    if ((test-path $directory) -and -not (gi $directory | ? { $_.PSIsContainer })) {
        throw ("EmptyDirectory вызвана не для каталога.");
    }

    $finished = $false;
    $attemptsLeft = 3;

    do {
        if (test-path $directory) {
            rm $directory -recurse -force
        }

        try {
            $null = mkdir $directory
            $finished = $true
        } 
        catch [System.IO.IOException] {
            Start-Sleep -Milliseconds 500
        }

        $attemptsLeft = $attemptsLeft - 1;
    } 
    while (-not $finished -and $attemptsLeft -gt 0)

    if (-not $finished) {
        throw ("Не удалось очистить и воссоздать каталог " + $directory)
    }
}

Чтобы удалить каталог и его содержимое, требуется два шага.
Сначала удалите содержимое, затем саму папку. Используя обходной путь для неисправного рекурсивного элемента удаления, решение будет выглядеть так:

Get-ChildItem -Path "$folder\\*" -Recurse | Remove-Item -Force -Recurse
Remove-Item $folder

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

Вот что у меня работает:

$Target = "c:\folder_to_delete"

Get-ChildItem -Path $Target -Recurse -force |
  Where-Object { -not ($_.psiscontainer) } |
   Remove-Item –Force

Remove-Item -Recurse -Force $Target

Первая линия удаляет все файлы в дереве. Вторая удаляет все папки, включая верхнюю.

Сначала возьмите на себя права на файлы/каталоги, используя Takeown.exe, затем удалите

https://learn-powershell.net/2014/06/24/changing-ownership-of-file-or-folder-using-powershell/

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

Что сработало для меня, так это сжатие родительского каталога с использованием 7-zip с опцией “Удалить файлы после сжатия”. После сжатия я смог удалить zip файл.

Решение выше работает для большинства моих скриптов. Но потом я нашел некоторые папки, на которых вышеуказанное решение не работало. Что исправило это для меня на стойкой папке, так это robocopy /PURGE
/PURGE: Удаляет файлы/папки в назначении, которые больше не существуют в источнике
Так что если вы создадите пустую папку, скопируйте ее в папку назначения с командой robocopy /PURGE. Robocopy очистит ее, затем вы можете удалить папку с помощью
$output_path | Remove-Item -Recurse -force

Мне удалось заставить это работать, перечислив все дочерние файлы и папки (с самыми дальними вначале) и удаляя каждый из них…

Это решение удаляет родительскую папку в обоих сценариях,
когда

  • дочерний файл может быть открыт или

  • дочерняя папка может быть открыта.

    if ((Test-Path $targetFolderPath)) {
         # Очистка содержимого
         Get-ChildItem -Path $targetFolderPath -Recurse | 
         Sort-Object { $_.FullName.Split('\').Count } -Descending | 
         ForEach-Object { Remove-Item $_.FullName -Force }
    
         # Подождите немного, чтобы обеспечить удаление любых заблокированных файлов/каталогов
         Start-Sleep -Seconds 1
    
         # Очистка родительской папки
         Remove-Item $targetFolderPath -Force
     }
    

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

Если вы хотите оставить незатронутую всю структуру папок, опустите строки 2 – 4.
Если вы хотите оставить папки в корне, добавьте “*” к вашему пути в строке 2.

Get-ChildItem "$($backupPath)\$((Get-Date).DayOfWeek)" -Recurse | ? {!($_.PSisContainer)} | Remove-Item -Force -Recurse -Confirm:$false
foreach ($folder in ((Get-ChildItem "$($backupPath)\$((Get-Date).DayOfWeek)" -Recurse | Select FullName | Sort FullName -Descending).FullName)) {
    Remove-Item -Path $folder -Force -Recurse -Confirm:$false
}

Ответ или решение

Удаление файлов и директорий в PowerShell может столкнуться с непредсказуемыми проблемами, особенно при использовании команды Remove-Item -Force -Recurse. Столкнувшись с ошибками "The directory is not empty" и наблюдая, как повторное выполнение той же команды проходит успешно, возникает логичный вопрос о том, что действительно происходит в процессе работы с файловой системой.

Теория

Для понимания проблемы важно отметить, что асинхронность является одним из основных факторов, влияющих на поведение операций удаления в Windows. API операционной системы для работы с файлами и директориями, такие как функции DeleteFile и RemoveDirectory, реализованы с асинхронным поведением. Это означает, что после запроса на удаление файла или директории, фактическое завершение процесса может занять некоторое время, и попытка удаления родительской директории может закончиться неудачей, если при выполнении команды удаление вложенных файлов все еще не завершено.

Пример

Допустим, у нас в PowerShell есть необходимость удалять старые каталоги с отчётами, например, в директории "C:\Reports\2023". Применяя команду:

Remove-Item -Force -Recurse "C:\Reports\2023"

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

Применение

Справиться с данной проблемой можно несколькими способами, которые включают в себя использование PowerShell с учётом его особенностей, а также обходные решения.

  1. Использование Get-ChildItem:

    Один из популярных способов избежать проблемы "директория не пуста" — это сначала удалить все вложенные файлы и директории с помощью Get-ChildItem и только затем удалять саму директорию:

    Get-ChildItem -Path "C:\Reports\2023" -Recurse | Remove-Item -Force
    Remove-Item -Path "C:\Reports\2023" -Force

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

  2. Синхронизация удаления:

    Недавно были введены улучшения в API функций Windows, что позволяет сделать операции синхронными. Если используется актуальная версия Windows 10 или новее, то в ней проблема решена на системном уровне. Необходимо убедиться, что используется актуальная версия через выполнение winver.exe.

  3. Использование .NET объектов:

    Если PowerShell всё равно генерирует ошибки, можно воспользоваться методами .NET, которые порой более надёжны в плане управления ресурсами:

    $directoryPath = (Resolve-Path "C:\Reports\2023").ProviderPath
    [System.IO.Directory]::Delete($directoryPath, $true)

    Однако важно помнить об абсолютных путях — Resolve-Path преобразует путь в абсолютный, что нейтрализует одну из распространенных ошибок при использовании относительных путей.

  4. Ретраймирование и исключения:

    Чтобы гарантированно удалить все объекты, также можно ввести алгоритм с ретраймированием, что позволит повторить попытку удаления после небольшой паузы, если это потребуется:

    function SafeRemove-Directory($directory) {
       $retries = 3
       for ($i = 0; $i -lt $retries; $i++) {
           try {
               Remove-Item -Path $directory -Recurse -Force
               if (-not (Test-Path $directory)) {
                   return
               }
           } catch {
               Start-Sleep -Milliseconds 500
           }
       }
       throw "Не удалось удалить директорию $directory после $retries попыток."
    }

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

Оцените материал
Добавить комментарий

Капча загружается...