Move-Item causes "File Already Exists" error, despite folder having been deleted

JohnLBevan picture JohnLBevan · Apr 21, 2017 · Viewed 8.8k times · Source

I have some code which deletes a folder, then copies files from a temporary directory to where that folder had been.

Remove-Item -Path '.\index.html' -Force
Remove-Item -Path '.\generated' -Force -Recurse  #folder containing generated files

#Start-Sleep -Seconds 10 #uncommenting this line fixes the issue

#$tempDir contains index.html and a sub folder, "generated", which contains additional files.  
#i.e. we're replacing the content we just deleted with new versions.
Get-ChildItem -Path $tempDir | %{
    Move-Item -Path $_.FullName -Destination $RelativePath -Force 
}

I get an intermittent error, Move-Item : Cannot create a file when that file already exists. on the Move-Item line for the generated path.

I've been able to prevent this by adding a hacky Start-Sleep -Seconds 10 after the second Remove-Item statement; though that's not a great solution.

I assume the issue is that the Remove-Item statement completes / code moves on to the next line, before the OS has caught up with the actual file deletion; though that seems odd/worrying. NB: There are ~2,500 files in the generated folder (all between 1-100 KBs).

There are no other processes accessing the folders (i.e. I've even closed my explorer windows & tested with this directory being excluded from my AV).

I've considered other options:

  • using Copy-Item instead of Move-Item. I don't like this as it requires creating new files when they're not required (i.e. a copy is slower than a move)... It's faster than my current sleep hack; but still not ideal.

  • deleting the files & not the folder, then iterating through the subfolders & copying files to the new locations. This would work, but is a lot more code for something that should be simple; so I don't want to pursue that option.

  • Robocopy would do the trick; but I'd prefer a pure PowerShell solution. This is the option I'll eventually pick if there is no clean solution though.

Question

  • Has anyone seen this before?
  • Is it a bug, or have I missed something?
  • Is anyone aware of a fix / good workaround?

Update

Running the remove in a separate job (i.e. using the code below) did not resolve the issue.

Start-Job -ScriptBlock {
    Remove-Item -Path '.\index.html' -Force
    Remove-Item -Path '.\generated' -Force -Recurse  #folder containing generated files
} | Wait-Job | Out-Null

#$tempDir contains index.html and a sub folder, "generated", which contains additional files.  
#i.e. we're replacing the content we just deleted with new versions.
Get-ChildItem -Path $tempDir | %{
    Move-Item -Path $_.FullName -Destination $RelativePath -Force 
}

Update #2

Adding this works; i.e. rather than waiting a fixed time, we wait for the path to be removed / checking every second. If it's not removed after 30 seconds we assume it's not going to be; so carry on regardless (which will cause the move-item to throw an error which gets handled elsewhere).

# ... remove-item code ...
Start-Job -ScriptBlock {
    param($Path)
    while(Test-Path $Path){start-sleep -Seconds 1}
} -ArgumentList '.\generated' | Wait-Job -Timeout 30 | Out-Null
# ... move-item code ...

Answer

JohnLBevan picture JohnLBevan · Apr 22, 2017

In the end I settled for this solution; not perfect, but it works.

Remove-Item -Path '.\index.html' -Force
Remove-Item -Path '.\generated' -Force -Recurse  #folder containing generated files

#wait until the .\generated directory is full removed; or until ~30 seconds has elapsed
1..30 | %{
    if (-not (Test-Path -Path '.\generated' -PathType Container)) {break;}
    Start-Sleep -Seconds 1
}

Get-ChildItem -Path $tempDir | %{
    Move-Item -Path $_.FullName -Destination $RelativePath -Force 
}

This does the same as the job in update #2 of the question; only doesn't require the overhead of a job; just loops until the file's removed.

Here's the above logic wrapped as a reuable cmdlet:

function Wait-Item {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipeline, HelpMessage = 'The path of the item you wish to wait for')]
        [string]$Path
        ,
        [Parameter(HelpMessage = 'How many seconds to wait for the item before giving up')]
        [ValidateRange(1,[int]::MaxValue)]
        [int]$TimeoutSeconds = 30
        ,
        [Parameter(HelpMessage = 'By default the function waits for an item to appear.  Adding this switch causes us to wait for the item to be removed.')]
        [switch]$Remove
    )
    process {
        [bool]$timedOut = $true
        1..$TimeoutSeconds | %{
            if ((Test-Path -Path $Path) -ne ($Remove.IsPresent)){$timedOut=$false; return;}
            Start-Sleep -Seconds 1
        }
        if($timedOut) {
            Write-Error "Wait-Item timed out after $TimeoutSeconds waiting for item '$Path'"
        }
    }
}

#example usage:
Wait-Item -Path '.\generated' -TimeoutSeconds 30 -Remove