Обновление : API удаления элементов файловой системы Windows, по-видимому, наконец-то стали синхронными с Windows 10 версии 1903 - см. https://github.com/dotnet/corefx/issues/33603#issuecomment-498470360.
Это в конечном итоге только хронометраж проблема : последний дескриптор подкаталога может быть еще не закрыт во время попытки удалить родительский каталог - и это фундаментальная проблема ,не ограничиваясь открытием окон Проводника Файла:
Невероятно, API удаления файлов и каталогов Windows асинхронный : то есть к моменту возврата вызова функции,не гарантируется, что удаление завершено еще .
К сожалению, Remove-Item
не в состоянии объяснить это - и никто не делает cmd.exe
's rd /s
и .NET [System.IO.Directory]::Delete()
- подробности см. в этом ответе .Это приводит к периодическим, непредсказуемым сбоям.
Обходной путь предоставлен этим видео на YouTube (начинается с7:35), реализация PowerShell которой приведена ниже:
Синхронная функция удаления каталогов Remove-FileSystemItem
:
Важно:
Синхронная пользовательская реализация требуется только в Windows , поскольку системные вызовы удаления файлов на Unix-подобных платформах изначально синхронны.Поэтому функция просто откладывается до Remove-Item
на Unix-подобных платформах.В Windows пользовательская реализация:
- требует, чтобы каталог parent удаляемого каталога был доступный для записи для синхронного пользовательскогореализация для работы.
- также применяется при удалении каталогов на любых сетевых дисках .
Что НЕ помешает надежномуудаление:
File Explorer, по крайней мере в Windows 10, не блокирует каталоги, которые он отображает, поэтому он не будет препятствовать удалению.
PowerShell также не блокирует каталоги, поэтому наличие другого окна PowerShell, текущее местоположение которого является целевым каталогом или один из его подкаталогов, не помешает удалению (напротив, cmd.exe
делает lock - см. Ниже).
Файлы, открытые с помощью FILE_SHARE_DELETE
/ [System.IO.FileShare]::Delete
(что редко) в поддереве целевого каталога, также не будут препятствовать удалению, хотя онижить под временным именем в родительском директореy до тех пор, пока последняя ручка к ним не будет закрыта.
Что будет препятствовать удалению :
Если есть проблема с разрешениями (если ACL препятствуют удалению), удаление прерывается.
Если на неопределенный срок заблокирован файл или каталог обнаружен, удаление отменено.В частности, это включает:
cmd.exe
(Командная строка), в отличие от PowerShell, блокирует каталог, который является его текущим каталогом, поэтому, если у вас есть cmd.exe
открыть окно, текущим каталогом которого является целевой каталог или один из его подкаталогов, удаление приведет к ошибке .
Если приложение сохраняет файл открытым в целевом каталогеподдерево, которое было не открыто с режимом обмена файлами FILE_SHARE_DELETE
/ [System.IO.FileShare]::Delete
(использование этого режима редко), удаление не удастся.Обратите внимание, что это относится только к приложениям, которые держат файлы открытыми при работе с их содержимым.(например, приложения Microsoft Office), в то время как текстовые редакторы, такие как Notepad и Visual Studio Code, напротив, не сохраняют их открытыми.
Скрытые файлы и файлы с атрибутом только для чтения:
- Это тихо удалено ;другими словами: эта функция неизменно ведет себя как
Remove-Item -Force
. - Обратите внимание, однако, что для нацеливания скрытых файлов / каталогов на input , вы должны указать их как literal paths, потому что они не будут найдены через выражение подстановочного знака.
Надежная пользовательская реализация в Windows достигается за счет снижения производительности.
function Remove-FileSystemItem {
<#
.SYNOPSIS
Removes files or directories reliably and synchronously.
.DESCRIPTION
Removes files and directories, ensuring reliable and synchronous
behavior across all supported platforms.
The syntax is a subset of what Remove-Item supports; notably,
-Include / -Exclude and -Force are NOT supported; -Force is implied.
As with Remove-Item, passing -Recurse is required to avoid a prompt when
deleting a non-empty directory.
IMPORTANT:
* On Unix platforms, this function is merely a wrapper for Remove-Item,
where the latter works reliably and synchronously, but on Windows a
custom implementation must be used to ensure reliable and synchronous
behavior. See https://github.com/PowerShell/PowerShell/issues/8211
* On Windows:
* The *parent directory* of a directory being removed must be
*writable* for the synchronous custom implementation to work.
* The custom implementation is also applied when deleting
directories on *network drives*.
* If an indefinitely *locked* file or directory is encountered, removal is aborted.
By contrast, files opened with FILE_SHARE_DELETE /
[System.IO.FileShare]::Delete on Windows do NOT prevent removal,
though they do live on under a temporary name in the parent directory
until the last handle to them is closed.
* Hidden files and files with the read-only attribute:
* These are *quietly removed*; in other words: this function invariably
behaves like `Remove-Item -Force`.
* Note, however, that in order to target hidden files / directories
as *input*, you must specify them as a *literal* path, because they
won't be found via a wildcard expression.
* The reliable custom implementation on Windows comes at the cost of
decreased performance.
.EXAMPLE
Remove-FileSystemItem C:\tmp -Recurse
Synchronously removes directory C:\tmp and all its content.
#>
[CmdletBinding(SupportsShouldProcess, ConfirmImpact='Medium', DefaultParameterSetName='Path', PositionalBinding=$false)]
param(
[Parameter(ParameterSetName='Path', Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
[string[]] $Path
,
[Parameter(ParameterSetName='Literalpath', ValueFromPipelineByPropertyName)]
[Alias('PSPath')]
[string[]] $LiteralPath
,
[switch] $Recurse
)
begin {
# !! Workaround for https://github.com/PowerShell/PowerShell/issues/1759
if ($ErrorActionPreference -eq [System.Management.Automation.ActionPreference]::Ignore) { $ErrorActionPreference = 'Ignore'}
$targetPath = ''
$yesToAll = $noToAll = $false
function trimTrailingPathSep([string] $itemPath) {
if ($itemPath[-1] -in '\', '/') {
# Trim the trailing separator, unless the path is a root path such as '/' or 'c:\'
if ($itemPath.Length -gt 1 -and $itemPath -notmatch '^[^:\\/]+:.$') {
$itemPath = $itemPath.Substring(0, $itemPath.Length - 1)
}
}
$itemPath
}
function getTempPathOnSameVolume([string] $itemPath, [string] $tempDir) {
if (-not $tempDir) { $tempDir = [IO.Path]::GetDirectoryName($itemPath) }
[IO.Path]::Combine($tempDir, [IO.Path]::GetRandomFileName())
}
function syncRemoveFile([string] $filePath, [string] $tempDir) {
# Clear the ReadOnly attribute, if present.
if (($attribs = [IO.File]::GetAttributes($filePath)) -band [System.IO.FileAttributes]::ReadOnly) {
[IO.File]::SetAttributes($filePath, $attribs -band -bnot [System.IO.FileAttributes]::ReadOnly)
}
$tempPath = getTempPathOnSameVolume $filePath $tempDir
[IO.File]::Move($filePath, $tempPath)
[IO.File]::Delete($tempPath)
}
function syncRemoveDir([string] $dirPath, [switch] $recursing) {
if (-not $recursing) { $dirPathParent = [IO.Path]::GetDirectoryName($dirPath) }
# Clear the ReadOnly attribute, if present.
# Note: [IO.File]::*Attributes() is also used for *directories*; [IO.Directory] doesn't have attribute-related methods.
if (($attribs = [IO.File]::GetAttributes($dirPath)) -band [System.IO.FileAttributes]::ReadOnly) {
[IO.File]::SetAttributes($dirPath, $attribs -band -bnot [System.IO.FileAttributes]::ReadOnly)
}
# Remove all children synchronously.
$isFirstChild = $true
foreach ($item in [IO.directory]::EnumerateFileSystemEntries($dirPath)) {
if (-not $recursing -and -not $Recurse -and $isFirstChild) { # If -Recurse wasn't specified, prompt for nonempty dirs.
$isFirstChild = $false
# Note: If -Confirm was also passed, this prompt is displayed *in addition*, after the standard $PSCmdlet.ShouldProcess() prompt.
# While Remove-Item also prompts twice in this scenario, it shows the has-children prompt *first*.
if (-not $PSCmdlet.ShouldContinue("The item at '$dirPath' has children and the -Recurse switch was not specified. If you continue, all children will be removed with the item. Are you sure you want to continue?", 'Confirm', ([ref] $yesToAll), ([ref] $noToAll))) { return }
}
$itemPath = [IO.Path]::Combine($dirPath, $item)
([ref] $targetPath).Value = $itemPath
if ([IO.Directory]::Exists($itemPath)) {
syncremoveDir $itemPath -recursing
} else {
syncremoveFile $itemPath $dirPathParent
}
}
# Finally, remove the directory itself synchronously.
([ref] $targetPath).Value = $dirPath
$tempPath = getTempPathOnSameVolume $dirPath $dirPathParent
[IO.Directory]::Move($dirPath, $tempPath)
[IO.Directory]::Delete($tempPath)
}
}
process {
$isLiteral = $PSCmdlet.ParameterSetName -eq 'LiteralPath'
if ($env:OS -ne 'Windows_NT') { # Unix: simply pass through to Remove-Item, which on Unix works reliably and synchronously
Remove-Item @PSBoundParameters
} else { # Windows: use synchronous custom implementation
foreach ($rawPath in ($Path, $LiteralPath)[$isLiteral]) {
# Resolve the paths to full, filesystem-native paths.
try {
# !! Convert-Path does find hidden items via *literal* paths, but not via *wildcards* - and it has no -Force switch (yet)
# !! See https://github.com/PowerShell/PowerShell/issues/6501
$resolvedPaths = if ($isLiteral) { Convert-Path -ErrorAction Stop -LiteralPath $rawPath } else { Convert-Path -ErrorAction Stop -path $rawPath}
} catch {
Write-Error $_ # relay error, but in the name of this function
continue
}
try {
$isDir = $false
foreach ($resolvedPath in $resolvedPaths) {
# -WhatIf and -Confirm support.
if (-not $PSCmdlet.ShouldProcess($resolvedPath)) { continue }
if ($isDir = [IO.Directory]::Exists($resolvedPath)) { # dir.
# !! A trailing '\' or '/' causes directory removal to fail ("in use"), so we trim it first.
syncRemoveDir (trimTrailingPathSep $resolvedPath)
} elseif ([IO.File]::Exists($resolvedPath)) { # file
syncRemoveFile $resolvedPath
} else {
Throw "Not a file-system path or no longer extant: $resolvedPath"
}
}
} catch {
if ($isDir) {
$exc = $_.Exception
if ($exc.InnerException) { $exc = $exc.InnerException }
if ($targetPath -eq $resolvedPath) {
Write-Error "Removal of directory '$resolvedPath' failed: $exc"
} else {
Write-Error "Removal of directory '$resolvedPath' failed, because its content could not be (fully) removed: $targetPath`: $exc"
}
} else {
Write-Error $_ # relay error, but in the name of this function
}
continue
}
}
}
}
}