Невозможно удалить элемент, каталог не пуст - PullRequest
0 голосов
/ 30 ноября 2018

При использовании команды Remove-Item, даже при использовании параметров -r и -Force, иногда возвращается следующее сообщение об ошибке:

Remove-Item: невозможно удалить элемент C: \Тестовая папка \ Тестовая папка \ Цель: каталог не пустой.

В частности, это происходит, когда каталог, который нужно удалить, открывается в проводнике Windows.

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

Существует ли параметр, более мощный, чем -Force, который я могу установить для достижения этой цели?

Чтобы надежно воспроизвести это, создайте папку C:\Test Folder\Origin изаполните его некоторыми файлами и подпапками (важно), затем возьмите следующий сценарий или один подобный и выполните его один раз,Теперь откройте одну из подпапок C:\Test Folder\Target (в моем случае я использовал C:\Test Folder\Target\Another Subfolder, содержащий A third file.txt) и попробуйте снова запустить скрипт.Теперь вы получите ошибку.Если вы запустите сценарий в третий раз, вы не получите ошибку снова (в зависимости от обстоятельств, которые мне еще предстоит определить, однако, ошибка иногда возникает во второй раз, а затем никогда больше, а в других случаях каждый второй раз).

$SourcePath =  "C:\Test Folder\Origin"
$TargetPath =  "C:\Test Folder\Target"

if (Test-Path $TargetPath) {
    Remove-Item -r $TargetPath -Force
}
New-Item -ItemType directory -Path $TargetPath 

Copy-Item $SourcePath -Destination $TargetPath -Force -Recurse -Container 

1 Ответ

0 голосов
/ 30 ноября 2018

Обновление : 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
          }
        }
      }
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...