Powershell: Move-Item : Can it sit and wait until the source fle is closed and released?

Viewed 123

I have an ObjectEvent that sees a new file getting created and then tries to move the file.

It works... except when it goes to move the file, the file is still open, so the Move-Item fails.

So I suppose there are two possible paths... I am open to either (or both!)

First, how would my ObjectEvent fire only after the file is closed? Current objectevent:

Register-ObjectEvent $Watcher -EventName Created -SourceIdentifier FileCreated -Action 

Second, is it possible for MoveItem to sit and keep trying for 5 seconds or so before failing? Current Move-Item call:

Move-Item $path -Destination $destination -Force -Verbose
1 Answers

The FileSystemWatcher class have a few events available. Changed, Created, Deleted, Error and Renamed. There is nothing there to indicate if the file was recently unlocked by the system. So there is no direct way to fire your event specifically after the file was closed.

What I would do personally to make that the files are moved properly would be to delay the move to X ms after the file creation event is fired.

Here's a summary of how that would work.

Prerequisites

  • A Filewatcher, which detects the file being created
  • A timer, which will move the files after their creation
  • A queue, on which the Filewatcher enqueue items being created and on which the timer dequeue items that need processing. It is what will communicate the information between the two.

Flow

  • A file is created
  • The Filewatcher created event trigger and add the item in the queue, then start the timer.
  • The timer elapsed event trigger, stop itself and process the queue. If any items remains in the queue, it restart itself.

Here's a base version of all of that.

$Params = @{
    # Path we are watching
    WatchPath         = 'C:\temp\11\test'
    # Path we are moving stuff to.
    DestinationPath   = 'C:\temp\11'
    # Stop after X attempts
    MoveAttempts      = 5
    # Timer heartbeat
    MoveAttemptsDelay = 1000 
}

# Create watcher
$Watcher = [System.IO.FileSystemWatcher]::new()
$Watcher.Path = $Params.WatchPath
$Watcher.EnableRaisingEvents = $true    

# Create Timer - stopped state
$WatchTimer = [System.Timers.Timer]::new()
$WatchTimer.Interval = 1000

#Create WatchQueue (Each items will be FullPath,Timestamp,MoveAttempts)
$WatchQueue = [System.Collections.Generic.Queue[psobject]]::new()

$FileWatcherReg = @{
    InputObject      = $Watcher
    EventName        = 'Created'
    SourceIdentifier = 'FileCreated'
    MessageData      = @{WatchQueue = $WatchQueue; Timer = $WatchTimer }
    Action           = {
        if ($null -ne $event) {
            $Queue = $Event.MessageData.WatchQueue
            $Timer = $Event.MessageData.Timer
            $Queue.Enqueue([PSCustomObject]@{
                    FullPath     = $Event.SourceArgs.FullPath
                    TimeStamp    = $Event.TimeGenerated
                    MoveAttempts = 0
                })
            
            # We only start the timer if it is not already counting down
            # We also don't want to start the timer if item is not 1 since this mean 
            # the timer logic is already running.
            if ($Queue.Count -eq 1 -and ! $Timer.Enabled) { $Timer.Start() }
        }
    }
   
}

$TimerReg = @{
    InputObject      = $WatchTimer
    EventName        = 'Elapsed'
    Sourceidentifier = 'WatchTimerElapsed'
    MessageData      = @{WatchQueue = $WatchQueue; ConfigParams = $Params }
    Action           = {
        $Queue = $Event.MessageData.WatchQueue
        $ConfigParams = $Event.MessageData.ConfigParams
        $Event.Sender.Stop()
        $SkipItemsCount = 0
        while ($Queue.Count -gt 0 + $SkipItemsCount ) {
            $Item = $Queue.Dequeue()
            $ItemName = Split-Path $item.FullPath -Leaf

            while ($Item.MoveAttempts -lt $ConfigParams.MoveAttempts) {
                try {
                    Write-Host 'test'
                    $Item.MoveAttempts += 1
                    Move-Item -Path $Item.FullPath -Destination "$($ConfigParams.DestinationPath)\$ItemName" -ErrorAction Stop
                    break
                }
                catch {
                    $ex = $_.Exception
                    if ( $Item.MoveAttempts -eq 5) {
                        # Do something about it... Log / Warn / etc...
                        Write-warning "Move attempts: $($ConfigParams.MoveAttempts)"
                        Write-Warning "FilePath: $($Item.FullPath)"
                        Write-Warning $ex
                        continue
                    }
                    else {
                        
                        $Queue.Enqueue($Item)
                        $SkipItemsCount += 1
                    }
                    
                }
            }
            # If we skipped any items, we don't want to dequeue until 0 anymore but rather we will stop
            if ($SkipItemsCount -gt 0){
                 $Event.Sender.Start()
            }
        }
    }

}

# ObjectEvent for FileWatcher
Register-ObjectEvent @FileWatcherReg 
# ObjectEvent for Timer which process stuff in a delayed fashion
Register-ObjectEvent @TimerReg

while ($true) {
    Start-Sleep -Milliseconds 100
}

# Unregister events at the end 
Unregister-Event -SourceIdentifier FileCreated | Out-Null # Will fail first time since never registered
Unregister-Event -SourceIdentifier WatchTimerElapsed | Out-Null
Related