PowerShell File Watcher Register-ObjectEvent - 等待文件完成复制

PowerShell File Watcher Register-ObjectEvent - Wait For File To Complete Copying

下面的代码正在检查指定目录中的新文件 $folderCompleted

目前,当一个小文件被放入该目录 (~1MB) 时,Move-Item 命令和其他文件读取检查成功完成。

但是,当将大文件移入此目录时,会在文件完全移入(复制)到该目录之前调用 Object 事件。这导致文件检查和 Move-Item 命令失败,因为该文件仍在使用中。

# File Watcher
$filter = '*.*'
$fsw = New-Object IO.FileSystemWatcher $folderCompleted, $filter -Property @{
    IncludeSubdirectories = $true
    NotifyFilter = [IO.NotifyFilters]'FileName, LastWrite'
}

$onCreated = Register-ObjectEvent $fsw Created -SourceIdentifier FileCreated -Action {
    $path = $Event.SourceEventArgs.FullPath
    $name = $Event.SourceEventArgs.Name
    $changeType = $Event.SourceEventArgs.ChangeType
    $timeStamp = $Event.TimeGenerated
    Write-Host "The file '$name' was $changeType at $timeStamp"

    # File Checks

    # Move File
    Move-Item $path -Destination $destinationPath -Verbose
}

如何检查文件是否仍在复制?

您需要构建一个测试来查看文件是否被锁定(仍在复制)。 为此,您可以使用此功能:

function Test-LockedFile {
    param (
        [parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('FullName', 'FilePath')]
        [ValidateScript({Test-Path $_ -PathType Leaf})]
        [string]$Path
    )
    $file = [System.IO.FileInfo]::new($Path)
    # old PowerShell versions use:
    # $file = New-Object System.IO.FileInfo $Path

    try {
        $stream = $file.Open([System.IO.FileMode]::Open,
                             [System.IO.FileAccess]::ReadWrite,
                             [System.IO.FileShare]::None)
        if ($stream) { $stream.Close() }
        return $false
    }
    catch {
        return $true
    }
}

有了它,在当前代码之上的某处,您可以:

# File Checks
while (Test-LockedFile $path) {
    Start-Sleep -Seconds 1
}

# Move File
Move-Item $path -Destination $destinationPath -Verbose

试试这个。 (之前也有类似的担忧)。

事件触发器正在将所有受影响的项目收集到一个同步的哈希表中,并且一个额外的脚本块处理所有项目,但仅当文件准备好读取且未锁定时。 如果文件被锁定,您将在控制台中看到它。只需尝试复制大于 1 GB 的文件并观察输出。

处理项目(如复制到备份文件)的脚本块 $fSItemEventProcessingJob 最初是为 "Start-Job" 创建的。 但是您无法从该后台作业的会话中访问和修改哈希表。因此这是一个简单的脚本块执行。

要停止一切,只需按 CTRL + C。 这仍然会执行 "finally" 块并取消注册并处理所有内容。

PS: 该脚本只是一个测试,仅在我的 PC 上进行了本地测试。

Clear-Host
$ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop


$fileSystemWatcherDirPath = 'C:\temp'
$fileSystemWatcherFilter = '*.*'

$fileSystemWatcher = [System.IO.FileSystemWatcher]::new($fileSystemWatcherDirPath , $fileSystemWatcherFilter)
$fileSystemWatcher.IncludeSubdirectories = $true
$fileSystemWatcher.EnableRaisingEvents = $true
$fileSystemWatcher.NotifyFilter = [System.IO.NotifyFilters]::FileName -bor [System.IO.NotifyFilters]::DirectoryName -bor [System.IO.NotifyFilters]::LastWrite  # [System.Linq.Enumerable]::Sum([System.IO.NotifyFilters].GetEnumValues())

# Create syncronized hashtable
$syncdFsItemEventHashT = [hashtable]::Synchronized([hashtable]::new())

$fileSystemWatcherAction = {
    try {
        $fsItemEvent = [pscustomobject]@{
            EventIdentifier  = $Event.EventIdentifier
            SourceIdentifier = $Event.SourceIdentifier
            TimeStamp        = $Event.TimeGenerated
            FullPath         = $Event.SourceEventArgs.FullPath
            ChangeType       = $Event.SourceEventArgs.ChangeType
        }

        # Collecting event in synchronized hashtable (overrides existing keys so that only the latest event details are available)
        $syncdFsItemEventHashT[$fsItemEvent.FullPath] = $fsItemEvent
    } catch {
        Write-Host ($_ | Format-List * | Out-String ) -ForegroundColor red
    }
}


# Script block which processes collected events and do further actions like copying for backup, etc...
# That scriptblock was initially used to test "Start-Job". Unfortunately it's not possible to access and modify the synchronized hashtable created within this scope.
$fSItemEventProcessingJob = {
    $keys = [string[]]$syncdFsItemEventHashT.psbase.Keys

    foreach ($key in $keys) {
        $fsEvent = $syncdFsItemEventHashT[$key]

        try {
            # in case changetype eq DELETED or the item can't be found on the filesystem by the script -> remove the item from hashtable without any further actions.
            # This affects temporary files from applications. BUT: Could also affect files with file permission issues.
            if (($fsEvent.ChangeType -eq [System.IO.WatcherChangeTypes]::Deleted) -or (! (Test-Path -LiteralPath $fsEvent.FullPath)) ) {
                $syncdFsItemEventHashT.Remove($key )
                Write-Host ("==> Item '$key' with changetype '$($fsEvent.ChangeType)' removed from hashtable without any further actions!") -ForegroundColor Blue
                continue
            }

            # get filesystem object
            $fsItem = Get-Item -LiteralPath $fsEvent.FullPath -Force

            if ($fsItem -is [System.IO.FileInfo]) {
                # file processing

                try {
                    # Check whether the file is still locked / in use by another process
                    [System.IO.FileStream]$fileStream = [System.IO.File]::Open( $fsEvent.FullPath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::Read)
                    $fileStream.Close()
                } catch [System.IO.IOException] {
                    Write-Host ("==> Item '$key' with changetype '$($fsEvent.ChangeType)' is still in use and can't be read!") -ForegroundColor Yellow
                    continue
                }
            } elseIf ($fsItem -is [System.IO.DirectoryInfo]) {
                # directory processing
            }

            $syncdFsItemEventHashT.Remove($key )
            Write-Host ("==> Item '$key' with changetype '$($fsEvent.ChangeType)' has been processed and removed from hashtable.") -ForegroundColor Blue

        } catch {
            Write-Host ($_ | Format-List * | Out-String ) -ForegroundColor red
        }
    }
}

[void] (Register-ObjectEvent -InputObject $fileSystemWatcher -EventName 'Created' -SourceIdentifier 'FSCreated' -Action $fileSystemWatcherAction)
[void] (Register-ObjectEvent -InputObject $fileSystemWatcher -EventName 'Changed' -SourceIdentifier 'FSChanged' -Action $fileSystemWatcherAction)
[void] (Register-ObjectEvent -InputObject $fileSystemWatcher -EventName 'Renamed' -SourceIdentifier 'FSRenamed' -Action $fileSystemWatcherAction)
[void] (Register-ObjectEvent -InputObject $fileSystemWatcher -EventName 'Deleted' -SourceIdentifier 'FSDeleted' -Action $fileSystemWatcherAction)



Write-Host "Watching for changes in '$fileSystemWatcherDirPath'.`r`nPress CTRL+C to exit!"
try {
    do {
        Wait-Event -Timeout 1

        if ($syncdFsItemEventHashT.Count -gt 0) {
            Write-Host "`r`n"
            Write-Host ('-' * 50) -ForegroundColor Green
            Write-Host "Collected events in hashtable queue:" -ForegroundColor Green
            $syncdFsItemEventHashT.Values | Format-Table | Out-String
        }

        # Process hashtable items and do something with them (like copying, ..)
        .$fSItemEventProcessingJob

        # Garbage collector
        [GC]::Collect()

    } while ($true)

} finally {
    # unregister
    Unregister-Event -SourceIdentifier 'FSChanged'
    Unregister-Event -SourceIdentifier 'FSCreated'
    Unregister-Event -SourceIdentifier 'FSDeleted'
    Unregister-Event -SourceIdentifier 'FSRenamed'

    # dispose
    $FileSystemWatcher.Dispose()
    Write-Host "`r`nEvent Handler removed."
}

在上面发表我的评论后不久就解决了这个问题。其他答案可能类似,但最终将此功能添加到脚本顶部。

# function Test-FileLock 
function Test-FileLock {
  param ([parameter(Mandatory=$true)][string]$Path)

  $oFile = New-Object System.IO.FileInfo $Path

  if ((Test-Path -Path $Path) -eq $false)
  {
    return $false
  }

  try
  {
      $oStream = $oFile.Open([System.IO.FileMode]::Open, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None)
      if ($oStream)
      {
        $oStream.Close()
      }
      $false
  }
  catch
  {
    # file is locked by a process.
    return $true
  }
}

在 $onCreated 部分的变量部分之后添加了这段代码。

# Test File Lock
Do {
    $filetest = Test-FileLock -Path $path
    sleep -Seconds 5
} While ($filetest -eq $true)