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

12,270

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
Share:
12,270
JohnLBevan
Author by

JohnLBevan

I work as a technical architect for an advertising company. Most of my day job revolves around ERP, Finance and Operations systems, though I also get involved with any technical issues which fall into knowledge gaps between the various IT teams' / people's roles. My (IT related) interests are more towards web applications, artificial intelligence, productivity utilities, and scripting. If you want to know more about me than that, just Google my username - it's a perfect example of how not to ensure personal privacy. by JohnLBevan (added for Google Authorship)

Updated on June 05, 2022

Comments

  • JohnLBevan
    JohnLBevan almost 2 years

    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 ...
    
  • Andrew Savinykh
    Andrew Savinykh over 6 years
    I'm not sure how it works. break breaks from a loop % is a shorthand for Foreach-Object which is not a loop but cmdlet. Thus, the break statement will break out something else in outer scope, which can be completely unexpected.