Powershell - "Процесс не может получить доступ к файлу, потому что он используется другим процессом"

Ниже приведен скрипт, который отслеживает каталог и его подпапки на предмет сохраненных файлов. Примерно каждые 10 минут я ищу новые файлы, а затем сопоставляю их с таблицей базы данных, в которой указано, куда их нужно перенести, - затем копирует файлы в локальный архив, перемещает их в места, которые необходимо переместить и вставляет запись в другую таблицу базы данных с атрибутами файла, а также с тем, куда она приходила и уходила. Если в базе данных нет совпадений - или есть ошибка сценария - он отправляет мне электронное письмо.

Однако, поскольку файлы постоянно помещаются в каталог, возможно, что файл все еще записывается при выполнении сценария. В результате я получаю сообщение об ошибке The process cannot access the file because it is being used by another process. по электронной почте мне все время. Кроме того, потому что я не имею дело с ошибкой заранее; он проходит цикл, и в мою таблицу журналов в базе данных вставляется ложная запись с неверными атрибутами файла. Когда файл, наконец, освобождается, он вставляется снова.

Я ищу способ идентифицировать файлы, к которым прикреплены процессы; и пропустить их при выполнении скрипта - но несколько дней поиска в Интернете и некоторого тестирования пока не дали ответа.

## CLEAR ERROR LOG
$error.clear()

Write-Host "***File Transfer Script***"

## PARAMETERS
$source_path = "D:\Files\In\"
$xferfail_path = "D:\Files\XferFailed\"
$archive_path = "D:\Files\XferArchive\"
$email_from = "SQLMail <[email protected]>"
$email_recip = [STRING]"[email protected]"
$smtp_server = "email.bar.com"
$secpasswd = ConvertTo-SecureString "Pa$$w0rd" -AsPlainText -Force
$smtp_cred = New-Object System.Management.Automation.PSCredential ("BAR\SQLAdmin", $secpasswd)

## SQL LOG FUNCTION
function Run-SQL ([string]$filename, [string]$filepath, [int]$filesize, [int]$rowcount, [string]$xferpath)
    {
        $date = get-date -format G
        $SqlConnection = New-Object System.Data.SqlClient.SqlConnection
        $SqlConnection.ConnectionString = "Server=SQLSERVER;Database=DATABASE;Uid=SQLAdmin;Pwd=Pa$$w0rd;"
        $SqlConnection.Open()
        $SqlCmd = New-Object System.Data.SqlClient.SqlCommand
        $SqlCmd.CommandText = "INSERT INTO DATABASE..Table VALUES ('$date','$filename','$filepath',$filesize,$rowcount,'$xferpath',0)"
        $SqlCmd.Connection = $SqlConnection
        $SqlCmd.ExecuteNonQuery()
        $SqlConnection.Close()
    }


## DETERMINE IF THERE ARE ANY FILES TO PROCESS
$file_count = Get-ChildItem -path $source_path |? {$_.PSIsContainer} '
              | Get-ChildItem -path {$_.FullName} -Recurse | Where {$_.psIsContainer -eq $false} | Where {$_.Fullname -notlike "D:\Files\In\MCI\*"} '
              | Measure-Object | Select Count

If ($file_count.Count -gt 0)
    {
        Write-Host $file_count.Count "File(s) Found - Processing."
        Start-Sleep -s 5


    ## CREATE LIST OF DIRECTORIES
    $dirs = Get-ChildItem -path $source_path -Recurse | Where {$_.psIsContainer -eq $true} | Where {$_.Fullname -ne "D:\Files\In\MCI"} '
                                                      | Where {$_.Fullname -notlike "D:\Files\In\MCI\*"}


    ## CREATE LIST OF FILES IN ALL DIRECTORIES
    $files = ForEach ($item in $dirs)     
        {
            Get-ChildItem -path $item.FullName | Where {$_.psIsContainer -eq $false} | Sort-Object -Property lastWriteTime -Descending
        }


    ## START LOOPING THROUGH FILE LIST
    ForEach ($item in $files)
        {
            ## QUERY DATABASE FOR FILENAME MATCH, AND RETURN TRANSFER DIRECTORY
            $SqlConnection = New-Object System.Data.SqlClient.SqlConnection
            $SqlConnection.ConnectionString = "Server=SQLSERVER;Database=DATABASE;Uid=SQLAdmin;Pwd=Pa$$w0rd;"
            $SqlConnection.Open()
            $SqlCmd = New-Object System.Data.SqlClient.SqlCommand
            $SqlCmd.CommandText = "SELECT F.DirTransfer FROM DATABASE..Files F WHERE '$item.Name.Trim()' LIKE F.FileName"
            $SqlCmd.Connection = $SqlConnection
            $DirTransfer = $SqlCmd.ExecuteScalar()
            $SqlConnection.Close()

            If ($DirTransfer) # if there is a match
                {
                    Write-Host $item.FullName"'t->'t"$DirTransfer
                    $filename = $item.Name
                    $filepath = $item.FullName
                    $filesize = $item.Length
                        If (!($filesize))
                            {
                                $filesize = 0
                            }
                    $rowcount = (Get-Content -Path $item.FullName).Length
                        If (!($rowcount))
                            {
                                $rowcount = 0
                            }
                    $xferpath = $DirTransfer
                    Run-SQL -filename "$filename" -filepath "$filepath" -filesize "$filesize" -rowcount "$rowcount" -xferpath "$DirTransfer"
                    Copy-Item -path $item.FullName -destination $DirTransfer -force -erroraction "silentlycontinue"
                    Move-Item -path $item.FullName -destination $archive_path -force -erroraction "silentlycontinue"
                    #Write-Host "$filename   $filepath   $filesize    $rowcount   $xferpath"

                }
            Else # if there is no match
                {
                    Write-Host $item.FullName "does not have a mapping"
                    Move-Item -path $item.FullName -destination $xferfail_path -force
                    $filename = $item.FullName
                    $email_body = "$filename 'r'n'r'n does not have a file transfer mapping setup"
                    Send-MailMessage -To $email_recip '
                                     -From $email_from '
                                     -SmtpServer $smtp_server '
                                     -Subject "File Transfer Error - $item" '
                                     -Body $email_body '
                                     -Priority "High" '
                                     -Credential $smtp_cred
                }
        }



}
## IF NO FILES, THEN CLOSE
Else
{
    Write-Host "No File(s) Found - Aborting."
    Start-Sleep -s 5
}

## SEND EMAIL NOTIFICATION IF SCRIPT ERROR

If ($error.count -gt 0)
    {
        $email_body = "$error"
        Send-MailMessage -To $email_recip '
                         -From $email_from '
                         -SmtpServer $smtp_server '
                         -Subject "File Transfer Error - Script" '
                         -Body $email_body '
                         -Priority "High" '
                         -Credential $smtp_cred
    }

Ответ 1

В качестве альтернативы вы можете проверить наличие ошибок либо с помощью try/catch, либо путем поиска коллекции $errors после попытки Move-Item, а затем соответствующим образом обработать это условие.

$error.Clear()
Move-Item -path $item.FullName -destination $xferfail_path -force -ea 0
if($error.Count -eq 0) {
  # do something useful
}
else {
  # do something that doesn't involve spamming oneself
}

Ответ 2

Вы можете использовать SysInternals handles.exe, чтобы найти открытые дескрипторы файла. EXE можно скачать из http://live.sysinternals.com/.

$targetfile = "C:\Users\me\Downloads\The-DSC-Book.docx"
$result = Invoke-Expression "C:\Users\me\Downloads\handle.exe $targetfile" | Select-String ([System.IO.Path]::GetFileNameWithoutExtension($targetfile))
$result

Выходы:

WINWORD.EXE        pid: 3744   type: File           1A0: C:\Users\me\Downloads\The-DSC-Book.docx

Ответ 3

Одним из способов избежать блокировок файлов, вызванных запуском script на таймере, является использование подхода, управляемого событиями, с использованием наблюдателя файловой системы. Он имеет возможность выполнять код, когда в папке, которую вы контролируете, создается событие, такое как новый файл.

Чтобы запустить код при завершении копирования файла, вам нужно будет прослушать событие changed. В этом случае есть небольшая проблема с тем, что он запускается один раз, когда файл начинает копировать и снова, когда он завершен. У меня возникла идея обойти эту проблему с курицей/яйцом после того, как вы проверили модуль Майк, связанный с комментариями. Я обновил код ниже, чтобы он только запускал код, когда файл полностью написан.

Чтобы попробовать, измените $folderToMonitor на папку, которую вы хотите контролировать, и добавьте код для обработки файла.

$processFile = {    
    try {
        $filePath = $event.sourceEventArgs.FullPath
        [IO.File]::OpenRead($filePath).Close()

        #A Way to prevent false positive for really small files.
        if (-not ($newFiles -contains $filePath)) {
            $newFiles += $filePath

            #Process $filePath here...
        }
    } catch {
        #File is still being created, we wait till next event.
    }   
}

$folderToMonitor = 'C:\Folder_To_Monitor'

$watcher = New-Object System.IO.FileSystemWatcher -Property @{
    Path = $folderToMonitor
    Filter = $null
    IncludeSubdirectories = $true
    EnableRaisingEvents = $true
    NotifyFilter = [System.IO.NotifyFilters]'FileName,LastWrite'
}

$script:newFiles = @()
Register-ObjectEvent $watcher -EventName Changed -Action $processFile > $null